feat(PROJ-13): add SYSTEM_ADMIN platform-wide role

users.isSystemAdmin (boolean, default false), DB-only to grant (no UI
in MVP scope, per spec). requesterIsSystemAdmin()/requireSystemAdmin()
in authz.ts — checked fresh from the DB on every request, not cached
in the JWT, so revoking takes effect immediately. GET /api/auth/me
returns the flag; DashboardPage shows a "System-Admin" nav button when
set (convenience only, not the access control — PROJ-14/15/16/18's
own routes enforce it).

/system-admin route doesn't exist yet (comes with PROJ-14) — the nav
button currently bounces back to / via the catch-all route, no stub
page added.

Verified end-to-end on talkhub: /api/auth/me flips true immediately
after a direct DB update, no re-login required.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-12 20:59:04 +02:00
1 parent c1c1ae9c88
commit 0f15432045
10 files changed
+1599 -13

No files matched your search

+6 -2
View File
@@ -4,6 +4,9 @@ import { clearCachedChatCredentials } from "@/lib/matrix";
interface AuthUser {
id: string;
email: string;
/** PROJ-13: platform-wide admin. Only drives nav visibility — every
* system-admin API route re-checks this server-side. */
isSystemAdmin: boolean;
}
interface AuthContextValue {
@@ -46,11 +49,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
if (!meRes.ok) {
throw new Error(await parseError(meRes));
}
const me = (await meRes.json()) as AuthUser;
const me = (await meRes.json()) as Partial<AuthUser> & { id: string; email: string };
localStorage.setItem(STORAGE_KEY, newToken);
setTokenState(newToken);
setUser(me);
// Default to false — never let a missing/unexpected field grant admin nav.
setUser({ id: me.id, email: me.email, isSystemAdmin: me.isSystemAdmin === true });
}, []);
useEffect(() => {
+7
View File
@@ -29,6 +29,13 @@ export function DashboardPage() {
<Button variant="outline" asChild>
<Link to="/audit-log">Aktivitäten</Link>
</Button>
{/* PROJ-13: only a convenience — the panel's APIs enforce SYSTEM_ADMIN
themselves, hiding the link is not the access control. */}
{user?.isSystemAdmin && (
<Button variant="outline" asChild>
<Link to="/system-admin">System-Admin</Link>
</Button>
)}
<Button variant="outline" onClick={logout}>
Abmelden
</Button>