fix(admin): 收紧 isSuper、plan-prices RBAC 与封禁状态机
避免 roles.write 绕过全部 can();定价读写挂 membership.plans 权限;去掉快捷封禁双路径并让 ban/unban 走 lifecycle。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -148,8 +148,6 @@ export const adminApi = {
|
||||
users: (q = '') =>
|
||||
request<{ items: UserListItem[] }>('GET', `/users?q=${encodeURIComponent(q)}`),
|
||||
user: (id: string) => request<UserDetail>('GET', `/users/${id}`),
|
||||
banUser: (id: string) => request<{ ok: boolean }>('POST', `/users/${id}/ban`),
|
||||
unbanUser: (id: string) => request<{ ok: boolean }>('POST', `/users/${id}/unban`),
|
||||
setUserStatus: (id: string, status: string, reason: string) =>
|
||||
request<UserDetail>('POST', `/users/${id}/status`, { status, reason }),
|
||||
statusTransitions: (id: string) =>
|
||||
|
||||
@@ -37,7 +37,7 @@ async function onLogout() {
|
||||
<RouterLink to="/cms">CMS</RouterLink>
|
||||
<RouterLink to="/catalogs">目录仓</RouterLink>
|
||||
<RouterLink to="/orders">订单</RouterLink>
|
||||
<RouterLink v-if="auth.isSuper" to="/pricing">定价</RouterLink>
|
||||
<RouterLink v-if="auth.can('admin.membership.plans.read')" to="/pricing">定价</RouterLink>
|
||||
<RouterLink to="/push">推送</RouterLink>
|
||||
<RouterLink v-if="auth.isSuper" to="/admins">管理员</RouterLink>
|
||||
<RouterLink to="/audit">审计</RouterLink>
|
||||
|
||||
@@ -125,22 +125,6 @@ async function changeStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBan() {
|
||||
statusMsg.value = ''
|
||||
try {
|
||||
if (detail.value?.status === 'banned') {
|
||||
await adminApi.unbanUser(String(route.params.id))
|
||||
statusMsg.value = '已解封'
|
||||
} else {
|
||||
await adminApi.banUser(String(route.params.id))
|
||||
statusMsg.value = '已封禁'
|
||||
}
|
||||
await load()
|
||||
} catch (e) {
|
||||
statusMsg.value = e instanceof Error ? e.message : '操作失败'
|
||||
}
|
||||
}
|
||||
|
||||
function fmtTime(iso?: string | null) {
|
||||
if (!iso) return '—'
|
||||
try {
|
||||
@@ -194,9 +178,6 @@ onMounted(load)
|
||||
</select>
|
||||
<input v-model="statusReason" class="reason" type="text" placeholder="原因(必填)" />
|
||||
<button class="btn" type="button" @click="changeStatus">变更状态</button>
|
||||
<button class="btn ghost" type="button" @click="toggleBan">
|
||||
{{ detail.status === 'banned' ? '快捷解封' : '快捷封禁' }}
|
||||
</button>
|
||||
<span v-if="statusMsg" class="muted">{{ statusMsg }}</span>
|
||||
</div>
|
||||
<div v-if="transitions.length" class="trans">
|
||||
|
||||
@@ -18,7 +18,7 @@ const router = createRouter({
|
||||
{ path: 'orders', name: 'orders', component: () => import('@/pages/OrdersPage.vue') },
|
||||
{ path: 'plans', name: 'plans', component: () => import('@/pages/MembershipPlansPage.vue') },
|
||||
{ path: 'codes', name: 'codes', component: () => import('@/pages/RedemptionPage.vue') },
|
||||
{ path: 'pricing', name: 'pricing', component: () => import('@/pages/PricingPage.vue'), meta: { superOnly: true } },
|
||||
{ path: 'pricing', name: 'pricing', component: () => import('@/pages/PricingPage.vue'), meta: { permission: 'admin.membership.plans.read' } },
|
||||
{ path: 'ask', name: 'ask', component: () => import('@/pages/AskPage.vue') },
|
||||
{ path: 'safety', name: 'safety', component: () => import('@/pages/SafetyPage.vue') },
|
||||
{ path: 'ai', name: 'ai', component: () => import('@/pages/AIConfigPage.vue') },
|
||||
@@ -41,6 +41,11 @@ router.beforeEach(async (to) => {
|
||||
if (!auth.username) await auth.hydrate()
|
||||
if (!auth.isSuper) return { name: 'dashboard' }
|
||||
}
|
||||
if (typeof to.meta.permission === 'string') {
|
||||
const auth = useAuthStore()
|
||||
if (!auth.username) await auth.hydrate()
|
||||
if (!auth.can(to.meta.permission)) return { name: 'dashboard' }
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const permissions = ref<string[]>([])
|
||||
|
||||
const isSuper = computed(
|
||||
() =>
|
||||
role.value === 'super' ||
|
||||
role.value === 'super_admin' ||
|
||||
permissions.value.includes('admin.roles.write'),
|
||||
() => role.value === 'super' || role.value === 'super_admin',
|
||||
)
|
||||
|
||||
function applyMe(me: AdminMe) {
|
||||
|
||||
Reference in New Issue
Block a user