import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs } from "react-router"; import { serverOnly$ } from "vite-env-only/macros"; import { buildSelectTenantVM, type SelectTenantData, type TenantRole } from "@toolctrl/view-models"; import type { VMState } from "@toolctrl/view-models"; import { TenantUserType } from "~/application/enums/tenants/TenantUserType"; import { appPath } from "~/utils/app/CanonicalUrlUtils"; import { getMyTenants, getTenantUserType } from "~/utils/db/tenants.db.server"; import { getUser } from "~/utils/db/users.db.server"; import { getUserInfo, validateCSRFToken } from "~/utils/session.server"; // D-D0a — Flow "Login Tenantauswahl (Admin)". Tenant-/Profil-Auswahl nach dem Login. // Der Loader baut serverseitig das VM (M013) aus getMyTenants + der Rolle des Users je Tenant; die // Subframe-Karten-UI (select-tenant.component) rendert nur. Klick auf eine Karte postet hierher, // die Action verifiziert die Mitgliedschaft serverseitig und leitet in den Tenant-Space. // Route wird per Ordner-Konvention autoregistriert (select-tenant.tsx). export type SelectTenantVM = VMState; export type LoaderData = { vm: SelectTenantVM; }; export type ActionData = { error?: string; }; function roleFromType(type: TenantUserType | null | undefined): TenantRole | null { switch (type) { case TenantUserType.OWNER: return "owner"; case TenantUserType.ADMIN: return "admin"; case TenantUserType.MEMBER: return "member"; default: return null; } } export const loader = serverOnly$(async ({ request }: LoaderFunctionArgs) => { const userInfo = await getUserInfo(request); if (!userInfo.userId) { throw redirect("/login"); } const user = await getUser(userInfo.userId); if (!user) { throw redirect("/login"); } const tenants = await getMyTenants(userInfo.userId); const memberships = await Promise.all(tenants.map((tenant) => getTenantUserType(userInfo.userId, tenant.id))); const vm = buildSelectTenantVM({ tenants: tenants.map((tenant, index) => ({ id: tenant.id, name: tenant.name, slug: tenant.slug, image: tenant.icon ?? tenant.logo ?? null, role: roleFromType(memberships[index]?.type), subtitle: user.email, })), currentTenantId: user.defaultTenantId, }); return Response.json({ vm } satisfies LoaderData); })!; export const action = serverOnly$(async ({ request }: ActionFunctionArgs) => { const form = await request.formData(); await validateCSRFToken(request, { formData: form }); const userInfo = await getUserInfo(request); if (!userInfo.userId) { throw redirect("/login"); } const tenantId = String(form.get("tenantId") ?? ""); const slug = String(form.get("slug") ?? ""); if (!tenantId || !slug) { return Response.json({ error: "account.selectTenant.errors.invalidSelection" } satisfies ActionData, { status: 400 }); } // Mitgliedschaft serverseitig verifizieren — der Client-Form nicht vertrauen. const membership = await getTenantUserType(userInfo.userId, tenantId); if (!membership) { return Response.json({ error: "account.selectTenant.errors.notMember" } satisfies ActionData, { status: 403 }); } throw redirect(appPath({ tenantSlug: slug, path: "dashboard", mode: "host-aware" })); })!;