Files
Fuchs_Intranet/Fuchs/Docs/Concepts/admin-user-management.md
T
StefanandClaude Sonnet 5 4386a6336a
Playwright Tests / test (push) Canceled after 0s
Let admins create and delete users in Benutzerverwaltung, modernize list
Wire up two previously-unwired stored procedures for the usr module:
- fis_admin_createUserAccount gained the missing @authaccount +
  intranet_admin auth gate, a duplicate-email check, and now returns
  the (possibly auto-generated) password so it can be emailed to the
  new user, mirroring fis_admin_resetUserPassword's pattern.
- fis_admin_delUserAccount gained the same missing auth gate plus a
  guard against an admin deleting their own account.

New /do/usr/create and /do/usr/delete endpoints in
IntranetController.AdminUsers.cs, a "Neuer Benutzer" button and a
"Benutzer löschen" ribbon action in fis.usr.js, and unit tests for the
new welcome-email content (BuildWelcomeEmail).

Also restyle the user list (fis.usr.scss) from a plain bordered table
to a card-style row grid with avatar-initial badges and status pills,
matching the Admin dashboard's visual language.

Documented as ADR 0020 and in the admin-user-management concept doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 14:51:59 +02:00

9.4 KiB

status, lastUpdated, applyTo, relatedDecisions
status lastUpdated applyTo relatedDecisions
Active 2026-09-14
Fuchs/Controllers/IntranetController.AdminUsers.cs
Fuchs/js/intranet/modules/fis.usr*.js
Fuchs/js/intranet/modules/fis.usr.scss
Fuchs/js/intranet/fis_main_menu.js
Fuchs_Database/dbo/Functions/fis_admin_*.sql
Fuchs_Database/dbo/Stored Procedures/fis_admin_*.sql
0014-admin-user-management.md
0015-split-admin-user-management-module.md
0020-user-management-create-and-delete.md

User Management module (usr)

Summary

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)

  1. 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.
  2. 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
  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, 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.
  • 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.

Frontend (fis.usr.js + fis.usr_txt_de.js + fis.usr.scss)

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 columns, a status pill) rather than a plain <table>; 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.

Key files

  • Fuchs/Controllers/IntranetController.AdminUsers.cs — Do_Process_AdminUsers dispatch + BuildPasswordResetEmail/BuildWelcomeEmail + NotifyAdminUserActionFailedAsync.
  • Fuchs/Controllers/IntranetController.Admin.cs — shared AdminMinAuthExclusive/GetSystemAdminAuthAsync/AdminJson this module reuses.
  • Fuchs/js/intranet/modules/fis.usr.js, fis.usr_txt_de.js, fis.usr.scss — the module, texts, styles (bundled via bdlconfig.json, intranet:usr context).
  • Fuchs/js/intranet/fis_main_menu.js (addAdminMenuButtonsIfAuthorized) + fis_main_go.js — conditional menu buttons (this module and admin).
  • 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, fis_admin_createUserAccount.sql, fis_admin_delUserAccount.sql — write-side user/group procs.
  • Fuchs.Tests/AdminUserManagementTests.cs — password-reset and welcome-email content tests.