How We Rebuilt a Fault‑Tolerant Vue Permission System After Three Failed Attempts
After three successive permission‑system failures caused mismatched UI, fragmented checks, and tightly coupled logic, we rebuilt the Vue.js permission architecture using a unified API, a TypeScript‑typed hook, and an optional v‑permission directive, achieving maintainable, debuggable, and easily extensible access control across pages, buttons, and fields.
In the first two months after launch, our permission system crashed three times. The problems were not missing API permissions but mismatched UI, scattered permission checks, and tight coupling between permission data and button logic.
We decided to completely refactor the permission architecture—from API to router, to components, to a
v-permissiondirective—following a full‑cycle process.
❌ First attempt: Hard‑coded button permissions in templates
<!-- 用户管理页 -->
<el-button v-if="authList.includes('user:add')">添加用户</el-button>The backend returned an array of permission keys:
["user:add", "user:delete", "user:list"]Cannot be reused; each component repeats the check.
Changing a permission key (e.g.,
user:add→
user:add_user) breaks the entire UI.
Backend updates require a global search‑and‑replace in the frontend.
Typical "write fast, maintain painfully" solution.
❌ Second attempt: Centralized router.meta.permission control
// router.ts
{
path: '/user',
component: User,
meta: { permission: 'user:list' }
} router.beforeEach((to, from, next) => {
const p = to.meta.permission
if (p && !authList.includes(p)) {
return next('/403')
}
next()
})This solved page‑level permission but broke component/button/field level checks. Many routes share the same path but require different permission scopes, making
meta.permissiontoo coarse.
❌ Third attempt: Encapsulated permission component
<Permission code="user:add">
<el-button>添加用户</el-button>
</Permission> const slots = useSlots()
if (!authList.includes(props.code)) return null
return slots.default()Logic looks fine but is unintuitive to use.
Nested components become hard to debug; breakpoints don’t hit the real component.
TypeScript reports type errors for slots.
When permission fails, the component renders nothing, making the issue invisible in development.
Final solution: Hook + Directive + Unified routing layer
🔹1. Unified API for permission keys (flat list)
export type AuthCode =
| 'user:add'
| 'user:delete'
| 'user:edit'
| 'order:export'
| 'dashboard:view'The backend returns the user's permission set, stored in a global store (Pinia/Vuex/Context) named
authStore.
🔹2. Centralized hook
import { useAuthStore } from '@/store/auth'
export function usePermission(code: string): boolean {
const store = useAuthStore()
return store.permissionList.includes(code)
}Usage:
<el-button v-if="usePermission('user:add')">添加用户</el-button>This provides clean, reusable, TypeScript‑supported permission checks.
🔹3. Optional v-permission directive
app.directive('permission', {
mounted(el, binding) {
const authList = getUserPermissions() // from global store
if (!authList.includes(binding.value)) {
el.remove()
}
}
})Template usage:
<el-button v-permission="'order:export'">导出订单</el-button>Ideal for dynamic components or render‑generated buttons, though not suited for complex nested logic.
🧪 How to handle page‑level permissions?
Instead of using
router.meta, wrap each route page with a permission‑guard component:
<template>
<PermissionView code="dashboard:view">
<Dashboard />
</PermissionView>
</template>The permission component automatically redirects to a 403 page when the user lacks the required key, otherwise it renders the content, preventing blank pages or flickering.
The core challenge is not how to hide UI but ensuring permission keys are consistent, reusable, and layered.
All pages, buttons, and fields share a unified permission entry.
Adding a new permission point only requires updating the enum, no massive code changes.
New team members can understand and debug the logic at a glance.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.