# Pattern: aggiungere un ruolo ad accesso limitato a un pannello admin esistente

Guida riutilizzabile per il caso "il pannello admin ha un solo livello di
accesso, serve dare a un sottoinsieme di utenti accesso solo a una
funzionalità specifica" — usato in Supernova per dare al team commerciale
accesso al solo tool Preventivi, senza toccare contatti/candidature/
formazione/impostazioni.

Riferimento: [[project_gix_supernova_ms365_oauth2]] nella memoria.

---

## Quando usarlo

- Il pannello admin esistente non ha concetto di ruoli (tutti gli utenti
  sono equivalenti)
- Serve dare a un gruppo di utenti (team commerciale, supporto, ecc.)
  accesso a **una sola sezione** senza duplicare login/2FA/gestione utenti
- Non è richiesto un sistema di permessi granulare (RBAC completo) — basta
  una distinzione binaria "accesso completo" vs "accesso a una feature"

Se serve più granularità (più ruoli, permessi per singola risorsa),
questo pattern va esteso da ENUM a una tabella `roles`/`permissions`.

## Perché non un login separato

Un sistema di login completamente separato (nuova tabella utenti, nuova
pagina di login, nuovo middleware) duplica login/2FA/reset password/gestione
sessioni già scritti e testati. Il costo di manutenzione doppio supera quasi
sempre il beneficio di isolamento — a meno che il nuovo gruppo di utenti
debba vivere su un dominio/sottodominio completamente diverso.

---

## Passi (backend)

### 1. Colonna ruolo sulla tabella utenti

```sql
ALTER TABLE users
  ADD COLUMN role ENUM('admin','sales') NOT NULL DEFAULT 'admin' AFTER last_name;
```

`DEFAULT 'admin'` è importante: retrocompatibile con gli utenti esistenti
senza bisogno di UPDATE separati.

Se il progetto usa il pattern "auto-migrazione al boot" (funzioni
`ensureXSchema()` idempotenti eseguite all'avvio, con `ALTER TABLE` in
try/catch che ignora `ER_DUP_FIELDNAME`), aggiungere l'ALTER lì invece che
in uno script SQL a parte — si applica automaticamente su ogni ambiente.

### 2. Propagare il ruolo nel JWT

In ogni punto del controller auth dove si firma un token `type: 'full'`
dopo login riuscito (diretto, post-2FA, post-onboarding-password),
aggiungere `role: user.role` al payload:

```js
const token = jwt.sign(
  { userId: user.id, email: user.email, name: displayName, role: user.role, type: 'full' },
  JWT_SECRET, { expiresIn: JWT_FULL_TTL }
)
```

Attenzione alle query `SELECT` esplicite (non `SELECT *`) nei punti di
firma — vanno estese per includere `role`, altrimenti risulta sempre
`undefined`.

Aggiornare anche l'endpoint `/auth/me` per restituire `role`.

### 3. Middleware `requireAdmin`

```js
// Da usare DOPO il middleware di autenticazione sulle route riservate
// al pannello completo. Un token senza 'role' (utenti pre-esistenti
// prima della migrazione) è trattato come admin per retrocompatibilità.
export function requireAdmin(req, res, next) {
  if (req.user?.role && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Accesso non consentito per questo ruolo.' })
  }
  next()
}
```

### 4. Applicare il middleware a tutte le route esistenti

Con `sed` su tutte le route che iniziano con il prefisso admin generico:

```bash
sed -i -E "s/(router\.(get|post|put|patch|delete)\('\/admin\/[^']*',\s*)authMiddleware,/\1authMiddleware, requireAdmin,/" routes.js
```

Poi **non** applicarlo alle nuove route della feature dedicata (nel nostro
caso `/admin/quotes/*`), che restano accessibili sia da admin che dal
ruolo limitato — il controller stesso filtra i dati per utente quando
`req.user.role !== 'admin'`.

Occhio ai casi limite:
- Un endpoint che serve **anche** al ruolo limitato ma è sensibile
  (es. avviare/revocare un'integrazione OAuth2 condivisa) va tenuto
  admin-only anche se sta "vicino" alla feature dedicata — separare in
  endpoint diversi per azione, non solo per risorsa (es. `/status` aperto
  a tutti, `/init` e `/revoke` solo admin).

### 5. Isolamento dati nel controller della feature dedicata

```js
export async function getQuotes(req, res) {
  const isAdmin = !req.user.role || req.user.role === 'admin'
  const rows = isAdmin
    ? await db.query('SELECT * FROM quotes ORDER BY created_at DESC')
    : await db.query('SELECT * FROM quotes WHERE created_by = ? ORDER BY created_at DESC', [req.user.userId])
  res.json(rows)
}
```

Ripetere lo stesso controllo su get-singolo, update, delete (non solo
sulla lista) — altrimenti un utente limitato può indovinare un ID e
accedere a dati non suoi via URL diretta.

---

## Passi (frontend)

### 1. Filtrare la navigazione per ruolo

```js
const visibleNavItems = computed(() => {
  if (currentUser.value?.role !== 'sales') return navItems.value
  return navItems.value.filter(item => item.id === 'quotes')
})
```

Questa è **solo UI** — non sostituisce il controllo backend, serve a non
mostrare voci di menu che porterebbero comunque a un 403.

### 2. Tab di default in base al ruolo

Se la navigazione usa un hash/tab persistente (`#overview`, `#settings`,
ecc.), il ruolo limitato non deve mai atterrare sulla tab di default
generica (a cui non ha accesso):

```js
function defaultTab() {
  return currentUser.value?.role === 'sales' ? 'quotes' : 'overview'
}
```

Aggiungere anche un `watch(currentUser, ...)` che forza la tab corretta se
`currentUser` si popola dopo il primo render (caso tipico: bootstrap
`onMounted` che fa `loadMe()` in modo asincrono).

### 3. Evitare chiamate API inutili al bootstrap

Se il pannello ha un `loadAll()` che all'avvio chiama tutte le API admin
indiscriminatamente, va condizionato per ruolo — altrimenti un utente
limitato genera N richieste 403 ad ogni login (funzionalmente innocuo ma
rumoroso in log/network tab):

```js
async function loadAll() {
  if (currentUser.value?.role === 'sales') {
    await loadQuotes()
    return
  }
  await Promise.all([loadSettings(), loadUsers(), /* ...tutto il resto */, loadQuotes()])
}
```

### 4. Gestione ruolo nel form utenti esistente

Se c'è già un form "crea utente"/"modifica utente", aggiungere un
`<select>` ruolo, con questi accorgimenti:
- Default `admin` per non rompere il flusso di creazione esistente
- Impedire che un utente cambi il **proprio** ruolo (rischio di
  auto-bloccarsi fuori dall'accesso admin) — sia lato UI (select
  disabilitato) che lato backend (controllo esplicito
  `if (role && id === req.user.userId && role !== 'admin') return 403`)

---

## Checklist di verifica end-to-end

Prima di considerare il pattern completo, testare via API diretta
(non solo UI) con un utente del ruolo limitato:

- [ ] Login → JWT contiene il ruolo corretto
- [ ] `/me` (o endpoint equivalente) espone il ruolo
- [ ] Endpoint della feature dedicata: 200
- [ ] Endpoint admin generici: 403 (provarne almeno 2-3 diversi)
- [ ] Endpoint sensibili "vicini" alla feature ma non suoi (es. init OAuth2
      condiviso): 403
- [ ] Endpoint di sola lettura condivisi (es. status di un'integrazione):
      200 anche per il ruolo limitato, se previsto
- [ ] Isolamento dati: creare una risorsa con l'utente limitato, verificare
      che un secondo utente limitato non la veda nella lista
