fix: resolve auth initialization race and username flicker on login

Always initialize auth before mounting the app so loading state completes
and onAuthStateChange is registered on every route. Gate header auth UI
on !loading to avoid flicker during email confirm and password reset
flows. Load profile before setting user ref so displayUsername shows the
correct name immediately instead of flashing the email.
This commit is contained in:
2026-05-24 16:25:26 +08:00
parent 85471274ab
commit 6fadd63336
3 changed files with 55 additions and 50 deletions
+36 -34
View File
@@ -179,20 +179,21 @@ async function handleLogout() {
</nav>
<NSpace align="center" :size="8" class="shrink-0 md:[&_.n-button]:px-4">
<RouterLink
v-if="authStore.isLoggedIn"
to="/upload"
class="no-underline"
>
<span
class="inline-flex h-8 items-center border border-sky-200 bg-sky-100 px-3 text-sm font-medium uppercase tracking-[0.12em] text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-colors hover:bg-sky-50 hover:text-sky-900 md:h-10 md:px-4"
:class="route.name === 'upload' ? 'ring-1 ring-sky-300' : ''"
<template v-if="!authStore.loading">
<RouterLink
v-if="authStore.isLoggedIn"
to="/upload"
class="no-underline"
>
上传
</span>
</RouterLink>
<span
class="inline-flex h-8 items-center border border-sky-200 bg-sky-100 px-3 text-sm font-medium uppercase tracking-[0.12em] text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-colors hover:bg-sky-50 hover:text-sky-900 md:h-10 md:px-4"
:class="route.name === 'upload' ? 'ring-1 ring-sky-300' : ''"
>
上传
</span>
</RouterLink>
<template v-if="authStore.isLoggedIn">
<template v-if="authStore.isLoggedIn">
<div
class="relative"
@mouseenter="openAccountCard"
@@ -281,29 +282,30 @@ async function handleLogout() {
</div>
</template>
<template v-else>
<RouterLink
to="/login"
class="no-underline"
>
<span
class="inline-flex h-8 items-center border border-slate-200 bg-white/80 px-3 text-sm font-medium text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800 md:h-10 md:px-4"
:class="route.name === 'login' ? 'bg-teal-50 ring-1 ring-teal-200 text-teal-800' : ''"
<template v-else>
<RouterLink
to="/login"
class="no-underline"
>
登录
</span>
</RouterLink>
<RouterLink
to="/register"
class="no-underline"
>
<span
class="inline-flex h-8 items-center border border-sky-200 bg-sky-100 px-3 text-sm font-medium text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-colors hover:bg-sky-50 hover:text-sky-900 md:h-10 md:px-4"
:class="route.name === 'register' ? 'ring-1 ring-sky-300' : ''"
<span
class="inline-flex h-8 items-center border border-slate-200 bg-white/80 px-3 text-sm font-medium text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800 md:h-10 md:px-4"
:class="route.name === 'login' ? 'bg-teal-50 ring-1 ring-teal-200 text-teal-800' : ''"
>
登录
</span>
</RouterLink>
<RouterLink
to="/register"
class="no-underline"
>
注册
</span>
</RouterLink>
<span
class="inline-flex h-8 items-center border border-sky-200 bg-sky-100 px-3 text-sm font-medium text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-colors hover:bg-sky-50 hover:text-sky-900 md:h-10 md:px-4"
:class="route.name === 'register' ? 'ring-1 ring-sky-300' : ''"
>
注册
</span>
</RouterLink>
</template>
</template>
</NSpace>
</div>
@@ -338,7 +340,7 @@ async function handleLogout() {
图鉴
</RouterLink>
<RouterLink
v-if="authStore.isLoggedIn"
v-if="!authStore.loading && authStore.isLoggedIn"
to="/profile"
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
:class="route.name === 'profile' ? activeNavClass : inactiveNavClass"
+2 -7
View File
@@ -14,12 +14,7 @@ app.use(pinia)
const authStore = useAuthStore()
if (['/auth/confirm', '/auth/reset-password'].includes(window.location.pathname)) {
authStore.initialize().then(() => {
app.use(router)
app.mount('#app')
} else {
authStore.initialize().then(() => {
app.use(router)
app.mount('#app')
})
}
})
+17 -9
View File
@@ -12,12 +12,13 @@ export const useAuthStore = defineStore('auth', () => {
const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => profile.value?.role === 'admin')
async function fetchProfile() {
if (!user.value) return
async function fetchProfile(userId?: string) {
const id = userId ?? user.value?.id
if (!id) return
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', user.value.id)
.eq('id', id)
.single()
if (data) {
profile.value = data as Profile
@@ -53,8 +54,8 @@ export const useAuthStore = defineStore('auth', () => {
}
throw error
}
await fetchProfile(data.user.id)
user.value = data.user
await fetchProfile()
}
async function register(email: string, password: string, username: string) {
@@ -122,14 +123,21 @@ export const useAuthStore = defineStore('auth', () => {
async function initialize() {
const { data: { session } } = await supabase.auth.getSession()
user.value = session?.user ?? null
if (user.value) await fetchProfile()
const initialUser = session?.user ?? null
if (initialUser) await fetchProfile(initialUser.id)
user.value = initialUser
loading.value = false
supabase.auth.onAuthStateChange((_event, session) => {
user.value = session?.user ?? null
if (user.value) fetchProfile()
else profile.value = null
const nextUser = session?.user ?? null
if (nextUser) {
fetchProfile(nextUser.id).then(() => {
user.value = nextUser
})
} else {
user.value = null
profile.value = null
}
})
}