fix: fix password reset recovery flow
This commit is contained in:
@@ -1,27 +1,50 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult } from 'naive-ui'
|
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult, NSpin } from 'naive-ui'
|
||||||
import { createClient } from '@supabase/supabase-js'
|
import { supabase } from '@/lib/supabase'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const success = ref(false)
|
const state = ref<'checking' | 'ready' | 'invalid' | 'success'>('checking')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const countdown = ref(5)
|
||||||
|
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
const canSubmit = computed(() => password.value.length >= 6 && password.value === confirmPassword.value)
|
const canSubmit = computed(() =>
|
||||||
|
state.value === 'ready' && password.value.length >= 6 && password.value === confirmPassword.value,
|
||||||
|
)
|
||||||
|
|
||||||
function getRecoveryTokens() {
|
function showInvalidRecoveryLink() {
|
||||||
const hash = new URLSearchParams(window.location.hash.slice(1))
|
error.value = '密码重置链接无效或已过期,请重新发送邮件。'
|
||||||
return {
|
state.value = 'invalid'
|
||||||
accessToken: hash.get('access_token'),
|
|
||||||
refreshToken: hash.get('refresh_token'),
|
|
||||||
type: hash.get('type'),
|
|
||||||
error: hash.get('error'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getResetErrorMessage(value: unknown) {
|
||||||
|
const message = value instanceof Error ? value.message : ''
|
||||||
|
if (
|
||||||
|
message.includes('Auth session missing')
|
||||||
|
|| message.includes('session_not_found')
|
||||||
|
|| message.includes('refresh_token_not_found')
|
||||||
|
|| message.includes('Invalid Refresh Token')
|
||||||
|
) {
|
||||||
|
return '密码重置链接无效或已过期,请重新发送邮件。'
|
||||||
|
}
|
||||||
|
|
||||||
|
return message || '密码重置失败,请稍后重试。'
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer)
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResetPassword() {
|
async function handleResetPassword() {
|
||||||
@@ -36,37 +59,45 @@ async function handleResetPassword() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken, refreshToken, type, error: recoveryError } = getRecoveryTokens()
|
const { data: { session }, error: sessionError } = await supabase.auth.getSession()
|
||||||
if (recoveryError || type !== 'recovery' || !accessToken || !refreshToken) {
|
if (sessionError || !session?.user) {
|
||||||
error.value = '密码重置链接无效或已过期,请重新发送邮件。'
|
showInvalidRecoveryLink()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const resetClient = createClient(
|
const { error: updateError } = await supabase.auth.updateUser({ password: password.value })
|
||||||
import.meta.env.VITE_SUPABASE_URL,
|
|
||||||
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
const { error: sessionError } = await resetClient.auth.setSession({
|
|
||||||
access_token: accessToken,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
})
|
|
||||||
if (sessionError) throw sessionError
|
|
||||||
|
|
||||||
const { error: updateError } = await resetClient.auth.updateUser({ password: password.value })
|
|
||||||
if (updateError) throw updateError
|
if (updateError) throw updateError
|
||||||
|
|
||||||
await resetClient.auth.signOut()
|
await supabase.auth.signOut()
|
||||||
window.history.replaceState(null, '', window.location.pathname)
|
window.history.replaceState(null, '', window.location.pathname)
|
||||||
success.value = true
|
countdown.value = 5
|
||||||
|
state.value = 'success'
|
||||||
|
startCountdown()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : '密码重置失败,请稍后重试。'
|
error.value = getResetErrorMessage(e)
|
||||||
|
if (error.value === '密码重置链接无效或已过期,请重新发送邮件。') {
|
||||||
|
state.value = 'invalid'
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const { data: { session }, error: sessionError } = await supabase.auth.getSession()
|
||||||
|
if (sessionError || !session?.user) {
|
||||||
|
showInvalidRecoveryLink()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value = 'ready'
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -74,21 +105,38 @@ async function handleResetPassword() {
|
|||||||
<div class="mx-auto max-w-2xl">
|
<div class="mx-auto max-w-2xl">
|
||||||
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||||
<NResult
|
<NResult
|
||||||
v-if="success"
|
v-if="state === 'success'"
|
||||||
status="success"
|
status="success"
|
||||||
title="密码已重置"
|
title="密码已重置"
|
||||||
description="现在可以使用新密码登录。"
|
description="现在可以使用新密码登录。"
|
||||||
>
|
>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-slate-500">{{ countdown }} 秒后自动跳转登录页面...</p>
|
||||||
<NButton type="primary" class="oc-primary-button oc-primary-button--teal" @click="router.push('/login')">返回登录</NButton>
|
<NButton type="primary" class="oc-primary-button oc-primary-button--teal" @click="router.push('/login')">返回登录</NButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NResult>
|
</NResult>
|
||||||
|
|
||||||
|
<template v-else-if="state === 'checking'">
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<NSpin size="large" />
|
||||||
|
<h1 class="mt-6 text-2xl font-bold text-slate-900">正在验证重置链接...</h1>
|
||||||
|
<p class="mt-2 text-slate-500">请稍候</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Password Recovery</div>
|
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Password Recovery</div>
|
||||||
<h1 class="mt-3 text-3xl font-bold text-slate-900">设置新密码</h1>
|
<h1 class="mt-3 text-3xl font-bold text-slate-900">设置新密码</h1>
|
||||||
<p class="mt-2 text-sm text-slate-500">请输入新的登录密码,提交后当前重置链接会失效。</p>
|
<p class="mt-2 text-sm text-slate-500">
|
||||||
|
{{
|
||||||
|
state === 'invalid'
|
||||||
|
? '当前重置链接不可用,请重新发送密码重置邮件。'
|
||||||
|
: '请输入新的登录密码,提交后当前重置链接会失效。'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NForm @submit.prevent="handleResetPassword">
|
<NForm @submit.prevent="handleResetPassword">
|
||||||
|
|||||||
Reference in New Issue
Block a user