From 0e2f24288f678192278a61cd32c90f668c7d08b0 Mon Sep 17 00:00:00 2001 From: Mplan Date: Sun, 31 May 2026 15:38:38 +0800 Subject: [PATCH] fix: handle auth email callbacks explicitly --- src/lib/supabase.ts | 7 ++- src/views/auth/AuthConfirmView.vue | 33 ++++++++++---- src/views/auth/ResetPasswordView.vue | 65 ++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 11bd985..95820f9 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -7,4 +7,9 @@ if (!supabaseUrl || !supabaseKey) { throw new Error('Missing Supabase environment variables') } -export const supabase = createClient(supabaseUrl, supabaseKey) +export const supabase = createClient(supabaseUrl, supabaseKey, { + auth: { + // Email confirmation and password recovery are handled by route components. + detectSessionInUrl: false, + }, +}) diff --git a/src/views/auth/AuthConfirmView.vue b/src/views/auth/AuthConfirmView.vue index 6be76a2..f6fac75 100644 --- a/src/views/auth/AuthConfirmView.vue +++ b/src/views/auth/AuthConfirmView.vue @@ -22,32 +22,47 @@ function startCountdown() { onMounted(async () => { try { + const search = new URLSearchParams(window.location.search) const hash = window.location.hash.slice(1) const params = new URLSearchParams(hash) + const code = search.get('code') const accessToken = params.get('access_token') const refreshToken = params.get('refresh_token') const type = params.get('type') - const error = params.get('error') + const error = search.get('error') ?? params.get('error') if (error) { state.value = 'failed' return } - if (type === 'signup' && accessToken && refreshToken) { + if (code || (type === 'signup' && accessToken && refreshToken)) { const confirmClient = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY, + { + auth: { + detectSessionInUrl: false, + }, + }, ) - const { error: setSessionError } = await confirmClient.auth.setSession({ - access_token: accessToken, - refresh_token: refreshToken, - }) + if (code) { + const { error: exchangeError } = await confirmClient.auth.exchangeCodeForSession(code) + if (exchangeError) { + state.value = 'failed' + return + } + } else { + const { error: setSessionError } = await confirmClient.auth.setSession({ + access_token: accessToken!, + refresh_token: refreshToken!, + }) - if (setSessionError) { - state.value = 'failed' - return + if (setSessionError) { + state.value = 'failed' + return + } } await confirmClient.auth.signOut() diff --git a/src/views/auth/ResetPasswordView.vue b/src/views/auth/ResetPasswordView.vue index 0c4bfe6..ba2669e 100644 --- a/src/views/auth/ResetPasswordView.vue +++ b/src/views/auth/ResetPasswordView.vue @@ -23,6 +23,20 @@ function showInvalidRecoveryLink() { state.value = 'invalid' } +function getRecoveryParams() { + const search = new URLSearchParams(window.location.search) + const hash = new URLSearchParams(window.location.hash.slice(1)) + + return { + code: search.get('code'), + accessToken: hash.get('access_token'), + refreshToken: hash.get('refresh_token'), + type: hash.get('type'), + error: search.get('error') ?? hash.get('error'), + errorDescription: search.get('error_description') ?? hash.get('error_description'), + } +} + function getResetErrorMessage(value: unknown) { const message = value instanceof Error ? value.message : '' if ( @@ -47,6 +61,47 @@ function startCountdown() { }, 1000) } +async function initializeRecoverySession() { + const { + code, + accessToken, + refreshToken, + type, + error: recoveryError, + errorDescription, + } = getRecoveryParams() + + if (recoveryError || errorDescription) { + showInvalidRecoveryLink() + return + } + + try { + if (code) { + const { error } = await supabase.auth.exchangeCodeForSession(code) + if (error) throw error + } else if (type === 'recovery' && accessToken && refreshToken) { + const { error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }) + if (error) throw error + } + + const { data: { session }, error } = await supabase.auth.getSession() + if (error || !session?.user) { + showInvalidRecoveryLink() + return + } + + window.history.replaceState(null, '', window.location.pathname) + state.value = 'ready' + } catch (e) { + error.value = getResetErrorMessage(e) + state.value = 'invalid' + } +} + async function handleResetPassword() { error.value = '' @@ -85,14 +140,8 @@ async function handleResetPassword() { } } -onMounted(async () => { - const { data: { session }, error: sessionError } = await supabase.auth.getSession() - if (sessionError || !session?.user) { - showInvalidRecoveryLink() - return - } - - state.value = 'ready' +onMounted(() => { + void initializeRecoverySession() }) onUnmounted(() => {