Skip to content

Commit f35bbc0

Browse files
committed
Improve Supabase password recovery flow handling
Enhances password recovery by listening for PASSWORD_RECOVERY events and redirecting users to the reset password page. Refines session detection and token parsing in ResetPassword.vue, updates redirect logic in useSupabase.ts, and adds router guard for malformed recovery token URLs. Prevents duplicate handling in Auth.vue for recovery flows.
1 parent 9daf00c commit f35bbc0

5 files changed

Lines changed: 55 additions & 19 deletions

File tree

src/App.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ import ImportExpenses from './components/ImportExpenses.vue'
9292
import ToastContainer from './components/ToastContainer.vue'
9393
import { type Expense } from './types'
9494
95-
const { user, loading, signOut, initAuth } = useSupabase()
95+
const { user, loading, signOut, initAuth, supabase } = useSupabase()
9696
const { refreshTrigger } = useExpenseManagement()
9797
const { showForm, editingExpense, openFormForNew, openFormForEdit } = useExpenseForm()
9898
const router = useRouter()
@@ -104,6 +104,13 @@ const showExport = ref(false)
104104
// Initialize auth on app load
105105
onMounted(async () => {
106106
await initAuth()
107+
108+
// Listen for password recovery event
109+
supabase.auth.onAuthStateChange((event, session) => {
110+
if (event === 'PASSWORD_RECOVERY') {
111+
router.push('/reset-password')
112+
}
113+
})
107114
})
108115
109116
const handleLogout = async () => {

src/components/Auth.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ const handleSubmit = async () => {
236236
onMounted(async () => {
237237
// Check if this is an email confirmation redirect
238238
const fullPath = route.fullPath
239+
// Ignore password recovery flows (handled by App.vue)
240+
if (fullPath.includes('type=recovery')) {
241+
return
242+
}
243+
239244
if (fullPath.includes('access_token=') || fullPath.includes('refresh_token=')) {
240245
loading.value = true
241246
try {

src/components/ResetPassword.vue

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,35 @@ const sessionError = ref('')
9696
// Check if user has valid session from reset link
9797
onMounted(async () => {
9898
try {
99+
// First check if we already have a session (e.g. handled by Supabase client or redirected from App.vue)
100+
const { data: { session } } = await supabase.auth.getSession()
101+
102+
if (session) {
103+
console.log('Session already established:', session.user?.email)
104+
checkingSession.value = false
105+
return
106+
}
107+
108+
// Fallback: Try to parse tokens from URL if session not yet established
99109
// The hash contains both the route and the auth tokens
100110
// Format: #/reset-password#access_token=...&refresh_token=...
101111
const fullHash = window.location.hash
102112
console.log('Full hash:', fullHash)
103113
104114
// Extract the auth params after the second #
105115
const hashParts = fullHash.split('#')
106-
const authHash = hashParts.length > 2 ? hashParts[2] : hashParts[1]
116+
// If we have multiple parts, the last one is likely the auth hash
117+
const authHash = hashParts.length > 1 ? hashParts[hashParts.length - 1] : ''
107118
108119
console.log('Auth hash:', authHash)
109120
110-
const hashParams = new URLSearchParams(authHash)
111-
const hasAccessToken = hashParams.has('access_token')
112-
const tokenType = hashParams.get('type')
113-
114-
console.log('Hash params:', {
115-
hasAccessToken,
116-
tokenType,
117-
accessToken: hashParams.get('access_token')?.substring(0, 20) + '...'
118-
})
119-
120-
if (!hasAccessToken) {
121-
sessionError.value = '重置链接无效。请从邮件中点击完整的重置链接。'
122-
checkingSession.value = false
123-
return
121+
if (!authHash) {
122+
sessionError.value = '无法检测到有效的会话或重置令牌。'
123+
checkingSession.value = false
124+
return
124125
}
125126
126-
// Manually set the session using the tokens from URL
127+
const hashParams = new URLSearchParams(authHash)
127128
const accessToken = hashParams.get('access_token')
128129
const refreshToken = hashParams.get('refresh_token')
129130
@@ -144,6 +145,7 @@ onMounted(async () => {
144145
console.log('Session established successfully:', data.session.user?.email)
145146
}
146147
} else {
148+
// Only show error if we really don't have a session and no tokens
147149
sessionError.value = '重置链接缺少必要的令牌。'
148150
}
149151
} catch (err) {

src/composables/useSupabase.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ export function useSupabase() {
113113
const resetPasswordForEmail = async (email: string) => {
114114
// Use current origin for redirect, fallback to env variable
115115
const baseUrl = import.meta.env.VITE_SUPABASE_REDIRECT_URL || window.location.origin
116-
const redirectUrl = baseUrl.endsWith('/') ? `${baseUrl}#/reset-password` : `${baseUrl}/#/reset-password`
116+
// Note: We don't append #/reset-password here because Supabase redirect validation often fails with hash fragments.
117+
// Instead, we rely on the onAuthStateChange event listener in App.vue to detect PASSWORD_RECOVERY and redirect.
118+
const redirectUrl = baseUrl
117119

118120
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
119121
redirectTo: redirectUrl

src/router/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,28 @@ const router = createRouter({
4949

5050
// Route guard for authentication
5151
router.beforeEach(async (to, _from, next) => {
52-
const { user, loading, initAuth } = useSupabase()
52+
const { user, loading, initAuth, supabase } = useSupabase()
5353

54+
// Handle malformed hash redirection from Supabase (access_token interpreted as path)
55+
if (to.fullPath.includes('access_token=') && to.fullPath.includes('type=recovery')) {
56+
console.log('Detected recovery token in path, handling manual redirect...')
57+
// Extract parameters from the path/hash
58+
// The path might look like /access_token=...&type=recovery...
59+
const paramsString = to.fullPath.startsWith('/') ? to.fullPath.substring(1) : to.fullPath
60+
const params = new URLSearchParams(paramsString)
61+
62+
const accessToken = params.get('access_token')
63+
const refreshToken = params.get('refresh_token')
64+
65+
if (accessToken && refreshToken) {
66+
await supabase.auth.setSession({
67+
access_token: accessToken,
68+
refresh_token: refreshToken
69+
})
70+
return next('/reset-password')
71+
}
72+
}
73+
5474
// Allow access to reset password page without auth check (session comes from URL)
5575
if (to.path === '/reset-password') {
5676
return next()

0 commit comments

Comments
 (0)