import { NextFetchEvent, NextMiddleware, NextResponse } from 'next/server'; import Settings from 'settings'; import { ROUTES } from 'routes'; import { PzNextRequest, withCheckoutProvider, withCompleteGpay, withCompleteMasterpass, withOauthLogin, withPrettyUrl, withRedirectionPayment, withSavedCardRedirection, withThreeDRedirection, withUrlRedirection, withCompleteWallet, withWalletCompleteRedirection, withMasterpassRestCallback, withBfcacheHeaders } from '.'; import { urlLocaleMatcherRegex } from '../utils'; import { getPzSegmentsConfig, encodePzValue, isLegacyMode } from '../utils/pz-segments'; import withCurrency from './currency'; import withLocale from './locale'; import logger from '../utils/log'; import { user } from '../data/urls'; import { getUrlPathWithLocale } from '../utils/localization'; import getRootHostname from '../utils/get-root-hostname'; import { LocaleUrlStrategy } from '../localization'; const POST_CHECKOUT_COOKIE_MAX_AGE_MS = 1000 * 60 * 30; // 30 minutes const withPzDefault = (middleware: NextMiddleware) => async (req: PzNextRequest, event: NextFetchEvent) => { const url = req.nextUrl.clone(); const commerceUrl = encodeURIComponent(decodeURI(Settings.commerceUrl)); // encodeURI doesn't work as expected in middleware const searchParams = new URLSearchParams(url.search); const ip = req.headers.get('x-forwarded-for') ?? ''; logger.debug('withPzDefault', { url: url.href, middlewareParams: req.middlewareParams, ip }); // Support legacy ?format=json query param if (searchParams.has('json') || searchParams.get('format') === 'json') { try { const { locales } = Settings.localization; const locale = req.cookies.get('pz-locale')?.value; const selectedLanguageValue = locales.find( (l) => l.value === locale )?.apiValue; searchParams.set('format', 'json'); const request = await fetch( `${encodeURI(Settings.commerceUrl)}${url.pathname.replace( urlLocaleMatcherRegex, '' )}?${searchParams.toString()}`, { next: { revalidate: 0 }, headers: { Cookie: req.headers.get('cookie') || '', Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'x-currency': req.cookies.get('pz-currency')?.value ?? '', 'Accept-Language': selectedLanguageValue ?? '' } } ); return NextResponse.json(await request.json()); } catch (error) { logger.error('?format=json error', { error, ip }); return NextResponse.next(); } } if (url.pathname === '/healthz') { return NextResponse.json({ status: 'ok' }); } if (url.pathname.startsWith('/metrics')) { return new NextResponse(null, { status: 200 }); } if (req.nextUrl.pathname.startsWith('/.well-known')) { const url = new URL(`${Settings.commerceUrl}${req.nextUrl.pathname}`); const req_ = await fetch(url.toString()); if (req_.ok) { return NextResponse.rewrite(url); } else { return NextResponse.next(); } } if ( req.nextUrl.pathname.includes('/orders/hooks/') || req.nextUrl.pathname.includes('/orders/checkout-with-token/') || req.nextUrl.pathname.includes('/orders/post-checkout-redirect/') || req.nextUrl.pathname.includes('/hooks/cash_register/complete/') || req.nextUrl.pathname.includes('/hooks/cash_register/pre_order/') ) { const queryString = searchParams.toString(); const currency = searchParams.get('currency')?.toLowerCase(); const response = NextResponse.rewrite( new URL( `${Settings.commerceUrl}${req.nextUrl.pathname.replace( urlLocaleMatcherRegex, '' )}${queryString ? `?${queryString}` : ''}` ) ); if (currency) { response.cookies.set('pz-currency', currency, { sameSite: 'none', secure: true, expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) }); } return response; } if (req.nextUrl.pathname.startsWith('/orders/redirection/')) { const queryString = searchParams.toString(); return NextResponse.rewrite( new URL( `${encodeURI(Settings.commerceUrl)}/orders/redirection/${ queryString ? `?${queryString}` : '' }` ) ); } if (req.nextUrl.pathname.includes('/orders/redirect-three-d')) { return NextResponse.rewrite( new URL(`${encodeURI(Settings.commerceUrl)}/orders/redirect-three-d/`) ); } if (req.nextUrl.pathname.includes('/orders/saved-card-redirect')) { return NextResponse.rewrite( new URL( `${encodeURI(Settings.commerceUrl)}/orders/saved-card-redirect/` ) ); } // If commerce redirects to /orders/checkout/ or /orders/post-checkout/ without locale const isPostCheckout = !!req.nextUrl.pathname.match( new RegExp('^/orders/post-checkout/$') ); const checkoutLocalePath = getUrlPathWithLocale( '/orders/checkout/', req.cookies.get('pz-locale')?.value ); if ( (isPostCheckout && req.nextUrl.searchParams.size === 0) || (req.nextUrl.pathname.match(new RegExp('^/orders/checkout/$')) && req.nextUrl.searchParams.size === 0 && checkoutLocalePath !== req.nextUrl.pathname) ) { const response = NextResponse.redirect( `${url.origin}${checkoutLocalePath}`, 303 ); if (isPostCheckout) { response.cookies.set('pz-post-checkout-flow', 'true', { path: '/', sameSite: 'none', secure: true, expires: new Date(Date.now() + POST_CHECKOUT_COOKIE_MAX_AGE_MS) }); } return response; } // Dynamically handle any payment gateway without specifying names const paymentGatewayRegex = new RegExp('^/payment-gateway/([^/]+)/$'); const gatewayMatch = req.nextUrl.pathname.match(paymentGatewayRegex); if ( gatewayMatch && // Check if the URL matches the /payment-gateway/ pattern getUrlPathWithLocale( `/payment-gateway/${gatewayMatch[1]}/`, req.cookies.get('pz-locale')?.value ) !== req.nextUrl.pathname ) { const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale( `/payment-gateway/${gatewayMatch[1]}/`, req.cookies.get('pz-locale')?.value )}?${req.nextUrl.searchParams.toString()}`; return NextResponse.redirect(redirectUrlWithLocale); } if (req.nextUrl.pathname.startsWith('/orders/checkout-provider/')) { try { const data = await req.json(); const request = await fetch( `${encodeURI(Settings.commerceUrl)}${url.pathname.replace( urlLocaleMatcherRegex, '' )}?${searchParams.toString()}`, { method: 'POST', body: JSON.stringify(data), next: { revalidate: 0 }, headers: { Cookie: req.headers.get('cookie') || '', Accept: 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'x-forwarded-for': ip } } ); return NextResponse.json(await request.json()); } catch (error) { return NextResponse.redirect(ROUTES.BASKET); } } req.middlewareParams = { commerceUrl, rewrites: {} }; return withOauthLogin( withLocale( withCurrency( withPrettyUrl( withRedirectionPayment( withThreeDRedirection( withCheckoutProvider( withUrlRedirection( withCompleteGpay( withCompleteMasterpass( withSavedCardRedirection( withCompleteWallet( withWalletCompleteRedirection( withMasterpassRestCallback( withBfcacheHeaders( async ( req: PzNextRequest, event: NextFetchEvent ) => { let middlewareResult: NextResponse | void = NextResponse.next(); try { const { locale, prettyUrl, currency } = req.middlewareParams.rewrites; const { defaultLocaleValue } = Settings.localization; const url = req.nextUrl.clone(); const pathnameWithoutLocale = url.pathname.replace( urlLocaleMatcherRegex, '' ); middlewareResult = (await middleware( req, event )) as NextResponse | void; let customRewriteUrlDiff = ''; if ( middlewareResult instanceof NextResponse && middlewareResult.headers.get( 'pz-override-response' ) && middlewareResult.headers.get( 'x-middleware-rewrite' ) ) { const rewriteUrl = new URL( middlewareResult.headers.get( 'x-middleware-rewrite' ) ); const originalUrl = new URL(req.url); customRewriteUrlDiff = rewriteUrl.pathname.replace( originalUrl.pathname, '' ); } let ordersPrefix: string; if (isLegacyMode(Settings)) { url.basePath = `/${commerceUrl}`; url.pathname = `/${locale.length ? `${locale}/` : ''}${currency}/${customRewriteUrlDiff}${ prettyUrl ?? pathnameWithoutLocale }`.replace(/\/+/g, '/'); ordersPrefix = `/${currency}/orders`; } else { const pzConfig = getPzSegmentsConfig(Settings); const fullUrlPath = `${customRewriteUrlDiff}${ prettyUrl ?? pathnameWithoutLocale }`.replace(/\/+/g, '/'); const fullUrl = `${req.nextUrl.origin}${fullUrlPath}`; const resolvedLocale = locale?.length > 0 ? locale : Settings.localization .defaultLocaleValue; const resolveContext = { req, event, url, locale: resolvedLocale, currency, pathname: pathnameWithoutLocale }; const customSegments = pzConfig.segments .filter( (seg) => seg.name !== 'locale' && seg.name !== 'currency' && seg.name !== 'url' ); const segmentValues: Record< string, string > = { locale: resolvedLocale, currency, url: encodeURIComponent(fullUrl), ...Object.fromEntries( customSegments .map((seg) => [ seg.name, req.middlewareParams.rewrites[ seg.name ] ?? (seg.resolve ? seg.resolve(resolveContext) : '') ]) ) }; const pzValue = encodePzValue( segmentValues, pzConfig ); url.pathname = `/${pzValue}/${fullUrlPath}`.replace( /\/+/g, '/' ); ordersPrefix = `/${pzValue}/orders`; } if ( Settings.usePrettyUrlRoute && url.searchParams.toString().length > 0 && !Object.entries(ROUTES).find( ([, value]) => new RegExp(`^${value}/?$`).test( pathnameWithoutLocale ) ) ) { url.pathname = url.pathname + (/\/$/.test(url.pathname) ? '' : '/') + `searchparams|${encodeURIComponent( url.searchParams.toString() )}`; } Settings.rewrites.forEach((rewrite) => { url.pathname = url.pathname.replace( rewrite.source, rewrite.destination ); }); // if middleware.ts has a return value for current url if ( middlewareResult instanceof NextResponse ) { // pz-override-response header is used to prevent 404 page for custom responses. if ( middlewareResult.headers.get( 'pz-override-response' ) !== 'true' ) { middlewareResult.headers.set( 'x-middleware-rewrite', url.href ); } else if ( middlewareResult.headers.get( 'x-middleware-rewrite' ) && middlewareResult.headers.get( 'pz-override-response' ) === 'true' ) { middlewareResult = NextResponse.rewrite(url); } } else { // if middleware.ts doesn't have a return value. // e.g. NextResponse.next() doesn't exist in middleware.ts middlewareResult = NextResponse.rewrite(url); } const { localeUrlStrategy } = Settings.localization; const fallbackHost = req.headers.get('x-forwarded-host') || req.headers.get('host'); const hostname = process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`; const rootHostname = localeUrlStrategy === LocaleUrlStrategy.Subdomain ? getRootHostname(hostname) : null; if ( !url.pathname.startsWith( ordersPrefix ) ) { middlewareResult.cookies.set( 'pz-locale', locale?.length > 0 ? locale : defaultLocaleValue, { domain: rootHostname, sameSite: 'none', secure: true, expires: new Date( Date.now() + 1000 * 60 * 60 * 24 * 7 ) // 7 days } ); } middlewareResult.cookies.set( 'pz-currency', currency, { domain: rootHostname, sameSite: 'none', secure: true, expires: new Date( Date.now() + 1000 * 60 * 60 * 24 * 7 ) // 7 days } ); if ( req.cookies.get('pz-locale') && req.cookies.get('pz-locale').value !== locale ) { logger.debug('Locale changed', { locale, oldLocale: req.cookies.get('pz-locale')?.value, ip }); } middlewareResult.headers.set( 'pz-url', req.nextUrl.toString() ); if (req.cookies.get('pz-set-currency')) { middlewareResult.cookies.delete( 'pz-set-currency' ); } if ( req.cookies.get( 'pz-post-checkout-flow' ) ) { if ( pathnameWithoutLocale.startsWith( '/orders/completed/' ) || pathnameWithoutLocale.startsWith( '/basket' ) ) { middlewareResult.cookies.delete( 'pz-post-checkout-flow' ); } } if (process.env.ACC_APP_VERSION) { middlewareResult.headers.set( 'acc-app-version', process.env.ACC_APP_VERSION ); } // Set CSRF token if not set try { const url = `${Settings.commerceUrl}${user.csrfToken}`; if (!req.cookies.get('csrftoken')) { const { csrf_token } = await ( await fetch(url) ).json(); middlewareResult.cookies.set( 'csrftoken', csrf_token, { domain: rootHostname } ); } } catch (error) { logger.error('CSRF Error', { error, ip }); } } catch (error) { logger.error('withPzDefault Error', { error, ip }); } return middlewareResult; }) ) ) ) ) ) ) ) ) ) ) ) ) ) )(req, event); }; export default withPzDefault;