|
| 1 | +import type { NextRequest } from 'next/server' |
| 2 | +import { NextResponse } from 'next/server' |
| 3 | +import { AUTH_URLS } from '@/configs/urls' |
| 4 | +import { l } from '@/core/shared/clients/logger/logger' |
| 5 | + |
| 6 | +// Auth.js renders its built-in `${basePath}/error` page when something fails |
| 7 | +// during the OAuth dance (most commonly a stale state/PKCE/nonce cookie that |
| 8 | +// expired while the user lingered on the Ory hosted UI). We point |
| 9 | +// `pages.error` here so the user never sees that page - we log the failure |
| 10 | +// for observability and bounce them back to /sign-in, which restarts the |
| 11 | +// flow with fresh cookies via the middleware -> oauth-start chain. |
| 12 | +// |
| 13 | +// A short-lived cookie prevents tight loops when the underlying failure is |
| 14 | +// genuinely persistent (e.g. ORY_SDK_URL misconfigured). After one recovery |
| 15 | +// attempt in the window, subsequent failures fall back to the marketing |
| 16 | +// root so the user isn't bounced indefinitely. |
| 17 | +const RECOVERY_COOKIE = 'auth_recover_attempted' |
| 18 | +const RECOVERY_COOKIE_MAX_AGE_SECONDS = 30 |
| 19 | + |
| 20 | +export async function GET(request: NextRequest) { |
| 21 | + const errorCode = request.nextUrl.searchParams.get('error') ?? 'unknown' |
| 22 | + const alreadyAttempted = request.cookies.get(RECOVERY_COOKIE)?.value === '1' |
| 23 | + |
| 24 | + l.error( |
| 25 | + { |
| 26 | + key: 'oauth_recover:auth_js_error', |
| 27 | + context: { error_code: errorCode, already_attempted: alreadyAttempted }, |
| 28 | + }, |
| 29 | + 'Auth.js OAuth flow failed; recovering user' |
| 30 | + ) |
| 31 | + |
| 32 | + const destination = alreadyAttempted ? '/' : AUTH_URLS.SIGN_IN |
| 33 | + const response = NextResponse.redirect(new URL(destination, request.url)) |
| 34 | + |
| 35 | + if (alreadyAttempted) { |
| 36 | + response.cookies.delete(RECOVERY_COOKIE) |
| 37 | + } else { |
| 38 | + response.cookies.set(RECOVERY_COOKIE, '1', { |
| 39 | + maxAge: RECOVERY_COOKIE_MAX_AGE_SECONDS, |
| 40 | + httpOnly: true, |
| 41 | + sameSite: 'lax', |
| 42 | + path: '/', |
| 43 | + secure: process.env.NODE_ENV === 'production', |
| 44 | + }) |
| 45 | + } |
| 46 | + |
| 47 | + return response |
| 48 | +} |
0 commit comments