diff --git a/src/auth.ts b/src/auth.ts index 5d0b2e9..a6dec2e 100644 --- a/src/auth.ts +++ b/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 }