import { verifyPassword, hashPassword, needsRehash, compareToDummyHash, } from '../../auth/password.js';
import { createSession, revokeSession } from '../../auth/session.js';
import { createPasswordReset, executePasswordReset } from '../../auth/reset.js';
import { getDB } from '../../db.js';
import { generateCodeVerifier, generateCodeChallenge, generateState, generateOAuthNonce, getAuthorizationUrl, handleOAuthCallback, } from '../../auth/oauth.js';
import { createMfaPendingToken, verifyMfaPendingToken, computeRequestFingerprint, } from '../../auth/mfa-pending.js';
import { generateToken as generateCsrfToken } from '../../security/csrf.js';
import { logEvent } from '../../security/audit.js';
import { verifyCaptcha, getCaptchaConfig } from '../../security/captcha.js';
import { encryptSecret, decryptSecret, encryptStringArray, decryptStringArray, } from '../../security/secret-storage.js';
import { getClientIp, isResolvedIp } from '../../security/client-ip.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { SECURITY_HEADERS, json, errorResponse, internalError, hasModel, modelNotAvailable, getSessionSecret, parseCookieHeader, requirePasswordReauth, requireAuth, loginLimiter, totpLimiter, checkRateLimitAsync, clientIp, enforceSessionLimitsForUser, getAdminPath, } from '../route-helpers.js';
import { createLogger } from '../../diagnostics/logger.js';
const authLogger = createLogger('auth');
export function registerAuthRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Auth routes
    // ---------------------------------------------------------------------------
    router.post('/auth/login', async (request) => {
        try {
            const body = (await request.json());
            const { email, password } = body;
            if (!email || !password) {
                return errorResponse('Email and password are required', 400);
            }
            const ip = clientIp(request);
            const userAgent = request.headers.get('user-agent');
            // Bucket by IP when we have a trustworthy one; otherwise fall back to a
            // global bucket. We deliberately avoid keying on the email alone — that
            // would let attackers lock out legitimate users by spamming logins.
            const rateLimitKey = isResolvedIp(ip) ? `login:${ip}` : 'login:unknown';
            if (!(await checkRateLimitAsync(loginLimiter, rateLimitKey))) {
                return errorResponse('Too many login attempts. Please try again later.', 429);
            }
            const captchaConfig = getCaptchaConfig();
            if (captchaConfig.provider !== 'none') {
                const captchaResult = await verifyCaptcha(body.captchaToken ?? '', captchaConfig, ip);
                if (!captchaResult.success) {
                    return errorResponse('CAPTCHA verification failed. Please try again.', 403);
                }
            }
            const d = db();
            if (!hasModel(d, 'user'))
                return modelNotAvailable('user');
            const user = await d.user.findFirst({
                where: { email: email.toLowerCase().trim() },
            });
            // User-enumeration timing defense (H5): when the email doesn't exist,
            // OR when the account is OAuth-only (no passwordHash), still spend
            // the same ~100-200ms running PBKDF2 against a dummy hash before
            // returning the error. Without this, attackers can distinguish
            // "no such user" / "OAuth-only" / "wrong password" by the response
            // delta, enabling targeted phishing & credential stuffing.
            if (!user) {
                await compareToDummyHash(password);
                return errorResponse('Invalid email or password', 401);
            }
            if (!user.passwordHash) {
                await compareToDummyHash(password);
                return errorResponse('This account uses social login. Please sign in with your OAuth provider.', 400);
            }
            const passwordValid = await verifyPassword(password, user.passwordHash);
            if (!passwordValid) {
                await logEvent({
                    event: 'login_failed',
                    userId: user.id,
                    ipAddress: isResolvedIp(ip) ? ip : undefined,
                    userAgent: userAgent ?? undefined,
                });
                return errorResponse('Invalid email or password', 401);
            }
            if (!user.isActive) {
                return errorResponse('Account is deactivated', 403);
            }
            // Suspended and locked accounts keep `isActive` true (so they remain
            // distinct lifecycle states with preserved content/history) but must not
            // be able to sign in. Checked here so an admin "Suspend"/"Lock" takes
            // effect on the very next login attempt, not just on session revocation.
            if (user.suspendedAt) {
                return errorResponse('Account is suspended', 403);
            }
            if (user.lockedAt) {
                return errorResponse('Account is locked', 403);
            }
            // H6: opportunistically upgrade stored hashes that use weaker
            // PBKDF2 parameters than the current policy. We have the plaintext
            // here (and we know it's correct) — re-hash and persist. Wrapped in
            // try/catch so a transient DB failure never fails an otherwise-valid
            // login; the user gets in, the upgrade just runs again next time.
            if (needsRehash(user.passwordHash)) {
                try {
                    const upgraded = await hashPassword(password);
                    await d.user.update({ where: { id: user.id }, data: { passwordHash: upgraded } });
                }
                catch (err) {
                    authLogger.warn('password rehash skipped', {
                        error: err instanceof Error ? err.message : String(err),
                    });
                }
            }
            if (user.totpEnabled) {
                // Hand back an opaque short-lived token instead of the raw userId.
                // The /auth/totp/login endpoint will verify both this token and a
                // stable browser fingerprint before checking the TOTP code.
                const fingerprint = await computeRequestFingerprint(ip, userAgent);
                const mfaPendingToken = await createMfaPendingToken({ userId: user.id, fingerprint }, getSessionSecret());
                return json({ data: { requiresTOTP: true, mfaPendingToken } });
            }
            await enforceSessionLimitsForUser(d, user.id);
            const tempSessionId = crypto.randomUUID();
            const token = await createSession({ userId: user.id, role: user.role, sessionId: tempSessionId }, { secret: getSessionSecret() });
            await db().session.create({
                data: {
                    id: tempSessionId,
                    userId: user.id,
                    token,
                    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
                    ipAddress: isResolvedIp(ip) ? ip : null,
                    userAgent: userAgent ?? null,
                },
            });
            // Surface whether org-wide MFA is required but this account hasn't
            // enrolled yet (past its grace window). The admin shell uses this to
            // prompt enrollment. Computed best-effort; never blocks login (the
            // account can still set up TOTP from inside the admin).
            let mfaSetupRequired = false;
            try {
                const { getStoredSecuritySettings } = await import('../../security/center/gather.js');
                const sec = await getStoredSecuritySettings(d);
                if (sec.mfa.required && !user.totpEnabled) {
                    const graceMs = sec.mfa.gracePeriodDays * 24 * 60 * 60 * 1000;
                    const requiredAt = sec.mfa.requiredAt ? Date.parse(sec.mfa.requiredAt) : 0;
                    const createdAt = user.createdAt ? new Date(user.createdAt).getTime() : 0;
                    mfaSetupRequired = Date.now() >= Math.max(requiredAt || 0, createdAt || 0) + graceMs;
                }
            }
            catch {
                /* policy unavailable — do not block login */
            }
            const response = json({
                data: {
                    token,
                    user: { id: user.id, email: user.email, name: user.name, role: user.role },
                    mfaSetupRequired,
                },
            });
            const isProduction = process.env.NODE_ENV === 'production';
            const sessionCookie = [
                `actuate_session=${token}`,
                'Path=/',
                'HttpOnly',
                'SameSite=Lax',
                'Max-Age=604800',
            ];
            if (isProduction)
                sessionCookie.push('Secure');
            const csrfToken = await generateCsrfToken();
            const csrfCookie = [`actuate_csrf=${csrfToken}`, 'Path=/', 'SameSite=Lax', 'Max-Age=86400'];
            if (isProduction)
                csrfCookie.push('Secure');
            response.headers.append('Set-Cookie', sessionCookie.join('; '));
            response.headers.append('Set-Cookie', csrfCookie.join('; '));
            await logEvent({
                event: 'login_success',
                userId: user.id,
                ipAddress: isResolvedIp(ip) ? ip : undefined,
                userAgent: userAgent ?? undefined,
            });
            return response;
        }
        catch (err) {
            return internalError(err, 'login');
        }
    });
    // ---------------------------------------------------------------------------
    // Dev-only auth bypass — `GET /api/cms/dev/login`
    //
    // Triple-gated escape hatch for local development:
    //   1. `ACTUATE_DEV_AUTH_BYPASS=1` must be set in the env, AND
    //   2. `NODE_ENV` must NOT be `production`, AND
    //   3. The request must originate from a loopback address (`127.*`, `::1`,
    //      or `localhost`).
    //
    // If any gate fails the route returns 404 — indistinguishable from a route
    // that doesn't exist, so probing prod can't even confirm the feature is
    // compiled in.
    //
    // When all three pass we look up or create a synthetic admin user
    // (`dev-bypass@actuate.local`), mint a real session JWT (so every other
    // route does normal session verification), set the `actuate_session`
    // cookie, and 302 → `/securelogin`. The result is a fully-real session;
    // only the credential check was skipped.
    // ---------------------------------------------------------------------------
    const handleDevLogin = async (request) => {
        if (process.env.ACTUATE_DEV_AUTH_BYPASS !== '1')
            return errorResponse('Not found', 404);
        if (process.env.NODE_ENV === 'production')
            return errorResponse('Not found', 404);
        const ip = getClientIp(request);
        const ipIsLoopback = ip === '127.0.0.1' ||
            ip === '::1' ||
            ip === '::ffff:127.0.0.1' ||
            ip === 'localhost' ||
            ip.startsWith('127.');
        // `getClientIp` returns `'unknown'` whenever no trusted proxy header is
        // present, which is the default for `next dev` — there's no proxy in
        // front of the dev server. In that case fall back to the request URL's
        // hostname, which Next.js sets from the actual TCP connection. We only
        // accept the literal loopback hostnames; anything else is rejected.
        let isLoopback = ipIsLoopback;
        if (!isLoopback && (ip === 'unknown' || !ip)) {
            try {
                const host = new URL(request.url).hostname;
                isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '::1';
            }
            catch {
                isLoopback = false;
            }
        }
        if (!isLoopback)
            return errorResponse('Not found', 404);
        try {
            const d = getDB();
            if (!hasModel(d, 'user')) {
                return errorResponse('Dev bypass requires the User model — run prisma migrate first.', 500);
            }
            // Use the dedicated dev-bypass user if it already exists; otherwise
            // mint one. The email is deliberately namespaced under `actuate.local`
            // so it can never collide with a real user (which uses a real domain).
            const DEV_BYPASS_EMAIL = 'dev-bypass@actuate.local';
            let user = await d.user.findUnique({ where: { email: DEV_BYPASS_EMAIL } });
            if (!user) {
                try {
                    user = await d.user.create({
                        data: {
                            email: DEV_BYPASS_EMAIL,
                            name: 'Dev Bypass',
                            role: 'ADMIN',
                            // Bcrypt-shaped placeholder. No real password is ever set;
                            // the user cannot log in through the normal flow.
                            passwordHash: '$2b$10$dev.bypass.user.cannot.login.through.normal.flow',
                        },
                    });
                }
                catch (err) {
                    authLogger.error('dev-bypass user create failed', {
                        error: err instanceof Error ? err.message : String(err),
                    });
                    throw err;
                }
            }
            const tempSessionId = crypto.randomUUID();
            const token = await createSession({ userId: user.id, role: user.role, sessionId: tempSessionId }, { secret: getSessionSecret() });
            if (hasModel(d, 'session')) {
                await d.session.create({
                    data: {
                        id: tempSessionId,
                        userId: user.id,
                        token,
                        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
                        ipAddress: isResolvedIp(ip) ? ip : null,
                        userAgent: request.headers.get('user-agent') ?? null,
                    },
                });
            }
            const csrfToken = await generateCsrfToken();
            const sessionCookie = [
                `actuate_session=${token}`,
                'Path=/',
                'HttpOnly',
                'SameSite=Lax',
                'Max-Age=604800',
            ].join('; ');
            const csrfCookie = [
                `actuate_csrf=${csrfToken}`,
                'Path=/',
                'SameSite=Lax',
                'Max-Age=86400',
            ].join('; ');
            // Redirect back to the admin so the cookie attaches on the next
            // navigation. `?next=` lets callers steer to a specific deep link.
            const url = new URL(request.url);
            const nextPath = url.searchParams.get('next') ?? '/securelogin';
            const safeNext = nextPath.startsWith('/') ? nextPath : '/securelogin';
            authLogger.warn('Issued admin session via dev-bypass — DO NOT USE IN PRODUCTION', {
                email: user.email,
                userId: user.id,
            });
            const response = new Response(null, {
                status: 302,
                headers: {
                    Location: safeNext,
                    ...SECURITY_HEADERS,
                },
            });
            response.headers.append('Set-Cookie', sessionCookie);
            response.headers.append('Set-Cookie', csrfCookie);
            return response;
        }
        catch (err) {
            return internalError(err, 'dev login');
        }
    };
    router.get('/dev/login', handleDevLogin);
    router.post('/dev/login', handleDevLogin);
    router.post('/auth/logout', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            await revokeSession(auth.session.sessionId, db());
            const ip = clientIp(request);
            await logEvent({
                event: 'logout',
                userId: auth.session.userId,
                ipAddress: isResolvedIp(ip) ? ip : undefined,
            });
            const response = json({ data: { success: true } });
            response.headers.set('Set-Cookie', 'actuate_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
            return response;
        }
        catch (err) {
            return internalError(err, 'logout');
        }
    });
    router.post('/auth/forgot-password', async (request) => {
        try {
            const body = (await request.json());
            const { email } = body;
            if (!email) {
                return errorResponse('Email is required', 400);
            }
            const ip = clientIp(request);
            const rateLimitKey = isResolvedIp(ip) ? `forgot:${ip}` : 'forgot:unknown';
            if (!(await checkRateLimitAsync(loginLimiter, rateLimitKey))) {
                return errorResponse('Too many requests. Please try again later.', 429);
            }
            const d = db();
            if (!hasModel(d, 'user'))
                return modelNotAvailable('user');
            const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? new URL(request.url).origin;
            const cmsConfig = getActuateConfig();
            await createPasswordReset(d, email.toLowerCase().trim(), {
                siteUrl,
                // Split-login installs serve the reset screen on the login mount.
                adminPath: cmsConfig?.admin?.loginPath ?? cmsConfig?.admin?.path ?? '/admin',
                platform: cmsConfig?.platform,
            });
            await logEvent({
                event: 'password_reset_request',
                ipAddress: isResolvedIp(ip) ? ip : undefined,
                userAgent: request.headers.get('user-agent') ?? undefined,
                details: { email: email.toLowerCase().trim() },
            });
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'forgot-password');
        }
    });
    router.post('/auth/reset-password', async (request) => {
        try {
            const body = (await request.json());
            const { token, password } = body;
            if (!token || !password) {
                return errorResponse('Token and new password are required', 400);
            }
            if (password.length < 8) {
                return errorResponse('Password must be at least 8 characters', 400);
            }
            const ip = clientIp(request);
            const rateLimitKey = isResolvedIp(ip) ? `reset:${ip}` : 'reset:unknown';
            if (!(await checkRateLimitAsync(loginLimiter, rateLimitKey))) {
                return errorResponse('Too many requests. Please try again later.', 429);
            }
            const d = db();
            if (!hasModel(d, 'user'))
                return modelNotAvailable('user');
            const result = await executePasswordReset(d, token, password);
            if (!result.success) {
                return errorResponse(result.error ?? 'Password reset failed', 400);
            }
            await logEvent({
                event: 'password_reset_complete',
                ipAddress: isResolvedIp(ip) ? ip : undefined,
                userAgent: request.headers.get('user-agent') ?? undefined,
            });
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'reset-password');
        }
    });
    router.get('/auth/me', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const user = await db().user.findUnique({
                where: { id: auth.session.userId },
                select: {
                    id: true,
                    email: true,
                    name: true,
                    role: true,
                    isActive: true,
                    totpEnabled: true,
                },
            });
            if (!user) {
                return errorResponse('User not found', 404);
            }
            const response = json({ data: user });
            // Bootstrap (or refresh) the double-submit CSRF cookie. Without this
            // the admin app's first non-GET after a hard reload races to populate
            // it; some users hit "Invalid CSRF token" on the very first save.
            const cookies = parseCookieHeader(request.headers.get('cookie') ?? '');
            if (!cookies['actuate_csrf']) {
                const csrfToken = await generateCsrfToken();
                const isProduction = process.env.NODE_ENV === 'production';
                const csrfCookie = [
                    `actuate_csrf=${csrfToken}`,
                    'Path=/',
                    'SameSite=Lax',
                    'Max-Age=86400',
                    ...(isProduction ? ['Secure'] : []),
                ].join('; ');
                response.headers.append('Set-Cookie', csrfCookie);
            }
            return response;
        }
        catch (err) {
            return internalError(err, 'auth/me');
        }
    });
    // ---------------------------------------------------------------------------
    // TOTP routes
    // ---------------------------------------------------------------------------
    router.post('/auth/totp/setup', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            // Sensitive operation — require recent password reauth so a stolen
            // session can't silently rotate someone's TOTP secret.
            const reauthErr = await requirePasswordReauth(request, auth.session.userId);
            if (reauthErr)
                return reauthErr;
            const { generateTOTPSecret, generateTOTPUri, generateBackupCodes } = await import('../../auth/totp.js');
            const user = await db().user.findUnique({
                where: { id: auth.session.userId },
                select: { email: true, totpEnabled: true },
            });
            if (!user)
                return errorResponse('User not found', 404);
            if (user.totpEnabled)
                return errorResponse('TOTP already enabled', 400);
            const secret = generateTOTPSecret();
            const uri = generateTOTPUri(secret, user.email);
            const backups = generateBackupCodes();
            // Persist encrypted; the plaintext is only returned to the user once
            // (so they can scan the QR / save backup codes).
            const encryptedSecret = await encryptSecret(secret);
            const encryptedBackups = await encryptStringArray(backups);
            await db().user.update({
                where: { id: auth.session.userId },
                data: { totpSecret: encryptedSecret, backupCodes: encryptedBackups },
            });
            return json({ data: { secret, uri, backupCodes: backups } });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/auth/totp/verify', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const body = (await request.json());
            if (!body.code)
                return errorResponse('Code is required', 400);
            const { verifyTOTP } = await import('../../auth/totp.js');
            const user = await db().user.findUnique({
                where: { id: auth.session.userId },
                select: { totpSecret: true },
            });
            if (!user?.totpSecret)
                return errorResponse('TOTP not set up', 400);
            const secret = await decryptSecret(user.totpSecret);
            const valid = verifyTOTP(body.code, secret);
            if (!valid)
                return errorResponse('Invalid code', 400);
            await db().user.update({ where: { id: auth.session.userId }, data: { totpEnabled: true } });
            await logEvent({ event: 'totp_enabled', userId: auth.session.userId });
            return json({ data: { enabled: true } });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/auth/totp/disable', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            // Disabling MFA is a high-impact security change. Require recent password
            // reauth and revoke every other session so a compromised cookie loses
            // access immediately after MFA goes away.
            const reauthErr = await requirePasswordReauth(request, auth.session.userId);
            if (reauthErr)
                return reauthErr;
            const d = db();
            await d.user.update({
                where: { id: auth.session.userId },
                data: { totpEnabled: false, totpSecret: null, backupCodes: null },
            });
            if (hasModel(d, 'session')) {
                await d.session.updateMany({
                    where: {
                        userId: auth.session.userId,
                        id: { not: auth.session.sessionId },
                        revokedAt: null,
                    },
                    data: { revokedAt: new Date() },
                });
            }
            await logEvent({ event: 'totp_disabled', userId: auth.session.userId });
            return json({ data: { enabled: false } });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/auth/totp/login', async (request) => {
        try {
            const body = (await request.json());
            if (!body.mfaPendingToken || !body.code) {
                return errorResponse('mfaPendingToken and code are required', 400);
            }
            const ip = clientIp(request);
            const userAgent = request.headers.get('user-agent');
            // Per-IP and per-token rate limits. The per-token limit is the actual
            // brute-force defence: even with IP rotation, an attacker has to obtain
            // a fresh mfaPendingToken (which requires the password) for every
            // window. The per-IP limit guards the IP-aware case and limits log noise.
            const ipBucket = isResolvedIp(ip) ? `totp-ip:${ip}` : 'totp-ip:unknown';
            if (!(await checkRateLimitAsync(totpLimiter, ipBucket))) {
                return errorResponse('Too many TOTP attempts. Please try again later.', 429);
            }
            let pending;
            try {
                pending = await verifyMfaPendingToken(body.mfaPendingToken, getSessionSecret());
            }
            catch {
                return errorResponse('Session expired. Please sign in again.', 401);
            }
            // Validate the request comes from the same browser that completed the
            // password step. This frustrates attempts to ship a captured pending
            // token to a different host.
            const fingerprint = await computeRequestFingerprint(ip, userAgent);
            if (fingerprint !== pending.fingerprint) {
                return errorResponse('Session fingerprint mismatch. Please sign in again.', 401);
            }
            // Per-userId bucket is what actually caps brute-force. With the default
            // 10 attempts / 15 min, an attacker would need ~190 years to cover the
            // 1M code space even with unbounded IPs.
            const userBucket = `totp-user:${pending.userId}`;
            if (!(await checkRateLimitAsync(totpLimiter, userBucket))) {
                return errorResponse('Too many TOTP attempts. Please try again later.', 429);
            }
            const { verifyTOTP } = await import('../../auth/totp.js');
            const user = await db().user.findUnique({
                where: { id: pending.userId },
                select: {
                    id: true,
                    email: true,
                    name: true,
                    role: true,
                    totpSecret: true,
                    totpEnabled: true,
                    backupCodes: true,
                    isActive: true,
                },
            });
            if (!user || !user.isActive || !user.totpEnabled || !user.totpSecret) {
                return errorResponse('Invalid request', 400);
            }
            const decryptedSecret = await decryptSecret(user.totpSecret);
            let valid = verifyTOTP(body.code, decryptedSecret);
            let usedBackupCode = false;
            // Backup-code fallback. A user who has lost their authenticator can sign
            // in with one of the one-time recovery codes issued at enrollment. Each
            // code is single-use: on a match we remove it and re-persist the
            // remaining codes so it can never be replayed. We compare against every
            // stored code in constant time to avoid leaking which/whether one matched.
            const storedBackups = Array.isArray(user.backupCodes) ? user.backupCodes : [];
            if (!valid && storedBackups.length > 0) {
                try {
                    const { findBackupCodeMatch } = await import('../../auth/totp.js');
                    const codes = await decryptStringArray(storedBackups);
                    const matchIdx = findBackupCodeMatch(codes, body.code);
                    if (matchIdx !== -1) {
                        valid = true;
                        usedBackupCode = true;
                        const remaining = codes.filter((_, i) => i !== matchIdx);
                        await db().user.update({
                            where: { id: user.id },
                            data: { backupCodes: await encryptStringArray(remaining) },
                        });
                    }
                }
                catch {
                    // Decryption failure (e.g. the encryption key was rotated) — treat
                    // as no match rather than throwing a 500 at the login screen.
                }
            }
            if (!valid) {
                await logEvent({
                    event: 'login_failed',
                    userId: user.id,
                    ipAddress: isResolvedIp(ip) ? ip : undefined,
                    userAgent: userAgent ?? undefined,
                    details: { reason: 'totp_invalid' },
                });
                return errorResponse('Invalid code', 401);
            }
            const d = db();
            await enforceSessionLimitsForUser(d, user.id);
            const tempSessionId = crypto.randomUUID();
            const token = await createSession({ userId: user.id, role: user.role, sessionId: tempSessionId }, { secret: getSessionSecret() });
            await d.session.create({
                data: {
                    id: tempSessionId,
                    userId: user.id,
                    token,
                    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
                    ipAddress: isResolvedIp(ip) ? ip : null,
                    userAgent: userAgent ?? null,
                },
            });
            await logEvent({
                event: 'login_success',
                userId: user.id,
                ipAddress: isResolvedIp(ip) ? ip : undefined,
                userAgent: userAgent ?? undefined,
                details: { mfa: usedBackupCode ? 'backup_code' : 'totp' },
            });
            const isProduction = process.env.NODE_ENV === 'production';
            const sessionCookie = [
                `actuate_session=${token}`,
                'Path=/',
                'HttpOnly',
                'SameSite=Lax',
                `Max-Age=${7 * 24 * 3600}`,
                ...(isProduction ? ['Secure'] : []),
            ].join('; ');
            const csrfToken = await generateCsrfToken();
            const csrfCookie = [
                `actuate_csrf=${csrfToken}`,
                'Path=/',
                'SameSite=Lax',
                'Max-Age=86400',
                ...(isProduction ? ['Secure'] : []),
            ].join('; ');
            const response = new Response(JSON.stringify({
                data: {
                    token,
                    user: { id: user.id, email: user.email, name: user.name, role: user.role },
                },
            }), { status: 200, headers: { ...SECURITY_HEADERS } });
            response.headers.append('Set-Cookie', sessionCookie);
            response.headers.append('Set-Cookie', csrfCookie);
            return response;
        }
        catch (err) {
            return internalError(err);
        }
    });
    // ---------------------------------------------------------------------------
    // OAuth routes
    // ---------------------------------------------------------------------------
    router.get('/auth/oauth/:provider', async (request, params) => {
        try {
            const provider = params.provider;
            if (!['google', 'github', 'microsoft'].includes(provider)) {
                return errorResponse('Unsupported OAuth provider', 400);
            }
            const secret = getSessionSecret();
            const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? new URL(request.url).origin;
            const oauthProviders = {};
            const envPrefix = provider.toUpperCase();
            const clientId = process.env[`OAUTH_${envPrefix}_CLIENT_ID`] ?? '';
            const clientSecret = process.env[`OAUTH_${envPrefix}_CLIENT_SECRET`] ?? '';
            if (!clientId || !clientSecret) {
                return errorResponse(`OAuth provider "${provider}" is not configured`, 400);
            }
            oauthProviders[provider] = {
                clientId,
                clientSecret,
                redirectUri: `${siteUrl}/api/cms/auth/oauth/${provider}/callback`,
            };
            const codeVerifier = generateCodeVerifier();
            const codeChallenge = await generateCodeChallenge(codeVerifier);
            // Generate a one-time nonce, embed it in the signed state, and also set
            // it on a host-only cookie. The callback will require both to match —
            // this binds the OAuth flow to the browser that started it and prevents
            // an attacker from delivering a state token to a victim's browser.
            const nonce = generateOAuthNonce();
            const state = await generateState(provider, codeVerifier, getAdminPath(), secret, nonce);
            const url = getAuthorizationUrl(provider, oauthProviders[provider], state, codeChallenge);
            const isProduction = process.env.NODE_ENV === 'production';
            const nonceCookie = [
                `actuate_oauth_nonce=${nonce}`,
                'Path=/',
                'HttpOnly',
                'SameSite=Lax',
                'Max-Age=600',
                ...(isProduction ? ['Secure'] : []),
            ].join('; ');
            const response = json({ data: { url } });
            response.headers.append('Set-Cookie', nonceCookie);
            return response;
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.get('/auth/oauth/:provider/callback', async (request, params) => {
        try {
            const provider = params.provider;
            if (!['google', 'github', 'microsoft'].includes(provider)) {
                return errorResponse('Unsupported OAuth provider', 400);
            }
            const url = new URL(request.url);
            const code = url.searchParams.get('code');
            const stateToken = url.searchParams.get('state');
            const errorParam = url.searchParams.get('error');
            if (errorParam) {
                const desc = url.searchParams.get('error_description') ?? errorParam;
                return new Response(null, {
                    status: 302,
                    headers: { Location: `${getAdminPath()}?error=${encodeURIComponent(desc)}` },
                });
            }
            if (!code || !stateToken) {
                return errorResponse('Missing code or state parameter', 400);
            }
            const secret = getSessionSecret();
            const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? url.origin;
            const oauthProviders = {};
            const envPrefix = provider.toUpperCase();
            oauthProviders[provider] = {
                clientId: process.env[`OAUTH_${envPrefix}_CLIENT_ID`] ?? '',
                clientSecret: process.env[`OAUTH_${envPrefix}_CLIENT_SECRET`] ?? '',
                redirectUri: `${siteUrl}/api/cms/auth/oauth/${provider}/callback`,
            };
            const cookies = parseCookieHeader(request.headers.get('cookie') ?? '');
            const expectedNonce = cookies['actuate_oauth_nonce'] ?? null;
            const cmsConfig = getActuateConfig();
            const allowSelfSignup = cmsConfig?.auth?.oauth?.allowSelfSignup === true;
            const result = await handleOAuthCallback(provider, code, stateToken, oauthProviders, secret, db(), { expectedNonce, allowSelfSignup });
            const isProduction = process.env.NODE_ENV === 'production';
            const sessionCookieFlags = [
                `actuate_session=${result.token}`,
                'Path=/',
                'HttpOnly',
                'SameSite=Lax',
                'Max-Age=604800',
            ];
            if (siteUrl.startsWith('https') || isProduction) {
                sessionCookieFlags.push('Secure');
            }
            const response = new Response(null, {
                status: 302,
                headers: { Location: getAdminPath() },
            });
            response.headers.append('Set-Cookie', sessionCookieFlags.join('; '));
            // Clear the one-time nonce cookie regardless of outcome.
            response.headers.append('Set-Cookie', 'actuate_oauth_nonce=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
            return response;
        }
        catch (err) {
            const message = err instanceof Error ? err.message : 'OAuth callback failed';
            const response = new Response(null, {
                status: 302,
                headers: { Location: `${getAdminPath()}?error=${encodeURIComponent(message)}` },
            });
            response.headers.append('Set-Cookie', 'actuate_oauth_nonce=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
            return response;
        }
    });
}
//# sourceMappingURL=auth.js.map