fix(auth): persist login state to localStorage

Save user data, role, and verification status on login so that
page refresh does not lose the session. On mount, AuthBootstrap
always verifies against the backend via getCurrentUserForLogin
to confirm the cookie JWT is still valid.

Remove unused verification store methods (submitVerification,
approveVerification, rejectVerification) — the verify page
already calls lib/api/users.ts directly.
This commit is contained in:
zetaloop
2026-05-01 04:20:02 +08:00
parent 86d1b05271
commit d76866ac3b
2 changed files with 81 additions and 73 deletions
+4 -4
View File
@@ -12,11 +12,11 @@ import { Toaster } from "sonner"
function AuthBootstrap() { function AuthBootstrap() {
const login = useAuthStore((s) => s.login) const login = useAuthStore((s) => s.login)
const isAuthenticated = useAuthStore((s) => s.isAuthenticated) const logout = useAuthStore((s) => s.logout)
const tried = useRef(false) const tried = useRef(false)
useEffect(() => { useEffect(() => {
if (tried.current || isAuthenticated) return if (tried.current) return
tried.current = true tried.current = true
getCurrentUserForLogin() getCurrentUserForLogin()
@@ -24,9 +24,9 @@ function AuthBootstrap() {
login(user, user.verifiedRoles ?? ["consumer"]) login(user, user.verifiedRoles ?? ["consumer"])
}) })
.catch(() => { .catch(() => {
// no valid session — stay logged out logout()
}) })
}, [login, isAuthenticated]) }, [login, logout])
return null return null
} }
+77 -69
View File
@@ -15,6 +15,53 @@ const defaultNotificationPrefs: NotificationPrefs = {
export type ThemePreference = "light" | "dark" | "system" export type ThemePreference = "light" | "dark" | "system"
const STORAGE_KEY = "juwan-auth"
interface PersistedAuth {
user: User
currentRole: UserRole
verifiedRoles: UserRole[]
verificationStatus: Partial<Record<UserRole, VerificationStatus>>
themePreference: ThemePreference
}
function loadPersisted(): PersistedAuth | null {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return null
const parsed = JSON.parse(raw) as PersistedAuth
if (!parsed.user?.id) return null
return parsed
} catch {
return null
}
}
function persist(state: PersistedAuth) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
} catch {}
}
function persistCurrent(state: AuthState) {
if (!state.user) return
persist({
user: state.user,
currentRole: state.currentRole,
verifiedRoles: state.verifiedRoles,
verificationStatus: state.verificationStatus,
themePreference: state.themePreference,
})
}
function clearPersisted() {
try {
localStorage.removeItem(STORAGE_KEY)
} catch {}
}
const persisted = loadPersisted()
interface AuthState { interface AuthState {
isAuthenticated: boolean isAuthenticated: boolean
currentRole: UserRole currentRole: UserRole
@@ -25,9 +72,6 @@ interface AuthState {
themePreference: ThemePreference themePreference: ThemePreference
user: User | null user: User | null
switchRole: (role: UserRole) => void switchRole: (role: UserRole) => void
submitVerification: (role: UserRole, materials?: Record<string, string>) => void
approveVerification: (role: UserRole) => void
rejectVerification: (role: UserRole, reason: string) => void
setNotificationPref: (type: keyof NotificationPrefs, enabled: boolean) => void setNotificationPref: (type: keyof NotificationPrefs, enabled: boolean) => void
setThemePreference: (theme: ThemePreference) => void setThemePreference: (theme: ThemePreference) => void
updateProfile: (patch: { nickname?: string; bio?: string; avatar?: string }) => void updateProfile: (patch: { nickname?: string; bio?: string; avatar?: string }) => void
@@ -36,74 +80,21 @@ interface AuthState {
} }
export const useAuthStore = create<AuthState>((set, get) => ({ export const useAuthStore = create<AuthState>((set, get) => ({
isAuthenticated: false, isAuthenticated: persisted !== null,
currentRole: "consumer", currentRole: persisted?.currentRole ?? "consumer",
verifiedRoles: ["consumer"], verifiedRoles: persisted?.verifiedRoles ?? ["consumer"],
verificationStatus: { consumer: "approved" }, verificationStatus: persisted?.verificationStatus ?? { consumer: "approved" },
verificationReasons: {}, verificationReasons: {},
notificationPrefs: defaultNotificationPrefs, notificationPrefs: defaultNotificationPrefs,
themePreference: "system", themePreference: persisted?.themePreference ?? "system",
user: null, user: persisted?.user ?? null,
switchRole: (role) => { switchRole: (role) => {
const { verifiedRoles } = get() const { verifiedRoles } = get()
if (verifiedRoles.includes(role)) { if (verifiedRoles.includes(role)) {
set({ currentRole: role }) set({ currentRole: role })
persistCurrent(get())
} }
}, },
submitVerification: (role, _materials) =>
set((state) => {
if (state.verifiedRoles.includes(role)) {
return state
}
const nextReasons = { ...state.verificationReasons }
delete nextReasons[role]
return {
verificationStatus: {
...state.verificationStatus,
[role]: "pending",
},
verificationReasons: nextReasons,
}
}),
approveVerification: (role) =>
set((state) => {
if (state.verifiedRoles.includes(role) && state.verificationStatus[role] === "approved") {
return state
}
const nextReasons = { ...state.verificationReasons }
delete nextReasons[role]
return {
verifiedRoles: state.verifiedRoles.includes(role)
? state.verifiedRoles
: [...state.verifiedRoles, role],
verificationStatus: {
...state.verificationStatus,
[role]: "approved",
},
verificationReasons: nextReasons,
}
}),
rejectVerification: (role, reason) =>
set((state) => {
if (state.verifiedRoles.includes(role)) {
return state
}
return {
verificationStatus: {
...state.verificationStatus,
[role]: "rejected",
},
verificationReasons: {
...state.verificationReasons,
[role]: reason.trim() || "认证资料不完整,请补充后重试",
},
}
}),
setNotificationPref: (type, enabled) => setNotificationPref: (type, enabled) =>
set((state) => ({ set((state) => ({
notificationPrefs: { notificationPrefs: {
@@ -111,12 +102,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
[type]: enabled, [type]: enabled,
}, },
})), })),
setThemePreference: (theme) => set({ themePreference: theme }), setThemePreference: (theme) => {
set({ themePreference: theme })
persistCurrent(get())
},
updateProfile: (patch) => updateProfile: (patch) =>
set((state) => { set((state) => {
if (!state.user) return state if (!state.user) return state
const next = {
return { ...state,
user: { user: {
...state.user, ...state.user,
nickname: patch.nickname ?? state.user.nickname, nickname: patch.nickname ?? state.user.nickname,
@@ -124,6 +118,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
avatar: patch.avatar ?? state.user.avatar, avatar: patch.avatar ?? state.user.avatar,
}, },
} }
persistCurrent(next)
return next
}), }),
login: (user, verifiedRoles, themePreference) => login: (user, verifiedRoles, themePreference) =>
set((state) => { set((state) => {
@@ -135,6 +131,16 @@ export const useAuthStore = create<AuthState>((set, get) => ({
return acc return acc
}, {}) }, {})
const nextTheme = themePreference ?? state.themePreference
persist({
user,
currentRole: user.role,
verifiedRoles: nextVerifiedRoles,
verificationStatus: nextVerificationStatus,
themePreference: nextTheme,
})
return { return {
isAuthenticated: true, isAuthenticated: true,
user, user,
@@ -143,10 +149,11 @@ export const useAuthStore = create<AuthState>((set, get) => ({
verificationStatus: nextVerificationStatus, verificationStatus: nextVerificationStatus,
verificationReasons: {}, verificationReasons: {},
notificationPrefs: state.notificationPrefs, notificationPrefs: state.notificationPrefs,
themePreference: themePreference ?? state.themePreference, themePreference: nextTheme,
} }
}), }),
logout: () => logout: () => {
clearPersisted()
set({ set({
isAuthenticated: false, isAuthenticated: false,
currentRole: "consumer", currentRole: "consumer",
@@ -156,5 +163,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
notificationPrefs: defaultNotificationPrefs, notificationPrefs: defaultNotificationPrefs,
themePreference: "system", themePreference: "system",
user: null, user: null,
}), })
},
})) }))