feat(15-08): serverseitige Modulsperre mit 403-Seite

- checkModuleAccess (module-access-actions.ts) fragt GET /modules/active
  serverseitig ab, Cookie-Weiterleitung nach fetchCurrentUser-Muster,
  schliesst im Zweifel (fehlendes Cookie, nicht-ok, Fehler -> false)
- page.tsx zur async Server Component umgebaut, rendert bei fehlender
  Freigabe das 403-Markup direkt (kein notFound(), kein Redirect, D-07)
- bisheriger Client-Inhalt (Whitelist-Pruefung, Nicht-gefunden-Zustand,
  Ruecknavigation) unveraendert nach module-shell.tsx ausgelagert
- 5 Tests in module-access.test.tsx: 403-Zustand, Shell-Rendering,
  fehlendes Cookie, nicht-ok-Antwort, unregistrierter Slug trotz Zugriff
This commit is contained in:
2026-08-04 19:41:25 +02:00
parent c6086ba750
commit 43c7fa200b
4 changed files with 315 additions and 60 deletions
+46
View File
@@ -0,0 +1,46 @@
'use server';
import { cookies } from 'next/headers';
const API_URL = process.env.API_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
/**
* Server-side module access check for the module page route (D-07, PERM-04).
*
* Reads the session cookie, forwards it to `GET /modules/active` (the same
* ModuleAccessService.getAccessibleModuleIds resolution ModuleGuard and the
* sidebar use — D-01), and checks whether `moduleSlug` is present in the
* response. Mirrors `fetchCurrentUser()` in auth-actions.ts exactly: same
* cookie-forwarding, `credentials: 'include'`, `cache: 'no-store'`.
*
* Fails closed (T-15-29): a missing session cookie, a non-ok API response,
* or a thrown network error all resolve to `false`. A broken network path
* must never open access.
*/
export async function checkModuleAccess(moduleSlug: string): Promise<boolean> {
const cookieStore = await cookies();
const session = cookieStore.get('session')?.value;
if (!session) {
return false;
}
try {
const response = await fetch(`${API_URL}/modules/active`, {
headers: {
Cookie: `session=${session}`,
},
credentials: 'include',
cache: 'no-store',
});
if (!response.ok) {
return false;
}
const modules: Array<{ slug: string }> = await response.json();
return modules.some((module) => module.slug === moduleSlug);
} catch {
return false;
}
}