update user

This commit is contained in:
2026-04-03 15:12:13 +08:00
parent 089041c7f1
commit b9544f028b
2 changed files with 79 additions and 31 deletions
+69 -28
View File
@@ -11,6 +11,8 @@ vi.mock('@/utils/request', () => ({
},
}))
import { fakeJwtWithRole, VALID_TOKEN, VALID_REFRESH_TOKEN } from '@/test/helpers'
describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
@@ -30,17 +32,17 @@ describe('useUserStore', () => {
})
it('restores token from localStorage', () => {
localStorage.setItem('access_token', 'header.payload.signature')
localStorage.setItem('refresh_token', 'rh.rp.rs')
localStorage.setItem('access_token', VALID_TOKEN)
localStorage.setItem('refresh_token', VALID_REFRESH_TOKEN)
const store = useUserStore()
expect(store.token).toBe('header.payload.signature')
expect(store.refreshToken).toBe('rh.rp.rs')
expect(store.token).toBe(VALID_TOKEN)
expect(store.refreshToken).toBe(VALID_REFRESH_TOKEN)
expect(store.isLoggedIn).toBe(true)
})
it('restores user info from localStorage', () => {
it('restores user info from localStorage (but isAdmin depends on JWT)', () => {
const savedUser = { id: 1, username: 'admin', email: 'a@b.com', role: 'administrator', status: 1 }
localStorage.setItem('user', JSON.stringify(savedUser))
@@ -48,7 +50,8 @@ describe('useUserStore', () => {
expect(store.user).toEqual(savedUser)
expect(store.username).toBe('admin')
expect(store.isAdmin).toBe(true)
// isAdmin is false because there's no JWT token set
expect(store.isAdmin).toBe(false)
})
it('handles corrupted user data in localStorage', () => {
@@ -64,12 +67,12 @@ describe('useUserStore', () => {
it('sets tokens in store and localStorage', () => {
const store = useUserStore()
store.setToken('header.payload.signature', 'rh.rp.rs')
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN)
expect(store.token).toBe('header.payload.signature')
expect(store.refreshToken).toBe('rh.rp.rs')
expect(localStorage.getItem('access_token')).toBe('header.payload.signature')
expect(localStorage.getItem('refresh_token')).toBe('rh.rp.rs')
expect(store.token).toBe(VALID_TOKEN)
expect(store.refreshToken).toBe(VALID_REFRESH_TOKEN)
expect(localStorage.getItem('access_token')).toBe(VALID_TOKEN)
expect(localStorage.getItem('refresh_token')).toBe(VALID_REFRESH_TOKEN)
expect(store.isLoggedIn).toBe(true)
})
})
@@ -78,10 +81,10 @@ describe('useUserStore', () => {
it('does not persist tokens to localStorage', () => {
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', false)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, false)
expect(store.token).toBe('h.p.s')
expect(store.refreshToken).toBe('r.p.s')
expect(store.token).toBe(VALID_TOKEN)
expect(store.refreshToken).toBe(VALID_REFRESH_TOKEN)
expect(localStorage.getItem('access_token')).toBeNull()
expect(localStorage.getItem('refresh_token')).toBeNull()
})
@@ -90,7 +93,7 @@ describe('useUserStore', () => {
localStorage.setItem('user', JSON.stringify({ id: 1, username: 'old' }))
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', false)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, false)
expect(localStorage.getItem('user')).toBeNull()
})
@@ -99,7 +102,7 @@ describe('useUserStore', () => {
describe('setUser', () => {
it('persists to localStorage when remember=true', () => {
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, true)
const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 }
store.setUser(userInfo)
@@ -122,7 +125,7 @@ describe('useUserStore', () => {
it('does not write to localStorage after setToken(remember=false)', () => {
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', false)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, false)
const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 }
store.setUser(userInfo)
@@ -135,7 +138,7 @@ describe('useUserStore', () => {
describe('logout', () => {
it('clears all state and localStorage', () => {
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s')
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN)
store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'administrator', status: 1 })
store.logout()
@@ -151,7 +154,7 @@ describe('useUserStore', () => {
it('resets _remember so subsequent setUser does not persist', () => {
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, true)
store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'administrator', status: 1 })
expect(localStorage.getItem('user')).not.toBeNull()
@@ -170,11 +173,14 @@ describe('useUserStore', () => {
vi.mocked(api.get).mockResolvedValueOnce(mockUser)
const store = useUserStore()
// Set a JWT token with admin role so isAdmin works
store.setToken(fakeJwtWithRole('administrator'), 'rt')
const result = await store.fetchCurrentUser()
expect(api.get).toHaveBeenCalledWith('/api/v1/me')
expect(result).toEqual(mockUser)
expect(store.user).toEqual(mockUser)
// isAdmin is now derived from JWT, not user.role
expect(store.isAdmin).toBe(true)
})
})
@@ -186,7 +192,7 @@ describe('useUserStore', () => {
vi.mocked(api.put).mockResolvedValueOnce(updatedUser)
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, true)
const result = await store.updateProfile({ email: 'new@e.com' })
expect(api.put).toHaveBeenCalledWith('/api/v1/me/profile', { email: 'new@e.com' })
@@ -214,7 +220,7 @@ describe('useUserStore', () => {
vi.mocked(api.put).mockResolvedValueOnce(undefined)
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, true)
store.setUser({ id: 1, username: 'u', email: 'u@e.com', role: 'user', status: 1 })
await store.changePassword({
@@ -238,7 +244,7 @@ describe('useUserStore', () => {
vi.mocked(api.put).mockRejectedValueOnce(new Error('旧密码错误'))
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
store.setToken(VALID_TOKEN, VALID_REFRESH_TOKEN, true)
store.setUser({ id: 1, username: 'u', email: 'u@e.com', role: 'user', status: 1 })
await expect(store.changePassword({
@@ -247,23 +253,58 @@ describe('useUserStore', () => {
new_password_confirmation: 'new123',
})).rejects.toThrow('旧密码错误')
expect(store.token).toBe('h.p.s')
expect(store.token).toBe(VALID_TOKEN)
expect(store.user).not.toBeNull()
})
})
describe('computed properties', () => {
it('isAdmin returns true for admin role', () => {
describe('JWT-based role and isAdmin', () => {
it('role computed derives from JWT token', () => {
const store = useUserStore()
store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'administrator', status: 1 })
store.setToken(fakeJwtWithRole('administrator'), VALID_REFRESH_TOKEN)
expect(store.isAdmin).toBe(true)
store.setToken(fakeJwtWithRole('accessor'), VALID_REFRESH_TOKEN)
expect(store.isAdmin).toBe(false)
})
it('isAdmin is false when token is null', () => {
const store = useUserStore()
expect(store.isAdmin).toBe(false)
})
it('localStorage user role tampering does not affect isAdmin', () => {
// Simulate attacker: localStorage user says administrator, but JWT says accessor
const savedUser = { id: 1, username: 'hacker', email: 'h@h.com', role: 'administrator', status: 1 }
localStorage.setItem('user', JSON.stringify(savedUser))
localStorage.setItem('access_token', fakeJwtWithRole('accessor'))
localStorage.setItem('refresh_token', 'rt')
const store = useUserStore()
// user.role says administrator (from localStorage)
expect(store.user?.role).toBe('administrator')
// but isAdmin is false (from JWT which says accessor)
expect(store.isAdmin).toBe(false)
})
it('setToken updates role and isAdmin reactively', () => {
const store = useUserStore()
store.setToken(fakeJwtWithRole('accessor'), 'rt')
expect(store.isAdmin).toBe(false)
store.setToken(fakeJwtWithRole('administrator'), 'rt')
expect(store.isAdmin).toBe(true)
})
it('isAdmin returns false for non-admin role', () => {
it('logout resets role to null and isAdmin to false', () => {
const store = useUserStore()
store.setUser({ id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 })
store.setToken(fakeJwtWithRole('administrator'), 'rt')
expect(store.isAdmin).toBe(true)
store.logout()
expect(store.isAdmin).toBe(false)
})
})
+10 -3
View File
@@ -1,4 +1,5 @@
import { api } from '@/utils/request'
import { decodeJwtPayload, extractRoleFromJwt } from '@/utils/jwt'
export interface UserInfo {
id: number
@@ -16,12 +17,17 @@ export const useUserStore = defineStore('user', () => {
// 默认不持久化;从 localStorage 推断:有 token 说明上次选了"记住我"
const _remember = ref(!!localStorage.getItem('access_token'))
// 基本 JWT 格式校验三段式,防止垃圾值绕过路由守卫
// JWT 格式校验三段式 + payload 可解码为有效 JSON,防止垃圾值绕过路由守卫
const isLoggedIn = computed(() => {
const t = token.value
return !!t && t.split('.').length === 3
return !!t && decodeJwtPayload(t) !== null
})
const isAdmin = computed(() => user.value?.role === 'administrator')
// role 从 JWT token 响应式派生,唯一可信数据源(不依赖 localStorage user JSON
const role = computed<string | null>(() => {
if (!token.value) return null
return extractRoleFromJwt(token.value)
})
const isAdmin = computed(() => role.value === 'administrator')
const username = computed(() => user.value?.username || '')
function hasPermission(action: 'read' | 'write'): boolean {
@@ -97,6 +103,7 @@ export const useUserStore = defineStore('user', () => {
refreshToken,
user,
isLoggedIn,
role,
isAdmin,
username,
setToken,