后端迁移到fastapi (#3)
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { NButton, NCard, NResult, NSpin } from 'naive-ui'
|
||||
import { createDetachedAuthClient, resolveEmailAuthCallback } from '@/lib/authEmail'
|
||||
import { apiRequest } from '@/lib/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const state = ref<'loading' | 'success' | 'failed'>('loading')
|
||||
@@ -22,18 +23,18 @@ function startCountdown() {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const confirmClient = createDetachedAuthClient()
|
||||
const result = await resolveEmailAuthCallback({
|
||||
client: confirmClient,
|
||||
allowedTypes: ['signup', 'email'],
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
const token = typeof route.query.token === 'string' ? route.query.token : ''
|
||||
if (!token) {
|
||||
state.value = 'failed'
|
||||
return
|
||||
}
|
||||
|
||||
await confirmClient.auth.signOut()
|
||||
await apiRequest('/auth/confirm-email', {
|
||||
method: 'POST',
|
||||
auth: false,
|
||||
body: { token },
|
||||
})
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
state.value = 'success'
|
||||
startCountdown()
|
||||
} catch {
|
||||
|
||||
@@ -30,16 +30,18 @@ async function handleLogin() {
|
||||
|
||||
<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.1fr_0.9fr] lg:items-center">
|
||||
<section class="border border-slate-200 bg-[linear-gradient(135deg,#f0fdfa_0%,#ffffff_48%,#eff6ff_100%)] p-8 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)]">
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-teal-700">Sky Log In</p>
|
||||
<h1 class="mt-4 max-w-xl text-5xl font-black leading-[1.05] text-slate-900">
|
||||
登录后继续记录
|
||||
<span class="block text-teal-700">你眼中的每一朵云</span>
|
||||
</h1>
|
||||
<p class="mt-6 max-w-lg text-base leading-8 text-slate-600">
|
||||
在地图、图鉴和画廊之间同步你的观测记录。上传新的云图后,图鉴会自动点亮,画廊也会按时间收纳你的作品。
|
||||
</p>
|
||||
<div class="mx-auto grid max-w-6xl gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-stretch">
|
||||
<section class="flex h-full flex-col justify-between border border-slate-200 bg-[linear-gradient(135deg,#f0fdfa_0%,#ffffff_48%,#eff6ff_100%)] p-8 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)]">
|
||||
<div>
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-teal-700">Sky Log In</p>
|
||||
<h1 class="mt-4 max-w-xl text-5xl font-black leading-[1.05] text-slate-900">
|
||||
登录后继续记录
|
||||
<span class="block text-teal-700">你眼中的每一朵云</span>
|
||||
</h1>
|
||||
<!-- <p class="mt-6 max-w-lg text-base leading-8 text-slate-600">
|
||||
在地图、图鉴和画廊之间同步你的观测记录。上传新的云图后,图鉴会自动点亮,画廊也会按时间收纳你的作品。
|
||||
</p> -->
|
||||
</div>
|
||||
<div class="mt-8 flex gap-4">
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Collection</div>
|
||||
@@ -54,7 +56,7 @@ async function handleLogin() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NCard class="h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<div class="mb-8">
|
||||
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Member Access</div>
|
||||
<h2 class="mt-3 text-3xl font-bold text-slate-900">登录 OpenCloud</h2>
|
||||
|
||||
@@ -55,9 +55,9 @@ async function handleRegister() {
|
||||
加入天空探索者
|
||||
<span class="block text-sky-700">建立你的云图档案</span>
|
||||
</h1>
|
||||
<p class="mt-6 max-w-lg text-base leading-8 text-slate-600">
|
||||
<!-- <p class="mt-6 max-w-lg text-base leading-8 text-slate-600">
|
||||
注册后即可上传云图、点亮图鉴、在社区画廊里按时间展示你的观测记录。所有页面都会围绕你的个人云层档案同步更新。
|
||||
</p>
|
||||
</p> -->
|
||||
</div>
|
||||
<div class="mt-8 flex gap-4">
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, 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'
|
||||
import { apiRequest } from '@/lib/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const password = ref('')
|
||||
@@ -13,10 +13,11 @@ const error = ref('')
|
||||
const state = ref<'checking' | 'ready' | 'invalid' | 'success'>('checking')
|
||||
const loading = ref(false)
|
||||
const countdown = ref(5)
|
||||
const resetToken = ref('')
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
state.value === 'ready' && password.value.length >= 6 && password.value === confirmPassword.value,
|
||||
state.value === 'ready' && password.value.length >= 8 && password.value === confirmPassword.value,
|
||||
)
|
||||
|
||||
function showInvalidRecoveryLink() {
|
||||
@@ -26,19 +27,12 @@ function showInvalidRecoveryLink() {
|
||||
|
||||
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')
|
||||
|| message.includes('otp_expired')
|
||||
|| message.includes('expired')
|
||||
) {
|
||||
if (message.includes('无效') || message.includes('过期') || message.includes('expired')) {
|
||||
return '密码重置链接无效或已过期,请重新发送邮件。'
|
||||
}
|
||||
|
||||
if (message.includes('Password should be at least')) {
|
||||
return '新密码至少需要 6 位。'
|
||||
if (message.includes('8')) {
|
||||
return '新密码至少需要 8 位。'
|
||||
}
|
||||
|
||||
return message || '密码重置失败,请稍后重试。'
|
||||
@@ -49,31 +43,27 @@ function startCountdown() {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
if (countdownTimer) clearInterval(countdownTimer)
|
||||
router.push('/')
|
||||
router.push('/login')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function initializeRecoverySession() {
|
||||
const result = await resolveEmailAuthCallback({
|
||||
client: supabase,
|
||||
allowedTypes: ['recovery'],
|
||||
})
|
||||
|
||||
if (!result.ok || !result.session?.user) {
|
||||
error.value = getResetErrorMessage(result.ok ? '链接无效或已过期。' : result.error)
|
||||
state.value = 'invalid'
|
||||
const token = typeof route.query.token === 'string' ? route.query.token : ''
|
||||
if (!token) {
|
||||
showInvalidRecoveryLink()
|
||||
return
|
||||
}
|
||||
|
||||
resetToken.value = token
|
||||
state.value = 'ready'
|
||||
}
|
||||
|
||||
async function handleResetPassword() {
|
||||
error.value = ''
|
||||
|
||||
if (password.value.length < 6) {
|
||||
error.value = '新密码至少需要 6 位。'
|
||||
if (password.value.length < 8) {
|
||||
error.value = '新密码至少需要 8 位。'
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
@@ -81,17 +71,13 @@ async function handleResetPassword() {
|
||||
return
|
||||
}
|
||||
|
||||
const { data: { session }, error: sessionError } = await supabase.auth.getSession()
|
||||
if (sessionError || !session?.user) {
|
||||
showInvalidRecoveryLink()
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { error: updateError } = await supabase.auth.updateUser({ password: password.value })
|
||||
if (updateError) throw updateError
|
||||
|
||||
await apiRequest('/auth/reset-password', {
|
||||
method: 'POST',
|
||||
auth: false,
|
||||
body: { token: resetToken.value, password: password.value },
|
||||
})
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
countdown.value = 5
|
||||
state.value = 'success'
|
||||
@@ -123,12 +109,12 @@ onUnmounted(() => {
|
||||
v-if="state === 'success'"
|
||||
status="success"
|
||||
title="密码已重置"
|
||||
description="你已使用新密码完成更新,正在返回地图页。"
|
||||
description="你已使用新密码完成更新,现在可以重新登录。"
|
||||
>
|
||||
<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('/')">进入地图</NButton>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</NResult>
|
||||
@@ -162,7 +148,7 @@ onUnmounted(() => {
|
||||
required
|
||||
show-password-on="click"
|
||||
autocomplete="new-password"
|
||||
placeholder="至少 6 位"
|
||||
placeholder="至少 8 位"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user