From 9f2511286159507e65514b03961fb62db63539bd Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 23:23:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20auth=20store=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20permissions=20=E7=8A=B6=E6=80=81=EF=BC=8C=E4=BB=8E=20JWT=20?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/stores/auth.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/web/src/stores/auth.ts b/apps/web/src/stores/auth.ts index 266b1fb..b95ab80 100644 --- a/apps/web/src/stores/auth.ts +++ b/apps/web/src/stores/auth.ts @@ -1,21 +1,31 @@ import { create } from 'zustand'; import { login as apiLogin, logout as apiLogout, type UserInfo } from '../api/auth'; -// Synchronously restore auth state from localStorage at store creation time. -// This eliminates the flash-of-login-page on refresh because isAuthenticated -// is already `true` before the first render. -function restoreInitialState(): { user: UserInfo | null; isAuthenticated: boolean } { +function extractPermissions(): string[] { + const token = localStorage.getItem('access_token'); + if (!token) return []; + try { + const parts = token.split('.'); + if (parts.length !== 3) return []; + const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); + return Array.isArray(payload.permissions) ? payload.permissions : []; + } catch { + return []; + } +} + +function restoreInitialState(): { user: UserInfo | null; isAuthenticated: boolean; permissions: string[] } { const token = localStorage.getItem('access_token'); const userStr = localStorage.getItem('user'); if (token && userStr) { try { const user = JSON.parse(userStr) as UserInfo; - return { user, isAuthenticated: true }; + return { user, isAuthenticated: true, permissions: extractPermissions() }; } catch { localStorage.removeItem('user'); } } - return { user: null, isAuthenticated: false }; + return { user: null, isAuthenticated: false, permissions: [] }; } const initial = restoreInitialState(); @@ -24,6 +34,7 @@ interface AuthState { user: UserInfo | null; isAuthenticated: boolean; loading: boolean; + permissions: string[]; login: (username: string, password: string) => Promise; logout: () => Promise; loadFromStorage: () => void; @@ -33,6 +44,7 @@ export const useAuthStore = create((set) => ({ user: initial.user, isAuthenticated: initial.isAuthenticated, loading: false, + permissions: initial.permissions, login: async (username, password) => { set({ loading: true }); @@ -41,7 +53,7 @@ export const useAuthStore = create((set) => ({ localStorage.setItem('access_token', resp.access_token); localStorage.setItem('refresh_token', resp.refresh_token); localStorage.setItem('user', JSON.stringify(resp.user)); - set({ user: resp.user, isAuthenticated: true, loading: false }); + set({ user: resp.user, isAuthenticated: true, loading: false, permissions: extractPermissions() }); } catch (error) { set({ loading: false }); throw error; @@ -57,13 +69,11 @@ export const useAuthStore = create((set) => ({ localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); localStorage.removeItem('user'); - set({ user: null, isAuthenticated: false }); + set({ user: null, isAuthenticated: false, permissions: [] }); }, - // Kept for backward compatibility but no longer needed since - // initial state is restored synchronously at store creation. loadFromStorage: () => { const state = restoreInitialState(); - set({ user: state.user, isAuthenticated: state.isAuthenticated }); + set({ user: state.user, isAuthenticated: state.isAuthenticated, permissions: state.permissions }); }, }));