From d76866ac3bf21e6447185f5f6c1c092369d634b9 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Fri, 1 May 2026 04:20:02 +0800 Subject: [PATCH] fix(auth): persist login state to localStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/providers.tsx | 8 +-- store/auth.ts | 146 ++++++++++++++++++++++++---------------------- 2 files changed, 81 insertions(+), 73 deletions(-) diff --git a/app/providers.tsx b/app/providers.tsx index 6ea9db5..46533ef 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -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 } diff --git a/store/auth.ts b/store/auth.ts index beba521..f00e2ea 100644 --- a/store/auth.ts +++ b/store/auth.ts @@ -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> + 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) => 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((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((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((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((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((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((set, get) => ({ notificationPrefs: defaultNotificationPrefs, themePreference: "system", user: null, - }), + }) + }, }))