L4 Einsatz mobil: Mobile Shell und Einsatz-Oberfläche
- /m in eigene Route-Group (field)/m verschoben (emergency-Platzhalter mit),
Zugriffsprüfungen aus (app)/layout.tsx nach server/app-access.ts extrahiert
und von Backoffice- und Mobile-Shell gemeinsam genutzt
- Mobile Shell mit Bottom-Nav (Heute · Aufträge · Notdienst · Sync · Profil)
und Online/Offline-Badge; Startseite rollenabhängig (Feldrollen → /m),
Login-Default-Redirect auf /
- Heute, Auftragsliste mit Tabs, Auftragsdetail mit einer Primäraktion je
Zustand, Unterseiten Fotos (Kamera, Kompression, Upload-Fortschritt),
Notizen + Sprachaufnahme, Material mit Stepper, Checkliste, Zeiten mit
Korrektur, Profil; Sync-Platzhalter für L7
- Client-Wrapper submitOp (lib/field/client-ops.ts), Upload mit Fortschritt,
Bildkompression, Formatierung; Texte in messages/{de,en}/field.json
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Session } from "next-auth";
|
||||
import { auth, signOut } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { isTokenStillValid } from "@/server/sessions";
|
||||
import { resolveMfaRequired } from "@/lib/mfa-policy";
|
||||
|
||||
/**
|
||||
* Gemeinsame Zugriffsprüfung der angemeldeten Mandanten-Shells (Backoffice `(app)/layout.tsx`
|
||||
* und Mobile `(field)/m/layout.tsx`). Aus dem Backoffice-Layout extrahiert (ARCHITEKTUR §5),
|
||||
* damit beide Shells identisch prüfen:
|
||||
* Session → aktiver Mandant → Kill-Switch → Konto-/Identity-Status → Passwortzwang → MFA-Gate.
|
||||
* Redirects werden hier ausgelöst; ein deaktiviertes Konto wird als `inactive` gemeldet,
|
||||
* die aufrufende Shell rendert dafür `AccountInactiveNotice`.
|
||||
*/
|
||||
export type AppAccess =
|
||||
| { kind: "inactive" }
|
||||
| { kind: "ok"; session: Session; identity: { uiLocale: string } };
|
||||
|
||||
export async function requireAppAccess(): Promise<AppAccess> {
|
||||
const session = await auth();
|
||||
if (!session?.user) redirect("/login");
|
||||
// Angemeldet, aber (noch) kein aktiver Mandant (mehrere Mitgliedschaften ohne Slug) →
|
||||
// Auswahlseite. MUSS vor jeder dbForTenant-Nutzung stehen (leerer tenantId).
|
||||
if (!session.user.tenantId) redirect("/select-tenant");
|
||||
|
||||
// Kontostatus (autoritativ aus DB, nicht aus dem JWT): deaktivierte Nutzer werden
|
||||
// abgemeldet, ein offener Passwortwechsel wird erzwungen. Membership-Status kommt vom
|
||||
// User, die globalen Auth-Zustände (Passwortzwang, Kill-Switch, MFA) von der Identity.
|
||||
const account = await dbForTenant(session.user.tenantId).user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { status: true },
|
||||
});
|
||||
const identity = session.user.identityId
|
||||
? await prisma.identity.findUnique({
|
||||
where: { id: session.user.identityId },
|
||||
select: { status: true, mustChangePassword: true, sessionsValidAfter: true, mfaEnrolledAt: true, uiLocale: true },
|
||||
})
|
||||
: null;
|
||||
// SEC2: serverseitig entwertete Sessions (Passwort-Reset/-Wechsel) sofort abmelden.
|
||||
if (identity && !isTokenStillValid(session.user.tokenIssuedAt, identity.sessionsValidAfter)) {
|
||||
await signOut({ redirectTo: "/login" });
|
||||
}
|
||||
if (!account || account.status !== "ACTIVE" || !identity || identity.status !== "ACTIVE") {
|
||||
return { kind: "inactive" };
|
||||
}
|
||||
if (identity.mustChangePassword) redirect("/change-password");
|
||||
|
||||
// SEC3-a/-b: MFA-Enrollment-Gate. Verlangt der Mandant MFA und hat die Identity weder TOTP
|
||||
// noch einen Passkey, wird die Einrichtung erzwungen (Pflichtseite außerhalb der Shells).
|
||||
if (!identity.mfaEnrolledAt) {
|
||||
const settings = await dbForTenant(session.user.tenantId).tenantSettings.findFirst({ select: { securityPolicy: true } });
|
||||
if (resolveMfaRequired(settings?.securityPolicy)) {
|
||||
const passkeys = session.user.identityId
|
||||
? await prisma.webAuthnCredential.count({ where: { identityId: session.user.identityId } })
|
||||
: 0;
|
||||
if (passkeys === 0) redirect("/enroll-mfa");
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "ok", session, identity: { uiLocale: identity.uiLocale } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Startseite nach dem Login (ARCHITEKTUR §5): Feldrollen (Einsatz ausführen, keine
|
||||
* Gesamtsicht auf Aufträge) landen in der Mobile-App `/m`, alle anderen im Backoffice.
|
||||
* Reiner UX-Komfort — die Zielseiten prüfen Rechte selbst.
|
||||
*/
|
||||
export function landingPath(session: Session | null): string {
|
||||
const perms = session?.user?.permissions ?? [];
|
||||
return perms.includes("field:execute") && !perms.includes("work_order:read_all") ? "/m" : "/dashboard";
|
||||
}
|
||||
Reference in New Issue
Block a user