Splits fn=admin into two independent modules: the original status
dashboard stays at fn=admin, user management moves to a new fn=usr
(IntranetController.AdminUsers.cs, fis.usr.js). Both are now reached
through a single "Administration" sidemenu dropdown (Systemüberwachung /
Benutzerverwaltung) gated by one shared fds_sys > 4 check, per ADR 0015
(supersedes 0014's "fold into one module" choice).
Also fixes the password-reset validation to check only a plain 6-char
minimum instead of silently also requiring ValidatePassword's default
2-uppercase/2-lowercase/2-digit composition, which contradicted the
"mind. 6 Zeichen" message shown to the admin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Benutzerverwaltung module lets a privileged operator see every non-deleted intranet
user account (name + email + active/disabled), reset a password, change an email address, and
view/change group assignments. It shares its sidemenu-visibility and server-side authorization
gate with the separate Admin/status-dashboard module (fds_sys > 4) but is otherwise fully
independent — own fn=usr, own frontend bundle, own controller partial. See
0015 for why these were split from
one module into two, and 0014 for the original
decisions this one carries forward (password always emailed, dual auth axis, reuse of
fis_admin_setUserAccount for email changes).
How it works
Authorization (two independent axes — read this before granting access)
Menu visibility + fds_sys gate — identical mechanism to the Admin module:
fis_main_menu.js#addAdminMenuButtonsIfAuthorized checks fds_sys > 4 once and, if
authorized, adds a single "Administration" sidemenu dropdown containing this module's
"Benutzerverwaltung" (init:usr) entry alongside Admin's "Systemüberwachung" (init:admin)
entry. Every /do/usr/* endpoint re-checks fds_sys > 4 server-side
(Do_Process_AdminUsers, reusing AdminMinAuthExclusive/GetSystemAdminAuthAsync from
IntranetController.Admin.cs) and 401s otherwise — defense in depth, same as Admin.
Separate SQL-layer gate — every fis_admin_* proc/function called below independently
checks the caller's intranet_admin setting via fis_admin_getUserAuth(@authaccount) > 1
(a fis_useraccounts_settings row, unrelated to fis_getModuleAuth/fis_module_auth). An
account with fds_sys > 4 but no intranet_admin setting reaches every usr endpoint but
gets empty results / silent no-op writes from SQL — not an error. Any account meant to
do user management needs both fds_sys > 4 and intranet_admin >= 2.
Request flow
click "Benutzerverwaltung" → $ocms.init('usr')
POST /do/usr/auth → { manage, level } (manage>0 ⇒ authorized)
load /web/fis.usr.de.js + /web/fis.usr.css
$ocms.usr.init2() → init3() → renderLst()
POST /do/usr/list → { users: [...] } grid of every non-deleted account
row click → per-row ribbon (iMn): Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit
POST /do/usr/pwdreset {useraccount_id, newpassword?} → { ok, emailed }
POST /do/usr/email {useraccount_id, email} → { ok } | 409 duplicate_email
POST /do/usr/groups {useraccount_id} → { groups: [...] }
POST /do/usr/groupset {useraccount_id, usergroup_id, member} → { ok, member }
Backend (IntranetController.AdminUsers.cs)
list → fis_admin_getUserAccountList(@authaccount, NULL, NULL, NULL), all non-deleted
accounts with decrypted name/email/disabled flag.
pwdreset → the admin may type a specific password or leave it blank; validated with a plain
ValidatePassword(minLength: 6, numUpper: 0, numLower: 0, numNumbers: 0, numSpecial: 0) — no
hidden composition requirements, matching what the UI actually tells the admin (a prior bug
had this call still requiring OCORE.commons.ValidatePassword's default 2-upper/2-lower/
2-digit minimums despite the "mind. 6 Zeichen" message). Calls
fis_admin_resetUserPassword (takes an optional @newpassword; NULL ⇒ the proc
auto-generates one). On success, 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. Email subject/body built by IntranetController.BuildPasswordResetEmail
(pure, unit-tested in Fuchs.Tests/AdminUserManagementTests.cs). Any unexpected failure (SQL
throw, email non-delivery, no usable email on file) calls IEventService.UserIssueAsync so the
admin gets a visible notification either way, per
0003.
email → fetches the current row and calls fis_admin_setUserAccount with only the email
changed (@password = NULL, so the WHEN MATCHED update path leaves password_enc alone). A
duplicate email (THROW 60001 in the proc) surfaces as HTTP 409 { error: "duplicate_email" }.
groups → the new fis_admin_getUserGroupMembership(@authaccount, @useraccount_id) function
returns every group plus a member bit for that one user.
groupset → calls fis_admin_addGroupMember/remGroupMember depending on the posted
member flag — applied immediately per checkbox toggle, no separate save step.
A self-contained list+ribbon module modeled on Banking (fis.bam.js)'s eM/iMn pattern
rather than the Admin dashboard's card layout: renderLst builds the table via
$fis.frm_list/$$.tblset; a row click toggles .selected and swaps the #topbar ribbon
between the base "Neu Laden" button (eM) and the three per-row actions (iMn). Each action is
a dialog (pwdResetDlg/emailDlg/groupsDlg) using $ocms.dlgform/$ocms.dlg with a manual
submit/postXT (not the built-in success-URL wiring) so a 409's error code can be mapped
to a specific German message via $uct.err.