37f1e7bbd5
- 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.
54 lines
1.9 KiB
TypeScript
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));
|
|
}
|