Skip to main content

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 |