feat(module-grants): Freigabestufe Verwalten – Datenbank, Zugriffsprüfung und Kantinen-Einstellungen

- Migration: ModuleGrant.level (USE/MANAGE), Bestand bleibt USE
- ModuleAccessService.getModuleAccessLevels als einzige Auflösung, MANAGE gewinnt
- @ModuleManage(slug) am ModuleGuard, GET /modules/active liefert canManage
- Kantinenabrechnung: Einstellungen für Benutzer mit Verwalten, Web-Hook useCanManageModule

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 13:28:24 +02:00
parent b94d267584
commit a222711ad9
17 changed files with 664 additions and 103 deletions
+2
View File
@@ -19,6 +19,8 @@ export interface ApiModule {
icon?: string;
version: string;
isSystem: boolean;
/** Freigabestufe Verwalten (261002-icv) — nur Anzeige, bindend bleibt die API. */
canManage?: boolean;
}
/**
+56
View File
@@ -0,0 +1,56 @@
'use client';
import { useEffect, useState } from 'react';
import { useAuthStore } from '@/lib/stores/auth-store';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
/**
* Darf der angemeldete Benutzer die Einstellungen dieses Moduls ändern
* (Freigabestufe Verwalten, 261002-icv)?
*
* Rückgabe: `null` solange noch unklar (kein Benutzer geladen bzw. Abfrage
* läuft), sonst `true`/`false`. Administratoren und Super-Administratoren
* sind sofort `true` — das spiegelt den Kurzschluss im Backend, es gibt
* dafür keine Abfrage. Alle anderen fragen einmal `GET /modules/active` ab
* und sind nur `true`, wenn der Eintrag dieses Moduls `canManage === true`
* trägt; ein Fehler zählt als `false`.
*
* Reine Anzeigehilfe: Welche Schaltflächen sichtbar sind, entscheidet nichts
* über die Berechtigung — bindend ist allein der ModuleGuard der API.
*/
export function useCanManageModule(moduleSlug: string): boolean | null {
const role = useAuthStore((s) => s.user?.role ?? null);
const hasUser = useAuthStore((s) => s.user !== null && s.user !== undefined);
const isAdmin = role === 'ADMIN' || role === 'SUPER_ADMIN';
const [fetched, setFetched] = useState<boolean | null>(null);
useEffect(() => {
if (!hasUser || isAdmin) return;
let cancelled = false;
(async () => {
try {
const response = await fetch(`${API_URL}/modules/active`, {
credentials: 'include',
cache: 'no-store',
});
if (!response.ok) {
if (!cancelled) setFetched(false);
return;
}
const modules = (await response.json()) as Array<{ slug: string; canManage?: boolean }>;
const entry = Array.isArray(modules) ? modules.find((m) => m.slug === moduleSlug) : null;
if (!cancelled) setFetched(entry?.canManage === true);
} catch {
if (!cancelled) setFetched(false);
}
})();
return () => {
cancelled = true;
};
}, [hasUser, isAdmin, moduleSlug]);
if (!hasUser) return null;
if (isAdmin) return true;
return fetched;
}