Frontend Development 7 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
How We Rebuilt a Fault‑Tolerant Vue Permission System After Three Failed Attempts

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-permission

directive—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.permission

too 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.

TypeScriptfrontend architectureVue.jsHooksPermission ManagementDirectives
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.