import type { Database } from '@tursodatabase/database'; import type { Cookies } from '@sveltejs/kit'; import { hasActiveTotpFactor, isFullAdministrator } from './bootstrap'; import { createAuthenticationTransaction, type AuthenticationTransaction, } from './authentication-transactions'; import { createSession, recordLogin, type SessionAuthLevel } from './session'; export type PrimaryAuthenticationProvider = 'password' | 'discord' | 'magic_link' | 'passkey'; export class PrimaryAuthenticationError extends Error { readonly code: 'account_restricted' | 'account_missing'; constructor(code: 'account_restricted' | 'account_missing') { super('This account is not permitted to sign in.'); this.name = 'PrimaryAuthenticationError'; this.code = code; } } export type PrimaryAuthenticationResult = | { kind: 'session'; sessionId: string; aal: SessionAuthLevel; redirectTo: string; } | { kind: 'mfa'; transaction: AuthenticationTransaction; redirectTo: '/auth/two-factor'; }; export type CompletePrimaryAuthenticationInput = { database: Database; cookies: Cookies; userId: string; provider: PrimaryAuthenticationProvider; request?: Request; getClientAddress?: () => string; credentialId?: string | null; returnPath?: string; }; /** * Completes a verified password, email, Discord, or passkey identity. This is * the only primary-auth path allowed to create a session. Accounts with an * active TOTP factor receive an opaque MFA transaction instead. */ export async function completePrimaryAuthentication( input: CompletePrimaryAuthenticationInput, ): Promise { const status = await ( await input.database.prepare(` SELECT users.id,user_status.blocked_at,user_status.banned_until, deleted_accounts.deleted_at FROM users LEFT JOIN user_status ON user_status.user_id=users.id LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id WHERE users.id=? `) ).get(input.userId); const banned = typeof status?.banned_until === 'string' && new Date(status.banned_until) > new Date(); if (!status) throw new PrimaryAuthenticationError('account_missing'); if (status.deleted_at || status.blocked_at || banned) { await recordLogin(input.userId, input.provider, false, { request: input.request, getClientAddress: input.getClientAddress, credentialId: input.credentialId, failureReason: 'account_restricted', }); throw new PrimaryAuthenticationError('account_restricted'); } const activeFactor = await hasActiveTotpFactor(input.database, input.userId); const fullAdministrator = await isFullAdministrator( input.database, input.userId, ); if (!activeFactor) { const sessionId = await createSession(input.cookies, input.userId, { aal: 1, }); const redirectTo = fullAdministrator ? '/account/security?required=admin' : (input.returnPath ?? '/account'); await recordLogin(input.userId, input.provider, true, { request: input.request, getClientAddress: input.getClientAddress, credentialId: input.credentialId, sessionId, aal: 1, }); return { kind: 'session', sessionId, aal: 1, redirectTo }; } const transaction = await createAuthenticationTransaction( input.cookies, input.database, { userId: input.userId, kind: 'login', provider: input.provider, returnPath: input.returnPath ?? '/account', }, ); await recordLogin(input.userId, input.provider, false, { request: input.request, getClientAddress: input.getClientAddress, credentialId: input.credentialId, failureReason: 'mfa_required', riskFlags: ['primary_authenticated_mfa_required'], aal: 1, }); return { kind: 'mfa', transaction, redirectTo: '/auth/two-factor' }; }