vue-nuxt-permission
v2.0.9
Published
A unified permission system for Vue 3 and Nuxt 3/4 with directives, guards, and async support.
Maintainers
Readme
Vue Nuxt Permission
A powerful, flexible permission management plugin for Vue 3 & Nuxt 3.
Provides a declarative directive (v-permission), route protection (globalGuard), permission evaluation utilities, Base64-encoded localStorage persistence, and caching.
Features
v-permissiondirective: Declarative show/hide/remove elements based on permissions- Route guards: Built-in
globalGuardwith fallback redirection - Permission utilities: Check permissions programmatically with
hasPermission,hasAny,hasAll - Complex logic: Supports
and,or,regex,startWith,exactmatching modes - Security: Permissions stored as Base64-encoded values in localStorage
- Performance: Built-in caching reduces re-evaluation overhead
- Reactive: Works with both static arrays and reactive
Ref<string[]> - TypeScript: Full type safety and intellisense support
Installation
Install Package
npm install vue-nuxt-permissionOr with Yarn or pnpm:
yarn add vue-nuxt-permission
# or
pnpm add vue-nuxt-permissionSetup for Nuxt 3
Add the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ["vue-nuxt-permission"],
permission: {
permissions: ["dashboard.view", "user.create", "user.edit"],
developmentMode: process.env.NODE_ENV === "development",
},
});The plugin and directive are automatically registered across your entire app.
Alternatively, add via Nuxt CLI:
npx nuxi module add vue-nuxt-permissionSetup for Vue 3
Install the plugin in your app entry point:
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { PermissionPlugin } from "vue-nuxt-permission";
const app = createApp(App);
app.use(PermissionPlugin, {
permissions: ["user.create", "user.view", "user.edit", "admin.panel"],
developmentMode: process.env.NODE_ENV === "development",
});
app.mount("#app");The directive and composables are now available throughout your app.
Using the v-permission Directive
The v-permission directive controls element visibility based on user permissions.
Basic Usage
<template>
<!-- Element is removed from DOM if permission is missing -->
<button v-permission="'user.create'">Create User</button>
<!-- Multiple permissions (OR logic by default - any permission matches) -->
<button v-permission="['user.edit', 'user.update']">Edit User</button>
<!-- Admin-only section -->
<section v-permission="'admin.panel'">
<h2>Admin Dashboard</h2>
</section>
</template>Understanding Remove vs Show Behavior
By default, elements without the required permission are removed from the DOM entirely.
Use the :show modifier to hide elements with display: none instead:
<template>
<!-- Removed from DOM (not in page source) -->
<button v-permission="'delete'">Delete</button>
<!-- Hidden with CSS (in page source but not visible) -->
<button v-permission:show="'delete'">Delete</button>
</template>When to use each:
- Default (remove): Use for sensitive features you don't want in the DOM at all
:showmodifier: Use when you need the element in the DOM (for testing, styling, or keeping layout)
Directive Modifiers
| Modifier | Behavior | Use Case |
| -------- | ----------------------------------- | ----------------------------------------- |
| (none) | Removes element from DOM | Hide sensitive features completely |
| :show | Hides with display: none | Keep in DOM for layout/testing |
| .once | Checks permission only on mount | One-time permission check, ignore changes |
| .lazy | Doesn't react to permission changes | Static permission check, ignore updates |
Example with modifiers:
<template>
<!-- Removed from DOM, updates reactively -->
<button v-permission="'edit'">Edit</button>
<!-- Hidden instead of removed, updates reactively -->
<button v-permission:show="'delete'">Delete</button>
<!-- Removed from DOM, checked only once on mount -->
<button v-permission.once="'admin'">Admin Setup</button>
<!-- Hidden, won't react to permission changes -->
<div v-permission:show.lazy="'premium'">Premium Feature</div>
</template>Advanced: Complex Permission Objects
For more control, use permission objects with different evaluation modes:
<template>
<!-- AND mode: User must have BOTH permissions -->
<button
v-permission="{ permissions: ['user.edit', 'user.approve'], mode: 'and' }"
>
Edit & Approve
</button>
<!-- Regex: Match patterns (e.g., any admin permission) -->
<div v-permission="{ permissions: ['^admin\\..*'], mode: 'regex' }">
All admin features here
</div>
<!-- Exact: Only exact matches (not substring) -->
<button v-permission="{ permissions: ['admin'], mode: 'exact' }">
Exact Admin
</button>
<!-- Start with pattern -->
<section v-permission="{ permissions: ['user'], mode: 'startWith' }">
User-related features
</section>
</template>Permission Utilities
Check permissions programmatically in your code using the composable or utility functions.
Using the Composable
Import usePermission() in any component:
import { usePermission } from "vue-nuxt-permission";
const { hasPermission, hasAny, hasAll } = usePermission();
// Check a single permission
if (await hasPermission("user.edit")) {
// User can edit
}
// Check if user has ANY of the listed permissions
if (await hasAny(["admin.panel", "moderator.panel"])) {
// User is either admin or moderator
}
// Check if user has ALL listed permissions
if (await hasAll(["user.edit", "user.approve"])) {
// User can both edit and approve
}Individual Utility Functions
You can also import functions directly:
import {
hasPermission,
clearPermissionCache,
getCurrentPermissions,
} from "vue-nuxt-permission";
// Check permission (works outside components too)
const allowed = await hasPermission("user.delete");
// Get all current permissions
const perms = getCurrentPermissions();
// Clear the permission cache (useful after user login/logout)
clearPermissionCache();
// Update permissions dynamically
import { configurePermission } from "vue-nuxt-permission";
configurePermission(["user.view", "user.edit"]);Sync vs Async behavior:
hasPermission()is async - always returns a Promise- Permissions are evaluated asynchronously, so always use
awaitor.then() - Cache makes subsequent checks very fast
Controlling Element Visibility in Different Scenarios
Hide Admin Features
<template>
<nav>
<router-link to="/dashboard">Dashboard</router-link>
<router-link to="/admin" v-permission="'admin.access'">Admin</router-link>
</nav>
</template>Show Different Content Based on Permissions
<template>
<section v-if="isEditor">
<!-- Show editing interface for editors -->
<EditPanel v-permission="'content.edit'" />
</section>
<section v-if="isViewer" v-permission:show="'content.view'">
<!-- Show read-only view for viewers -->
<ViewPanel />
</section>
</template>
<script setup>
import { usePermission } from "vue-nuxt-permission";
const { hasPermission } = usePermission();
const isEditor = await hasPermission("content.edit");
const isViewer = await hasPermission("content.view");
</script>Disable Controls Instead of Hiding
<template>
<button @click="deleteItem" :disabled="!canDelete">Delete</button>
</template>
<script setup>
import { usePermission } from "vue-nuxt-permission";
const { hasPermission } = usePermission();
const canDelete = await hasPermission("item.delete");
const deleteItem = async () => {
if (await hasPermission("item.delete")) {
// Delete the item
}
};
</script>Permission Modes Reference
Different evaluation modes for complex permission logic:
| Mode | Behavior | Example |
| -------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| or (default) | User needs ANY permission in the list | v-permission="['admin', 'editor']" → user is admin OR editor |
| and | User needs ALL permissions in the list | v-permission="{ permissions: ['admin', 'verified'], mode: 'and' }" → admin AND verified |
| exact | Exact string match only, no substring | v-permission="{ permissions: ['admin'], mode: 'exact' }" |
| startWith | Permission starts with pattern | v-permission="{ permissions: ['user.'], mode: 'startWith' }" → any user.* permission |
| endWith | Permission ends with pattern | v-permission="{ permissions: ['.edit'], mode: 'endWith' }" → any *.edit permission |
| regex | Regular expression matching | v-permission="{ permissions: ['^admin\\..*'], mode: 'regex' }" → matches admin.* permissions |
Route Protection
Protect routes based on user authentication and permissions.
Using globalGuard with Vue Router
The globalGuard function automatically handles authentication and permission checks:
// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import { globalGuard } from "vue-nuxt-permission";
const router = createRouter({
history: createWebHistory(),
routes: [
// Public routes
{
path: "/login",
component: () => import("@/views/Login.vue"),
meta: { isAuthRoute: true }, // Redirect authenticated users away
},
// Protected routes (require authentication)
{
path: "/dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { requiresAuth: true },
},
// Routes with specific permissions
{
path: "/admin",
component: () => import("@/views/Admin.vue"),
meta: {
requiresAuth: true,
checkPermission: true,
permissions: ["admin.access"], // User must have this permission
},
},
],
});
// Setup the guard
router.beforeEach((to, from, next) => {
globalGuard(to, from, next, {
authRoutes: ["/login", "/register"], // Routes only for unauthenticated users
getAuthState: () => ({
isAuthenticated: !!localStorage.getItem("token"),
permissions: JSON.parse(localStorage.getItem("permissions") || "[]"),
}),
});
});
export default router;Route Meta Fields:
| Meta Field | Type | Description |
| ----------------- | ---------------- | -------------------------------------------------------------------------- |
| requiresAuth | boolean | Route requires user to be logged in |
| isAuthRoute | boolean | Route is for unauthenticated users (login/register); redirect if logged in |
| checkPermission | boolean | Check permissions for this route |
| permissions | string|string[] | Required permission(s) |
Common Patterns:
// Require authentication + specific permission
{
path: "/admin",
component: Admin,
meta: {
requiresAuth: true,
checkPermission: true,
permissions: ["admin.access"],
},
}
// Require multiple permissions (user must have all)
{
path: "/moderation",
component: Moderation,
meta: {
requiresAuth: true,
checkPermission: true,
permissions: ["moderator.access", "moderator.ban"],
},
}
// Public route
{
path: "/about",
component: About,
// No meta needed - accessible to everyone
}Using globalGuard with Nuxt 3 Middleware
In Nuxt 3, use a route middleware instead:
// middleware/auth.ts
export default defineRouteMiddleware(async (to, from) => {
const { hasPermission } = usePermission();
// Check if user is authenticated
const token = useCookie("auth_token");
if (!token.value) {
return navigateTo("/login");
}
// Check if route requires specific permissions
if (to.meta.requiresPermission) {
const allowed = await hasPermission(to.meta.permissions as string);
if (!allowed) {
return navigateTo("/unauthorized");
}
}
});Then apply to pages:
<!-- pages/admin.vue -->
<script setup>
definePageMeta({
middleware: "auth",
requiresPermission: true,
permissions: "admin.access",
});
</script>Creating Custom Guards
For advanced scenarios, create a custom guard with full control:
// guards/createGuard.ts
import { hasPermission } from "vue-nuxt-permission";
interface GuardOptions {
loginPath?: string;
homePath?: string;
getAuthState: () => { isAuthenticated: boolean; permissions: string[] };
onDenied?: (to: any, from: any) => void;
onAllowed?: (to: any, from: any) => void;
}
export function createCustomGuard(options: GuardOptions) {
return async (to: any, from: any, next: any) => {
const state = options.getAuthState();
// Not authenticated but route requires auth
if (to.meta.requiresAuth && !state.isAuthenticated) {
return next(options.loginPath || "/login");
}
// Already authenticated trying to access auth route
if (to.meta.isAuthRoute && state.isAuthenticated) {
return next(options.homePath || "/dashboard");
}
// Check permissions
if (to.meta.checkPermission && to.meta.permissions) {
const allowed = await hasPermission(
to.meta.permissions,
state.permissions
);
if (!allowed) {
options.onDenied?.(to, from);
return next("/unauthorized");
}
options.onAllowed?.(to, from);
}
return next();
};
}Usage:
import { createCustomGuard } from "@/guards/createGuard";
const guard = createCustomGuard({
loginPath: "/login",
homePath: "/dashboard",
getAuthState: () => ({
isAuthenticated: !!localStorage.getItem("token"),
permissions: JSON.parse(localStorage.getItem("permissions") || "[]"),
}),
onDenied: (to) => console.warn("Access denied to:", to.path),
onAllowed: (to) => console.log("Access granted to:", to.path),
});
router.beforeEach(guard);Debugging & Troubleshooting
Enable Debug Logging
To see detailed logs during development, enable developmentMode:
// Nuxt 3
export default defineNuxtConfig({
modules: ["vue-nuxt-permission"],
permission: {
permissions: ["user.view", "user.edit"],
developmentMode: true, // Enable in development
},
});
// Vue 3
app.use(PermissionPlugin, {
permissions: ["user.view", "user.edit"],
developmentMode: process.env.NODE_ENV === "development",
});When enabled, you'll see console messages like:
[v-permission:core] Evaluated permission "user.edit": ALLOWED
[v-permission:core] Removing element from DOMCommon Issues & Solutions
Issue: Elements not hiding even though permission is missing
❌ Problem:
<button v-permission="'unknown.permission'">Delete</button>
<!-- Button still shows -->
✅ Solution: Make sure the permission exists in your configuration
export default defineNuxtConfig({
permission: {
permissions: ["user.edit", "unknown.permission"], // Add it
},
});Issue: Route guard not redirecting unauthorized users
❌ Problem:
// Guard isn't blocking access to /admin
✅ Solution: Ensure getAuthState returns correct values
router.beforeEach((to, from, next) => {
globalGuard(to, from, next, {
getAuthState: () => ({
// Make sure this reflects actual auth state
isAuthenticated: !!localStorage.getItem("token"),
permissions: JSON.parse(localStorage.getItem("permissions") || "[]"),
}),
});
});Issue: Permissions not updating after login
❌ Problem:
const { hasPermission } = usePermission();
// Permission still shows false after user logs in
✅ Solution: Update permissions after login, clear cache
import { configurePermission, clearPermissionCache } from "vue-nuxt-permission";
async function login(credentials) {
const response = await fetch("/api/login", { method: "POST", body: JSON.stringify(credentials) });
const user = await response.json();
// Store new permissions
localStorage.setItem("permissions", JSON.stringify(user.permissions));
// Clear old cache and update
clearPermissionCache();
configurePermission(user.permissions);
}Issue: Directive is reactive but I want it to check once on mount
❌ Problem:
<div v-permission="permission">Content</div>
<!-- Updates when permission changes -->
✅ Solution: Use the .once modifier
<div v-permission.once="permission">Content</div>
<!-- Only checks on mount -->Issue: Element in DOM but I want it completely hidden
❌ Problem:
<div v-permission:show="'admin'">Admin</div>
<!-- Element is in DOM, only hidden with CSS -->
✅ Solution: Remove :show modifier
<div v-permission="'admin'">Admin</div>
<!-- Element removed from DOM completely -->Issue: Composable says permission exists but directive hides element
❌ Problem:
const { hasPermission } = usePermission();
await hasPermission("edit") // Returns true
// But <div v-permission="'edit'"> is hidden
✅ Solution: This usually means the permissions aren't synchronized
// Make sure all calls use the same permission definitions
// Use the exact same string: "edit" vs "user.edit" are differentQuick Diagnostic Checklist
Before opening an issue:
- [ ] Is
developmentMode: trueenabled to see logs? - [ ] Are permissions defined in the initial configuration?
- [ ] Did you call
clearPermissionCache()after updating permissions? - [ ] Are you using the exact same permission string everywhere? (case-sensitive)
- [ ] Is the route guard's
getAuthState()returning correct values? - [ ] Are you using
awaitwithhasPermission()since it's async? - [ ] Did you reload the page after updating permissions in localStorage?
Summary
Key Takeaways:
- Installation: Use Nuxt module or Vue plugin depending on your setup
- Directives: Use
v-permissionfor UI elements,:showto hide instead of remove - Permissions: Define all permissions upfront in configuration
- Utilities: Use
hasPermission,hasAny,hasAllfor programmatic checks - Routes: Protect routes with metadata and
globalGuardor custom middleware - Debugging: Enable
developmentModeand check console logs - Updates: Clear cache with
clearPermissionCache()after permission changes
For more detailed documentation, see the complete guide.
v2.0.0
- Add
.lazyand.oncedirective modifiers - Rename
configurePermissionDirective→configurePermission - Optional
userPermissionsparam inhasPermission - Improved Base64 storage & caching
- Enhanced examples and docs
Author
Kerolos Zakaria
Portfolio • GitHub • VS Code Marketplace • npm • LinkedIn
License
MIT © 2025 Kerolos Zakaria
