Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,51 @@
import type { LoaderFunctionArgs } from "react-router";
import { getTranslations } from "~/locale/i18next.server";
import { createMetrics } from "~/modules/metrics/services/.server/MetricTracker";
import { adminGetAllTenantsWithUsage, type TenantWithUsage } from "~/utils/db/tenants.db.server";
import { getPaginationFromCurrentUrl } from "~/utils/helpers/RowPaginationHelper";
import PeriodHelper from "~/utils/helpers/PeriodHelper";
import { promiseHash } from "~/utils/promises/promiseHash";
import { getAdminDashboardStats } from "~/utils/services/adminDashboardService";
import { getSetupSteps } from "~/utils/services/setupService";
import type { SetupItem } from "~/application/dtos/setup/SetupItem";
import type { Stat } from "~/application/dtos/stats/Stat";
import type { PaginationDto } from "~/application/dtos/data/PaginationDto";
export type AdminDashboardLoaderData = {
title: string;
stats: Stat[];
setupSteps: SetupItem[];
tenants: {
items: TenantWithUsage[];
pagination: PaginationDto;
};
};
type AdminDashboardDataArgs = Pick<LoaderFunctionArgs, "request" | "params">;
export async function loadAdminDashboardRouteData({ request, params }: AdminDashboardDataArgs) {
const { time, getServerTimingHeader } = await createMetrics({ request, params }, "admin.dashboard");
const urlSearchParams = new URL(request.url).searchParams;
const currentPagination = getPaginationFromCurrentUrl(urlSearchParams);
const { t } = await time(getTranslations(request), "getTranslations");
const { stats, setupSteps, tenants } = await time(
promiseHash({
stats: time(
getAdminDashboardStats({ t, gte: PeriodHelper.getGreaterThanOrEqualsFromRequest({ request }) }),
"admin.dashboard.details.getAdminDashboardStats"
),
setupSteps: time(getSetupSteps(), "admin.dashboard.details.getSetupSteps"),
tenants: time(adminGetAllTenantsWithUsage(undefined, currentPagination), "admin.dashboard.details.adminGetAllTenantsWithUsage"),
}),
"admin.dashboard.details"
);
const data: AdminDashboardLoaderData = {
title: `${t("app.sidebar.dashboard")} | ${process.env.APP_NAME}`,
stats,
setupSteps,
tenants,
};
return { data, headers: getServerTimingHeader() };
}
@@ -0,0 +1,24 @@
// @vitest-environment node
import { describe, expect, test } from "vitest";
import { readRepoFile } from "~/utils/testing/repoPaths";
function readAdminDataSource() {
return readRepoFile("apps/web/app/utils/data/.server/adminData.ts");
}
describe("admin data MFA enforcement boundary", () => {
test("does not import MFA secret or recovery-code storage directly", () => {
const source = readAdminDataSource();
expect(source).not.toMatch(/mfaRecoveryCode|totpSecret|encryptedTotpSecret/i);
});
test("routes shared admin loader through the MFA enforcement boundary", () => {
const source = readAdminDataSource();
expect(source).toContain("getUserTotpStatus");
expect(source).toContain("checkMfaStepUpState");
expect(source).toContain("enforceAdminMfa");
expect(source).toMatch(/if \(!mfaEnforcement\.allowed\) \{\s*throw redirect\(mfaEnforcement\.redirectTo\);\s*\}/);
});
});
@@ -0,0 +1,87 @@
import { DefaultAdminRoles } from "~/application/dtos/shared/DefaultAdminRoles";
import { DefaultPermission } from "~/application/dtos/shared/DefaultPermissions";
import { TimeFunction } from "~/modules/metrics/services/.server/MetricTracker";
import OnboardingService from "~/modules/onboarding/services/OnboardingService";
import UrlUtils from "~/utils/app/UrlUtils";
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, getUserRoleInAdmin } from "~/utils/db/permissions/userRoles.db.server";
import { getTenantsForUser as getMyTenants } from "@toolctrl/db/auth-boundary.server";
import { getAllTenantTypes } from "~/utils/db/tenants/tenantTypes.db.server";
import { getUserTotpStatus } from "~/utils/db/users.db.server";
import { enforceAdminMfa } from "~/utils/auth/mfa/enforcement.server";
import { checkMfaStepUpState } from "~/utils/auth/mfa/step-up.server";
import { promiseHash } from "~/utils/promises/promiseHash";
import { getUserInfo, getUserSession } from "~/utils/session.server";
import { AdminLoaderData } from "../useAdminData";
import { getAuthUserWithoutPasswordById as getUser } from "@toolctrl/db/auth-boundary.server";
import { TFunction } from "i18next";
import { redirect } from "react-router";
import { NOT_FOUND_ENVELOPE } from "~/utils/errors.server";
export async function loadAdminData({ request, t }: { request: Request; t: TFunction }, time: TimeFunction) {
const userInfo = await time(getUserInfo(request), "getUserInfo");
const url = new URL(request.url);
if (UrlUtils.stripTrailingSlash(url.pathname) === `/admin`) {
throw Response.json(NOT_FOUND_ENVELOPE, { status: 404 });
}
const user = await time(getUser(userInfo?.userId), "getUser");
const redirectTo = url.pathname + url.search;
if (!userInfo || !user) {
let searchParams = new URLSearchParams([["redirect", redirectTo]]);
throw redirect(`/login?${searchParams}`);
}
if (!user.admin) {
throw Response.json({ error: t("admin.errors.adminOnly") }, { status: 401 });
}
const session = await time(getUserSession(request), "getUserSession");
const totpStatus = await time(getUserTotpStatus(user.id), "getUserTotpStatus");
const stepUp = checkMfaStepUpState(session, user.id);
const mfaEnforcement = enforceAdminMfa({
requestUrl: url,
userId: user.id,
isAdmin: Boolean(user.admin),
totpStatus,
stepUp,
redirectMode: "host-aware",
});
if (!mfaEnforcement.allowed) {
throw redirect(mfaEnforcement.redirectTo);
}
const myTenants = await time(getMyTenants(user.id), "getMyTenants");
const { allPermissions, superAdminRole, entities, entityGroups, allRoles, myGroups, onboardingSession } = await time(
promiseHash({
allPermissions: getPermissionsByUser(userInfo.userId, null),
superAdminRole: getUserRoleInAdmin(userInfo.userId, DefaultAdminRoles.SuperAdmin),
entities: getAllEntities({ tenantId: null }),
entityGroups: getAllEntityGroups(),
allRoles: getAllRolesWithoutPermissions("admin"),
myGroups: getMyGroups(user.id, null),
onboardingSession: OnboardingService.getUserActiveOnboarding({ userId: user.id, tenantId: null, request }),
}),
"loadAdminData.getDetails"
);
const data: AdminLoaderData = {
user,
myTenants,
currentTenant: null,
entities,
entityGroups,
// roles,
allRoles,
permissions: allPermissions.map((f) => f as DefaultPermission),
isSuperUser: !!superAdminRole,
isSuperAdmin: !!superAdminRole,
myGroups,
lng: user?.locale ?? userInfo.lng,
onboardingSession,
tenantTypes: await getAllTenantTypes(),
};
return data;
}
@@ -0,0 +1,95 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vitest";
import { TenantUserType } from "~/application/enums/tenants/TenantUserType";
type Deferred<T> = {
promise: Promise<T>;
resolve: (value: T) => void;
};
function deferred<T>(): Deferred<T> {
let resolve!: (value: T) => void;
const promise = new Promise<T>((done) => {
resolve = done;
});
return { promise, resolve };
}
const mocks = vi.hoisted(() => {
const postDetails = {
tenantUser: deferred<{ type: TenantUserType }>(),
tenantTypes: deferred<unknown[]>(),
credits: deferred<unknown>(),
productAccess: deferred<{ allowed: boolean }>(),
};
return { postDetails };
});
vi.mock("~/utils/services/.server/urlService", () => ({ getTenantIdFromUrl: vi.fn(async () => "tenant_1") }));
vi.mock("~/utils/session.server", () => ({ getUserInfo: vi.fn(async () => ({ userId: "user_1", lng: "en" })) }));
vi.mock("@toolctrl/db/auth-boundary.server", () => ({
getAuthUserWithoutPasswordById: vi.fn(async () => ({ id: "user_1", admin: false, locale: "en" })),
getTenantsForUser: vi.fn(async () => [{ id: "tenant_1", slug: "acme", name: "Acme" }]),
}));
vi.mock("~/utils/db/tenants.db.server", () => ({
adminGetAllActiveTenantSimple: vi.fn(async () => []),
getTenantSimple: vi.fn(async () => ({ id: "tenant_1", slug: "acme", name: "Acme", deactivatedReason: null })),
getTenantUserType: vi.fn(() => mocks.postDetails.tenantUser.promise),
}));
vi.mock("@toolctrl/config", () => ({ loadRequiredWebConfig: vi.fn(() => ({ routing: {} })) }));
vi.mock("~/utils/db/entities/entities.db.server", () => ({ getAllEntities: vi.fn(async () => []) }));
vi.mock("~/utils/db/entities/entityGroups.db.server", () => ({ getAllEntityGroups: vi.fn(async () => []) }));
vi.mock("~/utils/db/permissions/groups.db.server", () => ({ getMyGroups: vi.fn(async () => []) }));
vi.mock("~/utils/db/permissions/roles.db.server", () => ({ getAllRolesWithoutPermissions: vi.fn(async () => []) }));
vi.mock("~/utils/db/permissions/userRoles.db.server", () => ({
getPermissionsByUser: vi.fn(async () => []),
getUserRoleInAdmin: vi.fn(async () => null),
getUserRoleInTenant: vi.fn(async () => null),
}));
vi.mock("~/utils/db/tenants/tenantTypes.db.server", () => ({ getAllTenantTypes: vi.fn(() => mocks.postDetails.tenantTypes.promise) }));
vi.mock("~/utils/services/.server/subscriptionService", () => ({
getActiveTenantSubscriptions: vi.fn(async () => ({ products: [] })),
getPlanFeatureUsage: vi.fn(() => mocks.postDetails.credits.promise),
}));
vi.mock("~/modules/onboarding/services/OnboardingService", () => ({ default: { getUserActiveOnboarding: vi.fn(async () => null) } }));
vi.mock("~/utils/entitlements/.server/productAccess.server", () => ({
resolveTenantProductAccess: vi.fn(() => mocks.postDetails.productAccess.promise),
}));
import { getTenantUserType } from "~/utils/db/tenants.db.server";
import { getAllTenantTypes } from "~/utils/db/tenants/tenantTypes.db.server";
import { getPlanFeatureUsage } from "~/utils/services/.server/subscriptionService";
import { resolveTenantProductAccess } from "~/utils/entitlements/.server/productAccess.server";
import { loadAppData } from "./appData";
async function flushAsyncWork() {
await new Promise((resolve) => setImmediate(resolve));
}
describe("loadAppData performance", () => {
it("starts post-detail shell lookups in parallel", async () => {
const result = loadAppData({
request: new Request("https://app.example.test/acme/dashboard"),
params: { tenant: "acme" },
t: ((key: string) => key) as never,
});
await flushAsyncWork();
expect(getTenantUserType).toHaveBeenCalledWith("user_1", "tenant_1");
expect(getAllTenantTypes).toHaveBeenCalled();
expect(getPlanFeatureUsage).toHaveBeenCalledWith("tenant_1", "ai-credits");
expect(resolveTenantProductAccess).toHaveBeenCalled();
mocks.postDetails.tenantUser.resolve({ type: TenantUserType.MEMBER });
mocks.postDetails.tenantTypes.resolve([]);
mocks.postDetails.credits.resolve(undefined);
mocks.postDetails.productAccess.resolve({ allowed: true });
await expect(result).resolves.toMatchObject({
currentRole: TenantUserType.MEMBER,
tenantTypes: [],
productAccess: expect.any(Object),
});
});
});
@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("~/utils/services/.server/urlService", () => ({ getTenantIdFromUrl: vi.fn(async () => "tenant_1") }));
vi.mock("~/utils/session.server", () => ({ getUserInfo: vi.fn(async () => ({ userId: "user_1", lng: "en" })) }));
vi.mock("@toolctrl/db/auth-boundary.server", () => ({
getAuthUserWithoutPasswordById: vi.fn(async () => ({ id: "user_1", admin: false, locale: "en" })),
getTenantsForUser: vi.fn(),
}));
vi.mock("~/utils/db/tenants.db.server", () => ({
getTenantSimple: vi.fn(async () => ({ id: "tenant_1", slug: "acme", name: "Acme", deactivatedReason: null })),
getTenantUserType: vi.fn(),
}));
vi.mock("@toolctrl/config", () => ({ loadRequiredWebConfig: vi.fn(() => ({ routing: {} })) }));
vi.mock("~/utils/db/entities/entities.db.server", () => ({ getAllEntities: vi.fn() }));
vi.mock("~/utils/db/entities/entityGroups.db.server", () => ({ getAllEntityGroups: vi.fn() }));
vi.mock("~/utils/db/permissions/groups.db.server", () => ({ getMyGroups: vi.fn() }));
vi.mock("~/utils/db/permissions/roles.db.server", () => ({ getAllRolesWithoutPermissions: vi.fn() }));
vi.mock("~/utils/db/permissions/userRoles.db.server", () => ({
getPermissionsByUser: vi.fn(),
getUserRoleInAdmin: vi.fn(),
getUserRoleInTenant: vi.fn(),
}));
vi.mock("~/utils/db/tenants/tenantTypes.db.server", () => ({ getAllTenantTypes: vi.fn() }));
vi.mock("~/utils/services/.server/subscriptionService", () => ({ getActiveTenantSubscriptions: vi.fn(), getPlanFeatureUsage: vi.fn() }));
vi.mock("~/modules/onboarding/services/OnboardingService", () => ({ default: { getUserActiveOnboarding: vi.fn() } }));
import { loadAppData } from "./appData";
describe("loadAppData tenant canonicalization", () => {
it("redirects tenant id aliases to the canonical tenant slug and preserves path/search", async () => {
try {
await loadAppData({
request: new Request("https://app.example.test/tenant_1/settings/profile?tab=security"),
params: { tenant: "tenant_1" },
t: ((key: string) => key) as never,
});
} catch (error) {
expect(error).toBeInstanceOf(Response);
expect((error as Response).headers.get("Location")).toBe("/acme/settings/profile?tab=security");
}
});
});
@@ -0,0 +1,12 @@
// @vitest-environment node
import { describe, expect, test } from "vitest";
import { readRepoFile } from "~/utils/testing/repoPaths";
describe("loadAppData tenant role lookup", () => {
test("calls getTenantUserType with user id before tenant id", () => {
const source = readRepoFile("apps/web/app/utils/data/.server/appData.ts");
expect(source).toContain("getTenantUserType(userInfo.userId, tenantId)");
expect(source).not.toContain("getTenantUserType(tenantId, userInfo.userId)");
});
});
@@ -0,0 +1,169 @@
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<ReturnType<typeof getUserInfo>>;
user?: Awaited<ReturnType<typeof getUser>>;
}) {
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;
}
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { readRepoFile } from "~/utils/testing/repoPaths";
const dataLoaderPaths = [
{
path: "apps/web/app/utils/data/.server/rootData.ts",
blockedImports: ["~/utils/db/users.db.server", "~/utils/db/tenants.db.server"],
},
{
path: "apps/web/app/utils/data/.server/appData.ts",
blockedImports: ["~/utils/db/users.db.server"],
},
{
path: "apps/web/app/utils/data/.server/adminData.ts",
blockedImports: ["~/utils/db/tenants.db.server"],
},
];
const tenantSelectionRoutePaths = [
"apps/web/app/routes/app.tsx",
"apps/web/app/routes/login.tsx",
];
function readLoader(relativePath: string) {
return readRepoFile(relativePath);
}
describe("auth boundary imports for shared data loaders", () => {
it.each(dataLoaderPaths)("imports migrated auth/user/tenant reads from @toolctrl/db in $path", ({ path: relativePath }) => {
const source = readLoader(relativePath);
expect(source).toContain("@toolctrl/db/auth-boundary.server");
});
it.each(dataLoaderPaths)("does not import migrated app DB wrappers in $path", ({ path: relativePath, blockedImports }) => {
const source = readLoader(relativePath);
for (const importPath of blockedImports) {
expect(source).not.toContain(`from "${importPath}"`);
expect(source).not.toContain(`from '${importPath}'`);
}
});
});
describe("auth boundary imports for tenant selection routes", () => {
it.each(tenantSelectionRoutePaths)("imports migrated auth/user/tenant reads from @toolctrl/db in %s", (relativePath) => {
const source = readLoader(relativePath);
expect(source).toContain("@toolctrl/db/auth-boundary.server");
});
it.each(tenantSelectionRoutePaths)("does not import migrated app DB wrappers in %s", (relativePath) => {
const source = readLoader(relativePath);
for (const importPath of ["~/utils/db/users.db.server", "~/utils/db/tenants.db.server"]) {
expect(source).not.toContain(`from "${importPath}"`);
expect(source).not.toContain(`from '${importPath}'`);
}
});
});
@@ -0,0 +1,114 @@
import { loadRequiredWebConfig } from "@toolctrl/config";
import { getAuthUserWithoutPasswordById as getUser, getTenantById as getTenant, getTenantByIdOrSlug } from "@toolctrl/db/auth-boundary.server";
import { Params } from "react-router";
import { AnalyticsInfoDto } from "~/application/dtos/marketing/AnalyticsInfoDto";
import { ImpersonatingSessionDto } from "~/application/dtos/session/ImpersonatingSessionDto";
import { CookieCategory } from "~/application/cookies/CookieCategory";
import { remixI18Next, i18nCookie } from "~/locale/i18next.server";
import { getCurrentEnabledFlagNames } from "~/utils/services/.server/featureFlagsRequest.server";
import { createMetrics } from "~/modules/metrics/services/.server/MetricTracker";
import { getAnalyticsSession, generateAnalyticsUserId, commitAnalyticsSession, destroyAnalyticsSession } from "~/utils/analyticsCookie.server";
import { getAppConfiguration } from "~/utils/db/appConfiguration.db.server";
import AnalyticsService from "~/utils/helpers/.server/AnalyticsService";
import CookieHelper from "~/utils/helpers/CookieHelper";
import { promiseHash } from "~/utils/promises/promiseHash";
import { getUserInfo, getUserSession, setCSRFToken, createUserSession, commitSession } from "~/utils/session.server";
import { getAbsoluteApiUrl, getAbsoluteUrl, getBaseURL, getDomainName } from "~/utils/url.server";
import { AppRootData } from "../useRootData";
export async function loadRootData({ request, params }: { request: Request; params: Params }) {
const { time, getServerTimingHeader } = await createMetrics({ request, params }, "root");
const { userInfo, session, analyticsSession } = await time(
promiseHash({
userInfo: getUserInfo(request),
session: getUserSession(request),
analyticsSession: getAnalyticsSession(request),
}),
"loadRootData.session"
);
const user = userInfo.userId ? await getUser(userInfo.userId) : null;
const currentTenant = params.tenant ? await getTenantByIdOrSlug(params.tenant) : user?.defaultTenantId ? await getTenant(user.defaultTenantId) : null;
const existingCsrf = session.get("csrf");
const csrf = typeof existingCsrf === "string" && existingCsrf.length > 0 ? existingCsrf : setCSRFToken(session);
let analytics: AnalyticsInfoDto | undefined = undefined;
const appConfiguration = await time(getAppConfiguration({ request }), "getAppConfiguration");
const hasAnalyticsConsent = CookieHelper.hasConsent(userInfo, CookieCategory.ANALYTICS);
if (appConfiguration.analytics.enabled && hasAnalyticsConsent) {
try {
analytics = await AnalyticsService.getFromRequest({ appConfiguration, request, userId: userInfo.userId ?? null });
} catch (e: any) {
// eslint-disable-next-line no-console
console.error(e.message);
}
}
const headers = new Headers();
let userAnalyticsId = analyticsSession.get("userAnalyticsId");
headers.append("Set-Cookie", await commitSession(session));
if (hasAnalyticsConsent) {
if (!userAnalyticsId) {
userAnalyticsId = generateAnalyticsUserId();
analyticsSession.set("userAnalyticsId", userAnalyticsId);
}
if (analytics) {
analytics = { ...analytics, userAnalyticsId };
}
headers.append("Set-Cookie", await commitAnalyticsSession(analyticsSession));
} else {
userAnalyticsId = undefined;
headers.append("Set-Cookie", await destroyAnalyticsSession(analyticsSession));
}
let impersonatingSession: ImpersonatingSessionDto | null = null;
if (userInfo.impersonatingFromUserId && userInfo.userId?.length > 0) {
const fromUser = await getUser(userInfo.impersonatingFromUserId);
const toUser = await getUser(userInfo.userId);
if (fromUser && toUser) {
impersonatingSession = { fromUser, toUser };
}
}
const locale = await remixI18Next.getLocale(request);
const webConfig = loadRequiredWebConfig();
const data: AppRootData = {
metatags: [{ title: webConfig.appName }],
user,
currentTenant,
locale,
theme: userInfo.theme || appConfiguration.app.theme,
serverUrl: getBaseURL(request),
publicBaseUrl: getAbsoluteUrl(request, "/"),
apiBaseUrl: getAbsoluteApiUrl(request, "/"),
routing: webConfig.routing,
domainName: getDomainName(request),
userSession: userInfo,
authenticated: userInfo.userId?.length > 0,
debug: webConfig.nodeEnv === "development",
isStripeTest: webConfig.stripe.secretKey?.startsWith("sk_test_") ?? true,
chatWebsiteId: webConfig.crisp.chatWebsiteId,
appConfiguration,
csrf,
analytics,
analyticsToken: appConfiguration.analytics.enabled && hasAnalyticsConsent ? webConfig.analytics.token : undefined,
featureFlags: appConfiguration.featureFlags.enabled ? await getCurrentEnabledFlagNames({ request, params }) : [],
impersonatingSession,
};
const updateMetrics = userInfo.userId?.length > 0 && appConfiguration.metrics.enabled !== userInfo.metrics?.enabled;
const needsToUpdateSession = updateMetrics;
if (needsToUpdateSession) {
return createUserSession(
{
...userInfo,
metrics: appConfiguration.metrics,
},
new URL(request.url).pathname + new URL(request.url).search
);
}
headers.append("Server-Timing", getServerTimingHeader()["Server-Timing"]);
headers.append("Set-Cookie", await i18nCookie.serialize(locale));
return Response.json(data, { headers });
}