import { Router } from "express"; import bcrypt from "bcryptjs"; import { Role } from "@prisma/client"; import { z } from "zod"; import { authorize, requireAuth } from "../middleware/auth"; import { prisma } from "../lib/prisma"; import { HttpError } from "../lib/http-error"; import { generateSecurePassword } from "../lib/security"; import { logAudit } from "../services/audit.service"; import { toPrismaJsonValue } from "../lib/prisma-json"; const router = Router(); const createUserSchema = z.object({ email: z.string().email(), full_name: z.string().min(1).max(120), role: z.nativeEnum(Role), tenant_id: z.string().optional(), password: z.string().min(10).max(120).optional(), generate_password: z.boolean().default(true), is_active: z.boolean().default(true) }); const updateUserSchema = z.object({ full_name: z.string().min(1).max(120).optional(), role: z.nativeEnum(Role).optional(), tenant_id: z.string().nullable().optional(), is_active: z.boolean().optional() }); function rolesCatalog() { return [ { role: "SUPER_ADMIN", label: "Super Admin", scope: "Global", description: "Full platform control including billing, security, and system configuration." }, { role: "TENANT_ADMIN", label: "Tenant Admin", scope: "Tenant", description: "Owns a tenant environment, users, workloads, and tenant-level billing views." }, { role: "OPERATOR", label: "Operator", scope: "Ops", description: "Runs day-2 operations for compute, backup, and node workflows." }, { role: "VIEWER", label: "Viewer", scope: "Read-only", description: "Read-only access for auditors and stakeholders." } ]; } router.get("/roles", requireAuth, authorize("rbac:manage"), async (_req, res) => { res.json(rolesCatalog()); }); router.get("/users", requireAuth, authorize("user:read"), async (req, res, next) => { try { const tenantId = typeof req.query.tenant_id === "string" ? req.query.tenant_id : undefined; const role = typeof req.query.role === "string" ? req.query.role.toUpperCase() : undefined; const where: Record = {}; if (tenantId) where.tenant_id = tenantId; if (role && Object.values(Role).includes(role as Role)) where.role = role; const users = await prisma.user.findMany({ where, orderBy: { created_at: "desc" }, select: { id: true, email: true, full_name: true, role: true, tenant_id: true, is_active: true, mfa_enabled: true, must_change_password: true, created_at: true, updated_at: true, last_login_at: true, tenant: { select: { id: true, name: true, slug: true } } } }); res.json(users); } catch (error) { next(error); } }); router.post("/users", requireAuth, authorize("user:manage"), async (req, res, next) => { try { const payload = createUserSchema.parse(req.body ?? {}); if (req.user?.role !== "SUPER_ADMIN") { throw new HttpError(403, "Only SUPER_ADMIN can create administrative users", "FORBIDDEN"); } if (payload.role === "TENANT_ADMIN" && !payload.tenant_id) { throw new HttpError(400, "tenant_id is required for TENANT_ADMIN users", "VALIDATION_ERROR"); } const existing = await prisma.user.findUnique({ where: { email: payload.email.toLowerCase().trim() } }); if (existing) { throw new HttpError(409, "A user with this email already exists", "USER_EXISTS"); } const tempPassword = payload.generate_password || !payload.password ? generateSecurePassword(20) : payload.password; const passwordHash = await bcrypt.hash(tempPassword, 12); const user = await prisma.user.create({ data: { email: payload.email.toLowerCase().trim(), full_name: payload.full_name, role: payload.role, tenant_id: payload.tenant_id ?? null, is_active: payload.is_active, password_hash: passwordHash, must_change_password: true, password_changed_at: new Date() }, select: { id: true, email: true, full_name: true, role: true, tenant_id: true, is_active: true, must_change_password: true, created_at: true } }); await logAudit({ action: "rbac.user.create", resource_type: "USER", resource_id: user.id, resource_name: user.email, actor_email: req.user!.email, actor_role: req.user!.role, details: toPrismaJsonValue({ role: user.role, tenant_id: user.tenant_id }), ip_address: req.ip }); res.status(201).json({ user, temporary_password: tempPassword }); } catch (error) { next(error); } }); router.patch("/users/:id", requireAuth, authorize("user:manage"), async (req, res, next) => { try { const payload = updateUserSchema.parse(req.body ?? {}); const existing = await prisma.user.findUnique({ where: { id: req.params.id } }); if (!existing) { throw new HttpError(404, "User not found", "USER_NOT_FOUND"); } if (req.user?.role !== "SUPER_ADMIN" && existing.role === "SUPER_ADMIN") { throw new HttpError(403, "Only SUPER_ADMIN can modify this user", "FORBIDDEN"); } if (payload.role === "SUPER_ADMIN" && req.user?.role !== "SUPER_ADMIN") { throw new HttpError(403, "Only SUPER_ADMIN can assign SUPER_ADMIN role", "FORBIDDEN"); } const updated = await prisma.user.update({ where: { id: req.params.id }, data: payload, select: { id: true, email: true, full_name: true, role: true, tenant_id: true, is_active: true, must_change_password: true, created_at: true, updated_at: true, last_login_at: true } }); await logAudit({ action: "rbac.user.update", resource_type: "USER", resource_id: updated.id, resource_name: updated.email, actor_email: req.user!.email, actor_role: req.user!.role, details: toPrismaJsonValue({ changes: payload }), ip_address: req.ip }); res.json(updated); } catch (error) { next(error); } }); router.post("/users/:id/reset-password", requireAuth, authorize("user:manage"), async (req, res, next) => { try { const existing = await prisma.user.findUnique({ where: { id: req.params.id } }); if (!existing) { throw new HttpError(404, "User not found", "USER_NOT_FOUND"); } if (existing.role === "SUPER_ADMIN" && req.user?.role !== "SUPER_ADMIN") { throw new HttpError(403, "Only SUPER_ADMIN can reset this account", "FORBIDDEN"); } const tempPassword = generateSecurePassword(20); const passwordHash = await bcrypt.hash(tempPassword, 12); await prisma.user.update({ where: { id: existing.id }, data: { password_hash: passwordHash, must_change_password: true, password_changed_at: new Date(), mfa_enabled: false, mfa_secret: null, mfa_recovery_codes: [] } }); await prisma.authSession.updateMany({ where: { user_id: existing.id, revoked_at: null }, data: { revoked_at: new Date() } }); await logAudit({ action: "rbac.user.reset_password", resource_type: "USER", resource_id: existing.id, resource_name: existing.email, actor_email: req.user!.email, actor_role: req.user!.role, ip_address: req.ip }); res.json({ success: true, temporary_password: tempPassword }); } catch (error) { next(error); } }); export default router;