Extracted domain-specific sub-routers from monolithic `server/routes/admin.ts` and `server/routes/auth.ts` into isolated modules within `server/routes/admin/` and `server/routes/auth/` respectively. The original entry routers were updated to import and assemble these sub-routers without breaking their current HTTP interface or rate limiting/authorization middleware. Testing and linting were run ensuring perfect functionality and 100% test passing score. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
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 });
|
|
});
|