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: {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user