Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/routeModules/auth/select-tenant.impl.ts
T

84 lines
3.3 KiB
TypeScript

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<SelectTenantData>;
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" }));
})!;