fix: redesign password recovery flow
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
createClient,
|
||||
type EmailOtpType,
|
||||
type Session,
|
||||
type SupabaseClient,
|
||||
} from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
|
||||
|
||||
const knownEmailOtpTypes = new Set<EmailOtpType>([
|
||||
'signup',
|
||||
'invite',
|
||||
'magiclink',
|
||||
'recovery',
|
||||
'email_change',
|
||||
'email',
|
||||
])
|
||||
|
||||
type AuthCallbackParams = {
|
||||
accessToken: string | null
|
||||
code: string | null
|
||||
error: string | null
|
||||
errorCode: string | null
|
||||
errorDescription: string | null
|
||||
refreshToken: string | null
|
||||
tokenHash: string | null
|
||||
type: EmailOtpType | null
|
||||
}
|
||||
|
||||
type ResolveEmailAuthCallbackOptions = {
|
||||
allowedTypes: EmailOtpType[]
|
||||
client: SupabaseClient
|
||||
}
|
||||
|
||||
type ResolveEmailAuthCallbackResult =
|
||||
| {
|
||||
ok: true
|
||||
session: Session | null
|
||||
type: EmailOtpType | null
|
||||
}
|
||||
| {
|
||||
ok: false
|
||||
error: string
|
||||
}
|
||||
|
||||
function parseEmailOtpType(value: string | null): EmailOtpType | null {
|
||||
if (!value || !knownEmailOtpTypes.has(value as EmailOtpType)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return value as EmailOtpType
|
||||
}
|
||||
|
||||
export function readAuthCallbackParams(): AuthCallbackParams {
|
||||
const search = new URLSearchParams(window.location.search)
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1))
|
||||
|
||||
return {
|
||||
code: search.get('code'),
|
||||
tokenHash: search.get('token_hash') ?? hash.get('token_hash'),
|
||||
accessToken: hash.get('access_token'),
|
||||
refreshToken: hash.get('refresh_token'),
|
||||
type: parseEmailOtpType(search.get('type') ?? hash.get('type')),
|
||||
error: search.get('error') ?? hash.get('error'),
|
||||
errorCode: search.get('error_code') ?? hash.get('error_code'),
|
||||
errorDescription: search.get('error_description') ?? hash.get('error_description'),
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuthCallbackUrl() {
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
|
||||
export function createDetachedAuthClient() {
|
||||
return createClient(supabaseUrl, supabaseKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
persistSession: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function resolveEmailAuthCallback(
|
||||
options: ResolveEmailAuthCallbackOptions,
|
||||
): Promise<ResolveEmailAuthCallbackResult> {
|
||||
const { client, allowedTypes } = options
|
||||
const params = readAuthCallbackParams()
|
||||
const allowedTypeSet = new Set(allowedTypes)
|
||||
|
||||
if (params.error || params.errorCode || params.errorDescription) {
|
||||
return {
|
||||
ok: false,
|
||||
error: params.errorDescription || '链接无效或已过期。',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let session: Session | null = null
|
||||
|
||||
if (params.tokenHash && params.type && allowedTypeSet.has(params.type)) {
|
||||
const { data, error } = await client.auth.verifyOtp({
|
||||
token_hash: params.tokenHash,
|
||||
type: params.type,
|
||||
})
|
||||
if (error) throw error
|
||||
session = data.session
|
||||
} else if (params.code) {
|
||||
const { data, error } = await client.auth.exchangeCodeForSession(params.code)
|
||||
if (error) throw error
|
||||
session = data.session
|
||||
} else if (
|
||||
params.accessToken
|
||||
&& params.refreshToken
|
||||
&& params.type
|
||||
&& allowedTypeSet.has(params.type)
|
||||
) {
|
||||
const { data, error } = await client.auth.setSession({
|
||||
access_token: params.accessToken,
|
||||
refresh_token: params.refreshToken,
|
||||
})
|
||||
if (error) throw error
|
||||
session = data.session
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
error: '链接无效或已过期。',
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
const { data, error } = await client.auth.getSession()
|
||||
if (error) throw error
|
||||
session = data.session
|
||||
}
|
||||
|
||||
clearAuthCallbackUrl()
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
session,
|
||||
type: params.type,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '链接无效或已过期。'
|
||||
return {
|
||||
ok: false,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ const router = createRouter({
|
||||
component: () => import('@/views/auth/RegisterView.vue'),
|
||||
meta: { title: '注册', noindex: true },
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
name: 'forgot-password',
|
||||
component: () => import('@/views/auth/ForgotPasswordView.vue'),
|
||||
meta: { title: '忘记密码', noindex: true },
|
||||
},
|
||||
{
|
||||
path: '/auth/confirm',
|
||||
name: 'auth-confirm',
|
||||
|
||||
+6
-1
@@ -118,7 +118,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/auth/reset-password`,
|
||||
})
|
||||
if (error) throw error
|
||||
if (error) {
|
||||
if (error.message.includes('rate limit')) {
|
||||
throw new Error('发送过于频繁,请稍后再试。')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NButton, NCard, NResult, NSpin } from 'naive-ui'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { createDetachedAuthClient, resolveEmailAuthCallback } from '@/lib/authEmail'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -22,58 +22,20 @@ 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 = search.get('error') ?? params.get('error')
|
||||
const confirmClient = createDetachedAuthClient()
|
||||
const result = await resolveEmailAuthCallback({
|
||||
client: confirmClient,
|
||||
allowedTypes: ['signup', 'email'],
|
||||
})
|
||||
|
||||
if (error) {
|
||||
if (!result.ok) {
|
||||
state.value = 'failed'
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
await confirmClient.auth.signOut()
|
||||
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
state.value = 'success'
|
||||
startCountdown()
|
||||
return
|
||||
}
|
||||
|
||||
state.value = 'failed'
|
||||
await confirmClient.auth.signOut()
|
||||
state.value = 'success'
|
||||
startCountdown()
|
||||
} catch {
|
||||
state.value = 'failed'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
|
||||
const email = ref(typeof route.query.email === 'string' ? route.query.email : '')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const submitted = ref(false)
|
||||
const cooldown = ref(0)
|
||||
let cooldownTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const resendButtonLabel = computed(() => {
|
||||
if (loading.value) return '发送中...'
|
||||
if (cooldown.value > 0) return `重新发送(${cooldown.value}s)`
|
||||
return '重新发送'
|
||||
})
|
||||
|
||||
function clearCooldownTimer() {
|
||||
if (!cooldownTimer) return
|
||||
clearInterval(cooldownTimer)
|
||||
cooldownTimer = null
|
||||
}
|
||||
|
||||
function startCooldown() {
|
||||
clearCooldownTimer()
|
||||
cooldown.value = 60
|
||||
cooldownTimer = setInterval(() => {
|
||||
cooldown.value--
|
||||
if (cooldown.value <= 0) {
|
||||
cooldown.value = 0
|
||||
clearCooldownTimer()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleSendResetEmail() {
|
||||
error.value = ''
|
||||
|
||||
if (!email.value.trim()) {
|
||||
error.value = '请输入需要找回密码的邮箱。'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.sendPasswordReset(email.value.trim())
|
||||
submitted.value = true
|
||||
startCooldown()
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '重置邮件发送失败,请稍后重试。'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function resendResetEmail() {
|
||||
if (cooldown.value > 0 || loading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await handleSendResetEmail()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearCooldownTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
|
||||
<div class="mx-auto grid max-w-6xl gap-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
|
||||
<section class="border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_52%,#f0fdfa_100%)] p-8 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)]">
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-sky-700">Password Recovery</p>
|
||||
<h1 class="mt-4 max-w-xl text-5xl font-black leading-[1.05] text-slate-900">
|
||||
重新拿回你的
|
||||
<span class="block text-sky-700">OpenCloud 账号</span>
|
||||
</h1>
|
||||
<p class="mt-6 max-w-lg text-base leading-8 text-slate-600">
|
||||
输入注册邮箱后,我们会发送一封密码重置邮件。打开邮件中的链接,即可进入新的密码设置页面。
|
||||
</p>
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Step 1</div>
|
||||
<div class="mt-2 text-lg font-bold text-slate-900">发送邮件</div>
|
||||
<div class="mt-1 text-sm text-slate-500">使用注册邮箱请求重置链接</div>
|
||||
</div>
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Step 2</div>
|
||||
<div class="mt-2 text-lg font-bold text-slate-900">设置新密码</div>
|
||||
<div class="mt-1 text-sm text-slate-500">在专用页面完成新密码更新</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NResult
|
||||
v-if="submitted"
|
||||
status="success"
|
||||
title="重置邮件已发送"
|
||||
description="如果该邮箱已注册,请查收邮件中的重置链接。"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-slate-500">
|
||||
目标邮箱:
|
||||
<span class="font-semibold text-slate-700">{{ email }}</span>
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<NButton
|
||||
type="primary"
|
||||
class="oc-primary-button oc-primary-button--teal"
|
||||
:disabled="cooldown > 0"
|
||||
:loading="loading"
|
||||
@click="resendResetEmail"
|
||||
>
|
||||
{{ resendButtonLabel }}
|
||||
</NButton>
|
||||
<RouterLink :to="{ path: '/login', query: { email } }" class="no-underline">
|
||||
<NButton type="default" class="oc-panel-button oc-panel-button--neutral">返回登录</NButton>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NResult>
|
||||
|
||||
<template v-else>
|
||||
<div class="mb-8">
|
||||
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Reset Request</div>
|
||||
<h2 class="mt-3 text-3xl font-bold text-slate-900">忘记密码</h2>
|
||||
<p class="mt-2 text-sm text-slate-500">输入你的注册邮箱,我们会发送一封密码重置邮件。</p>
|
||||
</div>
|
||||
|
||||
<NForm @submit.prevent="handleSendResetEmail">
|
||||
<NFormItem label="邮箱">
|
||||
<NInput
|
||||
v-model:value="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NAlert v-if="error" type="error" class="mb-4">
|
||||
{{ error }}
|
||||
</NAlert>
|
||||
|
||||
<NButton
|
||||
attr-type="submit"
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
class="oc-primary-button oc-primary-button--sky"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ loading ? '发送中...' : '发送重置邮件' }}
|
||||
</NButton>
|
||||
</NForm>
|
||||
|
||||
<div class="mt-6 flex flex-wrap items-center justify-between gap-3 text-sm text-slate-500">
|
||||
<p>如果你已经记起密码,可以直接返回登录。</p>
|
||||
<RouterLink :to="{ path: '/login', query: { email } }" class="font-semibold text-sky-700 hover:text-sky-800">
|
||||
返回登录
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</NCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,13 +8,10 @@ const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const email = ref('')
|
||||
const email = ref(typeof route.query.email === 'string' ? route.query.email : '')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const resetMessage = ref('')
|
||||
const resetMode = ref(false)
|
||||
const loading = ref(false)
|
||||
const resetLoading = ref(false)
|
||||
|
||||
async function handleLogin() {
|
||||
error.value = ''
|
||||
@@ -29,26 +26,6 @@ async function handleLogin() {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendResetEmail() {
|
||||
error.value = ''
|
||||
resetMessage.value = ''
|
||||
|
||||
if (!email.value) {
|
||||
error.value = '请输入需要重置密码的邮箱。'
|
||||
return
|
||||
}
|
||||
|
||||
resetLoading.value = true
|
||||
try {
|
||||
await authStore.sendPasswordReset(email.value)
|
||||
resetMessage.value = '重置密码邮件已发送,请查收邮箱。'
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '重置邮件发送失败,请稍后重试'
|
||||
} finally {
|
||||
resetLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -84,7 +61,7 @@ async function handleSendResetEmail() {
|
||||
<p class="mt-2 text-sm text-slate-500">输入邮箱和密码,继续你的天空档案。</p>
|
||||
</div>
|
||||
|
||||
<NForm @submit.prevent="resetMode ? handleSendResetEmail() : handleLogin()">
|
||||
<NForm @submit.prevent="handleLogin">
|
||||
<NFormItem label="邮箱">
|
||||
<NInput
|
||||
v-model:value="email"
|
||||
@@ -94,7 +71,7 @@ async function handleSendResetEmail() {
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem v-if="!resetMode" label="密码">
|
||||
<NFormItem label="密码">
|
||||
<NInput
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
@@ -105,32 +82,19 @@ async function handleSendResetEmail() {
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<p v-else class="mb-5 text-sm leading-6 text-slate-500">
|
||||
输入注册邮箱,我们会发送一封密码重置邮件。点击邮件链接后即可设置新密码。
|
||||
</p>
|
||||
|
||||
<NAlert v-if="error" type="error" class="mb-4">
|
||||
{{ error }}
|
||||
</NAlert>
|
||||
|
||||
<NAlert v-if="resetMessage" type="success" class="mb-4">
|
||||
{{ resetMessage }}
|
||||
</NAlert>
|
||||
|
||||
<NButton
|
||||
attr-type="submit"
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
class="oc-primary-button oc-primary-button--teal"
|
||||
:loading="resetMode ? resetLoading : loading"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-if="resetMode">
|
||||
{{ resetLoading ? '发送中...' : '发送重置邮件' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</template>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</NButton>
|
||||
</NForm>
|
||||
|
||||
@@ -139,13 +103,12 @@ async function handleSendResetEmail() {
|
||||
没有账号?
|
||||
<RouterLink to="/register" class="font-semibold text-teal-700 hover:text-teal-800">去注册</RouterLink>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
<RouterLink
|
||||
:to="email ? { path: '/forgot-password', query: { email } } : { path: '/forgot-password' }"
|
||||
class="font-semibold text-teal-700 transition-colors hover:text-teal-800"
|
||||
@click="resetMode = !resetMode; error = ''; resetMessage = ''"
|
||||
>
|
||||
{{ resetMode ? '返回登录' : '忘记密码?' }}
|
||||
</button>
|
||||
忘记密码?
|
||||
</RouterLink>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult, NSpin } from 'naive-ui'
|
||||
import { resolveEmailAuthCallback } from '@/lib/authEmail'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -23,20 +24,6 @@ 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 (
|
||||
@@ -44,10 +31,16 @@ function getResetErrorMessage(value: unknown) {
|
||||
|| message.includes('session_not_found')
|
||||
|| message.includes('refresh_token_not_found')
|
||||
|| message.includes('Invalid Refresh Token')
|
||||
|| message.includes('otp_expired')
|
||||
|| message.includes('expired')
|
||||
) {
|
||||
return '密码重置链接无效或已过期,请重新发送邮件。'
|
||||
}
|
||||
|
||||
if (message.includes('Password should be at least')) {
|
||||
return '新密码至少需要 6 位。'
|
||||
}
|
||||
|
||||
return message || '密码重置失败,请稍后重试。'
|
||||
}
|
||||
|
||||
@@ -62,44 +55,18 @@ function startCountdown() {
|
||||
}
|
||||
|
||||
async function initializeRecoverySession() {
|
||||
const {
|
||||
code,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
type,
|
||||
error: recoveryError,
|
||||
errorDescription,
|
||||
} = getRecoveryParams()
|
||||
const result = await resolveEmailAuthCallback({
|
||||
client: supabase,
|
||||
allowedTypes: ['recovery'],
|
||||
})
|
||||
|
||||
if (recoveryError || errorDescription) {
|
||||
showInvalidRecoveryLink()
|
||||
if (!result.ok || !result.session?.user) {
|
||||
error.value = getResetErrorMessage(result.ok ? '链接无效或已过期。' : result.error)
|
||||
state.value = 'invalid'
|
||||
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'
|
||||
}
|
||||
state.value = 'ready'
|
||||
}
|
||||
|
||||
async function handleResetPassword() {
|
||||
@@ -215,6 +182,14 @@ onUnmounted(() => {
|
||||
{{ error }}
|
||||
</NAlert>
|
||||
|
||||
<RouterLink
|
||||
v-if="state === 'invalid'"
|
||||
to="/forgot-password"
|
||||
class="mb-4 block text-sm font-semibold text-teal-700 hover:text-teal-800"
|
||||
>
|
||||
返回重新发送重置邮件
|
||||
</RouterLink>
|
||||
|
||||
<NButton
|
||||
attr-type="submit"
|
||||
type="primary"
|
||||
|
||||
Reference in New Issue
Block a user