186 lines
8.8 KiB
TypeScript
186 lines
8.8 KiB
TypeScript
import { loadRequiredWebConfig } from "@hivegrid/config";
|
|
import { getAuthUserWithoutPasswordById as getUser, getTenantById as getTenant, getTenantByIdOrSlug } from "@hivegrid/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 { enabledLocales, remixI18Next, i18nCookie } from "~/locale/i18next.server";
|
|
import { resolveEnabledLocale } from "~/locale/i18n";
|
|
import { resolveAnrede } from "~/locale/anrede.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, getValidatedServerSession, setCSRFToken, createUserSession, commitSession } from "~/utils/session.server";
|
|
import { getAbsoluteApiUrl, getAbsoluteUrl, getBaseURL, getDomainName } from "~/utils/url.server";
|
|
import { createScopedLogger } from "~/utils/logger.server";
|
|
import { AppRootData } from "../useRootData";
|
|
import { CSP_NONCE_REQUEST_HEADER, buildDocumentContentSecurityPolicy, getRequestCspNonce } from "~/utils/security/contentSecurityPolicy";
|
|
import { getRequestDocumentContentSecurityPolicyOptions } from "~/utils/security/requestContentSecurityPolicy.server";
|
|
|
|
const rootDataLogger = createScopedLogger("root-data");
|
|
const browserBuildEnv = ((import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}) as Record<string, string | undefined>;
|
|
|
|
export async function loadRootData({ request, params }: { request: Request; params: Params }) {
|
|
const { time, getServerTimingHeader } = await createMetrics({ request, params }, "root");
|
|
const appConfigurationPromise = time(getAppConfiguration({ request }), "getAppConfiguration");
|
|
const localePromise = remixI18Next.getLocale(request);
|
|
const { userInfo, session, analyticsSession } = await time(
|
|
promiseHash({
|
|
userInfo: getUserInfo(request),
|
|
session: getUserSession(request),
|
|
analyticsSession: getAnalyticsSession(request),
|
|
}),
|
|
"loadRootData.session"
|
|
);
|
|
const [validatedSession, routeTenant] = await Promise.all([
|
|
userInfo.userId && !userInfo.impersonatingFromUserId ? getValidatedServerSession(request, userInfo) : Promise.resolve(null),
|
|
params.tenant ? getTenantByIdOrSlug(params.tenant) : Promise.resolve(null),
|
|
]);
|
|
const user =
|
|
validatedSession && "user" in validatedSession
|
|
? validatedSession.user
|
|
: validatedSession && "adminUser" in validatedSession
|
|
? {
|
|
id: validatedSession.adminUser.id,
|
|
email: validatedSession.adminUser.email,
|
|
firstName: validatedSession.adminUser.firstName,
|
|
lastName: validatedSession.adminUser.lastName,
|
|
avatar: validatedSession.adminUser.avatar,
|
|
phone: null,
|
|
admin: { userId: validatedSession.adminUser.id },
|
|
defaultTenantId: null,
|
|
createdAt: validatedSession.createdAt,
|
|
locale: validatedSession.adminUser.locale,
|
|
lightOrDarkMode: null,
|
|
}
|
|
: userInfo.userId
|
|
? await getUser(userInfo.userId)
|
|
: null;
|
|
const currentTenant = params.tenant ? routeTenant : user?.defaultTenantId ? await getTenant(user.defaultTenantId) : null;
|
|
|
|
const existingCsrf = session.get("csrf");
|
|
const csrf = typeof existingCsrf === "string" && existingCsrf.length > 0 ? existingCsrf : setCSRFToken(session);
|
|
|
|
const [appConfiguration, detectedLocale] = await Promise.all([appConfigurationPromise, localePromise]);
|
|
const locale = resolveEnabledLocale({
|
|
enabledLocales,
|
|
userLocale: user?.locale,
|
|
cookieLocale: detectedLocale,
|
|
acceptLanguage: request.headers.get("Accept-Language"),
|
|
});
|
|
const hasAnalyticsConsent = CookieHelper.hasConsent(userInfo, CookieCategory.ANALYTICS);
|
|
const analyticsPromise: Promise<AnalyticsInfoDto | undefined> =
|
|
appConfiguration.analytics.enabled && hasAnalyticsConsent
|
|
? AnalyticsService.getFromRequest({ appConfiguration, request, userId: userInfo.userId ?? null }).catch((error: unknown) => {
|
|
rootDataLogger.warn({ err: { name: error instanceof Error ? error.name : "Error" } }, "analytics request enrichment failed");
|
|
return undefined;
|
|
})
|
|
: Promise.resolve(undefined);
|
|
|
|
const impersonatingSessionPromise: Promise<ImpersonatingSessionDto | null> =
|
|
userInfo.impersonatingFromAdminUserId && userInfo.impersonatingFromAdminEmail && userInfo.userId?.length > 0
|
|
? getUser(userInfo.userId).then((toUser) =>
|
|
toUser
|
|
? {
|
|
fromUser: { id: userInfo.impersonatingFromAdminUserId!, email: userInfo.impersonatingFromAdminEmail! },
|
|
toUser: { id: toUser.id, email: toUser.email },
|
|
}
|
|
: null
|
|
)
|
|
: userInfo.impersonatingFromUserId && userInfo.userId?.length > 0
|
|
? Promise.all([getUser(userInfo.impersonatingFromUserId), getUser(userInfo.userId)]).then(([fromUser, toUser]) =>
|
|
fromUser && toUser ? { fromUser, toUser } : null
|
|
)
|
|
: Promise.resolve(null);
|
|
const anredePromise = resolveAnrede(request, currentTenant);
|
|
const featureFlagsPromise = appConfiguration.featureFlags.enabled ? getCurrentEnabledFlagNames({ request, params }) : Promise.resolve([] as string[]);
|
|
const sessionCookiePromise = commitSession(session);
|
|
|
|
const [resolvedAnalytics, impersonatingSession, anrede, featureFlags, sessionCookie] = await Promise.all([
|
|
analyticsPromise,
|
|
impersonatingSessionPromise,
|
|
anredePromise,
|
|
featureFlagsPromise,
|
|
sessionCookiePromise,
|
|
]);
|
|
let analytics = resolvedAnalytics;
|
|
|
|
const webConfig = loadRequiredWebConfig();
|
|
const headers = new Headers();
|
|
const cspNonce = getRequestCspNonce(request);
|
|
if (cspNonce) {
|
|
headers.set(CSP_NONCE_REQUEST_HEADER, cspNonce);
|
|
headers.set(
|
|
"Content-Security-Policy",
|
|
buildDocumentContentSecurityPolicy(cspNonce, undefined, getRequestDocumentContentSecurityPolicyOptions(request, webConfig, process.env, browserBuildEnv))
|
|
);
|
|
}
|
|
let userAnalyticsId = analyticsSession.get("userAnalyticsId");
|
|
headers.append("Set-Cookie", sessionCookie);
|
|
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));
|
|
}
|
|
|
|
// Formality axis (du/Sie). Default "du" → `effectiveLng(locale, "du") === locale`,
|
|
// so the language handed to `useChangeLanguage` in root.tsx is unchanged and
|
|
// rendering stays byte-identical. The tenant's persisted default applies when
|
|
// no explicit cookie/param is present.
|
|
const data: AppRootData = {
|
|
metatags: [{ title: webConfig.appName }],
|
|
user,
|
|
currentTenant,
|
|
locale,
|
|
enabledLocales,
|
|
anrede,
|
|
theme: userInfo.theme || appConfiguration.app.theme,
|
|
serverUrl: getBaseURL(request),
|
|
publicBaseUrl: getAbsoluteUrl(request, "/"),
|
|
apiBaseUrl: getAbsoluteApiUrl(request, "/"),
|
|
routing: webConfig.routing,
|
|
domainName: getDomainName(request),
|
|
userSession: { ...userInfo, lng: locale },
|
|
authenticated: userInfo.userId?.length > 0,
|
|
debug: webConfig.nodeEnv === "development",
|
|
chatWebsiteId: webConfig.crisp.chatWebsiteId,
|
|
appConfiguration,
|
|
csrf,
|
|
analytics,
|
|
analyticsToken: appConfiguration.analytics.enabled && hasAnalyticsConsent ? webConfig.analytics.token : undefined,
|
|
webPushVapidPublicKey: webConfig.webPush.vapidPublicKey,
|
|
cspNonce,
|
|
featureFlags,
|
|
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,
|
|
{ request }
|
|
);
|
|
}
|
|
|
|
headers.append("Server-Timing", getServerTimingHeader()["Server-Timing"]);
|
|
headers.append("Set-Cookie", await i18nCookie.serialize(locale));
|
|
return Response.json(data, { headers });
|
|
}
|