@scayle/storefront-nuxt
Version:
Nuxt integration for the SCAYLE Commerce Engine and Storefront API
203 lines (202 loc) • 6.63 kB
JavaScript
import { defineEventHandler, getCookie, deleteCookie, getRequestURL } from "h3";
import { decodeJwt } from "jose";
import { randomUUID } from "uncrypto";
import { useSession, unsignCookie } from "@scayle/h3-session";
import { trace, SpanStatusCode } from "@opentelemetry/api";
import { joinURL } from "ufo";
import { buildContext } from "../../context.js";
import { useCacheStorage, useSessionStorage } from "../utils/cacheStorage.js";
import {
convertShopsToList,
getCurrentShopConfigForRequest,
getPublicShopData,
getBootstrapPath,
getApiBasePath
} from "./bootstrap-utils.js";
import { useRedirects } from "./redirects.js";
import { useRuntimeConfig } from "#imports";
import * as moduleOptions from "#internal/storefront-options.mjs";
const generateSessionId = (event) => {
let userId;
if (event?.context.session?.data?.accessToken) {
const payload = decodeJwt(event?.context.session?.data?.accessToken);
userId = payload.customerId;
}
return userId ? `${userId}:${randomUUID()}` : randomUUID();
};
async function getOldSessionData(event, cookieName, store, secret) {
const rawCookie = getCookie(event, cookieName);
const normalizedSecrets = Array.isArray(secret) ? secret : [secret];
const sessionId = rawCookie ? await unsignCookie(rawCookie, normalizedSecrets) : null;
if (sessionId) {
const sessionData = await store.get(sessionId);
await store.destroy(sessionId);
return sessionData;
}
}
async function bootstrap(event, $shopConfig, $storefrontConfig, apiBasePath, config) {
const log = event.context.$log;
const bootstrapLog = log.space("bootstrap");
const $cache = useCacheStorage(
String($shopConfig.shopId),
$shopConfig.shopId,
bootstrapLog
);
const sessionDefaults = {
maxAge: void 0,
cookieName: "$session",
secret: "current-secret",
sameSite: "lax"
};
const sessionConfig = {
...sessionDefaults,
...$storefrontConfig.session,
...$shopConfig.sessionConfig
};
const sessionStorage = useSessionStorage(
sessionConfig,
$shopConfig.shopId,
log
);
const routeRules = event.context._nitro.routeRules;
const cacheableRoute = routeRules.cache || routeRules.isr || routeRules.swr;
bootstrapLog.debug("Route rules", {
rules: event.context._nitro.routeRules,
cacheableRoute,
path: event.path
});
if (!cacheableRoute) {
bootstrapLog.debug("Attaching session to request");
try {
const cookieName = sessionConfig?.cookieName ?? "$session";
const secret = sessionConfig?.secret ?? "current-secret";
const oldSessionData = $storefrontConfig.legacy?.enableSessionMigration ? await getOldSessionData(event, cookieName, sessionStorage, secret) : void 0;
await useSession(event, {
...sessionConfig,
store: sessionStorage,
genid: generateSessionId,
saveUninitialized: false,
cookie: {
maxAge: sessionConfig?.maxAge,
// secure: sessionConfig.isHttps, TODO
sameSite: sessionConfig.sameSite,
domain: sessionConfig?.domain
},
name: `${cookieName}-${$shopConfig.shopId}`,
secret
});
if (oldSessionData) {
event.context.session.data = oldSessionData;
event.context.session.save();
}
const oldSessionCookie = getCookie(event, cookieName);
if (oldSessionCookie) {
deleteCookie(event, cookieName, {
path: $storefrontConfig.shopSelector === "path" && $shopConfig.path ? `/${$shopConfig.path}` : void 0
});
}
} catch (e) {
bootstrapLog.error("Error attaching session: ", e);
}
} else {
bootstrapLog.debug("Skipping session for cached request");
}
event.context.$currentShop = getPublicShopData(
$shopConfig,
$shopConfig.shopId,
apiBasePath,
$storefrontConfig.publicShopData
);
event.context.$availableShops = convertShopsToList($storefrontConfig.shops).filter((store) => store.isEnabled !== false).map(
(config2) => getPublicShopData(
config2,
$shopConfig.shopId,
apiBasePath,
$storefrontConfig.publicShopData
)
);
event.context.$cache = $cache;
bootstrapLog.debug("Building context for request");
event.context.$rpcContext = await buildContext({
$cache,
$shopConfig,
$storefront: $storefrontConfig,
$log: log,
sessionId: cacheableRoute ? void 0 : event.context.sessionId,
session: cacheableRoute ? void 0 : event.context.session,
config,
event
});
}
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event);
const url = getRequestURL(event);
const { path, originalPath } = getBootstrapPath(url, config.app.baseURL);
const tracer = trace.getTracer("storefront-nuxt", "__sfc_version");
if (path.startsWith(joinURL(config.app.baseURL, "/__nuxt"))) {
return;
}
if (path === "/favicon.ico") {
return;
}
const $storefrontConfig = config.storefront;
if (path === `${getApiBasePath(moduleOptions, "/")}/up`) {
return;
}
const $shopConfig = getCurrentShopConfigForRequest(
event,
$storefrontConfig,
config.app
);
if (!$shopConfig) {
event.context.$log.debug(
"Could not find shop config for this request",
path
);
return;
}
const apiBasePath = getApiBasePath(moduleOptions, config.app.baseURL);
if (!event.context.$rpcContext) {
event.context.$log.debug(`Bootstrapping request: ${path}`);
await tracer.startActiveSpan(
`storefront-nuxt.middleware/bootstrap`,
{
attributes: {
path,
originalPath
}
},
async (span) => {
try {
await bootstrap(
event,
$shopConfig,
$storefrontConfig,
apiBasePath,
config
);
} catch (e) {
span.setStatus({ code: SpanStatusCode.ERROR });
if (e instanceof Error) {
event.context.$log.error(e);
span.recordException(e);
}
span.end();
throw e;
}
span.setStatus({ code: SpanStatusCode.OK });
span.end();
}
);
}
if ($storefrontConfig.redirects?.enabled && (!$storefrontConfig.redirects?.strategy || $storefrontConfig.redirects?.strategy === "before-request") && !path.startsWith(apiBasePath) && originalPath !== joinURL(config.app.baseURL, "/__nuxt_error")) {
await tracer.startActiveSpan(
`storefront-nuxt.middleware/redirects`,
{},
async (innerSpan) => {
await useRedirects(event);
innerSpan.end();
}
);
}
});