I’ve been trying to setup supabase auth to work with magic links for days now. I cannot seem to understand what the issue is, please see below:
Basically when the magic link is opened, which looks like this:
https://supabase.co/auth/v1/verify?token=03923092039&type=magiclink&redirect_to=http://localhost:3000/auth/callback
The page should open a protected page (a protected page should ideally be any page that is not in the public routes seen in middleware. Currently the issue, is it just redirects back to the signin page once the magic link is open (session does start, seen in local storage) why does it not go to /new-page?
Please let me know your thoughts, I greatly appreciate it!
app/(auth)//signin/page.tsx
'use client'
import { useState, useEffect } from 'react'
import { supabase } from '@/app/utils/supabase'
import AuthHeader from '../auth-header'
import AuthImage from '../auth-image'
const validateEmail = (email: string) => {
const re = /S+@S+.S+/
return re.test(email)
}
export default function SignIn() {
const [email, setEmail] = useState('')
const [showWarning, setShowWarning] = useState(false)
const [error, setError] = useState('')
const [countdown, setCountdown] = useState(20)
const [isCounting, setIsCounting] = useState(false)
const [buttonText, setButtonText] = useState('Send Magic Link')
useEffect(() => {
let timer: NodeJS.Timeout
if (isCounting && countdown > 0) {
timer = setTimeout(() => setCountdown(countdown - 1), 1000)
} else if (countdown === 0) {
setIsCounting(false)
setButtonText('Try Again ➜')
}
return () => clearTimeout(timer)
}, [countdown, isCounting])
const handleSendMagicLink = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
if (validateEmail(email)) {
try {
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
})
if (error) throw error
setShowWarning(true)
setError('')
setIsCounting(true)
setCountdown(20)
setButtonText('Try Again in (20)')
} catch (error: any) {
console.error('Error sending OTP:', error)
setError(error.message)
}
} else {
setError('Please provide a valid email address.')
}
}
const resetForm = () => {
setShowWarning(false)
setError('')
setIsCounting(false)
setCountdown(20)
setButtonText('Send Magic Link')
}
const handleResendMagicLink = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
resetForm()
}
// app/utils/supabase/middleware.ts:
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { type NextRequest, NextResponse } from 'next/server';
export const createClient = (request: NextRequest) => {
// Create an unmodified response
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
// If the cookie is updated, update the cookies for the request and response
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
// If the cookie is removed, update the cookies for the request and response
request.cookies.set({
name,
value: '',
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: '',
...options,
});
},
},
}
);
return { supabase, response };
};
// root/middleware:
import { NextResponse, type NextRequest } from 'next/server';
import { createClient } from './app/utils/supabase/middleware';
export async function middleware(request: NextRequest) {
const { supabase, response } = createClient(request);
// Retrieve the user and session information
const { data: { user }, error: userError } = await supabase.auth.getUser();
const { data: sessionData, error: sessionError } = await supabase.auth.getSession();
const session = sessionData?.session;
// Define public routes that don't require authentication
const publicRoutes = ['/signin', '/signup', '/auth/callback''];
// Check if the current path is a public route
const isPublicRoute = publicRoutes.some(route => request.nextUrl.pathname.startsWith(route));
// Log any errors for debugging
if (userError) {
console.error('Error fetching user:', userError);
}
if (sessionError) {
console.error('Error fetching session:', sessionError);
}
// Log session data for debugging
console.log('Session:', session);
// Check if the current path is a public route before redirecting
if (!session && !isPublicRoute) {
console.log('No session found, redirecting to signin');
return NextResponse.redirect(new URL('/signin', request.url));
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
app/(auth)/auth/callback/page.tsx:
"use client";
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { supabase } from '@/app/utils/supabase';
export default function AuthCallback() {
const router = useRouter();
const searchParams = useSearchParams();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleAuthCallback = async () => {
try {
const token = searchParams.get('token');
const type = searchParams.get('type');
if (token && type === 'magiclink') {
console.log('Magic link token found, exchanging for session...');
const { error } = await supabase.auth.setSession({ access_token: token, refresh_token: token });
if (error) throw error;
}
console.log('Fetching session...');
const { data, error } = await supabase.auth.getSession()
const { session } = data
if (error) {
console.error('Error fetching session:', error);
throw error;
}
if (session) {
console.log('Session found:', session);
console.log('Authentication successful, redirecting...');
router.push('/new-page');
} else {
console.log('No session found, redirecting to signin...');
router.push('/signin');
}
} catch (error) {
console.error('Authentication error:', error);
setError('Authentication failed. Please try again.');
} finally {
setLoading(false);
}
};
handleAuthCallback();
// Set a timeout to redirect if no session is found
const timeoutId = setTimeout(() => {
if (loading) {
console.log('Session check timed out, redirecting to signin...');
router.push('/signin');
}
}, 5000); // 5 seconds timeout
return () => clearTimeout(timeoutId);
}, [router, searchParams, supabase.auth]);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>{error}</p>;
}
return null;
}