- Introduced a new module for managing user groups (`usrgrp`) in the administration interface.
- Implemented CRUD operations for user groups: list, create, rename, and delete.
- Added authorization checks to ensure only users with appropriate permissions can manage groups.
- Created corresponding frontend JavaScript and SCSS files for the new module.
- Updated documentation to reflect the new group management functionality and its authorization model.
- Added SQL stored procedures for group management operations.
- Established a new login script for database access, ensuring proper security practices.
The Benutzerverwaltung module lets a privileged operator see every non-deleted intranet
user account (name + email + active/disabled), create a new account, delete an existing one,
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, 0014
for the original decisions this one carries forward (password always emailed, dual auth axis,
reuse of fis_admin_setUserAccount for email changes), and
0020 for create/delete.
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 (incl. `groups` csv)
POST /do/usr/list {useraccount_id} → { users: [one] } single-row refresh (refreshRow)
base ribbon (eM) → "Neuer Benutzer" (always available, not just when a row is selected)
POST /do/usr/create {name, firstname, email, title?, gender?, mobile?} → { ok, useraccount_id, emailed } | 409 duplicate_email
row click → per-row ribbon (iMn): Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit / Benutzer löschen
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 }
POST /do/usr/delete {useraccount_id} → { ok } | 409 cannot_delete_self
Backend (IntranetController.AdminUsers.cs)
list → fis_admin_getUserAccountList(@authaccount, @useraccount_id, NULL, NULL), all non-deleted
accounts with decrypted name/email/disabled flag plus a groups column — a ', '-separated,
alphabetically ordered csv of the account's active group memberships (STRING_AGG over
decrypted fis_usergroups.name_enc, deleted groups/memberships excluded). An optional
useraccount_id form field narrows the result to one account so the frontend can refresh a
single row without re-rendering the grid.
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. The group catalog itself (create/
rename/delete) is managed in the separate usrgrp module — see
admin-group-management.md.
groupset → calls fis_admin_addGroupMember/remGroupMember depending on the posted
member flag — applied immediately per checkbox toggle, no separate save step.
create → fis_admin_createUserAccount(@authaccount, @name, @firstname, @title, @gender, @email, @mobile, @password, @enc_key). Only name/firstname/email are required from the
dialog; the admin may leave password blank (the proc auto-generates one and returns it as
newpassword, mirroring fis_admin_resetUserPassword's pattern). The controller then emails
the new account's credentials via BuildWelcomeEmail — same "emailed, never shown on screen"
rule as pwdreset — and returns emailed so the UI can tell the admin whether delivery
succeeded. Duplicate email → 409 duplicate_email, same mapping as email.
delete → fis_admin_delUserAccount(@authaccount, @useraccount_id) (soft-delete: sets
UserDeleted/DateDeleted). The controller rejects a self-delete with 409
cannot_delete_self before even calling SQL; the proc enforces the same guard independently.
See 0020 for why both procs needed
the intranet_admin auth gate added when they were wired up.
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 a card-style row grid (avatar
initials badge, name/email/groups columns, a status pill) rather than a plain <table>, with
each row produced by buildRow(rw). Changes that only affect one account (emailDlg, every
checkbox toggle in groupsDlg) call refreshRow(rw), which re-posts usr/list with that
useraccount_id and swaps the row in place — selection and the per-row ribbon survive, and the
groups csv is always current. Create/delete/password-reset still do a full renderLst. A row click
toggles .selected and swaps the #topbar ribbon between the base ribbon (eM: "Neuer
Benutzer" always available, plus "Neu Laden" once a row was selected) and the four per-row
actions (iMn: Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit / Benutzer löschen).
pwdResetDlg/emailDlg/groupsDlg/createDlg use $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; deleteUser instead follows Banking's
confirm(...)-before-destructive-action pattern (fis.bam.js's smd/mbd) since there's
nothing to collect beyond the confirmation itself.