Keep things dry with Vue.js directives and Role-based access controls.
# Vue Role-based Access Control Directive
Add a `v-rbac` directive (`v-rbac="'admin'"``) for DRY logic.
## 1. Assign Custom Claims in Firebase
> Backend (via Admin SDK) -- one-time role assignment
```js
// Node.js Firebase Admin SDK
admin.auth().setCustomUserClaims(uid, {
role: 'admin'
});
```
Roles could be:
- `admin`
- `operator`
- `readonly`
## 2. Expose User Role in Frontend
> `lib/firebase.js`
```js
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import { getIdTokenResult } from 'firebase/auth';
import { ref } from 'vue';
export const currentUser = ref(null);
export const userRole = ref(null);
const auth = getAuth();
onAuthStateChanged(async (user) => {
currentUser.value = user;
if (user) {
const token = await getIdTokenResult(user, true);
userRole.value = token.claims.role || 'readonly';
}
});
```
## 3. User store
> `stores/useUserStore.js`
```js
import { defineStore } from 'pinia';
import { currentUser, userRole } from '@/lib/firebase';
export const useUserStore = defineStore('user', {
state: () => ({
user: currentUser,
role: userRole
})
});
```
## 4. Create the Directive File
> `directives/rbac.js`
```js
import { useUserStore } from '@/stores/useUserStore';
export default {
mounted(el, binding) {
const store = useUserStore();
const allowedRole = binding.value;
const currentRole = store.role?.value;
if (!currentRole || currentRole !== allowedRole) {
el.style.display = 'none';
}
},
updated(el, binding) {
const store = useUserStore();
const allowedRole = binding.value;
const currentRole = store.role?.value;
el.style.display = currentRole === allowedRole ? '' : 'none';
}
};
```
## 5. Register It Globally
> `main.js`
```js
import { createApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
import { router } from './router';
import rbac from './directives/rbac';
const app = createApp(App);
app.directive('rbac', rbac);
app.use(createPinia());
app.use(router);
app.mount('#app');
```
## 6. Use It in Your Components
> Example in `RecipientList.vue`
```vue
<template>
<div>
<button v-rbac="'admin'" @click="deleteRecipient">Delete Recipient</button>
<CreateRecipientForm v-rbac="'admin'" />
</div>
</template>
```
Example in `TemplatesView.vue`
```vue
<template>
<div>
<button v-rbac="'operator'">Trigger Send</button>
</div>
</template>
```
Example in `features/templates/components/CreateTemplateForm.vue`
```vue
<template>
<form v-rbac="['admin', 'operator']" @submit.prevent="submit">
<!-- form inputs -->
<button type="submit">Save</button>
</form>
</template>
```
Example in `features/templates/components/TemplateList.vue`
```vue
<template>
<div>
<h2>Templates</h2>
<ul>
<li v-for="t in templates" :key="t.id">
<strong>{{ t.name }}</strong> – {{ t.type }}
<button v-rbac="'admin'">Delete</button>
<button v-rbac="['admin', 'operator']">Trigger</button>
</li>
</ul>
</div>
</template>
```
Example in `features/recipients/components/RecipientList.vue`
```vue
<template>
<ul>
<li v-for="r in recipients" :key="r.id">
{{ r.name }} ({{ r.type }}) – {{ r.address }}
<button v-rbac="'admin'" @click="deleteRecipient(r.id)">Delete</button>
</li>
</ul>
</template>
```
## Optional: Multiple Role Support
Update `v-rbac` to accept multiple roles:
```js
const allowedRoles = Array.isArray(binding.value)
? binding.value
: [binding.value];
if (!allowedRoles.includes(currentRole)) {
el.style.display = 'none';
}
```
```vue
<!-- Accept multiple -->
<button v-rbac="['admin', 'operator']">Edit</button>
```
## DRY, Declarative, Clean
| Usage | Meaning |
| ------------------------------- | -------------------------- |
| `v-rbac="'admin'"` | Show only for admin |
| `v-rbac="['admin']"` | Show only for admin |
| `v-rbac="['admin','operator']"` | Show for admin or operator |