import express, { Request, Response } from "express"; import crypto from "crypto"; import jwt, { SignOptions } from "jsonwebtoken"; import ms, { type StringValue } from "ms"; import { PrismaClient, Prisma } from "./generated/client"; import { config } from "./config"; import { requireAuth, optionalAuth } from "./middleware/auth"; import { getCsrfTokenHeader, sanitizeText, validateCsrfToken } from "./security"; import rateLimit, { MemoryStore } from "express-rate-limit"; import { registerAccountRoutes } from "./auth/accountRoutes"; import { registerAdminRoutes } from "./auth/adminRoutes"; import { registerCoreRoutes } from "./auth/coreRoutes"; interface JwtPayload { userId: string; email: string; type: "access" | "refresh"; impersonatorId?: string; } const isJwtPayload = (decoded: unknown): decoded is JwtPayload => { if (typeof decoded !== "object" || decoded === null) { return false; } const payload = decoded as Record; return ( typeof payload.userId === "string" && typeof payload.email === "string" && (payload.type === "access" || payload.type === "refresh") ); }; const router = express.Router(); const prisma = new PrismaClient(); const BOOTSTRAP_USER_ID = "bootstrap-admin"; const DEFAULT_SYSTEM_CONFIG_ID = "default"; const ensureSystemConfig = async () => { return prisma.systemConfig.upsert({ where: { id: DEFAULT_SYSTEM_CONFIG_ID }, update: {}, create: { id: DEFAULT_SYSTEM_CONFIG_ID, authEnabled: false, registrationEnabled: false, authLoginRateLimitEnabled: true, authLoginRateLimitWindowMs: 15 * 60 * 1000, authLoginRateLimitMax: 20, }, }); }; const ensureAuthEnabled = async (res: Response): Promise => { const systemConfig = await ensureSystemConfig(); if (!systemConfig.authEnabled) { res.status(404).json({ error: "Not found", message: "Authentication is disabled", }); return false; } return true; }; type LoginRateLimitConfig = { enabled: boolean; windowMs: number; max: number; }; const DEFAULT_LOGIN_RATE_LIMIT: LoginRateLimitConfig = { enabled: true, windowMs: 15 * 60 * 1000, max: 20, }; let loginRateLimitConfig: LoginRateLimitConfig = { ...DEFAULT_LOGIN_RATE_LIMIT }; let loginAttemptLimiter: ReturnType | null = null; let loginLimiterInitPromise: Promise | null = null; const parseLoginRateLimitConfig = (systemConfig: Awaited>): LoginRateLimitConfig => { const enabled = typeof systemConfig.authLoginRateLimitEnabled === "boolean" ? systemConfig.authLoginRateLimitEnabled : DEFAULT_LOGIN_RATE_LIMIT.enabled; const windowMs = Number.isFinite(Number(systemConfig.authLoginRateLimitWindowMs)) && Number(systemConfig.authLoginRateLimitWindowMs) > 0 ? Number(systemConfig.authLoginRateLimitWindowMs) : DEFAULT_LOGIN_RATE_LIMIT.windowMs; const max = Number.isFinite(Number(systemConfig.authLoginRateLimitMax)) && Number(systemConfig.authLoginRateLimitMax) > 0 ? Number(systemConfig.authLoginRateLimitMax) : DEFAULT_LOGIN_RATE_LIMIT.max; return { enabled, windowMs, max }; }; const resolveAuthIdentifier = (req: Request): string | null => { const body = (req.body || {}) as Record; const raw = (typeof body.email === "string" && body.email) || (typeof body.username === "string" && body.username) || (typeof body.identifier === "string" && body.identifier) || null; if (!raw) return null; const trimmed = raw.trim().toLowerCase(); return trimmed.length > 0 ? trimmed.slice(0, 255) : null; }; const buildLoginAttemptLimiter = (cfg: LoginRateLimitConfig) => { const store = new MemoryStore(); const limiter = rateLimit({ windowMs: cfg.windowMs, max: cfg.max, message: { error: "Too many requests", message: "Too many login attempts, please try again later", }, standardHeaders: true, legacyHeaders: false, validate: { trustProxy: false, }, store, keyGenerator: (req) => { const identifier = resolveAuthIdentifier(req as Request); if (identifier) return `login:${identifier}`; const ip = (req as Request).ip || "unknown"; return `login-ip:${ip}`; }, }); loginAttemptLimiter = limiter; }; const initLoginAttemptLimiter = async () => { const systemConfig = await ensureSystemConfig(); loginRateLimitConfig = parseLoginRateLimitConfig(systemConfig); buildLoginAttemptLimiter(loginRateLimitConfig); }; const ensureLoginAttemptLimiter = async () => { if (loginAttemptLimiter) return; if (!loginLimiterInitPromise) { loginLimiterInitPromise = initLoginAttemptLimiter().finally(() => { loginLimiterInitPromise = null; }); } await loginLimiterInitPromise; }; const applyLoginRateLimitConfig = ( systemConfig: Pick>, "authLoginRateLimitEnabled" | "authLoginRateLimitWindowMs" | "authLoginRateLimitMax"> ): LoginRateLimitConfig => { loginRateLimitConfig = parseLoginRateLimitConfig(systemConfig as Awaited>); buildLoginAttemptLimiter(loginRateLimitConfig); return loginRateLimitConfig; }; const resetLoginAttemptKey = async (identifier: string): Promise => { await ensureLoginAttemptLimiter(); const key = `login:${identifier}`; try { await loginAttemptLimiter?.resetKey(key); } catch (error) { if (process.env.NODE_ENV === "development") { console.debug("Rate limit reset skipped:", error); } } }; const loginAttemptRateLimiter = async (req: Request, res: Response, next: express.NextFunction) => { await ensureLoginAttemptLimiter(); if (!loginRateLimitConfig.enabled) return next(); return (loginAttemptLimiter as ReturnType)(req, res, next); }; const accountActionRateLimiter = rateLimit({ windowMs: 5 * 60 * 1000, max: 60, message: { error: "Too many requests", message: "Too many requests, please try again later", }, standardHeaders: true, legacyHeaders: false, validate: { trustProxy: false, }, }); const generateTempPassword = (): string => { const buf = crypto.randomBytes(18); return buf.toString("base64").replace(/[+/=]/g, "").slice(0, 24); }; const findUserByIdentifier = async (identifier: string) => { const trimmed = identifier.trim(); if (trimmed.length === 0) return null; const looksLikeEmail = trimmed.includes("@"); if (looksLikeEmail) { return prisma.user.findUnique({ where: { email: trimmed.toLowerCase() }, }); } return prisma.user.findFirst({ where: { OR: [{ username: trimmed }, { email: trimmed.toLowerCase() }], }, }); }; const requireAdmin = ( req: Request, res: Response ): req is Request & { user: NonNullable } => { if (!req.user) { res.status(401).json({ error: "Unauthorized", message: "User not authenticated" }); return false; } if (req.user.role !== "ADMIN") { res.status(403).json({ error: "Forbidden", message: "Admin access required" }); return false; } return true; }; const getClientId = (req: Request): string => { const ip = req.ip || req.connection.remoteAddress || "unknown"; const userAgent = req.headers["user-agent"] || "unknown"; return `${ip}:${userAgent}`.slice(0, 256); }; const requireCsrf = (req: Request, res: Response): boolean => { const headerName = getCsrfTokenHeader(); const tokenHeader = req.headers[headerName]; const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader; if (!token) { res.status(403).json({ error: "CSRF token missing", message: `Missing ${headerName} header`, }); return false; } if (!validateCsrfToken(getClientId(req), token)) { res.status(403).json({ error: "CSRF token invalid", message: "Invalid or expired CSRF token. Please refresh and try again.", }); return false; } return true; }; const countActiveAdmins = async () => { return prisma.user.count({ where: { role: "ADMIN", isActive: true }, }); }; const generateTokens = ( userId: string, email: string, options?: { impersonatorId?: string } ) => { const signOptions: SignOptions = { expiresIn: config.jwtAccessExpiresIn as StringValue, }; const accessToken = jwt.sign( { userId, email, type: "access", impersonatorId: options?.impersonatorId }, config.jwtSecret, signOptions ); const refreshSignOptions: SignOptions = { expiresIn: config.jwtRefreshExpiresIn as StringValue, }; const refreshToken = jwt.sign( { userId, email, type: "refresh", impersonatorId: options?.impersonatorId }, config.jwtSecret, refreshSignOptions ); return { accessToken, refreshToken }; }; const resolveExpiresAt = (expiresIn: string, fallbackMs: number): Date => { const parsed = ms(expiresIn as StringValue); const ttlMs = typeof parsed === "number" && parsed > 0 ? parsed : fallbackMs; return new Date(Date.now() + ttlMs); }; const isMissingRefreshTokenTableError = (error: unknown): boolean => { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === "P2021") { return true; } } const message = typeof error === "object" && error && "message" in error ? String((error as any).message) : ""; return /no such table:\s*RefreshToken/i.test(message); }; const getRefreshTokenExpiresAt = (): Date => resolveExpiresAt(config.jwtRefreshExpiresIn, 7 * 24 * 60 * 60 * 1000); registerCoreRoutes({ router, prisma, requireAuth, optionalAuth, loginAttemptRateLimiter, ensureAuthEnabled, ensureSystemConfig, findUserByIdentifier, sanitizeText, requireCsrf, isJwtPayload, config, generateTokens, getRefreshTokenExpiresAt, isMissingRefreshTokenTableError, bootstrapUserId: BOOTSTRAP_USER_ID, defaultSystemConfigId: DEFAULT_SYSTEM_CONFIG_ID, }); registerAdminRoutes({ router, prisma, requireAuth, accountActionRateLimiter, ensureAuthEnabled, ensureSystemConfig, parseLoginRateLimitConfig, applyLoginRateLimitConfig, resetLoginAttemptKey, requireAdmin, findUserByIdentifier, countActiveAdmins, sanitizeText, generateTempPassword, generateTokens, getRefreshTokenExpiresAt, config, defaultSystemConfigId: DEFAULT_SYSTEM_CONFIG_ID, }); registerAccountRoutes({ router, prisma, requireAuth, loginAttemptRateLimiter, accountActionRateLimiter, ensureAuthEnabled, sanitizeText, config, generateTokens, getRefreshTokenExpiresAt, }); export default router;