import { NextRequest, NextResponse } from 'next/server'; const TOKEN_COOKIE = 'brainerce_customer_token'; const LOGGED_IN_COOKIE = 'brainerce_logged_in'; const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace( /\/$/, '' ); function isSecure(): boolean { return process.env.NODE_ENV === 'production'; } /** * Forward a failure to the client callback page as a stable snake_case code. * * The backend uses the RFC 6749 §4.1.2.1 shape (`oauth_error` = code, * `error_description` = English developer detail), so codes are what the page * switches on to pick localized copy. The description is deliberately NOT * forwarded — it is not shopper-facing copy. */ function errorRedirect(base: URL, code: string): NextResponse { base.searchParams.set('oauth_error', code); const response = NextResponse.redirect(base); response.headers.set('Referrer-Policy', 'no-referrer'); return response; } /** * OAuth callback handler. * * The backend redirects here with ?oauth_success=true&auth_code=. * We exchange the auth_code for the real JWT via a server-to-server POST (so the * JWT never appears in any URL), then set an httpOnly cookie and redirect to the * client-side callback page. */ export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; const authCode = searchParams.get('auth_code'); const oauthSuccess = searchParams.get('oauth_success'); const oauthError = searchParams.get('oauth_error'); const redirectUrl = new URL('/auth/callback', request.url); if (oauthError) { return errorRedirect(redirectUrl, oauthError); } if (oauthSuccess !== 'true' || !authCode) { return errorRedirect(redirectUrl, 'invalid_request'); } // Exchange the one-time auth_code for the real JWT. // This is a server-to-server call — the JWT never enters the browser URL. let token: string; try { const exchangeRes = await fetch(`${BACKEND_URL}/api/oauth/customer/exchange`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: authCode }), }); if (!exchangeRes.ok) { throw new Error(`exchange responded ${exchangeRes.status}`); } const data = await exchangeRes.json(); if (!data.token) throw new Error('missing token in exchange response'); token = data.token; } catch (err) { console.error('[oauth-callback] auth_code exchange failed:', err); return errorRedirect(redirectUrl, 'server_error'); } redirectUrl.searchParams.set('oauth_success', 'true'); const response = NextResponse.redirect(redirectUrl); response.cookies.set(TOKEN_COOKIE, token, { httpOnly: true, secure: isSecure(), sameSite: 'lax', path: '/', maxAge: COOKIE_MAX_AGE, }); response.cookies.set(LOGGED_IN_COOKIE, '1', { httpOnly: false, secure: isSecure(), sameSite: 'lax', path: '/', maxAge: COOKIE_MAX_AGE, }); response.headers.set('Referrer-Policy', 'no-referrer'); return response; }