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:
+4
-4
@@ -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
@@ -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,
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user