import { Hono } from "jsr:@hono/hono@4"; import { sqlWrapper } from "../../db.ts"; import { auditWrapper } from "../../audit.ts"; import { getAuthenticatedUser } from "../../auth-session.ts"; import { getClientIp } from "../../middleware.ts"; export const rolesAdminRoutes = new Hono(); rolesAdminRoutes.get("/", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const appId = c.req.query("appId"); let roles; if (appId) { roles = await sqlWrapper.sql` SELECT r.id, r.name, r.description, r.app_id, r.created_at, a.name AS app_name FROM roles r LEFT JOIN apps a ON r.app_id = a.id WHERE r.app_id IS NULL OR r.app_id = ${appId} ORDER BY r.app_id NULLS FIRST, r.name ASC `; } else { roles = await sqlWrapper.sql` SELECT r.id, r.name, r.description, r.app_id, r.created_at, a.name AS app_name FROM roles r LEFT JOIN apps a ON r.app_id = a.id ORDER BY r.app_id NULLS FIRST, r.name ASC `; } return c.json({ roles }); }); rolesAdminRoutes.post("/", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const { name, description, appId } = await c.req.json(); if ( !name || typeof name !== "string" || name.trim().length < 2 || name.trim().length > 32 ) { return c.json( { error: "Role name must be between 2 and 32 characters" }, 400, ); } const normalizedName = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "_"); let validatedAppId = null; if (appId && typeof appId === "string" && appId.trim()) { const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; if (!uuidRegex.test(appId)) { return c.json({ error: "Invalid App UUID" }, 400); } const appExists = await sqlWrapper .sql`SELECT id, name FROM apps WHERE id = ${appId}` .then((res: any) => res[0]); if (!appExists) { return c.json({ error: "Selected application does not exist" }, 404); } validatedAppId = appId; } try { const newRole = await sqlWrapper.sql` INSERT INTO roles (name, description, app_id) VALUES (${normalizedName}, ${ description?.trim() || null }, ${validatedAppId}) RETURNING id, name, description, app_id, created_at `.then((res: any) => res[0]); auditWrapper.auditLog(auth.userId, "role_created", validatedAppId, { role_name: newRole.name, scope: validatedAppId ? "app-specific" : "global", }, getClientIp(c)); return c.json({ success: true, role: newRole }); } catch (err: any) { if (err.code === "23505") { return c.json({ error: "This role already exists for the selected scope", }, 409); } return c.json({ error: "Failed to create role" }, 500); } }); rolesAdminRoutes.put("/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const roleId = c.req.param("id"); const { name, description } = await c.req.json(); if (!name || typeof name !== "string" || name.trim().length < 2) { return c.json({ error: "Role name must be at least 2 characters" }, 400); } const normalizedName = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "_"); try { const updatedRole = await sqlWrapper.sql` UPDATE roles SET name = ${normalizedName}, description = ${description?.trim() || null} WHERE id = ${roleId} RETURNING id, name, description, app_id, created_at `.then((res: any) => res[0]); if (!updatedRole) return c.json({ error: "Role not found" }, 404); auditWrapper.auditLog(auth.userId, "role_updated", updatedRole.app_id, { role_name: updatedRole.name, }, getClientIp(c)); return c.json({ success: true, role: updatedRole }); } catch (_err: any) { return c.json({ error: "Failed to update role" }, 500); } }); rolesAdminRoutes.delete("/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const roleId = c.req.param("id"); const role = await sqlWrapper .sql`SELECT id, name, app_id FROM roles WHERE id = ${roleId}`.then( (res: any) => res[0], ); if (!role) { return c.json({ error: "Role not found" }, 404); } if (role.name === "admin" && role.app_id === null) { return c.json({ error: "The global 'admin' role cannot be deleted" }, 400); } await sqlWrapper.sql`DELETE FROM roles WHERE id = ${roleId}`; auditWrapper.auditLog( auth.userId, "role_deleted", role.app_id, { role_name: role.name }, getClientIp(c), ); return c.json({ success: true }); });