Merge lane/einsatz in feature/craftvia-mvp
Konflikte gelöst: processors/index.ts (report-pdf + image-derivatives), (app)/layout.tsx (Logo, Glocke, AccountInactiveNotice). L5-Mobilseiten report/sign nach src/app/(field)/m/(core)/orders/[id]/ verschoben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { auth, signOut } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { isTokenStillValid } from "@/server/sessions";
|
||||
import { resolveMfaRequired } from "@/lib/mfa-policy";
|
||||
import { signOut } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { requireAppAccess } from "@/server/app-access";
|
||||
import { NAV_ITEMS, visibleNavItems } from "@/lib/nav";
|
||||
import { resolveTenantBranding } from "@/lib/brand";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -14,60 +12,16 @@ import { TenantSwitcher } from "@/components/tenant-switcher";
|
||||
import { UiLocaleSwitcher } from "@/components/ui-locale-switcher";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { NotificationBell } from "@/components/notifications/bell";
|
||||
import { AccountInactiveNotice } from "@/components/account-inactive-notice";
|
||||
|
||||
export default async function AppLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
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 (
|
||||
<main className="flex min-h-screen flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8 text-center">
|
||||
<CraftviaLogo variant="horizontal" height={34} className="mx-auto" />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Konto deaktiviert</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Ihr Zugang wurde deaktiviert. Bitte wenden Sie sich an Ihre Administration.</p>
|
||||
<form action={async () => { "use server"; await signOut({ redirectTo: "/login" }); }} className="mt-5">
|
||||
<Button type="submit" variant="outline" className="w-full">Abmelden</Button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
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 dieses Layouts).
|
||||
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");
|
||||
}
|
||||
}
|
||||
// Session-, Konto-, Passwort- und MFA-Prüfungen: gemeinsam mit der Mobile-Shell
|
||||
// (src/server/app-access.ts).
|
||||
const access = await requireAppAccess();
|
||||
if (access.kind === "inactive") return <AccountInactiveNotice />;
|
||||
const { session, identity } = access;
|
||||
|
||||
const t = await getTranslations("nav");
|
||||
const tc = await getTranslations("common");
|
||||
|
||||
Reference in New Issue
Block a user