"use client"; import { Fragment, useEffect, useMemo, useState } from "react"; import { api } from "@/lib/api"; import { useAuth } from "@/hooks/useAuth"; import { Check, Lock, Save } from "lucide-react"; interface PermissionDef { key: string; label: string; group: string; } type Matrix = Record>; const ROLE_LABELS: Record = { admin: "Admin", moderator: "Moderator", writer: "Writer", }; function buildMatrix( roles: string[], perms: PermissionDef[], granted: Record ): Matrix { const matrix: Matrix = {}; for (const role of roles) { matrix[role] = {}; const set = new Set(granted[role] ?? []); for (const perm of perms) { matrix[role][perm.key] = set.has(perm.key); } } return matrix; } export default function RolesPage() { const { can } = useAuth(); const allowed = can("roles.edit_permissions"); const [permissions, setPermissions] = useState([]); const [roles, setRoles] = useState([]); const [matrix, setMatrix] = useState({}); const [original, setOriginal] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [saving, setSaving] = useState(false); const [notice, setNotice] = useState(""); const load = async () => { setLoading(true); try { const [registry, current] = await Promise.all([ api.getPermissionRegistry(), api.getRolePermissions(), ]); const granted: Record = {}; for (const r of current.roles) granted[r.role] = r.permissions; const built = buildMatrix(registry.roles, registry.permissions, granted); setPermissions(registry.permissions); setRoles(registry.roles); setMatrix(built); setOriginal(built); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { if (allowed) load(); else setLoading(false); }, [allowed]); const groups = useMemo(() => { const order: string[] = []; const byGroup: Record = {}; for (const perm of permissions) { if (!byGroup[perm.group]) { byGroup[perm.group] = []; order.push(perm.group); } byGroup[perm.group].push(perm); } return order.map((group) => ({ group, perms: byGroup[group] })); }, [permissions]); const dirtyRoles = useMemo(() => { return roles.filter((role) => permissions.some((perm) => matrix[role]?.[perm.key] !== original[role]?.[perm.key]) ); }, [roles, permissions, matrix, original]); const toggle = (role: string, key: string) => { setNotice(""); setMatrix((prev) => ({ ...prev, [role]: { ...prev[role], [key]: !prev[role]?.[key] }, })); }; const saveAll = async () => { if (dirtyRoles.length === 0) return; setSaving(true); setError(""); setNotice(""); try { for (const role of dirtyRoles) { const keys = permissions.filter((p) => matrix[role]?.[p.key]).map((p) => p.key); await api.updateRolePermissions(role, keys); } setOriginal(() => { const next: Matrix = {}; for (const role of roles) next[role] = { ...matrix[role] }; return next; }); setNotice("Permissions saved."); } catch (err: any) { setError(err.message); } finally { setSaving(false); } }; if (!allowed) { return (
You do not have permission to manage roles.
); } if (loading) { return (
Loading roles...
); } return (

Roles and Permissions

Toggle what each role can do. SuperAdmin always has every permission and cannot be changed.

{error &&

{error}

} {notice &&

{notice}

}
{roles.map((role) => ( ))} {groups.map(({ group, perms }) => ( {perms.map((perm) => ( {roles.map((role) => ( ))} ))} ))}
Permission {ROLE_LABELS[role] ?? role} SuperAdmin
{group}
{perm.label}
{perm.key}
toggle(role, perm.key)} className="h-4 w-4 accent-primary cursor-pointer" aria-label={`${ROLE_LABELS[role] ?? role}: ${perm.label}`} />
{dirtyRoles.length > 0 && !saving && ( Unsaved changes to {dirtyRoles.map((r) => ROLE_LABELS[r] ?? r).join(", ")} )}
); }