Files
juwan-frontend/store/auth.ts
T
zetaloop d76866ac3b 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.
2026-05-01 17:32:06 +08:00

169 lines
4.6 KiB
TypeScript

import type { User, UserRole, VerificationStatus } from "@/lib/types"
import { create } from "zustand"
interface NotificationPrefs {
order: boolean
community: boolean
system: boolean
}
const defaultNotificationPrefs: NotificationPrefs = {
order: true,
community: true,
system: false,
}
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
verifiedRoles: UserRole[]
verificationStatus: Partial<Record<UserRole, VerificationStatus>>
verificationReasons: Partial<Record<UserRole, string>>
notificationPrefs: NotificationPrefs
themePreference: ThemePreference
user: User | null
switchRole: (role: UserRole) => void
setNotificationPref: (type: keyof NotificationPrefs, enabled: boolean) => void
setThemePreference: (theme: ThemePreference) => void
updateProfile: (patch: { nickname?: string; bio?: string; avatar?: string }) => void
login: (user: User, verifiedRoles?: UserRole[], themePreference?: ThemePreference) => void
logout: () => void
}
export const useAuthStore = create<AuthState>((set, get) => ({
isAuthenticated: persisted !== null,
currentRole: persisted?.currentRole ?? "consumer",
verifiedRoles: persisted?.verifiedRoles ?? ["consumer"],
verificationStatus: persisted?.verificationStatus ?? { consumer: "approved" },
verificationReasons: {},
notificationPrefs: defaultNotificationPrefs,
themePreference: persisted?.themePreference ?? "system",
user: persisted?.user ?? null,
switchRole: (role) => {
const { verifiedRoles } = get()
if (verifiedRoles.includes(role)) {
set({ currentRole: role })
persistCurrent(get())
}
},
setNotificationPref: (type, enabled) =>
set((state) => ({
notificationPrefs: {
...state.notificationPrefs,
[type]: enabled,
},
})),
setThemePreference: (theme) => {
set({ themePreference: theme })
persistCurrent(get())
},
updateProfile: (patch) =>
set((state) => {
if (!state.user) return state
const next = {
...state,
user: {
...state.user,
nickname: patch.nickname ?? state.user.nickname,
bio: patch.bio ?? state.user.bio,
avatar: patch.avatar ?? state.user.avatar,
},
}
persistCurrent(next)
return next
}),
login: (user, verifiedRoles, themePreference) =>
set((state) => {
const nextVerifiedRoles = verifiedRoles ?? user.verifiedRoles ?? [user.role]
const nextVerificationStatus =
user.verificationStatus ??
nextVerifiedRoles.reduce<Partial<Record<UserRole, VerificationStatus>>>((acc, role) => {
acc[role] = "approved"
return acc
}, {})
const nextTheme = themePreference ?? state.themePreference
persist({
user,
currentRole: user.role,
verifiedRoles: nextVerifiedRoles,
verificationStatus: nextVerificationStatus,
themePreference: nextTheme,
})
return {
isAuthenticated: true,
user,
currentRole: user.role,
verifiedRoles: nextVerifiedRoles,
verificationStatus: nextVerificationStatus,
verificationReasons: {},
notificationPrefs: state.notificationPrefs,
themePreference: nextTheme,
}
}),
logout: () => {
clearPersisted()
set({
isAuthenticated: false,
currentRole: "consumer",
verifiedRoles: ["consumer"],
verificationStatus: { consumer: "approved" },
verificationReasons: {},
notificationPrefs: defaultNotificationPrefs,
themePreference: "system",
user: null,
})
},
}))