import crypto from 'node:crypto'; import jwt from 'jsonwebtoken'; import { cookies, headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { cookieSecure } from './config.js'; const state = globalThis.__filesLehnertSecurityState || (globalThis.__filesLehnertSecurityState = {}); if (!state.jwtSecret) { state.jwtSecret = crypto.randomBytes(32).toString('hex'); } if (!state.loginAttempts) { state.loginAttempts = new Map(); } const authCookieName = 'auth'; const csrfCookieName = 'csrf'; const jwtMaxAgeSeconds = 2 * 60 * 60; const loginWindowMs = 15 * 60 * 1000; const loginMaxAttempts = 10; function firstForwardedPart(value) { if (!value) { return ''; } return value.split(',')[0].trim(); } async function expectedOrigin() { const headerStore = await headers(); const host = headerStore.get('x-forwarded-host') || headerStore.get('host') || ''; const forwardedProto = firstForwardedPart(headerStore.get('x-forwarded-proto')); const protocol = forwardedProto || (process.env.NODE_ENV === 'production' ? 'https' : 'http'); return host ? `${protocol}://${host}` : ''; } async function verifySameOrigin() { const headerStore = await headers(); const source = headerStore.get('origin') || headerStore.get('referer'); if (!source) { return; } let parsed; try { parsed = new URL(source); } catch { throw new Error('origin-check-failed'); } const expected = await expectedOrigin(); if (!expected || parsed.origin !== expected) { throw new Error('origin-check-failed'); } } export async function ensureCsrfToken() { const cookieStore = await cookies(); const cookieToken = cookieStore.get(csrfCookieName)?.value; if (cookieToken) { return cookieToken; } const headerStore = await headers(); const headerToken = headerStore.get('x-csrf-token'); if (headerToken) { return headerToken; } return crypto.randomBytes(32).toString('hex'); } export async function verifyCsrf(formData) { await verifySameOrigin(); const cookieStore = await cookies(); const expectedToken = cookieStore.get(csrfCookieName)?.value || ''; let providedToken = ''; if (formData && typeof formData.get === 'function') { providedToken = String(formData.get('csrfToken') || ''); } if (!providedToken) { const headerStore = await headers(); providedToken = String(headerStore.get('x-csrf-token') || ''); } if (!expectedToken || !providedToken || expectedToken !== providedToken) { throw new Error('csrf-token-mismatch'); } } export async function getRequestMeta() { const headerStore = await headers(); const forwardedFor = firstForwardedPart(headerStore.get('x-forwarded-for')); const realIp = headerStore.get('x-real-ip') || ''; const ip = forwardedFor || realIp || 'unknown'; const userAgent = headerStore.get('user-agent') || ''; return { ip, userAgent }; } export async function getAuthenticatedUser() { const cookieStore = await cookies(); const token = cookieStore.get(authCookieName)?.value; if (!token) { return null; } try { const payload = jwt.verify(token, state.jwtSecret); if (!payload || typeof payload !== 'object' || !payload.sub) { return null; } return { username: String(payload.sub), admin: Boolean(payload.admin), }; } catch { return null; } } export async function setAuthCookie(payload) { const token = jwt.sign(payload, state.jwtSecret, { expiresIn: jwtMaxAgeSeconds }); const cookieStore = await cookies(); cookieStore.set(authCookieName, token, { httpOnly: true, sameSite: 'lax', maxAge: jwtMaxAgeSeconds, secure: cookieSecure, path: '/', }); } export async function clearAuthCookie() { const cookieStore = await cookies(); cookieStore.set(authCookieName, '', { httpOnly: true, sameSite: 'lax', maxAge: 0, secure: cookieSecure, path: '/', }); } export async function requireAuthenticatedUser() { const user = await getAuthenticatedUser(); if (!user) { await clearAuthCookie(); redirect('/manage/login'); } return user; } export async function requireAdminUser() { const user = await getAuthenticatedUser(); if (!user || !user.admin) { await clearAuthCookie(); redirect('/manage/admin'); } return user; } async function loginAttemptKey(type) { const meta = await getRequestMeta(); return `${type}:${meta.ip || 'unknown'}`; } export async function checkLoginRateLimit(type) { const key = await loginAttemptKey(type); const now = Date.now(); const entry = state.loginAttempts.get(key) || { count: 0, resetAt: now + loginWindowMs, }; if (now > entry.resetAt) { entry.count = 0; entry.resetAt = now + loginWindowMs; } entry.count += 1; state.loginAttempts.set(key, entry); if (entry.count > loginMaxAttempts) { return Math.ceil((entry.resetAt - now) / 60_000); } return 0; } export async function clearLoginRateLimit(type) { const key = await loginAttemptKey(type); state.loginAttempts.delete(key); }