Wires the previously-unused fis_admin_* user/group stored procs into the Admin module (fds_sys-gated), adds a users grid with a per-row ribbon (reset password, change email, manage group membership), and always emails a password reset result to the user rather than showing it to the admin. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.9 KiB
status, lastUpdated, applyTo, relatedDecisions
| status | lastUpdated | applyTo | relatedDecisions | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Active | 2026-09-09 |
|
|
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)
- Menu visibility — at page load
fis_main_menu.js#addAdminMenuIfAuthorizedcalls$fis.getAuth('fds_sys'); only when the level is > 4 does it push theinit:adminbutton into$ocms.ocmsmenuand re-render#mainmenu. Users below the threshold never see the button and never fetch the module script. - Server-side gate — every
/do/admin/*endpoint resolvesfis_getModuleAuth('fds_sys', authuser)and returns 401 unless it is > 4. The only exception isadmin/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/<component> → { 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 aSystemProbeResult(status=ok/error/disabled/unconfigured,ok,message,detail,durationMs, optionalmetrics). They never throw — failures are captured in the result.databaserunsSELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME();keyvaultreads the first managed secret via the DISecretClient;blobcallsIBlobStorageService.CheckConnectivityAsync(account-info request plus a per-container blob count for the invoice/reminder containers, surfaced asmetricsand totalled in the message);mfrcallsIMfrClientFactory.Create().GetEntities(). The genericmetrics(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 viaIComService, so theFuchs:Email:OverrideRecipientredirect 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.probescounter tagged by component + status and ansystemstatus.probeactivity 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 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/userpwdresetcallsfis_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 viaIComService.SendEmailAsync— the admin never sees it. The email subject/body is built byIntranetController.BuildPasswordResetEmail(pure, unit-tested inFuchs.Tests/AdminUserManagementTests.cs). Failure to reset, or to deliver the email, callsIEventService.UserIssueAsyncso the admin gets a visible notification either way. - E-Mail ändern — a dialog with the email field prefilled.
POST admin/useremailfetches the current row and callsfis_admin_setUserAccountwith only the email changed (other fields passed through unchanged,@password = NULLso the update path leaves the password alone). A duplicate email (THROW 60001in the proc) surfaces as an HTTP 409{ error: "duplicate_email" }. - Gruppenzugehörigkeit — a dialog (
$adm.groupsDlg) listing every group as a checkbox, populated viaPOST admin/usergroups→ the newfis_admin_getUserGroupMembershipfunction. Each checkbox toggle immediately postsadmin/usergroupset, which callsfis_admin_addGroupMember/remGroupMemberdepending on the new state — no separate save step.
Key files
Fuchs/Controllers/IntranetController.Admin.cs—Do_Process_Admindispatch + auth gate + camelCase JSON helper + user-management endpoints.Fuchs/Services/ISystemStatusService.cs,SystemStatusService.cs,SystemStatusModels.cs— diagnostics service + DTOs.Fuchs/Services/StartupCheckReport.cs(StartupCheckReportersingleton) +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 viabdlconfig.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.