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.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user