feat: shop buy, forum replies, tickets, messages, sessions, and UX hardening
Complete remaining product gaps: credit-based shop purchases, guild thread replies, help ticket detail/reply/close, offline message compose, sign-out-everywhere via JWT version, ads delete confirm, soft-fail feedback, rate limits, loading states, and single auth() in site layout. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
ed7db6e048
commit
803e8f36c1
47 files changed
+2793
-358
No files matched your search
+56
-4
@@ -135,7 +135,12 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return { id: String(user.id), name: user.username, rank: user.rank };
|
||||
return {
|
||||
id: String(user.id),
|
||||
name: user.username,
|
||||
rank: user.rank,
|
||||
jwtVersion: user.websiteJwtVersion,
|
||||
};
|
||||
},
|
||||
}),
|
||||
// OAuth providers — enabled only when both id + secret are configured.
|
||||
@@ -200,6 +205,13 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
return "/login?error=NoAccount";
|
||||
},
|
||||
async jwt({ token, user, account }) {
|
||||
if (user) {
|
||||
token.jwtVersion =
|
||||
(user as { jwtVersion?: number }).jwtVersion ??
|
||||
token.jwtVersion ??
|
||||
0;
|
||||
}
|
||||
|
||||
if (user && account?.provider === "credentials") {
|
||||
token.rank = (user as { rank?: number }).rank;
|
||||
return token;
|
||||
@@ -228,12 +240,18 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
if (linked) {
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: Number(linked.userId) },
|
||||
select: { id: true, rank: true, username: true },
|
||||
select: {
|
||||
id: true,
|
||||
rank: true,
|
||||
username: true,
|
||||
websiteJwtVersion: true,
|
||||
},
|
||||
});
|
||||
if (dbUser) {
|
||||
token.sub = String(dbUser.id);
|
||||
token.rank = dbUser.rank;
|
||||
token.name = dbUser.username;
|
||||
token.jwtVersion = dbUser.websiteJwtVersion;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -247,22 +265,56 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
try {
|
||||
const dbUser = await prisma.user.findFirst({
|
||||
where: { mail: user.email, twoFactorConfirmedAt: null },
|
||||
select: { id: true, rank: true, username: true },
|
||||
select: {
|
||||
id: true,
|
||||
rank: true,
|
||||
username: true,
|
||||
websiteJwtVersion: true,
|
||||
},
|
||||
});
|
||||
if (dbUser) {
|
||||
token.sub = String(dbUser.id);
|
||||
token.rank = dbUser.rank;
|
||||
token.name = dbUser.username;
|
||||
token.jwtVersion = dbUser.websiteJwtVersion;
|
||||
}
|
||||
} catch {
|
||||
// leave token as-is on lookup failure
|
||||
}
|
||||
}
|
||||
|
||||
if (token.sub && !token.invalid) {
|
||||
try {
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: Number(token.sub) },
|
||||
select: {
|
||||
websiteJwtVersion: true,
|
||||
rank: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
if (
|
||||
!dbUser ||
|
||||
(token.jwtVersion ?? 0) !== dbUser.websiteJwtVersion
|
||||
) {
|
||||
token.invalid = true;
|
||||
delete token.sub;
|
||||
return token;
|
||||
}
|
||||
token.rank = dbUser.rank;
|
||||
token.name = dbUser.username;
|
||||
} catch {
|
||||
/* keep session on transient DB errors */
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
session({ session, token }) {
|
||||
if (token.sub && session.user) session.user.id = token.sub;
|
||||
if (token.invalid || !token.sub) {
|
||||
return session;
|
||||
}
|
||||
if (session.user) session.user.id = token.sub;
|
||||
if (typeof token.rank === "number" && session.user)
|
||||
session.user.rank = token.rank;
|
||||
return session;
|
||||
|
||||
Reference in new issue
Block a user