emilsoftware-utilities 2.0.0-dev.42 → 2.0.0-dev.44
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/dist/accessi-module/AccessiModule.d.ts +2 -1
- package/dist/accessi-module/AccessiModule.js +2 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +171 -42
- package/dist/accessi-module/Console/console.js.map +1 -1
- package/dist/accessi-module/Console/generated/accessiApi.d.ts +33 -3
- package/dist/accessi-module/Console/generated/accessiApi.js +27 -3
- package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/adminBootstrapStatusResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/adminBootstrapStatusResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/adminBootstrapStatusResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/adminBootstrapStatusResult.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/adminBootstrapStatusResult.js +9 -0
- package/dist/accessi-module/Console/generated/model/adminBootstrapStatusResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.js +3 -0
- package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.js +9 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.js +9 -0
- package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/groupWithMenusEntity.d.ts +1 -1
- package/dist/accessi-module/Console/generated/model/groupWithMenusEntity.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/index.d.ts +8 -0
- package/dist/accessi-module/Console/generated/model/index.js +8 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/registerRequest.d.ts +1 -1
- package/dist/accessi-module/Console/generated/model/registerRequest.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/userDto.d.ts +2 -2
- package/dist/accessi-module/Console/generated/model/userDto.js.map +1 -1
- package/dist/accessi-module/Console/help.js +3 -2
- package/dist/accessi-module/Console/help.js.map +1 -1
- package/dist/accessi-module/Console/index.html +1 -1
- package/dist/accessi-module/Console/wiki.js +9 -4
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.d.ts +1 -0
- package/dist/accessi-module/Controllers/AuthController.js +18 -0
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +14 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +128 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -0
- package/dist/accessi-module/Controllers/FiltriController.d.ts +8 -1
- package/dist/accessi-module/Controllers/FiltriController.js +21 -2
- package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
- package/dist/accessi-module/Controllers/PermissionController.d.ts +8 -1
- package/dist/accessi-module/Controllers/PermissionController.js +24 -4
- package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
- package/dist/accessi-module/Controllers/UserController.d.ts +6 -0
- package/dist/accessi-module/Controllers/UserController.js +89 -11
- package/dist/accessi-module/Controllers/UserController.js.map +1 -1
- package/dist/accessi-module/Dtos/AdminBootstrapDtos.d.ts +7 -0
- package/dist/accessi-module/Dtos/AdminBootstrapDtos.js +18 -1
- package/dist/accessi-module/Dtos/AdminBootstrapDtos.js.map +1 -1
- package/dist/accessi-module/Dtos/ConsoleEntryDtos.d.ts +24 -0
- package/dist/accessi-module/Dtos/ConsoleEntryDtos.js +77 -0
- package/dist/accessi-module/Dtos/ConsoleEntryDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/GetGroupsWithMenusResponse.js +1 -1
- package/dist/accessi-module/Dtos/GetGroupsWithMenusResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/RegisterRequest.js +1 -1
- package/dist/accessi-module/Dtos/RegisterRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/UserDto.js +2 -2
- package/dist/accessi-module/Dtos/UserDto.js.map +1 -1
- package/dist/accessi-module/Services/UserService/UserService.d.ts +10 -0
- package/dist/accessi-module/Services/UserService/UserService.js +70 -15
- package/dist/accessi-module/Services/UserService/UserService.js.map +1 -1
- package/dist/accessi-module/federated-auth/FederatedAuthController.d.ts +10 -1
- package/dist/accessi-module/federated-auth/FederatedAuthController.js +54 -14
- package/dist/accessi-module/federated-auth/FederatedAuthController.js.map +1 -1
- package/dist/accessi-module/federated-auth/FederatedAuthService.d.ts +2 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.js +10 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
- package/dist/accessi-module/security/accessControl.d.ts +17 -0
- package/dist/accessi-module/security/accessControl.js +23 -0
- package/dist/accessi-module/security/accessControl.js.map +1 -1
- package/dist/accessi-module/security/consoleEntry.d.ts +16 -0
- package/dist/accessi-module/security/consoleEntry.js +50 -0
- package/dist/accessi-module/security/consoleEntry.js.map +1 -0
- package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +1 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.44.tgz +0 -0
- package/package.json +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.42.tgz +0 -0
|
@@ -131,6 +131,18 @@ var bootstrapAdmin = async (bootstrapAdminRequest, options) => {
|
|
|
131
131
|
}
|
|
132
132
|
);
|
|
133
133
|
};
|
|
134
|
+
var getGetAdminBootstrapStatusUrl = () => {
|
|
135
|
+
return `/api/accessi/auth/bootstrap-admin/status`;
|
|
136
|
+
};
|
|
137
|
+
var getAdminBootstrapStatus = async (options) => {
|
|
138
|
+
return accessiFetch(
|
|
139
|
+
getGetAdminBootstrapStatusUrl(),
|
|
140
|
+
{
|
|
141
|
+
...options,
|
|
142
|
+
method: "GET"
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
};
|
|
134
146
|
var getGetRolesUrl = () => {
|
|
135
147
|
return `/api/accessi/permission/roles`;
|
|
136
148
|
};
|
|
@@ -429,6 +441,18 @@ var deleteUserPermanently = async (codiceUtente, options) => {
|
|
|
429
441
|
}
|
|
430
442
|
);
|
|
431
443
|
};
|
|
444
|
+
var getForcePasswordResetLegacyUrl = () => {
|
|
445
|
+
return `/api/accessi/user/force-password-reset-legacy`;
|
|
446
|
+
};
|
|
447
|
+
var forcePasswordResetLegacy = async (options) => {
|
|
448
|
+
return accessiFetch(
|
|
449
|
+
getForcePasswordResetLegacyUrl(),
|
|
450
|
+
{
|
|
451
|
+
...options,
|
|
452
|
+
method: "POST"
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
};
|
|
432
456
|
var getSetUserPasswordUrl = (codiceUtente) => {
|
|
433
457
|
return `/api/accessi/user/set-password/${codiceUtente}`;
|
|
434
458
|
};
|
|
@@ -489,6 +513,18 @@ var updateUtente = async (codiceUtente, userDto, options) => {
|
|
|
489
513
|
}
|
|
490
514
|
);
|
|
491
515
|
};
|
|
516
|
+
var getSetGdprUrl = (codiceUtente) => {
|
|
517
|
+
return `/api/accessi/user/set-gdpr/${codiceUtente}`;
|
|
518
|
+
};
|
|
519
|
+
var setGdpr = async (codiceUtente, options) => {
|
|
520
|
+
return accessiFetch(
|
|
521
|
+
getSetGdprUrl(codiceUtente),
|
|
522
|
+
{
|
|
523
|
+
...options,
|
|
524
|
+
method: "PATCH"
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
};
|
|
492
528
|
var getGetTipiFiltroUrl = () => {
|
|
493
529
|
return `/api/accessi/filtri/tipi`;
|
|
494
530
|
};
|
|
@@ -1237,8 +1273,10 @@ POST /api/accessi/auth/confirm-reset-password/{token}
|
|
|
1237
1273
|
["POST", "/api/accessi/user/register", "Registrazione pubblica, solo se publicRegistration.enabled."]
|
|
1238
1274
|
]
|
|
1239
1275
|
),
|
|
1240
|
-
note("warn", `Eliminazione: soft o definitiva`, `Il soft delete (STAREG=DELETE) \xE8 disponibile per admin e superutenti ed \xE8 reversibile. L'eliminazione definitiva rimuove utente e record collegati in transazione ed \xE8 riservata agli admin; il superutente senza flag Admin pu\xF2 solo flaggare l'utente come eliminato.`),
|
|
1276
|
+
note("warn", `Eliminazione: soft o definitiva`, `Il soft delete (STAREG=DELETE) \xE8 disponibile per admin e superutenti ed \xE8 reversibile. L'eliminazione definitiva rimuove utente e record collegati in transazione ed \xE8 riservata agli admin; il superutente senza flag Admin pu\xF2 solo flaggare l'utente come eliminato. Se l'utente ha dati collegati in altre tabelle (per esempio campi estesi del backend) l'eliminazione definitiva fallisce con code ACCESSI_USER_DELETE_HAS_REFERENCES e messaggio esplicito: usa allora il soft delete.`),
|
|
1277
|
+
note("ok", `Creazione con invito atomica`, `La creazione utente con invio email \xE8 atomica: se SMTP non \xE8 configurato si fallisce prima di scrivere, e se l'invio fallisce l'utente creato viene annullato (codice ACCESSI_USER_INVITE_EMAIL_FAILED). Non restano utenti senza password. In alternativa un admin crea l'utente e imposta la password dal form dedicato.`),
|
|
1241
1278
|
note("ok", `Admin: controllo completo del profilo`, `Dalla scheda utente un admin modifica tutti i campi Accessi: anagrafica, cellulare, lingua, avatar, pagina di default, metadata JSON, 2FA/passwordless/login con password, flag, GDPR, stato registrazione, scadenza password e filtri (tabella FILTRI). La password si imposta direttamente con set-password. Le scritture sono idempotenti (UPDATE OR INSERT su UTENTI_CONFIG) e verificate dal backend; non serve accedere al database.`),
|
|
1279
|
+
note("info", `Consenso GDPR`, `Il pulsante "Registra consenso GDPR" completa il processo: imposta FLGGDPR e DATGDPR e scrive la riga storica in UTENTI_GDPR (endpoint set-gdpr). Anche impostare flagGdpr via update-user completa il processo con la stessa riga storica.`),
|
|
1242
1280
|
good("ts", [
|
|
1243
1281
|
"await fetch('/api/accessi/user/create-managed-user', {",
|
|
1244
1282
|
" method: 'POST',",
|
|
@@ -1445,10 +1483,13 @@ POST /api/accessi/auth/confirm-reset-password/{token}
|
|
|
1445
1483
|
`SQL: esporta il DDL delle entit\xE0 Accessi (tabelle, generatori, FK, indici, check, trigger) per replicare il database.`
|
|
1446
1484
|
]),
|
|
1447
1485
|
note("info", `Superutente (FLGSUPER)`, `Riceve TUTTE le abilitazioni al livello massimo (30) dopo il login. In console vede solo la sezione Utenti e gestisce utenti, ruoli e grant.`),
|
|
1448
|
-
note("info", `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU.`),
|
|
1449
|
-
note("info", `
|
|
1486
|
+
note("info", `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU. Gerarchia: ADMIN > SUPER > utente.`),
|
|
1487
|
+
note("info", `Gerarchia dei ruoli`, `Vale ADMIN (2) > SUPER (1) > utente comune (0). Un attore pu\xF2 gestire solo utenti di rango pari o inferiore: un superutente pu\xF2 amministrare totalmente utenti comuni e superutenti (profilo, password, flag, ruoli, grant, stato, eliminazione, filtri e identit\xE0/utenti SSO) ma NON pu\xF2 toccare un admin. Un admin pu\xF2 gestire chiunque. La regola \xE8 imposta dal backend (ensureCanManageTargetUser): un tentativo non consentito restituisce 403.`),
|
|
1488
|
+
note("info", `Gestione dei flag`, `Dalla scheda utente i flag sono mostrati in base al ruolo di chi \xE8 connesso: un admin pu\xF2 assegnare o togliere sia il flag Admin sia il flag Superutente; un superutente senza flag Admin pu\xF2 gestire solo il flag Superutente. I flag si impostano anche in creazione (utente locale o SSO).`),
|
|
1489
|
+
note("warn", `Ultimo admin`, `Per non restare chiusi fuori dalla console, l'ultimo admin attivo non pu\xF2 essere disattivato, bloccato, eliminato n\xE9 privato del flag Admin: l'operazione restituisce 400 finch\xE9 non viene promosso un altro admin.`),
|
|
1450
1490
|
note("info", `Accesso`, `Apri /api/accessi/console e accedi con un utente con FLGSUPER o FLGADMIN. Le sezioni sono raggiungibili anche tramite deep link (per esempio /api/accessi/console/menu-e-gruppi/menu).`),
|
|
1451
|
-
note("info", `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio).
|
|
1491
|
+
note("info", `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio). La sezione "Crea utente admin" nella schermata di login compare solo quando adminBootstrap \xE8 abilitato (stato esposto da GET /api/accessi/auth/bootstrap-admin/status): incolla il token e crea il primo admin senza email di conferma. La password \xE8 opzionale: se assente viene generata e mostrata una sola volta.`),
|
|
1492
|
+
note("ok", `Auto-ingresso dal frontend`, `Se un frontend ha gi\xE0 una sessione Accessi valida e l'utente \xE8 abilitato alla console, pu\xF2 aprire la console senza rifare il login: \`POST /api/accessi/console/entry\` con \`Authorization: Bearer <jwt>\` restituisce un ticket monouso; apri poi \`/api/accessi/console#entry=<ticket>\`. Il JWT non finisce mai nell'URL; il ticket dura 60 secondi ed \xE8 utilizzabile una sola volta (endpoint di scambio protetto da rate limit).`)
|
|
1452
1493
|
]
|
|
1453
1494
|
},
|
|
1454
1495
|
// -------------------------------------------------------------------------
|
|
@@ -1589,7 +1630,7 @@ function buildAiDigest() {
|
|
|
1589
1630
|
}
|
|
1590
1631
|
|
|
1591
1632
|
// src/accessi-module/Console/changelog.md
|
|
1592
|
-
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\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";
|
|
1633
|
+
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\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";
|
|
1593
1634
|
|
|
1594
1635
|
// src/accessi-module/Console/help.ts
|
|
1595
1636
|
var p2 = (text) => ({ kind: "p", text });
|
|
@@ -1772,9 +1813,10 @@ var HELP = {
|
|
|
1772
1813
|
]
|
|
1773
1814
|
},
|
|
1774
1815
|
"user-gdpr": {
|
|
1775
|
-
title: "GDPR
|
|
1816
|
+
title: "Consenso GDPR",
|
|
1776
1817
|
blocks: [
|
|
1777
|
-
p2(`
|
|
1818
|
+
p2(`Lo stato mostra se il consenso \xE8 registrato. Il pulsante "Registra consenso GDPR" (endpoint set-gdpr) imposta FLGGDPR e DATGDPR e scrive la riga storica in UTENTI_GDPR.`),
|
|
1819
|
+
note2("info", "Completo e tracciato", `A differenza del flag impostato a mano, il flusso dedicato registra anche la data e lo storico dell'accettazione.`)
|
|
1778
1820
|
]
|
|
1779
1821
|
},
|
|
1780
1822
|
"user-json": {
|
|
@@ -2175,7 +2217,7 @@ function roleAssignmentEditor(roles, assignedRoles, groups) {
|
|
|
2175
2217
|
const assigned = (assignedRoles ?? []).some((current) => current.codiceRuolo === role.codiceRuolo);
|
|
2176
2218
|
const groupedMenus = groups.map((group) => ({ group, entries: group.menus.map((menu) => ({ menu, assignment: roleMenus.find((item) => item.codiceMenu === menu.codiceMenu) })).filter((entry) => entry.assignment) })).filter((entry) => entry.entries.length > 0);
|
|
2177
2219
|
const missingMenus = roleMenus.filter((item) => !catalogMenus.has(item.codiceMenu));
|
|
2178
|
-
return `<article class="role-assignment-card"><div class="role-assignment-heading"><label class="check"><input type="checkbox" name="role" value="${escapeHtml(role.codiceRuolo)}" ${assigned ? "checked" : ""}><span><strong>${escapeHtml(role.descrizioneRuolo)}</strong><small>Codice ruolo: ${escapeHtml(role.codiceRuolo)}</small></span></label><span class="role-menu-count">${roleMenus.length} menu</span></div><div class="role-permission-groups">${groupedMenus.map(({ group, entries }) => `<section class="role-permission-group"><div class="role-permission-group-heading"><strong>${escapeHtml(group.descrizioneGruppo)}</strong>${groupMetadata(group)}</div>${entries.map(({ menu, assignment }) => `<div class="role-permission-entry ${menu.enabled === false ? "is-disabled" : ""}"><div><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menuMetadata(menu)}</div><span class="permission-level">${escapeHtml(rolePermissionLevel(assignment?.tipoAbilitazione))}</span></div>`).join("")}</section>`).join("") || '<p class="form-help">Questo ruolo non contiene menu attivi nel catalogo.</p>'}${missingMenus.length ? `<section class="role-permission-group missing-role-menus"><strong>Menu non pi\
|
|
2220
|
+
return `<article class="role-assignment-card"><div class="role-assignment-heading"><label class="check"><input type="checkbox" name="role" value="${escapeHtml(role.codiceRuolo)}" ${assigned ? "checked" : ""}><span><strong>${escapeHtml(role.descrizioneRuolo)}</strong><small>Codice ruolo: ${escapeHtml(role.codiceRuolo)}</small></span></label><span class="role-menu-count">${roleMenus.length} menu</span></div><div class="role-permission-groups">${groupedMenus.map(({ group, entries }) => `<section class="role-permission-group"><div class="role-permission-group-heading"><strong>${escapeHtml(group.descrizioneGruppo)}</strong>${groupMetadata(group)}</div>${entries.map(({ menu, assignment }) => `<div class="role-permission-entry ${menu.enabled === false ? "is-disabled" : ""}"><div><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menuMetadata(menu)}</div><span class="permission-level">${escapeHtml(rolePermissionLevel(assignment?.tipoAbilitazione))}</span></div>`).join("")}</section>`).join("") || '<p class="form-help">Questo ruolo non contiene menu attivi nel catalogo.</p>'}${missingMenus.length ? `<section class="role-permission-group missing-role-menus"><strong>Menu non pi\xF9 presenti nel catalogo</strong>${missingMenus.map((menu) => `<div class="role-permission-entry"><div><strong>${escapeHtml(menu.codiceMenu)}</strong><p class="entity-note">Il menu \xE8 ancora associato al ruolo ma non \xE8 disponibile nel catalogo corrente.</p></div><span class="permission-level">${escapeHtml(rolePermissionLevel(menu.tipoAbilitazione))}</span></div>`).join("")}</section>` : ""}</div></article>`;
|
|
2179
2221
|
}).join("")}</div><button>Salva ruoli</button></form>`;
|
|
2180
2222
|
}
|
|
2181
2223
|
function roleOverview(role, groups) {
|
|
@@ -2185,7 +2227,7 @@ function roleOverview(role, groups) {
|
|
|
2185
2227
|
const tree = groups.map((group) => ({ group, entries: group.menus.filter((menu) => assignments.has(menu.codiceMenu)) })).filter((node) => node.entries.length > 0);
|
|
2186
2228
|
const orphans = roleMenus.filter((item) => !knownMenus.has(item.codiceMenu));
|
|
2187
2229
|
const groupNodes = tree.map(({ group, entries }) => `<li class="role-tree-group ${group.enabled === false ? "is-disabled" : ""}"><div class="role-tree-group-head"><strong>${escapeHtml(group.descrizioneGruppo)}</strong><span class="role-tree-count">${entries.length}</span></div><ul class="role-tree-menus">${entries.map((menu) => `<li class="role-tree-menu ${menu.enabled === false ? "is-disabled" : ""}"><span class="role-tree-menu-name">${escapeHtml(menu.descrizioneMenu)}</span><span class="permission-level">${escapeHtml(rolePermissionLevel(assignments.get(menu.codiceMenu)?.tipoAbilitazione))}</span></li>`).join("")}</ul></li>`).join("");
|
|
2188
|
-
const orphanNode = orphans.length ? `<li class="role-tree-group is-orphan"><div class="role-tree-group-head"><strong>Menu non pi\
|
|
2230
|
+
const orphanNode = orphans.length ? `<li class="role-tree-group is-orphan"><div class="role-tree-group-head"><strong>Menu non pi\xF9 nel catalogo</strong><span class="role-tree-count">${orphans.length}</span></div><ul class="role-tree-menus">${orphans.map((item) => `<li class="role-tree-menu"><span class="role-tree-menu-name"><code>${escapeHtml(item.codiceMenu)}</code></span><span class="permission-level">${escapeHtml(rolePermissionLevel(item.tipoAbilitazione))}</span></li>`).join("")}</ul></li>` : "";
|
|
2189
2231
|
return `<article class="role-card">
|
|
2190
2232
|
<div class="role-card-head">
|
|
2191
2233
|
<div class="role-card-title">
|
|
@@ -2340,11 +2382,11 @@ function federatedIdentityFields(providers) {
|
|
|
2340
2382
|
<label>Nota ${helpDot("sso-note")}<input name="note"><span class="form-help">Nota interna, non inviata al provider.</span></label>`;
|
|
2341
2383
|
}
|
|
2342
2384
|
function federatedIdentitySection(identities, providers) {
|
|
2343
|
-
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\
|
|
2385
|
+
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>`;
|
|
2344
2386
|
}
|
|
2345
2387
|
function userDetailTabs(hasSso) {
|
|
2346
2388
|
const tabs = [
|
|
2347
|
-
...hasSso ? [{ id: "sso", label: "Identit\
|
|
2389
|
+
...hasSso ? [{ id: "sso", label: "Identit\xE0 SSO" }] : [],
|
|
2348
2390
|
{ id: "profile", label: "Profilo e accesso" },
|
|
2349
2391
|
{ id: "roles", label: "Ruoli" },
|
|
2350
2392
|
{ id: "grants", label: "Grant diretti" }
|
|
@@ -2401,7 +2443,7 @@ function pageNumbers(current, total) {
|
|
|
2401
2443
|
return result2;
|
|
2402
2444
|
}
|
|
2403
2445
|
function pagerMarkup(totalPages) {
|
|
2404
|
-
const context = `<span class="pager-context">${usersTotal} utent${usersTotal === 1 ? "e" : "i"}${totalPages > 1 ? ` \
|
|
2446
|
+
const context = `<span class="pager-context">${usersTotal} utent${usersTotal === 1 ? "e" : "i"}${totalPages > 1 ? ` \xB7 pagina ${usersPage + 1} di ${totalPages}` : ""}</span>`;
|
|
2405
2447
|
if (totalPages <= 1) {
|
|
2406
2448
|
return `<nav class="pager" aria-label="Paginazione utenti">${context}</nav>`;
|
|
2407
2449
|
}
|
|
@@ -2443,6 +2485,16 @@ function bindRolesToggles() {
|
|
|
2443
2485
|
};
|
|
2444
2486
|
});
|
|
2445
2487
|
}
|
|
2488
|
+
async function applyAdminBootstrapVisibility() {
|
|
2489
|
+
const panel = document.getElementById("bootstrap-admin-panel");
|
|
2490
|
+
if (!panel) return;
|
|
2491
|
+
try {
|
|
2492
|
+
const status = result(await getAdminBootstrapStatus());
|
|
2493
|
+
panel.hidden = status.enabled !== true;
|
|
2494
|
+
} catch {
|
|
2495
|
+
panel.hidden = true;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2446
2498
|
async function detectFederatedAuthentication() {
|
|
2447
2499
|
if (!currentUser) return;
|
|
2448
2500
|
try {
|
|
@@ -2548,19 +2600,48 @@ async function bootstrap() {
|
|
|
2548
2600
|
byId("login-error").textContent = error instanceof Error ? error.message : "Sessione non valida.";
|
|
2549
2601
|
}
|
|
2550
2602
|
}
|
|
2603
|
+
async function exchangeConsoleEntry(ticket) {
|
|
2604
|
+
try {
|
|
2605
|
+
const response = await fetch(`${consoleBasePath}/entry/exchange`, {
|
|
2606
|
+
method: "POST",
|
|
2607
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2608
|
+
body: JSON.stringify({ ticket })
|
|
2609
|
+
});
|
|
2610
|
+
const payload = await response.json().catch(() => void 0);
|
|
2611
|
+
const value = payload?.Result?.token?.value;
|
|
2612
|
+
if (!response.ok || typeof value !== "string" || value === "") {
|
|
2613
|
+
throw new Error(payload?.message ?? "Apertura automatica della console non riuscita.");
|
|
2614
|
+
}
|
|
2615
|
+
token = value;
|
|
2616
|
+
sessionStorage.setItem("accessi-console-token", value);
|
|
2617
|
+
setAccessiConsoleToken(value);
|
|
2618
|
+
await bootstrap();
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
token = null;
|
|
2621
|
+
setAccessiConsoleToken(null);
|
|
2622
|
+
sessionStorage.removeItem("accessi-console-token");
|
|
2623
|
+
finishBoot("login");
|
|
2624
|
+
byId("login-error").textContent = error instanceof Error ? error.message : "Apertura automatica della console non riuscita.";
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2551
2627
|
async function showUsers() {
|
|
2552
2628
|
const { users, total } = await loadUsersPage();
|
|
2553
2629
|
usersTotal = total;
|
|
2554
2630
|
const totalPages = Math.max(1, Math.ceil(total / USERS_PAGE_SIZE));
|
|
2555
2631
|
if (usersPage >= totalPages) usersPage = totalPages - 1;
|
|
2556
|
-
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></div>
|
|
2632
|
+
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>
|
|
2557
2633
|
<div class="pager-row pager-row-top">${pagerMarkup(totalPages)}</div>
|
|
2558
2634
|
<table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
|
|
2559
|
-
${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}">Gestisci</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Nessun utente.</td></tr>'}
|
|
2635
|
+
${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>'}
|
|
2560
2636
|
</tbody></table><div class="pager-row pager-row-bottom">${pagerMarkup(totalPages)}</div>`);
|
|
2561
2637
|
byId("new-local").onclick = () => showLocalUserForm();
|
|
2562
2638
|
if (federatedAuthenticationAvailable) byId("new-sso").onclick = () => handleAction(showSsoUserForm);
|
|
2563
2639
|
byId("reload-users").onclick = () => handleAction(() => show("users"));
|
|
2640
|
+
byId("reset-legacy").onclick = () => handleAction(async () => {
|
|
2641
|
+
if (!window.confirm("Inviare l email di reset a tutti gli utenti con password non ancora migrata? Richiede un servizio email configurato.")) return;
|
|
2642
|
+
await forcePasswordResetLegacy();
|
|
2643
|
+
showNotice("Email di reset inviate agli utenti con password legacy.");
|
|
2644
|
+
});
|
|
2564
2645
|
document.querySelectorAll("[data-goto]").forEach((button) => {
|
|
2565
2646
|
button.onclick = () => handleAction(async () => {
|
|
2566
2647
|
const target = button.dataset.goto;
|
|
@@ -2578,7 +2659,7 @@ async function showUsers() {
|
|
|
2578
2659
|
function authenticationPolicyFields(user) {
|
|
2579
2660
|
return `<fieldset><legend>Verifica dell'accesso (opzionale)</legend>
|
|
2580
2661
|
<label class="check"><input name="flagDueFattori" type="checkbox" ${user.flagDueFattori ? "checked" : ""}> Richiedi un codice via email (2FA) ${helpDot("user-2fa")}</label>
|
|
2581
|
-
<p class="form-help">Se attiva, il codice \
|
|
2662
|
+
<p class="form-help">Se attiva, il codice \xE8 richiesto dopo password o SSO.</p>
|
|
2582
2663
|
<label class="check"><input name="passwordlessLoginEnabled" type="checkbox" ${user.passwordlessLoginEnabled ? "checked" : ""} ${user.flagDueFattori ? "" : "disabled"}> Consenti accesso con il solo codice email ${helpDot("user-passwordless")}</label>
|
|
2583
2664
|
<p class="form-help">Accesso senza password (richiede il codice email).</p>
|
|
2584
2665
|
</fieldset>`;
|
|
@@ -2589,6 +2670,36 @@ function canManageSuperFlag() {
|
|
|
2589
2670
|
function canManageAdminFlag() {
|
|
2590
2671
|
return Boolean(currentUser?.flagAdmin);
|
|
2591
2672
|
}
|
|
2673
|
+
function consoleUserRank(user) {
|
|
2674
|
+
if (user?.flagAdmin) return 2;
|
|
2675
|
+
if (user?.flagSuper) return 1;
|
|
2676
|
+
return 0;
|
|
2677
|
+
}
|
|
2678
|
+
function canManageUser(user) {
|
|
2679
|
+
if (currentUser && user.codiceUtente === currentUser.codiceUtente) return true;
|
|
2680
|
+
return consoleUserRank(currentUser) >= consoleUserRank(user);
|
|
2681
|
+
}
|
|
2682
|
+
function setUserManagementEnabled(enabled) {
|
|
2683
|
+
for (const id of ["disable-user", "purge-user", "save-state", "set-password", "save-profile", "set-gdpr"]) {
|
|
2684
|
+
const element = document.getElementById(id);
|
|
2685
|
+
if (element) element.disabled = !enabled;
|
|
2686
|
+
}
|
|
2687
|
+
for (const selector of ["#roles button", "#grants button", "#link-sso button"]) {
|
|
2688
|
+
document.querySelectorAll(selector).forEach((button) => {
|
|
2689
|
+
button.disabled = !enabled;
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
for (const selector of ["#profile", "#password-form", "#roles", "#grants", "#link-sso"]) {
|
|
2693
|
+
document.querySelectorAll(
|
|
2694
|
+
`${selector} input, ${selector} select, ${selector} textarea`
|
|
2695
|
+
).forEach((field) => {
|
|
2696
|
+
field.disabled = !enabled;
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
document.querySelectorAll("[data-identity-toggle], [data-identity-delete]").forEach((button) => {
|
|
2700
|
+
button.disabled = !enabled;
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2592
2703
|
function privilegeFields(user) {
|
|
2593
2704
|
const manageSuper = canManageSuperFlag();
|
|
2594
2705
|
const manageAdmin = canManageAdminFlag();
|
|
@@ -2639,7 +2750,7 @@ function extendedProfileFields(user) {
|
|
|
2639
2750
|
<label>Codice lingua ${helpDot("user-lingua")}<input name="codiceLingua" maxlength="2" value="${escapeHtml(user.codiceLingua ?? "")}"></label>
|
|
2640
2751
|
<label>Avatar ${helpDot("user-avatar")}<input name="avatar" maxlength="30" value="${escapeHtml(user.avatar ?? "")}"></label>
|
|
2641
2752
|
<label>Scadenza password ${helpDot("user-scadenza-password")}<input name="dataScadenzaPassword" type="date" value="${dateInputValue(user.dataScadenzaPassword)}"></label>
|
|
2642
|
-
<
|
|
2753
|
+
<fieldset class="gdpr-status"><legend>GDPR ${helpDot("user-gdpr")}</legend><p class="form-help">${user.flagGdpr ? `Consenso registrato${dateInputValue(user.dataGdpr) ? ` il ${escapeHtml(dateInputValue(user.dataGdpr))}` : ""}.` : "Consenso non ancora registrato."}</p><button type="button" id="set-gdpr" class="secondary">Registra consenso GDPR</button></fieldset>
|
|
2643
2754
|
<label>Metadata JSON ${helpDot("user-json")}<textarea name="jsonMetadata" rows="3">${escapeHtml(user.jsonMetadata ?? "")}</textarea></label>
|
|
2644
2755
|
</fieldset>`;
|
|
2645
2756
|
}
|
|
@@ -2671,7 +2782,6 @@ function profilePayload(form, codiceUtente) {
|
|
|
2671
2782
|
paginaDefault: optionalText(values, "paginaDefault"),
|
|
2672
2783
|
jsonMetadata: optionalText(values, "jsonMetadata"),
|
|
2673
2784
|
dataScadenzaPassword: optionalText(values, "dataScadenzaPassword"),
|
|
2674
|
-
flagGdpr: fields.has("flagGdpr"),
|
|
2675
2785
|
flagDueFattori: fields.has("flagDueFattori"),
|
|
2676
2786
|
passwordlessLoginEnabled: fields.has("passwordlessLoginEnabled"),
|
|
2677
2787
|
numRep: optionalNumber(values, "numRep"),
|
|
@@ -2692,14 +2802,15 @@ function profilePayload(form, codiceUtente) {
|
|
|
2692
2802
|
return payload;
|
|
2693
2803
|
}
|
|
2694
2804
|
function passwordFields() {
|
|
2695
|
-
return `<
|
|
2805
|
+
return `<form id="password-form" class="user-editor-section" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile">
|
|
2806
|
+
<h3>Password</h3>
|
|
2696
2807
|
<label>Nuova password ${helpDot("user-set-password")}<input name="newPassword" type="password" autocomplete="new-password" minlength="8" maxlength="100"><span class="form-help">Almeno 8 caratteri con maiuscola, minuscola, cifra e carattere speciale; nessuno spazio. Viene salvata come hash e la scadenza segue la policy; nessuna email inviata.</span></label>
|
|
2697
|
-
<button type="
|
|
2698
|
-
</
|
|
2808
|
+
<button type="submit" id="set-password" class="secondary">Imposta password</button>
|
|
2809
|
+
</form>`;
|
|
2699
2810
|
}
|
|
2700
2811
|
function showLocalUserForm() {
|
|
2701
2812
|
render(`<button id="back">Indietro</button><h2>Nuovo utente locale</h2><p class="muted">Viene inviata l'e-mail per impostare la password.</p>
|
|
2702
|
-
<form id="local-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Identificativo di accesso; ricever\
|
|
2813
|
+
<form id="local-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Identificativo di accesso; ricever\xE0 l'email per la password.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label>${privilegeFields({})}<button>Crea utente</button></form>`);
|
|
2703
2814
|
byId("back").onclick = () => show("users");
|
|
2704
2815
|
const localUserForm = document.getElementById("local-user");
|
|
2705
2816
|
if (localUserForm) localUserForm.onsubmit = (event) => {
|
|
@@ -2717,7 +2828,7 @@ async function showSsoUserForm() {
|
|
|
2717
2828
|
if (!providers.some((provider) => provider.active)) {
|
|
2718
2829
|
throw new Error("Prima di creare un utente SSO, censisci e abilita almeno un provider SSO.");
|
|
2719
2830
|
}
|
|
2720
|
-
render(`<button id="back">Indietro</button><h2>Nuovo utente SSO</h2><p class="muted">Salva solo collegamenti gi\
|
|
2831
|
+
render(`<button id="back">Indietro</button><h2>Nuovo utente SSO</h2><p class="muted">Salva solo collegamenti gi\xE0 verificati dal backend.</p>
|
|
2721
2832
|
<form id="sso-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Email di contatto e identificativo Accessi dell'utente.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label>${federatedIdentityFields(providers)}<label class="check"><input name="passwordLoginEnabled" type="checkbox"> Abilita anche la password locale ${helpDot("user-password-login")}</label><span class="form-help">Se disattivato, l'utente accede solo via SSO.</span>${privilegeFields({})}<button>Crea utente SSO</button></form>`);
|
|
2722
2833
|
byId("back").onclick = () => show("ssoUsers");
|
|
2723
2834
|
const ssoUserForm = document.getElementById("sso-user");
|
|
@@ -2735,15 +2846,15 @@ async function showSsoUserForm() {
|
|
|
2735
2846
|
};
|
|
2736
2847
|
await createFederatedUser(request);
|
|
2737
2848
|
showNotice("Utente SSO creato e collegamento registrato.");
|
|
2738
|
-
await show("ssoUsers");
|
|
2849
|
+
await show(currentUser?.flagAdmin ? "ssoUsers" : "users");
|
|
2739
2850
|
});
|
|
2740
2851
|
};
|
|
2741
2852
|
}
|
|
2742
2853
|
async function showSsoProviders() {
|
|
2743
|
-
if (!federatedAuthenticationAvailable) throw new Error("SSO non \
|
|
2854
|
+
if (!federatedAuthenticationAvailable) throw new Error("SSO non \xE8 abilitato in questa istanza Accessi.");
|
|
2744
2855
|
const providerList = result(await getFederatedProviders());
|
|
2745
2856
|
render(`<div class="toolbar"><button id="new-provider">Nuovo provider</button><button id="reload-providers" class="secondary">Aggiorna</button></div>
|
|
2746
|
-
<h2>Provider SSO</h2><p class="muted">La validazione di token, issuer e segreti resta nel backend. Un provider si elimina solo se nessuna identit\
|
|
2857
|
+
<h2>Provider SSO</h2><p class="muted">La validazione di token, issuer e segreti resta nel backend. Un provider si elimina solo se nessuna identit\xE0 \xE8 collegata.</p>
|
|
2747
2858
|
<div class="menu-table-wrap"><table><thead><tr><th>Provider</th><th>Descrizione</th><th>Stato</th><th></th></tr></thead><tbody>${providerList.map((provider) => `<tr class="${provider.active ? "" : "is-disabled"}"><td><code>${escapeHtml(provider.provider)}</code></td><td>${escapeHtml(provider.description)}</td><td>${provider.active ? "Attivo" : "Disabilitato"}</td><td class="row-actions"><button data-provider="${escapeHtml(provider.provider)}">Gestisci</button></td></tr>`).join("") || '<tr><td colspan="4" class="muted">Nessun provider censito.</td></tr>'}</tbody></table></div>`);
|
|
2748
2859
|
byId("new-provider").onclick = () => showProviderForm();
|
|
2749
2860
|
byId("reload-providers").onclick = () => handleAction(showSsoProviders);
|
|
@@ -2752,11 +2863,11 @@ async function showSsoProviders() {
|
|
|
2752
2863
|
});
|
|
2753
2864
|
}
|
|
2754
2865
|
async function showSsoUsers() {
|
|
2755
|
-
if (!federatedAuthenticationAvailable) throw new Error("SSO non \
|
|
2866
|
+
if (!federatedAuthenticationAvailable) throw new Error("SSO non \xE8 abilitato in questa istanza Accessi.");
|
|
2756
2867
|
const users = await loadUsers();
|
|
2757
2868
|
render(`<div class="toolbar"><button id="new-sso">Nuovo utente SSO</button><button id="reload-sso-users" class="secondary">Aggiorna</button></div>
|
|
2758
|
-
<h2>Utenti e identit\
|
|
2759
|
-
<div class="menu-table-wrap"><table><thead><tr><th>Utente</th><th>Accesso</th><th></th></tr></thead><tbody>${users.map(({ utente }) => `<tr><td>${escapeHtml(utente.email)}</td><td>${utente.passwordLoginEnabled === false ? "solo SSO" : "password e SSO"}</td><td class="row-actions"><button data-sso-user="${utente.codiceUtente}">Gestisci identit\
|
|
2869
|
+
<h2>Utenti e identit\xE0 SSO</h2><p class="muted">Gestisci i collegamenti provider/subject gi\xE0 verificati dal backend.</p>
|
|
2870
|
+
<div class="menu-table-wrap"><table><thead><tr><th>Utente</th><th>Accesso</th><th></th></tr></thead><tbody>${users.map(({ utente }) => `<tr><td>${escapeHtml(utente.email)}</td><td>${utente.passwordLoginEnabled === false ? "solo SSO" : "password e SSO"}</td><td class="row-actions"><button data-sso-user="${utente.codiceUtente}">Gestisci identit\xE0</button></td></tr>`).join("") || '<tr><td colspan="3" class="muted">Nessun utente.</td></tr>'}</tbody></table></div>`);
|
|
2760
2871
|
byId("new-sso").onclick = () => handleAction(showSsoUserForm);
|
|
2761
2872
|
byId("reload-sso-users").onclick = () => handleAction(showSsoUsers);
|
|
2762
2873
|
document.querySelectorAll("[data-sso-user]").forEach((button) => {
|
|
@@ -2833,9 +2944,11 @@ async function showUser(rawCode) {
|
|
|
2833
2944
|
const linked = federatedAuthenticationAvailable ? result(await getFederatedIdentities(codiceUtente)) : [];
|
|
2834
2945
|
const identitySection = federatedAuthenticationAvailable ? federatedIdentitySection(linked, providerList) : "";
|
|
2835
2946
|
const defaultUserTab = federatedAuthenticationAvailable ? "sso" : "profile";
|
|
2836
|
-
|
|
2947
|
+
const manageable = canManageUser(user);
|
|
2948
|
+
render(`${manageable ? "" : '<div class="notice notice-error" role="alert"><span class="notice-icon" aria-hidden="true">!</span><span class="notice-text">Stai visualizzando un utente di rango superiore: scheda in sola lettura.</span></div>'}<div class="user-action-bar"><button id="back" type="button" class="secondary">Indietro</button><label class="inline-field">Stato registrazione ${helpDot("user-state")}<select id="user-state">${registrationStateOptions(user.statoRegistrazione)}</select></label><button id="save-state" type="button" class="secondary">Aggiorna stato</button><button id="disable-user" type="button" class="danger-button">Imposta stato eliminato</button>${helpDot("user-delete")}${currentUser?.flagAdmin ? `<button id="purge-user" type="button" class="danger-button">Elimina definitivamente</button>${helpDot("user-delete-permanent")}` : ""}</div><div class="page-header"><div><p class="eyebrow">Utente ${codiceUtente}</p><h2>${escapeHtml(user.email)}</h2></div><span class="muted">Gestione profilo, ruoli e autorizzazioni</span></div>${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
|
|
2837
2949
|
${identitySection}
|
|
2838
|
-
<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><label>Pagina di default ${helpDot("user-default-page")}<input name="paginaDefault" maxlength="50" value="${escapeHtml(user.paginaDefault)}"><span class="form-help">Pagina mostrata dopo il login (percorso o codice pagina).</span></label>${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\
|
|
2950
|
+
<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><label>Pagina di default ${helpDot("user-default-page")}<input name="paginaDefault" maxlength="50" value="${escapeHtml(user.paginaDefault)}"><span class="form-help">Pagina mostrata dopo il login (percorso o codice pagina).</span></label>${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)}${filterFields(user)}<button id="save-profile" type="submit">Salva</button></form>
|
|
2951
|
+
${passwordFields()}
|
|
2839
2952
|
${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
|
|
2840
2953
|
${directGrantEditor(menuGroups, userGrants.abilitazioni)}
|
|
2841
2954
|
</div>`);
|
|
@@ -2883,14 +2996,24 @@ async function showUser(rawCode) {
|
|
|
2883
2996
|
});
|
|
2884
2997
|
};
|
|
2885
2998
|
}
|
|
2886
|
-
const
|
|
2887
|
-
if (
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2999
|
+
const passwordForm = document.getElementById("password-form");
|
|
3000
|
+
if (passwordForm) passwordForm.onsubmit = (event) => {
|
|
3001
|
+
event.preventDefault();
|
|
3002
|
+
handleAction(async () => {
|
|
3003
|
+
const newPassword = (document.querySelector('#password-form input[name="newPassword"]')?.value ?? "").trim();
|
|
3004
|
+
const violations = passwordPolicyViolations(newPassword);
|
|
3005
|
+
if (violations.length > 0) throw new Error(`La password non rispetta i requisiti: richiede ${violations.join(", ")}.`);
|
|
3006
|
+
if (!window.confirm(`Impostare una nuova password per ${user.email}?`)) return;
|
|
3007
|
+
await setUserPassword(codiceUtente, { newPassword });
|
|
3008
|
+
showNotice("Password aggiornata.");
|
|
3009
|
+
await showUser(String(codiceUtente));
|
|
3010
|
+
});
|
|
3011
|
+
};
|
|
3012
|
+
const setGdprButton = document.getElementById("set-gdpr");
|
|
3013
|
+
if (setGdprButton) setGdprButton.onclick = () => handleAction(async () => {
|
|
3014
|
+
if (!window.confirm(`Registrare il consenso GDPR per ${user.email}?`)) return;
|
|
3015
|
+
await setGdpr(codiceUtente);
|
|
3016
|
+
showNotice("Consenso GDPR registrato.");
|
|
2894
3017
|
await showUser(String(codiceUtente));
|
|
2895
3018
|
});
|
|
2896
3019
|
const rolesForm = document.getElementById("roles");
|
|
@@ -2933,7 +3056,7 @@ async function showUser(rawCode) {
|
|
|
2933
3056
|
});
|
|
2934
3057
|
document.querySelectorAll("[data-identity-delete]").forEach((button) => {
|
|
2935
3058
|
button.onclick = () => handleAction(async () => {
|
|
2936
|
-
if (!window.confirm("Eliminare definitivamente questo collegamento SSO? L\
|
|
3059
|
+
if (!window.confirm("Eliminare definitivamente questo collegamento SSO? L\u2019utente Accessi non verr\xE0 eliminato.")) return;
|
|
2937
3060
|
await deleteFederatedIdentityPermanently(codiceUtente, button.dataset.identityDelete ?? "");
|
|
2938
3061
|
showNotice("Collegamento SSO eliminato definitivamente.");
|
|
2939
3062
|
await showUser(String(codiceUtente));
|
|
@@ -2959,6 +3082,7 @@ async function showUser(rawCode) {
|
|
|
2959
3082
|
showNotice("Utente eliminato definitivamente.");
|
|
2960
3083
|
await show("users");
|
|
2961
3084
|
});
|
|
3085
|
+
if (!manageable) setUserManagementEnabled(false);
|
|
2962
3086
|
}
|
|
2963
3087
|
async function showRoles() {
|
|
2964
3088
|
const [rolesResponse, groupsResponse] = await Promise.all([getRoles(), getGroupsWithMenus({ includeDisabled: true })]);
|
|
@@ -3079,7 +3203,7 @@ async function showMenuItems() {
|
|
|
3079
3203
|
async function showMenuTypes() {
|
|
3080
3204
|
const menuTypes = result(await getMenuTypes());
|
|
3081
3205
|
render(`<div class="toolbar"><button id="new-menu-type">Nuovo tipo menu</button><button id="reload-menu-types" class="secondary">Aggiorna</button></div>
|
|
3082
|
-
<h2>Tipi menu</h2><p class="form-help">Classificano i menu (es. amministrazione, operativit\
|
|
3206
|
+
<h2>Tipi menu</h2><p class="form-help">Classificano i menu (es. amministrazione, operativit\xE0). Un tipo si elimina solo se non \xE8 usato da alcun menu.</p>
|
|
3083
3207
|
${menuTypeTable(menuTypes)}`);
|
|
3084
3208
|
byId("new-menu-type").onclick = () => showMenuTypeForm();
|
|
3085
3209
|
byId("reload-menu-types").onclick = () => handleAction(showMenuTypes);
|
|
@@ -3301,7 +3425,7 @@ async function showServiceTokens(includeRevoked = false) {
|
|
|
3301
3425
|
});
|
|
3302
3426
|
}
|
|
3303
3427
|
function showServiceTokenForm() {
|
|
3304
|
-
render(`<button id="back" type="button" class="secondary">Indietro</button><h2>Nuovo token di servizio</h2><form id="token-form"><label>Descrizione ${helpDot("token-label")}<input name="label" required maxlength="100"><span class="form-help">Identifica l'integrazione, ad esempio "IA - produzione".</span></label><label>Scope ${helpDot("token-scopes")}<input name="scopes" placeholder="ia, chat"><span class="form-help">Separati da virgola (lettere, numeri, ':', '_', '-'). Definiscono cosa pu\
|
|
3428
|
+
render(`<button id="back" type="button" class="secondary">Indietro</button><h2>Nuovo token di servizio</h2><form id="token-form"><label>Descrizione ${helpDot("token-label")}<input name="label" required maxlength="100"><span class="form-help">Identifica l'integrazione, ad esempio "IA - produzione".</span></label><label>Scope ${helpDot("token-scopes")}<input name="scopes" placeholder="ia, chat"><span class="form-help">Separati da virgola (lettere, numeri, ':', '_', '-'). Definiscono cosa pu\xF2 fare il token.</span></label><label>Durata in giorni ${helpDot("token-ttl")}<input name="ttlDays" type="number" min="1" step="1"><span class="form-help">Opzionale. Lascia vuoto per un token senza scadenza.</span></label><button>Crea token</button></form>`);
|
|
3305
3429
|
byId("back").onclick = () => void show("tokens");
|
|
3306
3430
|
const form = document.getElementById("token-form");
|
|
3307
3431
|
if (form) form.onsubmit = (event) => {
|
|
@@ -3670,8 +3794,13 @@ window.addEventListener("keydown", (event) => {
|
|
|
3670
3794
|
});
|
|
3671
3795
|
window.addEventListener("resize", positionHelp);
|
|
3672
3796
|
window.addEventListener("scroll", positionHelp, true);
|
|
3797
|
+
void applyAdminBootstrapVisibility();
|
|
3798
|
+
var entryTicket = /^#entry=([a-f0-9]{64})$/i.exec(window.location.hash)?.[1];
|
|
3673
3799
|
var ssoChallenge = /^#two-factor=([a-f0-9]{64})$/.exec(window.location.hash)?.[1];
|
|
3674
|
-
if (
|
|
3800
|
+
if (entryTicket) {
|
|
3801
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
3802
|
+
void exchangeConsoleEntry(entryTicket);
|
|
3803
|
+
} else if (ssoChallenge) {
|
|
3675
3804
|
token = null;
|
|
3676
3805
|
setAccessiConsoleToken(null);
|
|
3677
3806
|
sessionStorage.removeItem("accessi-console-token");
|