后端迁移到fastapi (#3)

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-07-18 20:49:09 +08:00
parent 217b81c506
commit 51743645e2
71 changed files with 845 additions and 3580 deletions
+68 -116
View File
@@ -1,149 +1,102 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { supabase } from '@/lib/supabase'
import type { User } from '@supabase/supabase-js'
import {
AUTH_EXPIRED_EVENT,
AUTH_UPDATED_EVENT,
apiRequest,
clearAccessToken,
refreshSession,
setAccessToken,
} from '@/lib/api'
import type { AuthResponse, AuthUser } from '@/types/api'
import type { Profile } from '@/types/database'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const user = ref<AuthUser | null>(null)
const profile = ref<Profile | null>(null)
const loading = ref(true)
let listenersAttached = false
const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => profile.value?.role === 'admin')
async function fetchProfile(userId?: string) {
const id = userId ?? user.value?.id
if (!id) return
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', id)
.single()
if (data) {
profile.value = data as Profile
}
function applyAuth(auth: AuthResponse) {
setAccessToken(auth.access_token)
user.value = auth.user
profile.value = auth.profile
}
async function ensureUsernameAvailable(username: string, currentUserId?: string) {
let query = supabase
.from('profiles')
.select('id')
.eq('username', username)
.limit(1)
if (currentUserId) {
query = query.neq('id', currentUserId)
}
const { data, error } = await query
if (error) throw error
if (data?.length) {
throw new Error('这个昵称已经被使用,请换一个。')
}
}
async function login(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
if (error.message.includes('Invalid login credentials')) {
throw new Error('邮箱或密码错误')
}
if (error.message.includes('Email not confirmed')) {
throw new Error('邮箱未确认,请先查收确认邮件')
}
throw error
}
await fetchProfile(data.user.id)
user.value = data.user
}
async function register(email: string, password: string, username: string) {
await ensureUsernameAvailable(username)
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: { username },
emailRedirectTo: `${window.location.origin}/auth/confirm`,
},
})
if (error) {
if (error.message.includes('already registered')) {
throw new Error('该邮箱已被注册')
}
if (error.message.includes('Password')) {
throw new Error('密码强度不足,请使用至少6位密码')
}
throw error
}
}
async function logout() {
const { error } = await supabase.auth.signOut()
if (error) throw error
function clearAuth() {
clearAccessToken()
user.value = null
profile.value = null
}
async function updateUsername(username: string) {
if (!user.value) throw new Error('请先登录。')
async function login(email: string, password: string) {
const auth = await apiRequest<AuthResponse>('/auth/login', {
method: 'POST',
auth: false,
body: { email, password },
})
applyAuth(auth)
}
await ensureUsernameAvailable(username, user.value.id)
async function register(email: string, password: string, username: string) {
await apiRequest('/auth/register', {
method: 'POST',
auth: false,
body: { email, password, username },
})
}
const { data, error } = await supabase
.from('profiles')
.update({ username })
.eq('id', user.value.id)
.select('*')
.single()
if (error) {
if (error.code === '23505') {
throw new Error('这个昵称已经被使用,请换一个。')
}
throw error
async function logout() {
try {
await apiRequest('/auth/logout', { method: 'POST', retry: false })
} finally {
clearAuth()
}
}
profile.value = data as Profile
async function updateUsername(username: string) {
const updatedProfile = await apiRequest<Profile>('/profiles/me', {
method: 'PATCH',
body: { username },
})
profile.value = updatedProfile
}
async function updatePassword(password: string) {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
const auth = await apiRequest<AuthResponse>('/auth/password', {
method: 'PATCH',
body: { password },
})
applyAuth(auth)
}
async function sendPasswordReset(email: string) {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`,
await apiRequest('/auth/forgot-password', {
method: 'POST',
auth: false,
body: { email },
})
if (error) {
if (error.message.includes('rate limit')) {
throw new Error('发送过于频繁,请稍后再试。')
}
throw error
}
}
async function initialize() {
const { data: { session } } = await supabase.auth.getSession()
const initialUser = session?.user ?? null
if (initialUser) await fetchProfile(initialUser.id)
user.value = initialUser
loading.value = false
if (!listenersAttached) {
window.addEventListener(AUTH_UPDATED_EVENT, event => {
applyAuth((event as CustomEvent<AuthResponse>).detail)
})
window.addEventListener(AUTH_EXPIRED_EVENT, clearAuth)
listenersAttached = true
}
supabase.auth.onAuthStateChange((_event, session) => {
const nextUser = session?.user ?? null
if (nextUser) {
fetchProfile(nextUser.id).then(() => {
user.value = nextUser
})
} else {
user.value = null
profile.value = null
}
})
try {
applyAuth(await refreshSession())
} catch {
clearAuth()
} finally {
loading.value = false
}
}
return {
@@ -152,7 +105,6 @@ export const useAuthStore = defineStore('auth', () => {
loading,
isLoggedIn,
isAdmin,
fetchProfile,
login,
register,
logout,