import { NextFetchEvent, NextMiddleware, NextRequest, NextResponse } from 'next/server'; import Settings from 'settings'; import { getUrlPathWithLocale } from '../utils/localization'; import logger from '../utils/log'; const LOGIN_URL_REGEX = /^\/(\w+)\/login\/?$/; const CALLBACK_URL_REGEX = /^\/(\w+)\/login\/callback\/?$/; function buildCommerceHeaders(req: NextRequest): Record { return { 'x-forwarded-host': req.headers.get('x-forwarded-host') || req.headers.get('host') || '', 'x-forwarded-for': req.headers.get('x-forwarded-for') ?? '', 'x-forwarded-proto': req.headers.get('x-forwarded-proto') || 'https', 'x-currency': req.cookies.get('pz-currency')?.value ?? '', cookie: req.headers.get('cookie') ?? '' }; } async function getRequestBody( req: NextRequest ): Promise<{ content: string; contentType: string } | undefined> { if (req.method !== 'POST') return undefined; const content = await req.text(); if (!content.length) return undefined; return { content, contentType: req.headers.get('content-type') ?? 'application/x-www-form-urlencoded' }; } function fetchCommerce( url: string, req: NextRequest, body?: { content: string; contentType: string } ): Promise { const headers = buildCommerceHeaders(req); if (body) { headers['content-type'] = body.contentType; } return fetch(url, { method: body ? 'POST' : 'GET', headers, body: body?.content, redirect: 'manual' }); } function forwardCookies(from: Response, to: NextResponse): void { from.headers.getSetCookie().forEach((cookie) => { to.headers.append('set-cookie', cookie); }); } function buildRedirectResponse( commerceResponse: Response, location: string, origin: string ): NextResponse { const response = NextResponse.redirect(new URL(location, origin)); forwardCookies(commerceResponse, response); return response; } function commercePassthrough(commerceResponse: Response): NextResponse { return new NextResponse(commerceResponse.body, { status: commerceResponse.status, headers: commerceResponse.headers }); } function buildOAuthCallbackCookie(referer: string): string { return `pz-oauth-callback-url=${encodeURIComponent(referer)}; Path=/`; } async function handleLogin( req: NextRequest, provider: string ): Promise<{ response: NextResponse; redirected: boolean }> { const commerceResponse = await fetchCommerce( `${Settings.commerceUrl}/${provider}/login/`, req ); const location = commerceResponse.headers.get('location'); if (!location) { return { response: commercePassthrough(commerceResponse), redirected: false }; } const response = buildRedirectResponse( commerceResponse, location, req.nextUrl.origin ); response.headers.append( 'set-cookie', buildOAuthCallbackCookie(req.headers.get('referer') || '') ); return { response, redirected: true }; } async function handleCallback( req: NextRequest, provider: string, search: string ): Promise<{ response: NextResponse; redirected: boolean }> { const body = await getRequestBody(req); const commerceResponse = await fetchCommerce( `${Settings.commerceUrl}/${provider}/login/callback/${search}`, req, body ); const location = commerceResponse.headers.get('location'); if (!location) { return { response: commercePassthrough(commerceResponse), redirected: false }; } return { response: buildRedirectResponse( commerceResponse, location, req.nextUrl.origin ), redirected: true }; } function handleBasketRedirect(req: NextRequest): NextResponse | null { const hasSession = req.cookies.get('osessionid'); const messages = req.cookies.get('messages')?.value; if (!messages) return null; if (!messages.includes('Successfully signed in') && !hasSession) return null; let redirectUrl = `${req.nextUrl.origin}${getUrlPathWithLocale( '/auth/oauth-login', req.cookies.get('pz-locale')?.value )}`; const callbackUrl = req.cookies.get('pz-oauth-callback-url')?.value ?? ''; if (callbackUrl.length) { redirectUrl += `?next=${encodeURIComponent(callbackUrl)}`; } const response = NextResponse.redirect(redirectUrl); response.cookies.delete('messages'); response.cookies.delete('pz-oauth-callback-url'); return response; } const withOauthLogin = (middleware: NextMiddleware) => async (req: NextRequest, event: NextFetchEvent) => { const { pathname, search } = req.nextUrl; if (!pathname.includes('/login') && !pathname.startsWith('/baskets/basket')) { return middleware(req, event); } logger.info('OAuth login redirect', { host: req.headers.get('host'), 'x-forwarded-host': req.headers.get('x-forwarded-host'), 'x-forwarded-for': req.headers.get('x-forwarded-for') }); const loginMatch = LOGIN_URL_REGEX.exec(pathname); if (loginMatch) { try { const { response, redirected } = await handleLogin(req, loginMatch[1]); if (!redirected) { logger.warn('OAuth login: no redirect from commerce', { provider: loginMatch[1] }); } return response; } catch (error) { logger.error('OAuth login fetch failed', { error }); return middleware(req, event); } } const callbackMatch = CALLBACK_URL_REGEX.exec(pathname); if (callbackMatch) { try { const { response, redirected } = await handleCallback( req, callbackMatch[1], search ); if (!redirected) { logger.warn('OAuth callback: no redirect from commerce', { provider: callbackMatch[1] }); } return response; } catch (error) { logger.error('OAuth callback fetch failed', { error }); return middleware(req, event); } } if (pathname.startsWith('/baskets/basket')) { const response = handleBasketRedirect(req); if (response) return response; } return middleware(req, event); }; export default withOauthLogin;