// R-P5 (P5) §P3 — shared loader/action guard for Apps-Engine routes (mirrors // taskAccess/hrAccess). Resolves tenant context + permissions once; module-off // renders a locked empty VM on GET, 404s on write (Bauprinzip A.5.3). import { CatalogError, foundationErrors, requirePermission } from "@hivegrid/foundation"; import { requireAuth } from "~/utils/loaders.middleware"; import { getTenantIdFromUrl } from "~/utils/services/.server/urlService"; import { getUserInfo } from "~/utils/session.server"; import { tenantHasModule } from "~/utils/connector/tenantConnector.server"; import { getPermissionsByUser } from "~/utils/db/permissions/userRoles.db.server"; import { AppsEngineServiceError } from "./appsEngineService.server"; import { INTEGRATIONS_MODULE_KEY } from "./integrationsRoutePaths"; // Re-exported for back-compat; the canonical definition lives in the client-safe // integrationsRoutePaths module so route `handle` exports can reference it without // pulling this server-only guard into the client bundle. export { INTEGRATIONS_MODULE_KEY }; export type IntegrationsRouteContext = { tenantId: string; userId: string; permissions: readonly string[]; hasModule: boolean; }; export async function loadIntegrationsContext({ request, params, requireModule = false, }: { request: Request; params: Record; requireModule?: boolean; }): Promise { await requireAuth({ request, params }); const [tenantId, userInfo] = await Promise.all([getTenantIdFromUrl(params), getUserInfo(request)]); if (!tenantId || !userInfo.userId) { throw new CatalogError(foundationErrors.notFound); } const [hasModule, permissions] = await Promise.all([ tenantHasModule({ tenantId, userId: userInfo.userId, moduleKey: INTEGRATIONS_MODULE_KEY }), getPermissionsByUser(userInfo.userId, tenantId), ]); if (requireModule && !hasModule) { throw new CatalogError(foundationErrors.notFound, { module: INTEGRATIONS_MODULE_KEY }); } return { tenantId, userId: userInfo.userId, permissions, hasModule }; } /** True if the user holds `permission` or a broader bundle (`integrations.admin` * implies everything). */ export function integrationsHasPermission(ctx: IntegrationsRouteContext, permission: string): boolean { return ctx.permissions.includes(permission) || ctx.permissions.includes("integrations.admin"); } /** Require an integrations.* permission (admin-aware); 404 if missing (A.5.3). */ export function requireIntegrationsPermission(ctx: IntegrationsRouteContext, permission: string): IntegrationsRouteContext { if (!integrationsHasPermission(ctx, permission)) { throw new CatalogError(foundationErrors.notFound, { permission }); } if (ctx.permissions.includes(permission)) { requirePermission({ tenantId: ctx.tenantId, userId: ctx.userId, permissions: ctx.permissions }, permission); } return ctx; } export function notFoundResponse(): Response { return new Response("Not Found", { status: 404 }); } export function badRequestResponse(): Response { return new Response("Bad Request", { status: 400 }); } /** Map an Apps-Engine action error to a JSON Response. */ export function integrationsActionErrorResponse(error: unknown): Response { if (error instanceof CatalogError) { return Response.json({ error: error.i18nKey, code: error.code }, { status: error.status }); } if (error instanceof AppsEngineServiceError) { return Response.json({ error: error.messageKey }, { status: 400 }); } throw error; }