146 lines
4.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|