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:
jackyu66git
2026-08-07 17:19:45 +08:00
co-authored by Cursor
parent 85ed0901bb
commit b5a05941d9
35 changed files with 1518 additions and 49 deletions
+32 -2
View File
@@ -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') {
+13 -2
View File
@@ -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)}`),
+2
View File
@@ -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; }
+17 -3
View File
@@ -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 }
})