refactor(auth): update auth configuration, env validation, and test mocks

- Update src/auth.ts auth configuration

- Update src/env.mjs environment variable validation

- Update exam-data and question-data mocks for testing
This commit is contained in:
SpecialX
2026-06-23 17:39:32 +08:00
parent 4f0ef217a0
commit f40ce0f560

View File

@@ -29,10 +29,12 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
credentials: { credentials: {
email: { label: "Email", type: "email" }, email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }, password: { label: "Password", type: "password" },
totpCode: { label: "2FA Code", type: "text" },
}, },
authorize: async (credentials) => { authorize: async (credentials) => {
const email = String(credentials?.email ?? "").trim().toLowerCase() const email = String(credentials?.email ?? "").trim().toLowerCase()
const password = String(credentials?.password ?? "") const password = String(credentials?.password ?? "")
const totpCode = String(credentials?.totpCode ?? "").trim()
if (!email || !password) return null if (!email || !password) return null
// Rate limit by IP + email to slow brute-force attempts // Rate limit by IP + email to slow brute-force attempts
@@ -100,6 +102,25 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
await resetFailedLogin(db, passwordSecurity, user.id) await resetFailedLogin(db, passwordSecurity, user.id)
resetRateLimit(loginLimitKey) resetRateLimit(loginLimitKey)
// 2FA verification (if user has enabled it)
const { verifyTwoFactorForLogin } = await import("@/modules/settings/actions-security")
const twoFactorResult = await verifyTwoFactorForLogin({
userId: user.id,
token: totpCode || undefined,
})
if (twoFactorResult.required && !twoFactorResult.valid) {
await logLoginEvent({
userId: user.id,
userEmail: email,
action: "signin",
status: "failure",
errorMessage: totpCode
? "Invalid 2FA code"
: "2FA required but not provided",
})
return null
}
const roleRows = await db const roleRows = await db
.select({ name: roles.name }) .select({ name: roles.name })
.from(usersToRoles) .from(usersToRoles)
@@ -130,6 +151,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
const allRoles = (u.roles ?? [u.role ?? "student"]).filter(isRole) const allRoles = (u.roles ?? [u.role ?? "student"]).filter(isRole)
token.roles = allRoles token.roles = allRoles
token.permissions = resolvePermissions(allRoles) token.permissions = resolvePermissions(allRoles)
// Onboarding status is resolved from DB below; default to false on first login
token.onboarded = false
} }
// Refresh roles/permissions from DB on each JWT refresh // Refresh roles/permissions from DB on each JWT refresh
@@ -144,7 +167,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
const [fresh, roleRows] = await Promise.all([ const [fresh, roleRows] = await Promise.all([
db.query.users.findFirst({ db.query.users.findFirst({
where: eq(users.id, userId), where: eq(users.id, userId),
columns: { name: true }, columns: { name: true, onboardedAt: true },
}), }),
db db
.select({ name: roles.name }) .select({ name: roles.name })
@@ -159,6 +182,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
token.name = fresh.name ?? token.name token.name = fresh.name ?? token.name
token.roles = allRoles token.roles = allRoles
token.permissions = resolvePermissions(allRoles) token.permissions = resolvePermissions(allRoles)
token.onboarded = Boolean(fresh.onboardedAt)
} }
} }
@@ -170,6 +194,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
session.user.role = normalizeRole(token.role) session.user.role = normalizeRole(token.role)
session.user.roles = (token.roles ?? []).filter(isRole) session.user.roles = (token.roles ?? []).filter(isRole)
session.user.permissions = (token.permissions ?? []) as typeof token.permissions session.user.permissions = (token.permissions ?? []) as typeof token.permissions
session.user.onboarded = Boolean(token.onboarded)
if (typeof token.name === "string") { if (typeof token.name === "string") {
session.user.name = token.name session.user.name = token.name
} }