emilsoftware-utilities 2.0.0-dev.56 → 2.0.0-dev.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +14 -0
- package/dist/accessi-module/Console/console.css +10 -1
- package/dist/accessi-module/Console/console.js +100 -7
- package/dist/accessi-module/Console/console.js.map +1 -1
- package/dist/accessi-module/Console/generated/accessiApi.d.ts +11 -1
- package/dist/accessi-module/Console/generated/accessiApi.js +10 -2
- package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/index.d.ts +3 -0
- package/dist/accessi-module/Console/generated/model/index.js +3 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/loginAsRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/loginAsRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/loginAsRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/loginAsResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/loginAsResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/loginAsResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/loginAsResult.d.ts +16 -0
- package/dist/accessi-module/Console/generated/model/loginAsResult.js +9 -0
- package/dist/accessi-module/Console/generated/model/loginAsResult.js.map +1 -0
- package/dist/accessi-module/Console/help.js +17 -0
- package/dist/accessi-module/Console/help.js.map +1 -1
- package/dist/accessi-module/Console/wiki.js +18 -2
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/UserController.d.ts +2 -0
- package/dist/accessi-module/Controllers/UserController.js +49 -0
- package/dist/accessi-module/Controllers/UserController.js.map +1 -1
- package/dist/accessi-module/Dtos/LoginAsDtos.d.ts +15 -0
- package/dist/accessi-module/Dtos/LoginAsDtos.js +65 -0
- package/dist/accessi-module/Dtos/LoginAsDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/index.d.ts +1 -0
- package/dist/accessi-module/Dtos/index.js +1 -0
- package/dist/accessi-module/Dtos/index.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.d.ts +16 -1
- package/dist/accessi-module/Services/AuthService/AuthService.js +28 -8
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/index.d.ts +1 -0
- package/dist/accessi-module/index.js +5 -1
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/security/authenticatedToken.d.ts +6 -0
- package/dist/accessi-module/security/authenticatedToken.js +13 -0
- package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
- package/dist/accessi-module/security/loginAs.d.ts +10 -0
- package/dist/accessi-module/security/loginAs.js +47 -0
- package/dist/accessi-module/security/loginAs.js.map +1 -0
- package/dist/emilsoftware-utilities-2.0.0-dev.57.tgz +0 -0
- package/package.json +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.56.tgz +0 -0
package/README.MD
CHANGED
|
@@ -130,6 +130,20 @@ import { ServiceTokenGuard, RequireServiceTokenScopes } from 'emilsoftware-utili
|
|
|
130
130
|
export class IaController {}
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
## Login as (impersonazione temporanea)
|
|
134
|
+
|
|
135
|
+
Superutenti e admin possono generare un accesso temporaneo a un utente per entrare in un'altra
|
|
136
|
+
applicazione senza conoscerne la password. Dalla console Accessi il pulsante **Login as** nella
|
|
137
|
+
lista utenti apre un pannello in cui inserire l'URL dell'applicazione e generare il link.
|
|
138
|
+
|
|
139
|
+
- Endpoint: `POST /api/accessi/user/login-as/:codiceUtente` con body `{ "url": "https://app.example.com/..." }`.
|
|
140
|
+
- Risposta: `Result.loginUrl` con `?loginAsToken=<jwt>`, `token`, `expiresInSeconds` e `expiresAt`.
|
|
141
|
+
- Il token dura **5 minuti** ed e' un normale access token Accessi con `amr: ["login_as"]`, `loginAs: true` e `act` valorizzato con l'amministratore che lo ha generato.
|
|
142
|
+
- E' accettato da `get-user-by-token` e da `authorizeAccessi`, anche se l'utente target ha la 2FA attiva: l'attore ha gia' completato l'autenticazione amministrativa.
|
|
143
|
+
- Gerarchia **ADMIN > SUPER**: un admin puo' impersonare chiunque, un superutente non puo' impersonare un admin.
|
|
144
|
+
- L'URL viene composto con `buildLoginAsUrl` (esportata dalla libreria), che accetta solo URL assoluti `http`/`https` e conserva query e fragment esistenti.
|
|
145
|
+
- Accessi non consuma il token al posto dell'applicazione: da quel momento e' l'app di destinazione a leggere `loginAsToken`, validarlo e decidere cosa consentire. Il token e' una credenziale: non condividerlo e non registrarlo nei log.
|
|
146
|
+
|
|
133
147
|
## Hardening e opzioni
|
|
134
148
|
|
|
135
149
|
- I log delle query **non includono i valori dei parametri** (segreti, credenziali, PII). Per riattivare esplicitamente i valori: `logQueryParameters: true` nelle `databaseOptions`.
|
|
@@ -508,7 +508,16 @@ article h3 { margin-bottom: var(--s2); }
|
|
|
508
508
|
/* Token di servizio */
|
|
509
509
|
.token-reveal { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); margin: var(--s4) 0; padding: var(--s4); border: 1px solid var(--brand-line); border-radius: var(--r-md); background: var(--brand-soft); }
|
|
510
510
|
.token-reveal code { flex: 1 1 100%; word-break: break-all; color: var(--brand-strong); font-family: var(--font-mono); font-size: var(--fs-sm); }
|
|
511
|
-
.token-reveal button { flex: 0 0 auto; }
|
|
511
|
+
.token-reveal button, .token-reveal .button-link { flex: 0 0 auto; }
|
|
512
|
+
.button-link { display: inline-flex; align-items: center; justify-content: center; min-height: 38px; padding: 0 var(--s4); border: 1px solid var(--line-strong); border-radius: var(--r-sm); color: var(--ink-2); background: var(--surface); font-weight: 600; text-decoration: none; }
|
|
513
|
+
.button-link:hover { border-color: #b9c4cc; color: var(--ink); background: var(--surface-sunken); }
|
|
514
|
+
|
|
515
|
+
/* Login as */
|
|
516
|
+
.login-as-info { margin: var(--s3) 0 var(--s4); padding: var(--s3) var(--s4); border: 1px solid var(--warn-line); border-radius: var(--r-md); color: var(--warn-ink); background: var(--warn-soft); }
|
|
517
|
+
.login-as-info strong { display: block; margin-bottom: var(--s1); }
|
|
518
|
+
.login-as-info ul { margin: 0; padding-left: 1.2em; }
|
|
519
|
+
.login-as-info li + li { margin-top: var(--s1); }
|
|
520
|
+
.login-as-info code { padding: 1px 5px; border-radius: var(--r-sm); background: rgba(255, 255, 255, .55); font-family: var(--font-mono); }
|
|
512
521
|
|
|
513
522
|
/* Paginatore — contenitore dedicato sopra/sotto la tabella, sempre orizzontale */
|
|
514
523
|
.pager-row { display: flex; justify-content: flex-end; }
|
|
@@ -453,6 +453,21 @@ var forcePasswordResetLegacy = async (options) => {
|
|
|
453
453
|
}
|
|
454
454
|
);
|
|
455
455
|
};
|
|
456
|
+
var getCreateLoginAsUrlUrl = (codiceUtente) => {
|
|
457
|
+
return `/api/accessi/user/login-as/${codiceUtente}`;
|
|
458
|
+
};
|
|
459
|
+
var createLoginAsUrl = async (codiceUtente, loginAsRequest, options) => {
|
|
460
|
+
return accessiFetch(
|
|
461
|
+
getCreateLoginAsUrlUrl(codiceUtente),
|
|
462
|
+
{
|
|
463
|
+
...options,
|
|
464
|
+
method: "POST",
|
|
465
|
+
body: JSON.stringify(
|
|
466
|
+
loginAsRequest
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
};
|
|
456
471
|
var getSetUserPasswordUrl = (codiceUtente) => {
|
|
457
472
|
return `/api/accessi/user/set-password/${codiceUtente}`;
|
|
458
473
|
};
|
|
@@ -1379,7 +1394,7 @@ POST /api/accessi/auth/confirm-reset-password/{token}
|
|
|
1379
1394
|
blocks: [
|
|
1380
1395
|
p(`La console \xE8 servita dal modulo su /api/accessi/console. Non ha stato proprio: ogni azione chiama le API protette con il JWT dell'utente connesso.`),
|
|
1381
1396
|
list([
|
|
1382
|
-
`Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA.`,
|
|
1397
|
+
`Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA e Login as.`,
|
|
1383
1398
|
`Ruoli e grant: CRUD dei ruoli con l'albero delle abilitazioni.`,
|
|
1384
1399
|
`Menu e gruppi: CRUD di gruppi, menu e tipi menu.`,
|
|
1385
1400
|
`Token di servizio: creazione, rotazione, revoca.`,
|
|
@@ -1398,6 +1413,33 @@ POST /api/accessi/auth/confirm-reset-password/{token}
|
|
|
1398
1413
|
]
|
|
1399
1414
|
},
|
|
1400
1415
|
// -------------------------------------------------------------------------
|
|
1416
|
+
{
|
|
1417
|
+
id: "login-as",
|
|
1418
|
+
group: "Console",
|
|
1419
|
+
title: "Login as (accesso temporaneo)",
|
|
1420
|
+
summary: "Superutenti e admin possono generare un URL per entrare in un applicazione come un altro utente.",
|
|
1421
|
+
blocks: [
|
|
1422
|
+
p(`Dalla lista utenti della console il pulsante Login as apre il pannello dedicato: inserisci l'URL dell'applicazione e premi "Genera URL di accesso". Accessi emette un JWT per l'utente target e lo allega all'URL come query parameter \`loginAsToken\`.`),
|
|
1423
|
+
code("http", `POST /api/accessi/user/login-as/:codiceUtente
|
|
1424
|
+
Content-Type: application/json
|
|
1425
|
+
Authorization: Bearer <jwt amministratore>
|
|
1426
|
+
|
|
1427
|
+
{ "url": "https://app.example.com/dashboard" }`, "Richiesta"),
|
|
1428
|
+
code("json", `{
|
|
1429
|
+
"Result": {
|
|
1430
|
+
"loginUrl": "https://app.example.com/dashboard?loginAsToken=eyJ...",
|
|
1431
|
+
"token": "eyJ...",
|
|
1432
|
+
"expiresInSeconds": 300,
|
|
1433
|
+
"expiresAt": "2026-10-23T14:05:00.000Z"
|
|
1434
|
+
}
|
|
1435
|
+
}`, "Risposta"),
|
|
1436
|
+
note("danger", `Il token dura 5 minuti`, `Il token \xE8 una credenziale: chiunque abbia l'URL pu\xF2 usarlo fino alla scadenza per entrare nell'applicazione come l'utente target. Non condividerlo, non salvarlo in log o documenti e non usarlo in produzione per operazioni distruttive.`),
|
|
1437
|
+
note("info", `Chi pu\xF2 generarlo`, `Solo utenti con FLGSUPER o FLGADMIN. Vale la stessa gerarchia della gestione utenti: un superutente non pu\xF2 impersonare un admin; un admin pu\xF2 impersonare chiunque. Il tentativo non consentito restituisce 403.`),
|
|
1438
|
+
note("warn", `Responsabilit\xE0 dell applicazione`, `Accessi non controlla l'URL di destinazione n\xE9 consuma il token al posto dell'applicazione. L'app deve leggere \`loginAsToken\`, validarlo (per esempio con \`POST /api/accessi/auth/get-user-by-token\` o \`authorizeAccessi\`) e decidere cosa consentire. Il token \xE8 un access token Accessi a tutti gli effetti, con claim \`loginAs: true\`, \`amr: ["login_as"]\` e \`act\` valorizzato con l'attore.`),
|
|
1439
|
+
p(`Il token viene emesso anche per utenti con 2FA attiva: l'attore ha gi\xE0 completato l'autenticazione amministrativa. Non usare questa funzione per aggirare la 2FA di un utente comune al di fuori di un intervento amministrativo autorizzato.`)
|
|
1440
|
+
]
|
|
1441
|
+
},
|
|
1442
|
+
// -------------------------------------------------------------------------
|
|
1401
1443
|
{
|
|
1402
1444
|
id: "errori",
|
|
1403
1445
|
group: "Riferimenti",
|
|
@@ -1443,7 +1485,7 @@ POST /api/accessi/auth/confirm-reset-password/{token}
|
|
|
1443
1485
|
[
|
|
1444
1486
|
["Auth", "POST auth/login, auth/get-user-by-token, auth/confirm-reset-password/:token, auth/two-factor/verify, auth/two-factor/resend"],
|
|
1445
1487
|
["Email", "POST email/send-reset-password-email, GET email/reset-password-page/:token"],
|
|
1446
|
-
["Utenti", "GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, DELETE user/delete-user-permanent/:codiceUtente, POST user/set-password/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register"],
|
|
1488
|
+
["Utenti", "GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, DELETE user/delete-user-permanent/:codiceUtente, POST user/set-password/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register, POST user/login-as/:codiceUtente"],
|
|
1447
1489
|
["Permessi", "GET permission/roles, POST permission/create-role, PUT permission/update-role/:codiceRuolo, DELETE permission/delete-role/:codiceRuolo, POST permission/assign-roles/:codiceUtente, POST permission/assign-permissions/:codiceUtente, GET permission/grants/:codiceUtente, GET permission/menus, GET permission/menu-types, GET permission/groups-with-menus"],
|
|
1448
1490
|
["Catalogo", "POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]"],
|
|
1449
1491
|
["Configurator", "PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled"],
|
|
@@ -1534,7 +1576,7 @@ function buildAiDigest() {
|
|
|
1534
1576
|
}
|
|
1535
1577
|
|
|
1536
1578
|
// src/accessi-module/Console/changelog.md
|
|
1537
|
-
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n- Console: la pagina di default nella scheda utente \xE8 ora un autocomplete con ricerca. Propone tutte le rotte del catalogo menu, si filtra scrivendo ed \xE8 ancora possibile inserire un valore libero.\n- Autorizzazioni: i grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu. Un permesso diretto con livello inferiore (o 0) non annulla pi\xF9 il grant del ruolo.\n- Ruoli legacy: un menu senza descrizione non viene pi\xF9 scartato e un `TIPABI` mancante viene letto come livello 10, evitando utenti con ruolo ma senza accesso.\n- Grant dei ruoli arricchiti con i metadati di navigazione del catalogo menu (`pagina`, icona, gruppo, tipo): il frontend riceve gli stessi campi dei grant diretti e pu\xF2 costruire correttamente menu e rotte.\n- Sicurezza: `GET /api/accessi/permission/grants/:codiceUtente` accetta solo un JWT Accessi valido e consente la lettura solo al proprietario o a chi pu\xF2 gestire quel target (gerarchia ADMIN > SUPER). Un superutente non pu\xF2 leggere i grant di un admin.\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente Accessi.\").\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
|
|
1579
|
+
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Console: nuova funzione **Login as** per superutenti e admin. Dalla lista utenti si genera un URL verso l'applicazione scelta con `?loginAsToken=<jwt>`; il token dura 5 minuti, registra l'attore nelle claim `act`/`loginAs` e resta soggetto alla gerarchia ADMIN > SUPER. Endpoint: `POST /api/accessi/user/login-as/:codiceUtente`.\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n- Console: la pagina di default nella scheda utente \xE8 ora un autocomplete con ricerca. Propone tutte le rotte del catalogo menu, si filtra scrivendo ed \xE8 ancora possibile inserire un valore libero.\n- Autorizzazioni: i grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu. Un permesso diretto con livello inferiore (o 0) non annulla pi\xF9 il grant del ruolo.\n- Ruoli legacy: un menu senza descrizione non viene pi\xF9 scartato e un `TIPABI` mancante viene letto come livello 10, evitando utenti con ruolo ma senza accesso.\n- Grant dei ruoli arricchiti con i metadati di navigazione del catalogo menu (`pagina`, icona, gruppo, tipo): il frontend riceve gli stessi campi dei grant diretti e pu\xF2 costruire correttamente menu e rotte.\n- Sicurezza: `GET /api/accessi/permission/grants/:codiceUtente` accetta solo un JWT Accessi valido e consente la lettura solo al proprietario o a chi pu\xF2 gestire quel target (gerarchia ADMIN > SUPER). Un superutente non pu\xF2 leggere i grant di un admin.\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente Accessi.\").\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
|
|
1538
1580
|
|
|
1539
1581
|
// src/accessi-module/Console/help.ts
|
|
1540
1582
|
var p2 = (text) => ({ kind: "p", text });
|
|
@@ -1918,6 +1960,23 @@ var HELP = {
|
|
|
1918
1960
|
note2("info", "Solo admin", `Il pulsante \xE8 mostrato e accettato solo per il flag Admin. Un superutente senza Admin pu\xF2 solo flaggare l'utente come eliminato.`)
|
|
1919
1961
|
]
|
|
1920
1962
|
},
|
|
1963
|
+
"user-login-as": {
|
|
1964
|
+
title: "Login as",
|
|
1965
|
+
blocks: [
|
|
1966
|
+
p2(`Genera un accesso temporaneo all'applicazione come l'utente selezionato. Non richiede la password dell'utente e non modifica il suo profilo.`),
|
|
1967
|
+
note2("danger", "Il token scade in 5 minuti", `Il token \xE8 una credenziale: chiunque ottenga l'URL pu\xF2 usarlo fino alla scadenza per entrare nell'applicazione come l'utente target. Non condividerlo, non salvarlo in log o chat e non incollarlo in documenti.`),
|
|
1968
|
+
note2("info", "Chi pu\xF2 usarlo", `Solo superutenti e admin. Vale la gerarchia ADMIN > SUPER: un superutente non pu\xF2 impersonare un admin.`),
|
|
1969
|
+
p2(`Il token viene aggiunto all'URL scelto come parametro di query loginAsToken. Da quel momento \xE8 l'applicazione di destinazione a decidere come validarlo e cosa consentire.`)
|
|
1970
|
+
]
|
|
1971
|
+
},
|
|
1972
|
+
"login-as-url": {
|
|
1973
|
+
title: "URL dell applicazione",
|
|
1974
|
+
blocks: [
|
|
1975
|
+
p2(`Inserisci l'indirizzo assoluto dell'applicazione in cui vuoi entrare, per esempio https://app.example.com/dashboard.`),
|
|
1976
|
+
p2(`Il modulo aggiunge automaticamente ?loginAsToken=<token> conservando le query e il fragment gi\xE0 presenti. Sono ammessi solo URL http e https.`),
|
|
1977
|
+
note2("warn", "Nessun controllo sull applicazione", `Accessi non verifica che l'URL appartenga a un ambiente autorizzato: incollalo con attenzione per non inviare il token a un dominio sbagliato.`)
|
|
1978
|
+
]
|
|
1979
|
+
},
|
|
1921
1980
|
// ----------------------------------------------------- selezioni di menu
|
|
1922
1981
|
"menu-group-select": {
|
|
1923
1982
|
title: "Gruppo",
|
|
@@ -2228,12 +2287,22 @@ function federatedIdentityFields(providers) {
|
|
|
2228
2287
|
function federatedIdentitySection(identities, providers) {
|
|
2229
2288
|
return `<section class="user-editor-section sso-identity-section" data-user-panel="sso" role="tabpanel" aria-labelledby="user-tab-sso"><div class="section-heading"><div><p class="eyebrow">Autenticazione esterna</p><h3>Identit\xE0 SSO</h3></div><span class="muted">Collegamenti verificati dal backend</span></div><p class="form-help">Disabilita conserva lo storico; elimina rimuove il collegamento (l'utente resta).</p><div class="sso-identity-list">${identities.map((identity) => `<article class="sso-identity-item ${identity.active ? "" : "is-disabled"}"><div class="sso-identity-data"><strong>${escapeHtml(identity.provider)}</strong><dl class="entity-metadata"><div><dt>Subject</dt><dd>${escapeHtml(identity.subject)}</dd></div><div><dt>Stato</dt><dd>${identity.active ? "Abilitata" : "Disabilitata"}</dd></div></dl>${identity.note ? `<p class="entity-note"><strong>Nota</strong>${escapeHtml(identity.note)}</p>` : ""}</div><div class="sso-identity-actions"><button type="button" class="secondary" data-identity-toggle="${escapeHtml(identity.identityKey)}" data-active="${String(!identity.active)}">${identity.active ? "Disabilita" : "Abilita"}</button><button type="button" class="icon-button danger-button" data-identity-delete="${escapeHtml(identity.identityKey)}" aria-label="Elimina definitivamente il collegamento SSO ${escapeHtml(identity.provider)}" title="Elimina definitivamente il collegamento SSO"><span aria-hidden="true">🗑</span></button></div></article>`).join("") || '<p class="muted">Nessun collegamento SSO.</p>'}</div>${providers.some((provider) => provider.active) ? `<form id="link-sso" class="sso-link-form">${federatedIdentityFields(providers)}<button>Collega SSO verificato</button></form>` : '<p class="form-help">Non ci sono provider SSO attivi. Censiscine uno nella sezione SSO prima di creare collegamenti.</p>'}</section>`;
|
|
2230
2289
|
}
|
|
2231
|
-
function
|
|
2290
|
+
function loginAsPanel(user) {
|
|
2291
|
+
return `<form id="login-as" class="user-editor-section" data-user-panel="loginAs" role="tabpanel" aria-labelledby="user-tab-loginAs">
|
|
2292
|
+
<div class="section-heading"><div><p class="eyebrow">Accesso temporaneo</p><h3>Login as ${helpDot("user-login-as")}</h3></div><span class="muted">Solo superutenti e admin</span></div>
|
|
2293
|
+
<div class="login-as-info"><strong>Prima di generare l'URL</strong><ul><li>Il token vale <strong>5 minuti</strong> e permette di entrare nell'applicazione come <strong>${escapeHtml(user.email)}</strong>.</li><li>\xC8 una credenziale: chiunque abbia l'URL pu\xF2 usarlo fino alla scadenza. Non condividerlo e non salvarlo.</li><li>Il token viene allegato come <code>?loginAsToken=</code> all'URL indicato. L'applicazione di destinazione decide come consumarlo.</li><li>L'operazione \xE8 consentita solo a superutenti e admin, nel rispetto della gerarchia ADMIN > SUPER.</li></ul></div>
|
|
2294
|
+
<label>URL dell'applicazione ${helpDot("login-as-url")}<input name="url" type="url" placeholder="https://app.example.com/" maxlength="2048" required><span class="form-help">URL assoluto http/https. Eventuali query o fragment esistenti vengono conservati.</span></label>
|
|
2295
|
+
<button type="submit">Genera URL di accesso</button>
|
|
2296
|
+
<div id="login-as-result" class="token-reveal" hidden><code id="login-as-url"></code><a id="login-as-open" class="button-link" href="#" target="_blank" rel="noopener noreferrer">Apri</a><button id="login-as-copy" type="button">Copia URL</button></div>
|
|
2297
|
+
</form>`;
|
|
2298
|
+
}
|
|
2299
|
+
function userDetailTabs(hasSso, hasLoginAs) {
|
|
2232
2300
|
const tabs = [
|
|
2233
2301
|
...hasSso ? [{ id: "sso", label: "Identit\xE0 SSO" }] : [],
|
|
2234
2302
|
{ id: "profile", label: "Profilo e accesso" },
|
|
2235
2303
|
{ id: "roles", label: "Ruoli" },
|
|
2236
|
-
{ id: "grants", label: "Grant diretti" }
|
|
2304
|
+
{ id: "grants", label: "Grant diretti" },
|
|
2305
|
+
...hasLoginAs ? [{ id: "loginAs", label: "Login as" }] : []
|
|
2237
2306
|
];
|
|
2238
2307
|
return `<div class="user-tabs" role="tablist" aria-label="Sezioni utente">${tabs.map((tab) => `<button id="user-tab-${tab.id}" type="button" role="tab" aria-controls="user-panel-${tab.id}" data-user-tab="${tab.id}">${tab.label}</button>`).join("")}</div>`;
|
|
2239
2308
|
}
|
|
@@ -2471,7 +2540,7 @@ async function showUsers() {
|
|
|
2471
2540
|
render(`<div class="toolbar"><button id="new-local">Nuovo utente locale</button>${federatedAuthenticationAvailable ? '<button id="new-sso">Nuovo utente SSO</button>' : ""}<button id="reload-users" class="secondary">Aggiorna</button><button id="reset-legacy" class="secondary">Invia reset password legacy</button></div>
|
|
2472
2541
|
<div class="pager-row pager-row-top">${pagerMarkup(totalPages)}</div>
|
|
2473
2542
|
<table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
|
|
2474
|
-
${users.map(({ utente, userGrants }) => `<tr><td>${escapeHtml(utente.email)}<br><span class="muted">${escapeHtml(utente.nome)} ${escapeHtml(utente.cognome)}</span></td><td>${escapeHtml(utente.statoRegistrazione)}</td><td>${utente.passwordlessLoginEnabled ? "codice email" : utente.passwordLoginEnabled === false ? "solo SSO" : "password"}</td><td>${utente.flagDueFattori ? "Attiva" : "Disattiva"}</td><td class="roles-col">${rolesCell(utente.codiceUtente, userGrants)}</td><td><button data-user="${utente.codiceUtente}"${canManageUser(utente) ? "" : ' disabled title="Non puoi gestire un utente di rango superiore" aria-disabled="true"'}>Gestisci</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Nessun utente.</td></tr>'}
|
|
2543
|
+
${users.map(({ utente, userGrants }) => `<tr><td>${escapeHtml(utente.email)}<br><span class="muted">${escapeHtml(utente.nome)} ${escapeHtml(utente.cognome)}</span></td><td>${escapeHtml(utente.statoRegistrazione)}</td><td>${utente.passwordlessLoginEnabled ? "codice email" : utente.passwordLoginEnabled === false ? "solo SSO" : "password"}</td><td>${utente.flagDueFattori ? "Attiva" : "Disattiva"}</td><td class="roles-col">${rolesCell(utente.codiceUtente, userGrants)}</td><td class="row-actions"><button type="button" class="secondary" data-login-as="${utente.codiceUtente}"${canManageUser(utente) ? "" : ' disabled title="Non puoi impersonare un utente di rango superiore" aria-disabled="true"'}>Login as</button><button data-user="${utente.codiceUtente}"${canManageUser(utente) ? "" : ' disabled title="Non puoi gestire un utente di rango superiore" aria-disabled="true"'}>Gestisci</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Nessun utente.</td></tr>'}
|
|
2475
2544
|
</tbody></table><div class="pager-row pager-row-bottom">${pagerMarkup(totalPages)}</div>`);
|
|
2476
2545
|
byId("new-local").onclick = () => showLocalUserForm();
|
|
2477
2546
|
if (federatedAuthenticationAvailable) byId("new-sso").onclick = () => handleAction(showSsoUserForm);
|
|
@@ -2493,6 +2562,12 @@ async function showUsers() {
|
|
|
2493
2562
|
document.querySelectorAll("[data-user]").forEach((button) => {
|
|
2494
2563
|
button.onclick = () => handleAction(() => showUser(button.dataset.user ?? ""));
|
|
2495
2564
|
});
|
|
2565
|
+
document.querySelectorAll("[data-login-as]").forEach((button) => {
|
|
2566
|
+
button.onclick = () => handleAction(async () => {
|
|
2567
|
+
await showUser(button.dataset.loginAs ?? "");
|
|
2568
|
+
activateUserTab("loginAs");
|
|
2569
|
+
});
|
|
2570
|
+
});
|
|
2496
2571
|
bindRolesToggles();
|
|
2497
2572
|
}
|
|
2498
2573
|
function authenticationPolicyFields(user) {
|
|
@@ -2917,12 +2992,13 @@ async function showUser(rawCode) {
|
|
|
2917
2992
|
</div>
|
|
2918
2993
|
</div>
|
|
2919
2994
|
</form>
|
|
2920
|
-
${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
|
|
2995
|
+
${userDetailTabs(federatedAuthenticationAvailable, manageable)}<div class="user-editor">
|
|
2921
2996
|
${identitySection}
|
|
2922
2997
|
<form id="profile" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile"><h3>Profilo e accesso</h3><label>Nome ${helpDot("user-name")}<input name="nome" value="${escapeHtml(user.nome)}"></label><label>Cognome ${helpDot("user-name")}<input name="cognome" value="${escapeHtml(user.cognome)}"></label><label>Email ${helpDot("user-email")}<input name="email" type="email" value="${escapeHtml(user.email)}" required><span class="form-help">Identificativo di accesso e recapito.</span></label>${pageDefaultField(user)}${federatedAuthenticationAvailable ? `<label class="check"><input name="passwordLoginEnabled" type="checkbox" ${user.passwordLoginEnabled !== false ? "checked" : ""}> Login con password ${helpDot("user-password-login")}</label><span class="form-help">Se disabilitato, il login con email e password restituisce un errore esplicito; restano valide le identit\xE0 SSO attive.</span>` : ""}${authenticationPolicyFields(user)}${extendedProfileFields(user)}${privilegeFields(user)}<button id="save-profile" type="submit">Salva</button></form>
|
|
2923
2998
|
${passwordFields()}
|
|
2924
2999
|
${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
|
|
2925
3000
|
${directGrantEditor(menuGroups, userGrants.abilitazioni)}
|
|
3001
|
+
${manageable ? loginAsPanel(user) : ""}
|
|
2926
3002
|
</div>`);
|
|
2927
3003
|
bindPageDefaultCombobox(pageRouteSuggestions(menuGroups));
|
|
2928
3004
|
byId("back").onclick = () => show("users");
|
|
@@ -2982,6 +3058,23 @@ async function showUser(rawCode) {
|
|
|
2982
3058
|
await showUser(String(codiceUtente));
|
|
2983
3059
|
});
|
|
2984
3060
|
};
|
|
3061
|
+
const loginAsForm = document.getElementById("login-as");
|
|
3062
|
+
if (loginAsForm) loginAsForm.onsubmit = (event) => {
|
|
3063
|
+
event.preventDefault();
|
|
3064
|
+
handleAction(async () => {
|
|
3065
|
+
const values = formValues(eventForm(event));
|
|
3066
|
+
const issued = result(await createLoginAsUrl(codiceUtente, { url: values.url ?? "" }));
|
|
3067
|
+
const box = byId("login-as-result");
|
|
3068
|
+
box.hidden = false;
|
|
3069
|
+
byId("login-as-url").textContent = issued.loginUrl;
|
|
3070
|
+
byId("login-as-open").href = issued.loginUrl;
|
|
3071
|
+
byId("login-as-copy").onclick = () => handleAction(async () => {
|
|
3072
|
+
await navigator.clipboard.writeText(issued.loginUrl);
|
|
3073
|
+
showNotice("URL di accesso copiato negli appunti.");
|
|
3074
|
+
});
|
|
3075
|
+
showNotice(`URL di accesso generato. Scade alle ${new Date(issued.expiresAt).toLocaleTimeString()}.`);
|
|
3076
|
+
});
|
|
3077
|
+
};
|
|
2985
3078
|
const setGdprButton = document.getElementById("set-gdpr");
|
|
2986
3079
|
if (setGdprButton) setGdprButton.onclick = () => handleAction(async () => {
|
|
2987
3080
|
if (!window.confirm(`Registrare il consenso GDPR per ${user.email}?`)) return;
|