import type { Request, Response } from "express"; import { withRlsTx } from "@/lib/postgres.js"; import { z } from "zod"; const ActionEnum = z.enum(["read", "create", "update", "delete", "manage"]); type Action = z.infer; type Effect = "allow" | "deny"; function rlsCtx(req: Request) { return { userId: req.user!.id, hasSensitiveAccess: req.user?.accessLevel?.name === "super-admin", }; } /** GET /api/admin/acl/permissions/role/:roleId * Returns: [{ resourceId, resourceKey, resourcePattern, actions: { read, create, update, delete, manage } }] * action value is "allow" | "deny" | null (no row) */ export async function listRolePermissions(req: Request, res: Response) { try { const roleId = z.string().uuid().parse(req.params.roleId); const rows = await withRlsTx(rlsCtx(req), async (db) => { const { rows } = await db.query<{ resource_id: string; resource_key: string; resource_pattern: string; read: Effect | null; create: Effect | null; update: Effect | null; delete: Effect | null; manage: Effect | null; }>( ` SELECT res.id AS resource_id, res.key AS resource_key, res.pattern AS resource_pattern, MAX(CASE WHEN rp.action = 'read' THEN rp.effect::text END) AS read, MAX(CASE WHEN rp.action = 'create' THEN rp.effect::text END) AS create, MAX(CASE WHEN rp.action = 'update' THEN rp.effect::text END) AS update, MAX(CASE WHEN rp.action = 'delete' THEN rp.effect::text END) AS delete, MAX(CASE WHEN rp.action = 'manage' THEN rp.effect::text END) AS manage FROM public.resources res LEFT JOIN public.role_permissions rp ON rp.resource_id = res.id AND rp.role_id = $1::uuid GROUP BY res.id, res.key, res.pattern ORDER BY res.key `, [roleId] ); return rows; }); res.json({ roleId, grants: rows }); } catch (e: any) { res.status(400).json({ error: "listRolePermissions", message: e.message }); } } /** GET /api/admin/acl/permissions/user/:userId * Same shape, but for direct user overrides */ export async function listUserPermissions(req: Request, res: Response) { try { const userId = z.string().uuid().parse(req.params.userId); const rows = await withRlsTx(rlsCtx(req), async (db) => { const { rows } = await db.query<{ resource_id: string; resource_key: string; resource_pattern: string; read: Effect | null; create: Effect | null; update: Effect | null; delete: Effect | null; manage: Effect | null; }>( ` SELECT res.id AS resource_id, res.key AS resource_key, res.pattern AS resource_pattern, MAX(CASE WHEN up.action = 'read' THEN up.effect::text END) AS read, MAX(CASE WHEN up.action = 'create' THEN up.effect::text END) AS create, MAX(CASE WHEN up.action = 'update' THEN up.effect::text END) AS update, MAX(CASE WHEN up.action = 'delete' THEN up.effect::text END) AS delete, MAX(CASE WHEN up.action = 'manage' THEN up.effect::text END) AS manage FROM public.resources res LEFT JOIN public.user_permissions up ON up.resource_id = res.id AND up.user_id = $1::uuid GROUP BY res.id, res.key, res.pattern ORDER BY res.key `, [userId] ); return rows; }); res.json({ userId, grants: rows }); } catch (e: any) { res.status(400).json({ error: "listUserPermissions", message: e.message }); } }