UNPKG

@scayle/storefront-nuxt

Version:

Nuxt integration for the SCAYLE Commerce Engine and Storefront API

203 lines (202 loc) 6.63 kB
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(); } ); } });