import { CatalogError, foundationErrors, requirePermission } from "@hivegrid/foundation"; import { KwpProjectDomainError } from "@hivegrid/integrations/kwp-project-domain.server"; 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 { db } from "~/utils/db.server"; import { getPermissionsByUser } from "~/utils/db/permissions/userRoles.db.server"; import { PROJECTS_MODULE_KEY } from "./projectRoutePaths"; import { AuftragServiceError } from "./auftragService.server"; import { ProjectServiceError, type ProjectVisibilityContext } from "./projectService.server"; export type ProjectRouteContext = ProjectVisibilityContext & { tenantId: string; hasModule: boolean; }; export const PROJECT_READ_PERMISSIONS = [ "projects.read_all", "projects.read_own", "projects.read_own_pl", "projects.read_own_member", ] as const; export async function loadProjectContext({ 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, roleAssignments, groupMemberships] = await Promise.all([ tenantHasModule({ tenantId, userId: userInfo.userId, moduleKey: PROJECTS_MODULE_KEY }), getPermissionsByUser(userInfo.userId, tenantId), db.userRole.findMany({ where: { tenantId, userId: userInfo.userId }, select: { roleId: true } }), db.groupUser.findMany({ where: { userId: userInfo.userId, group: { tenantId } }, select: { groupId: true } }), ]); if (requireModule && !hasModule) { throw new CatalogError(foundationErrors.notFound, { module: PROJECTS_MODULE_KEY }); } return { tenantId, userId: userInfo.userId, permissions, roleIds: roleAssignments.map(({ roleId }) => roleId), groupIds: groupMemberships.map(({ groupId }) => groupId), hasModule, }; } export function requireProjectPermission(ctx: ProjectRouteContext, permission: string): ProjectRouteContext { requirePermission({ tenantId: ctx.tenantId, userId: ctx.userId, permissions: ctx.permissions }, permission); return ctx; } /** 404 for a route the caller may not see (Bauprinzip A.5.3: Permission-Denied * renders 404, not 403 — and never a 500). */ export function projectNotFoundResponse(): Response { return new Response("Not Found", { status: 404 }); } /** * Loader-side access guard: resolves the project context and renders a graceful * 404 when the projects module is off OR the viewer lacks the required * permission(s) — instead of letting the underlying CatalogError surface as a * 500. Auth redirects from loadProjectContext propagate untouched (module is not * required here, so requireAuth's redirect Response is not swallowed). Use in * LOADERS only; actions keep throwing so projectActionErrorResponse can map them. */ export async function requireProjectView( args: { request: Request; params: Record }, permission: string | readonly string[], ): Promise { const ctx = await loadProjectContext({ request: args.request, params: args.params, requireModule: false }); if (!ctx.hasModule) { throw projectNotFoundResponse(); } try { return Array.isArray(permission) ? requireAnyProjectPermission(ctx, permission) : requireProjectPermission(ctx, permission as string); } catch (error) { if (error instanceof Response) throw error; throw projectNotFoundResponse(); } } export function requireAnyProjectPermission(ctx: ProjectRouteContext, permissions: readonly string[]): ProjectRouteContext { if (permissions.some((permission) => ctx.permissions.includes(permission))) return ctx; requirePermission({ tenantId: ctx.tenantId, userId: ctx.userId, permissions: ctx.permissions }, permissions[0] ?? "projects.read_own"); return ctx; } export function projectActionErrorResponse(error: unknown): Response { if (error instanceof CatalogError) { return Response.json({ error: error.i18nKey, code: error.code }, { status: error.status }); } if (error instanceof ProjectServiceError) { return Response.json({ error: error.messageKey }, { status: 400 }); } if (error instanceof AuftragServiceError) { return Response.json({ error: error.messageKey }, { status: 400 }); } // A KWP-bound project refuses writes HiveGrid does not own. That is a normal // "no" for the actor, not a crash: rethrowing turned a drag onto another board // column into a full-page 500 instead of a message plus a card that snaps back. if (error instanceof KwpProjectDomainError) { return Response.json({ error: `projects.error.kwp.${error.code}` }, { status: 400 }); } throw error; }