import { Params, redirect } from "react-router"; import { DefaultAdminRoles } from "~/application/dtos/shared/DefaultAdminRoles"; import { DefaultAppRoles } from "~/application/dtos/shared/DefaultAppRoles"; import { DefaultEntityTypes } from "~/application/dtos/shared/DefaultEntityTypes"; import { DefaultPermission } from "~/application/dtos/shared/DefaultPermissions"; import { TenantUserType } from "~/application/enums/tenants/TenantUserType"; import { timeFake, TimeFunction } from "~/modules/metrics/services/.server/MetricTracker"; import OnboardingService from "~/modules/onboarding/services/OnboardingService"; import { absoluteAppRootPath } from "~/utils/app/CrossHostUrls"; import { appPath } from "~/utils/app/CanonicalUrlUtils"; import UrlUtils from "~/utils/app/UrlUtils"; import { loadRequiredWebConfig } from "@toolctrl/config"; import { getAllEntities } from "~/utils/db/entities/entities.db.server"; import { getAllEntityGroups } from "~/utils/db/entities/entityGroups.db.server"; import { getMyGroups } from "~/utils/db/permissions/groups.db.server"; import { getAllRolesWithoutPermissions } from "~/utils/db/permissions/roles.db.server"; import { getPermissionsByUser, getUserRoleInTenant, getUserRoleInAdmin } from "~/utils/db/permissions/userRoles.db.server"; import { adminGetAllActiveTenantSimple, getTenantSimple, getTenantUserType } from "~/utils/db/tenants.db.server"; import { getAuthUserWithoutPasswordById as getUser, getTenantsForUser as getMyTenants } from "@toolctrl/db/auth-boundary.server"; import { getAllTenantTypes } from "~/utils/db/tenants/tenantTypes.db.server"; import { promiseHash } from "~/utils/promises/promiseHash"; import { getTenantIdFromUrl } from "~/utils/services/.server/urlService"; import { getActiveTenantSubscriptions, getPlanFeatureUsage } from "~/utils/services/.server/subscriptionService"; import { getUserInfo } from "~/utils/session.server"; import { AppLoaderData } from "../useAppData"; import { PlanFeatureUsageDto } from "~/application/dtos/subscriptions/PlanFeatureUsageDto"; import { CreditTypes } from "~/modules/usage/dtos/CreditType"; import { DefaultFeatures } from "~/application/dtos/shared/DefaultFeatures"; import { TFunction } from "i18next"; import { featureKeys, type FeatureKey } from "@toolctrl/entitlements"; import { resolveTenantProductAccess } from "~/utils/entitlements/.server/productAccess.server"; import { WALKING_SKELETON_MODULE_KEY } from "~/modules/walkingSkeleton/constants"; const tenantNavigationFeatureKeys = [ featureKeys.crmBasic, featureKeys.emailMarketing, featureKeys.api, featureKeys.workflowEngine, featureKeys.credits, ] as const satisfies readonly FeatureKey[]; // R-P1c: Konnektor-Demo-Modul (kein Entitlement-FeatureKey) — speist das Sidebar-Gate der // Walking-Skeleton-Route. Liefert für reale Tenants "blueprint-disabled" → Eintrag ausgeblendet. const tenantConnectorDemoModuleKeys: readonly string[] = [WALKING_SKELETON_MODULE_KEY]; export async function loadAppData({ request, params, t, time = timeFake, tenantId: preloadedTenantId, userInfo: preloadedUserInfo, user: preloadedUser, }: { request: Request; params: Params; t: TFunction; time?: TimeFunction; tenantId?: string; userInfo?: Awaited>; user?: Awaited>; }) { const { tenantId, userInfo } = await time( promiseHash({ tenantId: preloadedTenantId ? Promise.resolve(preloadedTenantId) : getTenantIdFromUrl(params), userInfo: preloadedUserInfo ? Promise.resolve(preloadedUserInfo) : getUserInfo(request), }), "loadAppData.session" ); const url = new URL(request.url); const { user, currentTenant } = await time( promiseHash({ user: preloadedUser ? Promise.resolve(preloadedUser) : getUser(userInfo?.userId), currentTenant: getTenantSimple(tenantId), }), "loadAppData.getUserAndTenant" ); const redirectTo = url.pathname + url.search; if (!userInfo || !user) { let searchParams = new URLSearchParams([["redirect", redirectTo]]); throw redirect(`/login?${searchParams}`); } if (!currentTenant) { throw redirect(absoluteAppRootPath(loadRequiredWebConfig().routing)); } const [, tenantPathSegment = "", ...childPathSegments] = url.pathname.split("/"); const childPath = childPathSegments.join("/"); if (params.tenant && params.tenant !== currentTenant.slug) { throw redirect(`${appPath({ tenantSlug: currentTenant.slug, path: childPath || "dashboard", mode: "host-aware" })}${url.search}`); } if (UrlUtils.stripTrailingSlash(url.pathname) === UrlUtils.stripTrailingSlash(appPath({ tenantSlug: tenantPathSegment, mode: "host-aware" }))) { throw redirect(`${appPath({ tenantSlug: currentTenant.slug, path: "dashboard", mode: "host-aware" })}${url.search}`); } let { myTenants, mySubscription, allPermissions, superUserRole, superAdminRole, entities, entityGroups, allRoles, myGroups, onboardingSession } = await time( promiseHash({ myTenants: time(user.admin ? adminGetAllActiveTenantSimple() : getMyTenants(user.id), "loadAppData.getDetails.getMyTenants"), mySubscription: time(getActiveTenantSubscriptions(tenantId), "loadAppData.getDetails.getActiveTenantSubscriptions"), allPermissions: time(getPermissionsByUser(userInfo.userId, tenantId), "loadAppData.getDetails.getPermissionsByUser"), superUserRole: time(getUserRoleInTenant(userInfo.userId, tenantId, DefaultAppRoles.SuperUser), "loadAppData.getDetails.getUserRoleInTenant"), superAdminRole: time(getUserRoleInAdmin(userInfo.userId, DefaultAdminRoles.SuperAdmin), "loadAppData.getDetails.getUserRoleInAdmin"), entities: time( getAllEntities({ tenantId, active: true, types: [DefaultEntityTypes.All, DefaultEntityTypes.AppOnly] }), "loadAppData.getDetails.getAllEntities" ), entityGroups: time(getAllEntityGroups(), "loadAppData.getDetails.getAllEntityGroups()"), allRoles: time(getAllRolesWithoutPermissions("app"), "loadAppData.getDetails.getAllRolesWithoutPermissions"), myGroups: time(getMyGroups(user.id, currentTenant.id), "loadAppData.getDetails.getMyGroups"), onboardingSession: time( OnboardingService.getUserActiveOnboarding({ userId: user.id, tenantId: currentTenant.id, request }), "loadAppData.getDetails.OnboardingService.getUserActiveOnboarding" ), }), "loadAppData.getDetails" ); const { tenantUser, tenantTypes, credits, productAccessEntries } = await time( promiseHash({ tenantUser: time(getTenantUserType(userInfo.userId, tenantId), "loadAppData.postDetails.getTenantUserType"), tenantTypes: time(getAllTenantTypes(), "loadAppData.postDetails.getAllTenantTypes"), credits: CreditTypes.length > 0 ? time(getPlanFeatureUsage(tenantId, DefaultFeatures.Credits), "loadAppData.postDetails.getPlanFeatureUsage") : Promise.resolve(undefined as PlanFeatureUsageDto | undefined), productAccessEntries: time( Promise.all( [...tenantNavigationFeatureKeys, ...tenantConnectorDemoModuleKeys].map(async (featureKey) => [ featureKey, await resolveTenantProductAccess({ tenantId, userId: userInfo.userId, featureKey, subscription: mySubscription, tenantRolePermissionNames: allPermissions, bypassTenantRolePermission: !!user.admin || !!superAdminRole, }), ] as const) ), "loadAppData.postDetails.resolveTenantProductAccess" ), }), "loadAppData.postDetails" ); const currentRole = user.admin ? TenantUserType.ADMIN : tenantUser?.type ?? TenantUserType.MEMBER; const data: AppLoaderData = { // i18n: i18n.translations, user, myTenants, currentTenant, currentRole, mySubscription, entities, entityGroups, // roles, allRoles, permissions: allPermissions.map((f) => f as DefaultPermission), myGroups, isSuperUser: !!superUserRole, isSuperAdmin: !!superAdminRole, lng: user?.locale ?? userInfo.lng, onboardingSession, tenantTypes, credits, productAccess: Object.fromEntries(productAccessEntries), }; return data; }