update user
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user