feat(ECR-013A): Admin RBAC 实现并 Closed
角色权限、RequirePermission、/me permissions 与 migration 000015; Reviewer Approve → Closed。Next:ECR-013B Contract Definition。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,7 +21,22 @@ test('admin login users grant and audit with mocked API', async ({ page }) => {
|
||||
await ok({
|
||||
token: 'adm_e2e_token',
|
||||
expires_at: new Date(Date.now() + 3600_000).toISOString(),
|
||||
admin: { id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', username: 'admin' },
|
||||
admin: {
|
||||
id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
username: 'admin',
|
||||
role: 'super_admin',
|
||||
permissions: [
|
||||
'admin.users.read',
|
||||
'admin.users.membership.grant',
|
||||
'admin.users.ask_quota.grant',
|
||||
'admin.orders.read',
|
||||
'admin.audit.read',
|
||||
'admin.analytics.read',
|
||||
'admin.content.write',
|
||||
'admin.roles.read',
|
||||
'admin.roles.write',
|
||||
],
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -55,7 +70,22 @@ test('admin login users grant and audit with mocked API', async ({ page }) => {
|
||||
return
|
||||
}
|
||||
if (url.endsWith('/me') || url.includes('/me?')) {
|
||||
await ok({ id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', username: 'admin' })
|
||||
await ok({
|
||||
id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
username: 'admin',
|
||||
role: 'super_admin',
|
||||
permissions: [
|
||||
'admin.users.read',
|
||||
'admin.users.membership.grant',
|
||||
'admin.users.ask_quota.grant',
|
||||
'admin.orders.read',
|
||||
'admin.audit.read',
|
||||
'admin.analytics.read',
|
||||
'admin.content.write',
|
||||
'admin.roles.read',
|
||||
'admin.roles.write',
|
||||
],
|
||||
})
|
||||
return
|
||||
}
|
||||
if (url.includes(`/users/${userId}/ask-quota/grant`) && method === 'POST') {
|
||||
|
||||
@@ -94,14 +94,25 @@ export type UserDetail = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type AdminMe = {
|
||||
id: string
|
||||
username: string
|
||||
role?: string
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
login: (username: string, password: string) =>
|
||||
request<{ token: string; admin: { id: string; username: string } }>('POST', '/auth/login', {
|
||||
request<{ token: string; admin: AdminMe }>('POST', '/auth/login', {
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
logout: () => request<{ ok: boolean }>('POST', '/auth/logout'),
|
||||
me: () => request<{ id: string; username: string }>('GET', '/me'),
|
||||
me: () => request<AdminMe>('GET', '/me'),
|
||||
roles: () =>
|
||||
request<{ items: Array<{ id: string; name: string; system: boolean }> }>('GET', '/roles'),
|
||||
role: (id: string) =>
|
||||
request<{ id: string; name: string; system: boolean; permissions: string[] }>('GET', `/roles/${id}`),
|
||||
stats: () => request<DashboardStats>('GET', '/stats'),
|
||||
users: (q = '') =>
|
||||
request<{ items: UserListItem[] }>('GET', `/users?q=${encodeURIComponent(q)}`),
|
||||
|
||||
@@ -33,6 +33,7 @@ async function onLogout() {
|
||||
</nav>
|
||||
<div class="foot">
|
||||
<span class="muted">{{ auth.username || '管理员' }}</span>
|
||||
<span v-if="auth.role" class="role">{{ auth.role }} · {{ auth.permissions.length }} 权</span>
|
||||
<button class="btn ghost" type="button" @click="onLogout">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -72,6 +73,7 @@ nav a.router-link-active {
|
||||
box-shadow: 0 4px 12px rgba(229, 77, 66, 0.08);
|
||||
}
|
||||
.foot { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.role { font-size: 0.75rem; color: var(--muted); }
|
||||
.main { padding: 1.5rem 1.75rem; }
|
||||
@media (max-width: 800px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -5,19 +5,27 @@ import { adminApi, getToken, setToken } from '@/api/client'
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string | null>(getToken())
|
||||
const username = ref<string>('')
|
||||
const role = ref<string>('')
|
||||
const permissions = ref<string[]>([])
|
||||
|
||||
function applyMe(me: { username: string; role?: string; permissions?: string[] }) {
|
||||
username.value = me.username
|
||||
role.value = me.role || ''
|
||||
permissions.value = me.permissions ?? []
|
||||
}
|
||||
|
||||
async function login(user: string, password: string) {
|
||||
const res = await adminApi.login(user, password)
|
||||
setToken(res.token)
|
||||
token.value = res.token
|
||||
username.value = res.admin.username
|
||||
applyMe(res.admin)
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
if (!token.value) return false
|
||||
try {
|
||||
const me = await adminApi.me()
|
||||
username.value = me.username
|
||||
applyMe(me)
|
||||
return true
|
||||
} catch {
|
||||
setToken(null)
|
||||
@@ -35,7 +43,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
setToken(null)
|
||||
token.value = null
|
||||
username.value = ''
|
||||
role.value = ''
|
||||
permissions.value = []
|
||||
}
|
||||
|
||||
return { token, username, login, logout, hydrate }
|
||||
function can(code: string) {
|
||||
return permissions.value.includes(code)
|
||||
}
|
||||
|
||||
return { token, username, role, permissions, can, login, logout, hydrate }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user