L15 Testphase & Onboarding: Lese-Modus am moduleGuard für Seitenkontexte, Sperre für Auftragsdokument-Upload

Der HTTP-Smoke zeigte 500 auf /m für abgelaufene Testmandanten: mobile Seitenkontexte (field,
emergency) und der Import-Datei-Download nutzen moduleGuard zum Lesen. moduleGuard(key, { read: true })
überspringt dort die Schreibsperre; der Guard-Check verbietet den Lese-Modus in Server-Actions.
POST /api/v1/work-orders/[id]/documents läuft nicht über withApi und prüft die Sperre jetzt explizit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 19:07:20 +02:00
co-authored by Claude Opus 5
parent d9290a187c
commit 273a4535e3
7 changed files with 22 additions and 7 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ const INLINE = new Set(["application/pdf", "image/jpeg", "image/png"]);
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
let file: { storageKey: string; mimeType: string; fileName: string };
try {
const ctx = ctxFromGuard(await moduleGuard("imports")("import:write"));
const ctx = ctxFromGuard(await moduleGuard("imports", { read: true })("import:write")); // L15: GET download — no trial write lock
const { id } = await params;
file = await getImportFile(ctx, id);
} catch {
+1 -1
View File
@@ -6,7 +6,7 @@ import { EmergencyWizard } from "@/components/emergency/emergency-wizard";
/** `/m/emergency` — Notdienst erfassen (spec §19.2, US-010): max. 3 steps, then straight into the call-out. */
export default async function EmergencyPage() {
const ctx = ctxFromGuard(await moduleGuard("emergency")());
const ctx = ctxFromGuard(await moduleGuard("emergency", { read: true })()); // L15: read path — no trial write lock
const t = await getTranslations("emergency.capture");
if (!can(ctx, "emergency:create") || !can(ctx, "field:execute")) {
return <p className="p-4 text-[15px]">{t("noAccess")}</p>;
+5 -2
View File
@@ -27,7 +27,10 @@ import { assertTenantWritable } from "@/server/services/trial/state";
* Der Vollständigkeitscheck (`scripts/check-module-guards.ts`) verlässt sich darauf,
* dass jede Action eines gegateten Moduls über einen so erzeugten `guard(...)` läuft.
*/
export function moduleGuard(moduleKey: string) {
export function moduleGuard(moduleKey: string, opts: { read?: boolean } = {}) {
// L15 Testphase: `{ read: true }` nur für Lesepfade (Seitenkontexte, GET-Downloads), die denselben
// DB-autoritativen Guard nutzen — dort greift die Schreibsperre abgelaufener Testmandanten nicht.
// scripts/check-module-guards.ts verbietet den Lese-Modus in Action-Dateien.
return async function guard(...permissions: Permission[]) {
const session = await requireSession();
const db = dbForTenant(session.user.tenantId);
@@ -86,7 +89,7 @@ export function moduleGuard(moduleKey: string) {
await assertModuleEnabled(session, moduleKey);
// L15 Testphase: abgelaufene Testmandanten sind nur lesbar — zentrale Schreibsperre
// (wirft ServiceError "blocked"/"trial_expired").
await assertTenantWritable(session.user.tenantId);
if (!opts.read) await assertTenantWritable(session.user.tenantId);
// `permissions` = DB-authoritative effective set; domain services derive their
// scope decisions from it (src/server/services/context.ts#ctxFromGuard).
return { session, db, permissions: effective as ReadonlySet<string> };
+1 -1
View File
@@ -7,7 +7,7 @@ import { can, ctxFromGuard, type ServiceCtx } from "@/server/services/context";
* the visibility scopes of the services.
*/
export async function fieldPageContext(): Promise<ServiceCtx> {
return ctxFromGuard(await moduleGuard("field")());
return ctxFromGuard(await moduleGuard("field", { read: true })()); // L15: read path — no trial write lock
}
export function canUseFieldApp(ctx: ServiceCtx): boolean {