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:
27
src/auth.ts
27
src/auth.ts
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user