import bcrypt from "bcrypt"; import express, { Request, Response } from "express"; import { Prisma, PrismaClient } from "../generated/client"; import { logAuditEvent } from "../utils/audit"; import { adminCreateUserSchema, adminRoleUpdateSchema, adminUpdateUserSchema, impersonateSchema, loginRateLimitResetSchema, loginRateLimitUpdateSchema, registrationToggleSchema, } from "./schemas"; type RegisterAdminRoutesDeps = { router: express.Router; prisma: PrismaClient; requireAuth: express.RequestHandler; accountActionRateLimiter: express.RequestHandler; ensureAuthEnabled: (res: Response) => Promise; ensureSystemConfig: () => Promise<{ id: string; authLoginRateLimitEnabled: boolean; authLoginRateLimitWindowMs: number; authLoginRateLimitMax: number; }>; parseLoginRateLimitConfig: (systemConfig: { authLoginRateLimitEnabled: boolean; authLoginRateLimitWindowMs: number; authLoginRateLimitMax: number; }) => { enabled: boolean; windowMs: number; max: number }; applyLoginRateLimitConfig: (systemConfig: { authLoginRateLimitEnabled: boolean; authLoginRateLimitWindowMs: number; authLoginRateLimitMax: number; }) => { enabled: boolean; windowMs: number; max: number }; resetLoginAttemptKey: (identifier: string) => Promise; requireAdmin: ( req: Request, res: Response ) => req is Request & { user: NonNullable }; findUserByIdentifier: (identifier: string) => Promise<{ id: string; username: string | null; email: string; name: string; role: string; isActive: boolean; mustResetPassword: boolean; passwordHash: string; } | null>; countActiveAdmins: () => Promise; sanitizeText: (input: unknown, maxLength?: number) => string; generateTempPassword: () => string; generateTokens: ( userId: string, email: string, options?: { impersonatorId?: string } ) => { accessToken: string; refreshToken: string }; getRefreshTokenExpiresAt: () => Date; config: { enableAuditLogging: boolean; enableRefreshTokenRotation: boolean; }; defaultSystemConfigId: string; }; export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => { const { router, prisma, requireAuth, accountActionRateLimiter, ensureAuthEnabled, ensureSystemConfig, parseLoginRateLimitConfig, applyLoginRateLimitConfig, resetLoginAttemptKey, requireAdmin, findUserByIdentifier, countActiveAdmins, sanitizeText, generateTempPassword, generateTokens, getRefreshTokenExpiresAt, config, defaultSystemConfigId, } = deps; router.post("/registration/toggle", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const parsed = registrationToggleSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Bad request", message: "Invalid toggle payload" }); } const updated = await prisma.systemConfig.upsert({ where: { id: defaultSystemConfigId }, update: { registrationEnabled: parsed.data.enabled }, create: { id: defaultSystemConfigId, registrationEnabled: parsed.data.enabled }, }); res.json({ registrationEnabled: updated.registrationEnabled }); } catch (error) { console.error("Registration toggle error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to update registration setting", }); } }); router.post("/admins", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const parsed = adminRoleUpdateSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Bad request", message: "Invalid admin update payload" }); } const target = await findUserByIdentifier(parsed.data.identifier); if (!target) { return res.status(404).json({ error: "Not found", message: "User not found" }); } if (target.id === req.user.id && parsed.data.role !== "ADMIN") { return res.status(409).json({ error: "Conflict", message: "You cannot change your own role from ADMIN", }); } if (target.role === "ADMIN" && parsed.data.role !== "ADMIN" && target.isActive) { const admins = await countActiveAdmins(); if (admins <= 1) { return res.status(409).json({ error: "Conflict", message: "There must be at least one active admin", }); } } const updated = await prisma.user.update({ where: { id: target.id }, data: { role: parsed.data.role }, select: { id: true, username: true, email: true, name: true, role: true, mustResetPassword: true, isActive: true, }, }); res.json({ user: updated }); } catch (error) { console.error("Admin role update error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to update user role", }); } }); router.get("/users", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const users = await prisma.user.findMany({ orderBy: [{ createdAt: "asc" }], select: { id: true, username: true, email: true, name: true, role: true, mustResetPassword: true, isActive: true, createdAt: true, updatedAt: true, }, }); res.json({ users }); } catch (error) { console.error("List users error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to list users", }); } }); router.get("/rate-limit/login", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const systemConfig = await ensureSystemConfig(); const cfg = parseLoginRateLimitConfig(systemConfig); res.json({ config: cfg }); } catch (error) { console.error("Get login rate limit config error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to fetch login rate limit config", }); } }); router.put("/rate-limit/login", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const parsed = loginRateLimitUpdateSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Validation error", message: "Invalid rate limit config", }); } const updated = await prisma.systemConfig.update({ where: { id: defaultSystemConfigId }, data: { authLoginRateLimitEnabled: parsed.data.enabled, authLoginRateLimitWindowMs: parsed.data.windowMs, authLoginRateLimitMax: parsed.data.max, }, }); const nextConfig = applyLoginRateLimitConfig(updated); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "admin_login_rate_limit_updated", resource: "system_config", ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { ...nextConfig }, }); } res.json({ config: nextConfig }); } catch (error) { console.error("Update login rate limit config error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to update login rate limit config", }); } }); router.post("/rate-limit/login/reset", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const parsed = loginRateLimitResetSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Validation error", message: "Invalid reset payload", }); } const identifier = parsed.data.identifier.trim().toLowerCase(); await resetLoginAttemptKey(identifier); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "admin_login_rate_limit_reset", resource: `rate_limit:login:${identifier}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { identifier }, }); } res.json({ ok: true }); } catch (error) { console.error("Reset login rate limit error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to reset login rate limit", }); } }); router.post("/users", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const parsed = adminCreateUserSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Validation error", message: "Invalid user payload", }); } const { email, password, name, username, role, mustResetPassword, isActive } = parsed.data; const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { return res.status(409).json({ error: "Conflict", message: "User with this email already exists", }); } if (username) { const existingUsername = await prisma.user.findFirst({ where: { username }, select: { id: true }, }); if (existingUsername) { return res.status(409).json({ error: "Conflict", message: "User with this username already exists", }); } } const saltRounds = 10; const passwordHash = await bcrypt.hash(password, saltRounds); const sanitizedName = sanitizeText(name, 100); const user = await prisma.user.create({ data: { email, username: username ?? null, passwordHash, name: sanitizedName, role: role ?? "USER", mustResetPassword: mustResetPassword ?? false, isActive: isActive ?? true, }, select: { id: true, username: true, email: true, name: true, role: true, mustResetPassword: true, isActive: true, createdAt: true, updatedAt: true, }, }); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "admin_user_created", resource: `user:${user.id}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { createdUserId: user.id }, }); } res.status(201).json({ user }); } catch (error) { console.error("Create user error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to create user", }); } }); router.patch("/users/:id", requireAuth, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const userId = String(req.params.id || "").trim(); if (!userId) { return res.status(400).json({ error: "Bad request", message: "Invalid user id" }); } const parsed = adminUpdateUserSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Bad request", message: "Invalid update payload" }); } if (userId === req.user.id && parsed.data.isActive === false) { return res.status(409).json({ error: "Conflict", message: "You cannot deactivate your own account", }); } if (userId === req.user.id && parsed.data.role && parsed.data.role !== "ADMIN") { return res.status(409).json({ error: "Conflict", message: "You cannot change your own role from ADMIN", }); } const current = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, role: true, isActive: true }, }); if (!current) { return res.status(404).json({ error: "Not found", message: "User not found" }); } const nextRole = typeof parsed.data.role === "undefined" ? current.role : parsed.data.role; const nextActive = typeof parsed.data.isActive === "undefined" ? current.isActive : parsed.data.isActive; const removingAdmin = current.role === "ADMIN" && current.isActive && (nextRole !== "ADMIN" || nextActive === false); if (removingAdmin) { const admins = await countActiveAdmins(); if (admins <= 1) { return res.status(409).json({ error: "Conflict", message: "There must be at least one active admin", }); } } const data: Record = {}; if (typeof parsed.data.username !== "undefined") data.username = parsed.data.username; if (typeof parsed.data.name !== "undefined") data.name = sanitizeText(parsed.data.name, 100); if (typeof parsed.data.role !== "undefined") data.role = parsed.data.role; if (typeof parsed.data.mustResetPassword !== "undefined") data.mustResetPassword = parsed.data.mustResetPassword; if (typeof parsed.data.isActive !== "undefined") data.isActive = parsed.data.isActive; const updated = await prisma.user.update({ where: { id: userId }, data, select: { id: true, username: true, email: true, name: true, role: true, mustResetPassword: true, isActive: true, createdAt: true, updatedAt: true, }, }); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "admin_user_updated", resource: `user:${updated.id}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { updatedUserId: updated.id, fields: Object.keys(data) }, }); } res.json({ user: updated }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { return res.status(409).json({ error: "Conflict", message: "User with this username already exists", }); } console.error("Update user error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to update user", }); } }); router.post("/users/:id/reset-password", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; if (req.user.impersonatorId) { return res.status(403).json({ error: "Forbidden", message: "Password resets are not allowed while impersonating", }); } const userId = String(req.params.id || "").trim(); if (!userId) { return res.status(400).json({ error: "Bad request", message: "Invalid user id" }); } if (userId === req.user.id) { return res.status(409).json({ error: "Conflict", message: "Use Profile -> Change Password for your own account", }); } const target = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, username: true, role: true, isActive: true, }, }); if (!target) { return res.status(404).json({ error: "Not found", message: "User not found" }); } const tempPassword = generateTempPassword(); const saltRounds = 10; const passwordHash = await bcrypt.hash(tempPassword, saltRounds); await prisma.user.update({ where: { id: target.id }, data: { passwordHash, mustResetPassword: true, isActive: true, }, }); try { await prisma.refreshToken.updateMany({ where: { userId: target.id, revoked: false }, data: { revoked: true }, }); } catch { if (process.env.NODE_ENV === "development") { console.debug("Refresh token revocation skipped (feature disabled or table missing)"); } } await resetLoginAttemptKey(target.email.toLowerCase()); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "admin_password_reset_generated", resource: `user:${target.id}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { targetUserId: target.id, targetEmail: target.email }, }); } res.json({ user: { id: target.id, email: target.email, username: target.username, role: target.role }, tempPassword, }); } catch (error) { console.error("Reset password error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to reset password", }); } }); router.post("/impersonate", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => { try { if (!(await ensureAuthEnabled(res))) return; if (!requireAdmin(req, res)) return; const parsed = impersonateSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "Bad request", message: "Invalid impersonation payload" }); } const target = parsed.data.userId ? await prisma.user.findUnique({ where: { id: parsed.data.userId } }) : await findUserByIdentifier(parsed.data.identifier || ""); if (!target) { return res.status(404).json({ error: "Not found", message: "User not found" }); } if (!target.isActive) { return res.status(403).json({ error: "Forbidden", message: "Target user is inactive" }); } const { accessToken, refreshToken } = generateTokens(target.id, target.email, { impersonatorId: req.user.id, }); if (config.enableRefreshTokenRotation) { const expiresAt = getRefreshTokenExpiresAt(); try { await prisma.refreshToken.create({ data: { userId: target.id, token: refreshToken, expiresAt }, }); } catch { if (process.env.NODE_ENV === "development") { console.debug("Refresh token storage skipped (feature disabled or table missing)"); } } } if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "impersonation_started", resource: `user:${target.id}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { targetUserId: target.id }, }); } res.json({ user: { id: target.id, username: target.username ?? null, email: target.email, name: target.name, role: target.role, mustResetPassword: target.mustResetPassword, }, accessToken, refreshToken, }); } catch (error) { console.error("Impersonation error:", error); res.status(500).json({ error: "Internal server error", message: "Failed to impersonate user", }); } }); };