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', () => {
|
describe('useUserStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia())
|
setActivePinia(createPinia())
|
||||||
@@ -30,17 +32,17 @@ describe('useUserStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('restores token from localStorage', () => {
|
it('restores token from localStorage', () => {
|
||||||
localStorage.setItem('access_token', 'header.payload.signature')
|
localStorage.setItem('access_token', VALID_TOKEN)
|
||||||
localStorage.setItem('refresh_token', 'rh.rp.rs')
|
localStorage.setItem('refresh_token', VALID_REFRESH_TOKEN)
|
||||||
|
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
|
|
||||||
expect(store.token).toBe('header.payload.signature')
|
expect(store.token).toBe(VALID_TOKEN)
|
||||||
expect(store.refreshToken).toBe('rh.rp.rs')
|
expect(store.refreshToken).toBe(VALID_REFRESH_TOKEN)
|
||||||
expect(store.isLoggedIn).toBe(true)
|
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 }
|
const savedUser = { id: 1, username: 'admin', email: 'a@b.com', role: 'administrator', status: 1 }
|
||||||
localStorage.setItem('user', JSON.stringify(savedUser))
|
localStorage.setItem('user', JSON.stringify(savedUser))
|
||||||
|
|
||||||
@@ -48,7 +50,8 @@ describe('useUserStore', () => {
|
|||||||
|
|
||||||
expect(store.user).toEqual(savedUser)
|
expect(store.user).toEqual(savedUser)
|
||||||
expect(store.username).toBe('admin')
|
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', () => {
|
it('handles corrupted user data in localStorage', () => {
|
||||||
@@ -64,12 +67,12 @@ describe('useUserStore', () => {
|
|||||||
it('sets tokens in store and localStorage', () => {
|
it('sets tokens in store and localStorage', () => {
|
||||||
const store = useUserStore()
|
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.token).toBe(VALID_TOKEN)
|
||||||
expect(store.refreshToken).toBe('rh.rp.rs')
|
expect(store.refreshToken).toBe(VALID_REFRESH_TOKEN)
|
||||||
expect(localStorage.getItem('access_token')).toBe('header.payload.signature')
|
expect(localStorage.getItem('access_token')).toBe(VALID_TOKEN)
|
||||||
expect(localStorage.getItem('refresh_token')).toBe('rh.rp.rs')
|
expect(localStorage.getItem('refresh_token')).toBe(VALID_REFRESH_TOKEN)
|
||||||
expect(store.isLoggedIn).toBe(true)
|
expect(store.isLoggedIn).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -78,10 +81,10 @@ describe('useUserStore', () => {
|
|||||||
it('does not persist tokens to localStorage', () => {
|
it('does not persist tokens to localStorage', () => {
|
||||||
const store = useUserStore()
|
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.token).toBe(VALID_TOKEN)
|
||||||
expect(store.refreshToken).toBe('r.p.s')
|
expect(store.refreshToken).toBe(VALID_REFRESH_TOKEN)
|
||||||
expect(localStorage.getItem('access_token')).toBeNull()
|
expect(localStorage.getItem('access_token')).toBeNull()
|
||||||
expect(localStorage.getItem('refresh_token')).toBeNull()
|
expect(localStorage.getItem('refresh_token')).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -90,7 +93,7 @@ describe('useUserStore', () => {
|
|||||||
localStorage.setItem('user', JSON.stringify({ id: 1, username: 'old' }))
|
localStorage.setItem('user', JSON.stringify({ id: 1, username: 'old' }))
|
||||||
|
|
||||||
const store = useUserStore()
|
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()
|
expect(localStorage.getItem('user')).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -99,7 +102,7 @@ describe('useUserStore', () => {
|
|||||||
describe('setUser', () => {
|
describe('setUser', () => {
|
||||||
it('persists to localStorage when remember=true', () => {
|
it('persists to localStorage when remember=true', () => {
|
||||||
const store = useUserStore()
|
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 }
|
const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 }
|
||||||
|
|
||||||
store.setUser(userInfo)
|
store.setUser(userInfo)
|
||||||
@@ -122,7 +125,7 @@ describe('useUserStore', () => {
|
|||||||
|
|
||||||
it('does not write to localStorage after setToken(remember=false)', () => {
|
it('does not write to localStorage after setToken(remember=false)', () => {
|
||||||
const store = useUserStore()
|
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 }
|
const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 }
|
||||||
store.setUser(userInfo)
|
store.setUser(userInfo)
|
||||||
@@ -135,7 +138,7 @@ describe('useUserStore', () => {
|
|||||||
describe('logout', () => {
|
describe('logout', () => {
|
||||||
it('clears all state and localStorage', () => {
|
it('clears all state and localStorage', () => {
|
||||||
const store = useUserStore()
|
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.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'administrator', status: 1 })
|
||||||
|
|
||||||
store.logout()
|
store.logout()
|
||||||
@@ -151,7 +154,7 @@ describe('useUserStore', () => {
|
|||||||
|
|
||||||
it('resets _remember so subsequent setUser does not persist', () => {
|
it('resets _remember so subsequent setUser does not persist', () => {
|
||||||
const store = useUserStore()
|
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 })
|
store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'administrator', status: 1 })
|
||||||
expect(localStorage.getItem('user')).not.toBeNull()
|
expect(localStorage.getItem('user')).not.toBeNull()
|
||||||
|
|
||||||
@@ -170,11 +173,14 @@ describe('useUserStore', () => {
|
|||||||
vi.mocked(api.get).mockResolvedValueOnce(mockUser)
|
vi.mocked(api.get).mockResolvedValueOnce(mockUser)
|
||||||
|
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
|
// Set a JWT token with admin role so isAdmin works
|
||||||
|
store.setToken(fakeJwtWithRole('administrator'), 'rt')
|
||||||
const result = await store.fetchCurrentUser()
|
const result = await store.fetchCurrentUser()
|
||||||
|
|
||||||
expect(api.get).toHaveBeenCalledWith('/api/v1/me')
|
expect(api.get).toHaveBeenCalledWith('/api/v1/me')
|
||||||
expect(result).toEqual(mockUser)
|
expect(result).toEqual(mockUser)
|
||||||
expect(store.user).toEqual(mockUser)
|
expect(store.user).toEqual(mockUser)
|
||||||
|
// isAdmin is now derived from JWT, not user.role
|
||||||
expect(store.isAdmin).toBe(true)
|
expect(store.isAdmin).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -186,7 +192,7 @@ describe('useUserStore', () => {
|
|||||||
vi.mocked(api.put).mockResolvedValueOnce(updatedUser)
|
vi.mocked(api.put).mockResolvedValueOnce(updatedUser)
|
||||||
|
|
||||||
const store = useUserStore()
|
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' })
|
const result = await store.updateProfile({ email: 'new@e.com' })
|
||||||
|
|
||||||
expect(api.put).toHaveBeenCalledWith('/api/v1/me/profile', { 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)
|
vi.mocked(api.put).mockResolvedValueOnce(undefined)
|
||||||
|
|
||||||
const store = useUserStore()
|
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 })
|
store.setUser({ id: 1, username: 'u', email: 'u@e.com', role: 'user', status: 1 })
|
||||||
|
|
||||||
await store.changePassword({
|
await store.changePassword({
|
||||||
@@ -238,7 +244,7 @@ describe('useUserStore', () => {
|
|||||||
vi.mocked(api.put).mockRejectedValueOnce(new Error('旧密码错误'))
|
vi.mocked(api.put).mockRejectedValueOnce(new Error('旧密码错误'))
|
||||||
|
|
||||||
const store = useUserStore()
|
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 })
|
store.setUser({ id: 1, username: 'u', email: 'u@e.com', role: 'user', status: 1 })
|
||||||
|
|
||||||
await expect(store.changePassword({
|
await expect(store.changePassword({
|
||||||
@@ -247,23 +253,58 @@ describe('useUserStore', () => {
|
|||||||
new_password_confirmation: 'new123',
|
new_password_confirmation: 'new123',
|
||||||
})).rejects.toThrow('旧密码错误')
|
})).rejects.toThrow('旧密码错误')
|
||||||
|
|
||||||
expect(store.token).toBe('h.p.s')
|
expect(store.token).toBe(VALID_TOKEN)
|
||||||
expect(store.user).not.toBeNull()
|
expect(store.user).not.toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('computed properties', () => {
|
describe('JWT-based role and isAdmin', () => {
|
||||||
it('isAdmin returns true for admin role', () => {
|
it('role computed derives from JWT token', () => {
|
||||||
const store = useUserStore()
|
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)
|
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()
|
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)
|
expect(store.isAdmin).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { api } from '@/utils/request'
|
import { api } from '@/utils/request'
|
||||||
|
import { decodeJwtPayload, extractRoleFromJwt } from '@/utils/jwt'
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
id: number
|
id: number
|
||||||
@@ -16,12 +17,17 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
// 默认不持久化;从 localStorage 推断:有 token 说明上次选了"记住我"
|
// 默认不持久化;从 localStorage 推断:有 token 说明上次选了"记住我"
|
||||||
const _remember = ref(!!localStorage.getItem('access_token'))
|
const _remember = ref(!!localStorage.getItem('access_token'))
|
||||||
|
|
||||||
// 基本 JWT 格式校验(三段式),防止垃圾值绕过路由守卫
|
// JWT 格式校验:三段式 + payload 可解码为有效 JSON,防止垃圾值绕过路由守卫
|
||||||
const isLoggedIn = computed(() => {
|
const isLoggedIn = computed(() => {
|
||||||
const t = token.value
|
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 || '')
|
const username = computed(() => user.value?.username || '')
|
||||||
|
|
||||||
function hasPermission(action: 'read' | 'write'): boolean {
|
function hasPermission(action: 'read' | 'write'): boolean {
|
||||||
@@ -97,6 +103,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
refreshToken,
|
refreshToken,
|
||||||
user,
|
user,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
role,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
username,
|
username,
|
||||||
setToken,
|
setToken,
|
||||||
|
|||||||
Reference in New Issue
Block a user