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() {
const login = useAuthStore((s) => s.login)
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
const logout = useAuthStore((s) => s.logout)
const tried = useRef(false)
useEffect(() => {
if (tried.current || isAuthenticated) return
if (tried.current) return
tried.current = true
getCurrentUserForLogin()
@@ -24,9 +24,9 @@ function AuthBootstrap() {
login(user, user.verifiedRoles ?? ["consumer"])
})
.catch(() => {
// no valid session — stay logged out
logout()
})
}, [login, isAuthenticated])
}, [login, logout])
return null
}
+77 -69
View File
@@ -15,6 +15,53 @@ const defaultNotificationPrefs: NotificationPrefs = {
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 {
isAuthenticated: boolean
currentRole: UserRole
@@ -25,9 +72,6 @@ interface AuthState {
themePreference: ThemePreference
user: User | null
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
setThemePreference: (theme: ThemePreference) => void
updateProfile: (patch: { nickname?: string; bio?: string; avatar?: string }) => void
@@ -36,74 +80,21 @@ interface AuthState {
}
export const useAuthStore = create<AuthState>((set, get) => ({
isAuthenticated: false,
currentRole: "consumer",
verifiedRoles: ["consumer"],
verificationStatus: { consumer: "approved" },
isAuthenticated: persisted !== null,
currentRole: persisted?.currentRole ?? "consumer",
verifiedRoles: persisted?.verifiedRoles ?? ["consumer"],
verificationStatus: persisted?.verificationStatus ?? { consumer: "approved" },
verificationReasons: {},
notificationPrefs: defaultNotificationPrefs,
themePreference: "system",
user: null,
themePreference: persisted?.themePreference ?? "system",
user: persisted?.user ?? null,
switchRole: (role) => {
const { verifiedRoles } = get()
if (verifiedRoles.includes(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) =>
set((state) => ({
notificationPrefs: {
@@ -111,12 +102,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
[type]: enabled,
},
})),
setThemePreference: (theme) => set({ themePreference: theme }),
setThemePreference: (theme) => {
set({ themePreference: theme })
persistCurrent(get())
},
updateProfile: (patch) =>
set((state) => {
if (!state.user) return state
return {
const next = {
...state,
user: {
...state.user,
nickname: patch.nickname ?? state.user.nickname,
@@ -124,6 +118,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
avatar: patch.avatar ?? state.user.avatar,
},
}
persistCurrent(next)
return next
}),
login: (user, verifiedRoles, themePreference) =>
set((state) => {
@@ -135,6 +131,16 @@ export const useAuthStore = create<AuthState>((set, get) => ({
return acc
}, {})
const nextTheme = themePreference ?? state.themePreference
persist({
user,
currentRole: user.role,
verifiedRoles: nextVerifiedRoles,
verificationStatus: nextVerificationStatus,
themePreference: nextTheme,
})
return {
isAuthenticated: true,
user,
@@ -143,10 +149,11 @@ export const useAuthStore = create<AuthState>((set, get) => ({
verificationStatus: nextVerificationStatus,
verificationReasons: {},
notificationPrefs: state.notificationPrefs,
themePreference: themePreference ?? state.themePreference,
themePreference: nextTheme,
}
}),
logout: () =>
logout: () => {
clearPersisted()
set({
isAuthenticated: false,
currentRole: "consumer",
@@ -156,5 +163,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
notificationPrefs: defaultNotificationPrefs,
themePreference: "system",
user: null,
}),
})
},
}))