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: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
totpCode: { label: "2FA Code", type: "text" },
},
authorize: async (credentials) => {
const email = String(credentials?.email ?? "").trim().toLowerCase()
const password = String(credentials?.password ?? "")
const totpCode = String(credentials?.totpCode ?? "").trim()
if (!email || !password) return null
// 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)
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
.select({ name: roles.name })
.from(usersToRoles)
@@ -130,6 +151,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
const allRoles = (u.roles ?? [u.role ?? "student"]).filter(isRole)
token.roles = 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
@@ -144,7 +167,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
const [fresh, roleRows] = await Promise.all([
db.query.users.findFirst({
where: eq(users.id, userId),
columns: { name: true },
columns: { name: true, onboardedAt: true },
}),
db
.select({ name: roles.name })
@@ -159,6 +182,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
token.name = fresh.name ?? token.name
token.roles = 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.roles = (token.roles ?? []).filter(isRole)
session.user.permissions = (token.permissions ?? []) as typeof token.permissions
session.user.onboarded = Boolean(token.onboarded)
if (typeof token.name === "string") {
session.user.name = token.name
}