"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 [savingRole, setSavingRole] = useState(null); 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 saveRole = async (role: string) => { setSavingRole(role); setError(""); setNotice(""); try { const keys = permissions.filter((p) => matrix[role]?.[p.key]).map((p) => p.key); await api.updateRolePermissions(role, keys); setOriginal((prev) => ({ ...prev, [role]: { ...matrix[role] } })); setNotice(`${ROLE_LABELS[role] ?? role} permissions saved.`); } catch (err: any) { setError(err.message); } finally { setSavingRole(null); } }; 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}`} />
{roles.map((role) => { const dirty = dirtyRoles.includes(role); return ( ); })}
); }