85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
// 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<string, string | undefined>;
|
|
requireModule?: boolean;
|
|
}): Promise<IntegrationsRouteContext> {
|
|
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;
|
|
}
|