Files
linumiq_net-web_app/app/auth/confirm/route.ts
T
Gerhard Scheikl 37f1e7bbd5 feat(auth): SSR email confirmation flow
- Add /auth/confirm GET route handler that verifies the signup token via
  verifyOtp({ type, token_hash }) and falls back to exchangeCodeForSession
  when a PKCE code is present. Whitelists  to same-origin paths.
- Signup: pass emailRedirectTo=<APP_URL>/auth/confirm, show a
  'check your email' confirmation state with a resend action (cooldown),
  and handle the already-registered case.
- Login: detect email_not_confirmed and offer a resend-confirmation action;
  surface verification_failed errors from the confirm route.
2026-05-31 20:38:48 +02:00

54 lines
1.9 KiB
TypeScript

import { NextResponse, type NextRequest } from 'next/server';
import type { EmailOtpType } from '@supabase/supabase-js';
import { createSupabaseServerClient } from '@/lib/supabase/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
/**
* Whitelist `next` to same-origin relative paths only (open-redirect guard).
* Anything that isn't a single-slash-rooted path falls back to /dashboard.
*/
function safeNext(raw: string | null): string {
if (!raw || !raw.startsWith('/') || raw.startsWith('//')) {
return '/dashboard';
}
return raw;
}
/**
* Email confirmation / OTP verification landing route.
*
* Handles both GoTrue link styles defensively:
* - token_hash template: /auth/confirm?token_hash=<hash>&type=signup -> verifyOtp
* - PKCE/code redirect: /auth/confirm?code=<code> -> exchangeCodeForSession
*
* On success the server client persists the session cookies and we redirect to
* `next` (default /dashboard). On any failure we send the user to /login with a
* verification_failed flag so they can resend a fresh confirmation email.
*/
export async function GET(request: NextRequest) {
const url = request.nextUrl;
const token_hash = url.searchParams.get('token_hash');
const type = url.searchParams.get('type') as EmailOtpType | null;
const code = url.searchParams.get('code');
const next = safeNext(url.searchParams.get('next'));
const base = process.env.NEXT_PUBLIC_APP_URL ?? url.origin;
const supabase = createSupabaseServerClient();
if (token_hash && type) {
const { error } = await supabase.auth.verifyOtp({ type, token_hash });
if (!error) {
return NextResponse.redirect(new URL(next, base));
}
} else if (code) {
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(new URL(next, base));
}
}
return NextResponse.redirect(new URL('/login?error=verification_failed', base));
}