Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/sites/utils/with-site-authentication.ts
T

146 lines
4.6 KiB
TypeScript

import { setLegacyApiKeyLogStatus as setApiKeyLogStatus } from "@toolctrl/db/api-keys.server";
import { Params } from "react-router";
import { Site } from "@toolctrl/db/prisma-browser";
import { getSiteNameByTypeAndSiteId } from "../db/sites.db.server";
import { getSiteType, getSiteTypeKey, SITE_TYPE, SiteTypeConfig } from "../site-types";
import { createApiKeyLog } from "~/utils/db/apiKeys.db.server";
import { buildCopyWithFallbacks } from "../lib/site-settings-utils";
import { getTranslations } from "~/locale/i18next.server";
/**
* Context provided to site API handlers
*/
export type SiteAuthContext = {
site: Site & {
tenant: { name: string; slug: string };
};
siteType: SiteTypeConfig;
branding: Record<string, unknown>;
copy: Record<string, string>;
};
type WithSiteAuthenticationParams = {
siteType: SITE_TYPE;
request: Request;
params: Params;
handler: (context: SiteAuthContext) => Promise<Response>;
};
/**
* Authentication wrapper for site-specific API endpoints.
*
* ALL endpoints require:
* - X-Site-Key header (site type API key)
* - X-Site-Id header (site identification - slug, subdomain, or domain)
*
* @example
* export const loader = async ({ request, params }: LoaderFunctionArgs) => {
* return withSiteAuthentication({
* siteType: "booking",
* request,
* params,
* handler: async ({ site, siteType, branding, copy }) => {
* return Response.json({ branding, copy });
* },
* });
* };
*/
export async function withSiteAuthentication({ siteType, request, handler }: WithSiteAuthenticationParams): Promise<Response> {
const startTime = performance.now();
let apiKeyLogId: string | undefined;
let apiKeyLogTenantId: string | undefined;
const { t } = await getTranslations(request);
try {
// 1. Get site type config
const siteTypeConfig = getSiteType(siteType);
if (!siteTypeConfig) {
return Response.json({ error: t("models.site.auth.errors.invalidType") }, { status: 400 });
}
// 2. Validate X-Site-Key (required for all endpoints)
const siteKey = request.headers.get("X-Site-Key");
const expectedKey = getSiteTypeKey(siteType);
if (!siteKey) {
return Response.json({ error: t("models.site.auth.errors.siteKeyRequired") }, { status: 401 });
}
if (!expectedKey || siteKey !== expectedKey) {
// Log failed attempt
await createApiKeyLog(request, {
apiKeyId: null,
tenantId: null,
endpoint: new URL(request.url).pathname,
error: t("models.site.auth.errors.invalidKey"),
status: 401,
alias: `${siteType}: ${t("models.site.auth.logs.unknownSiteKey")}`,
});
return Response.json({ error: t("models.site.auth.errors.invalidKey") }, { status: 401 });
}
// 3. Get site ID from X-Site-Id header
const xSiteId = request.headers.get("X-Site-Id");
if (!xSiteId) {
return Response.json({ error: t("models.site.auth.errors.siteIdRequired") }, { status: 400 });
}
// 4. Look up site by type + siteId
const site = await getSiteNameByTypeAndSiteId(siteType, xSiteId);
if (!site) {
return Response.json({ error: t("models.site.auth.errors.notFound") }, { status: 404 });
}
if (!site.isActive) {
return Response.json({ error: t("models.site.auth.errors.inactive") }, { status: 403 });
}
// 5. Create API log entry
const apiKeyLog = await createApiKeyLog(request, {
tenantId: site.tenantId,
apiKeyId: null,
endpoint: new URL(request.url).pathname,
alias: `${siteType}: ${site.siteId}`,
});
apiKeyLogId = apiKeyLog.id;
apiKeyLogTenantId = site.tenantId;
// 6. Parse site configuration (branding/copy stored as JSON strings)
const branding = JSON.parse(site.branding || "{}");
const copy = JSON.parse(site.copy || "{}");
// 7. Call handler
const fullCopy = buildCopyWithFallbacks(siteType, copy, t);
const response = await handler({
site,
siteType: siteTypeConfig,
branding,
// copy,
copy: fullCopy,
});
// 8. Log success
await setApiKeyLogStatus({ id: apiKeyLogId, tenantId: site.tenantId }, {
status: response.status,
startTime,
});
return response;
} catch (error: unknown) {
void error;
const errorMessage = t("models.site.auth.errors.internalFailure");
// Log error
if (apiKeyLogId && apiKeyLogTenantId) {
await setApiKeyLogStatus({ id: apiKeyLogId, tenantId: apiKeyLogTenantId }, {
status: 500,
startTime,
error: errorMessage,
});
}
return Response.json({ success: false, error: errorMessage }, { status: 500 });
}
}