--- status: Active lastUpdated: 2026-09-09 applyTo: - "Fuchs/Controllers/IntranetController.Admin.cs" - "Fuchs/Services/SystemStatusService.cs" - "Fuchs/Services/ISystemStatusService.cs" - "Fuchs/Services/SystemStatusModels.cs" - "Fuchs/js/intranet/modules/fis.admin*.js" - "Fuchs/js/intranet/modules/fis.admin.scss" - "Fuchs/js/intranet/fis_main_menu.js" - "Fuchs_Database/dbo/Functions/fis_admin_*.sql" - "Fuchs_Database/dbo/Stored Procedures/fis_admin_*.sql" relatedDecisions: - "0011-admin-module-system-status.md" - "0014-admin-user-management.md" --- # Admin / System-Status module ## Summary The **Admin** module gives a privileged operator a live, in-app view of the running deployment's configuration and health, plus a test-email tool. It answers "which host am I on, can this instance reach SQL Server / Key Vault / Blob Storage / the MFR ERP, how is email wired up (including the OverrideRecipient redirect), and does sending actually work". Access is restricted to users whose `fds_sys` module authorization is greater than 4. ## How it works ### Authorization (two layers) 1. **Menu visibility** — at page load `fis_main_menu.js#addAdminMenuIfAuthorized` calls `$fis.getAuth('fds_sys')`; only when the level is > 4 does it push the `init:admin` button into `$ocms.ocmsmenu` and re-render `#mainmenu`. Users below the threshold never see the button and never fetch the module script. 2. **Server-side gate** — every `/do/admin/*` endpoint resolves `fis_getModuleAuth('fds_sys', authuser)` and returns 401 unless it is > 4. The only exception is `admin/auth`, which returns `{ manage: 0 }` for unauthorized users so the frontend can cleanly decline to render. This is defense in depth: hiding the button is not a security control on its own. ### Request flow ``` click "Administration" → $ocms.init('admin') POST /do/admin/auth → { manage, level } (manage>0 ⇒ authorized) load /web/fis.admin.de.js + /web/fis.admin.css $ocms.admin.init2() → init3() renders the page POST /do/admin/status → { info, probes } full snapshot + all probes per-card "Aktualisieren" → POST /do/admin/probe/ → { probe } "Test-E-Mail senden" → POST /do/admin/testmail (to/subject/body) → { result } ``` ### Backend (`ISystemStatusService` / `SystemStatusService`, scoped) - `GetInfo()` — passive snapshot, no network calls: host (machine/OS/framework/environment/ test-deployment/uptime), database (server/catalog/login parsed from the connection string — **never the password**), email (mailer enabled/base-url/account/server-id/token-present + OverrideRecipient), blob (enabled/configured/containers), MFR (host/creds-present/sync), Key Vault (vault-uri/app-prefix/managed-secret-count/client-registered). - Probes (`database`, `keyvault`, `blob`, `mfr`) each return a `SystemProbeResult` (`status` = `ok`/`error`/`disabled`/`unconfigured`, `ok`, `message`, `detail`, `durationMs`, optional `metrics`). They never throw — failures are captured in the result. `database` runs `SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME()`; `keyvault` reads the first managed secret via the DI `SecretClient`; `blob` calls `IBlobStorageService.CheckConnectivityAsync` (account-info request **plus a per-container blob count** for the invoice/reminder containers, surfaced as `metrics` and totalled in the message); `mfr` calls `IMfrClientFactory.Create().GetEntities()`. The generic `metrics` (label/value pairs) is how a probe reports extra facts for display — the blob probe uses it for the file counts. - `SendTestEmailAsync(to, subject, body)` HTML-encodes the body and sends via `IComService`, so the `Fuchs:Email:OverrideRecipient` redirect applies exactly as for any other mail; the result reports the requested recipient and, when active, the override target. - Every probe emits the `fuchs.systemstatus.probes` counter tagged by component + status and an `systemstatus.probe` activity span. ### Startup-checks widget (non-refreshable) `StartupSelfTestService` runs the boot self-test once (Key Vault / Database / MFR / PDF-license / mailer, gated by `Fuchs:StartupChecks:*`). It now also writes its outcome to the singleton `StartupCheckReporter`, which `GetInfo()` returns as `StartupChecks`. The Admin page renders it as a single **non-refreshable** card (there is no per-check retry — the live connectivity probes above cover on-demand re-testing; the startup card is a historical record of the boot run). When the self-test is disabled (`Enabled=false`, the appsettings default) or has not completed, the card shows a "nicht ausgeführt" note. Each item shows OK / Fehler / übersprungen (a disabled check reports `enabled=false`). ### Frontend (`fis.admin.js` + `fis.admin_txt_de.js` + `fis.admin.scss`) Standard lazy-loaded module (same contract as `inv`/`rep`/`bam`). Renders a system card plus a responsive grid of status cards; connectivity cards carry a colored status pill and a per-card refresh button, the email card carries the test-email dialog. The topbar has an "Alle prüfen" button that reloads the whole snapshot. Admin responses are serialized **camelCase** (the DTOs are PascalCase) so the JS reads `probe.status`, `info.database.server`, etc. ## User management (`users`/`userpwdreset`/`useremail`/`usergroups`/`usergroupset`) See [0014](../Decisions/0014-admin-user-management.md) for why this lives here and for the dual-authorization caveat (`fds_sys > 4` gates the endpoints; the SQL layer separately requires the caller's `intranet_admin` setting to be `> 1`). A card below the status grid (`$adm.usersCard`) renders a table of every non-deleted user account (name, email, active/disabled) via `POST admin/users` → `fis_admin_getUserAccountList(@authaccount, NULL, NULL, NULL)`. Clicking a row toggles a per-row ribbon (`$adm.uMn`, replacing the topbar exactly like Banking's `iMn`/`eM` swap) with three actions: - **Passwort zurücksetzen** — a dialog with one optional password field ("leave blank to auto-generate"). `POST admin/userpwdreset` calls `fis_admin_resetUserPassword` (now takes an optional `@newpassword`; `NULL` ⇒ the proc auto-generates one as before). On success the controller re-fetches the target's row (server-trusted email — never client-supplied) and emails the plaintext password via `IComService.SendEmailAsync` — the admin never sees it. The email subject/body is built by `IntranetController.BuildPasswordResetEmail` (pure, unit-tested in `Fuchs.Tests/AdminUserManagementTests.cs`). Failure to reset, or to deliver the email, calls `IEventService.UserIssueAsync` so the admin gets a visible notification either way. - **E-Mail ändern** — a dialog with the email field prefilled. `POST admin/useremail` fetches the current row and calls `fis_admin_setUserAccount` with only the email changed (other fields passed through unchanged, `@password = NULL` so the update path leaves the password alone). A duplicate email (`THROW 60001` in the proc) surfaces as an HTTP 409 `{ error: "duplicate_email" }`. - **Gruppenzugehörigkeit** — a dialog (`$adm.groupsDlg`) listing every group as a checkbox, populated via `POST admin/usergroups` → the new `fis_admin_getUserGroupMembership` function. Each checkbox toggle immediately posts `admin/usergroupset`, which calls `fis_admin_addGroupMember`/`remGroupMember` depending on the new state — no separate save step. ## Key files - `Fuchs/Controllers/IntranetController.Admin.cs` — `Do_Process_Admin` dispatch + auth gate + camelCase JSON helper + user-management endpoints. - `Fuchs/Services/ISystemStatusService.cs`, `SystemStatusService.cs`, `SystemStatusModels.cs` — diagnostics service + DTOs. - `Fuchs/Services/StartupCheckReport.cs` (`StartupCheckReporter` singleton) + `StartupSelfTestService.cs` — boot self-test result captured for the non-refreshable widget. - `Fuchs/Services/IBlobStorageService.cs` / `AzureBlobStorageService.cs` — `CheckConnectivityAsync` + `BlobConnectivity`. - `Fuchs/js/intranet/modules/fis.admin*.js`, `fis.admin.scss` — the module, texts, styles (bundled via `bdlconfig.json`). - `Fuchs/js/intranet/fis_main_menu.js` (`addAdminMenuIfAuthorized`) + `fis_main_go.js` — conditional menu button. - `Fuchs_Database/dbo/Functions/fis_admin_getUserAccountList.sql`, `fis_admin_getUserGroupList.sql`, `fis_admin_getUserGroupMembership.sql` — read-side user/group queries. - `Fuchs_Database/dbo/Stored Procedures/fis_admin_resetUserPassword.sql`, `fis_admin_setUserAccount.sql`, `fis_admin_addGroupMember.sql`, `fis_admin_remGroupMember.sql` — write-side user/group procs. - `Fuchs.Tests/SystemStatusServiceTests.cs` — service tests. - `Fuchs.Tests/AdminUserManagementTests.cs` — password-reset email content tests. ## Related decisions - [0011 — Admin module gated on `fds_sys` > 4](../Decisions/0011-admin-module-system-status.md) - [0014 — Admin user management: folded into the `admin` module, dual auth axis, password always emailed](../Decisions/0014-admin-user-management.md)