Let admins create and delete users in Benutzerverwaltung, modernize list
Playwright Tests / test (push) Canceled after 0s

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>
This commit is contained in:
Stefan
2026-09-14 14:51:59 +02:00
co-authored by Claude Sonnet 5
parent 38145a917e
commit 4386a6336a
14 changed files with 716 additions and 138 deletions
@@ -0,0 +1,91 @@
---
status: Accepted
date: 2026-09-14
applyTo:
- "Fuchs/Controllers/IntranetController.AdminUsers.cs"
- "Fuchs/js/intranet/modules/fis.usr*.js"
- "Fuchs/js/intranet/modules/fis.usr.scss"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_createUserAccount.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserAccount.sql"
supersededBy: ""
---
# 0020 — User management: wire up create/delete, welcome email on create, guard delete
## Context
The `usr` module (see [0015](0015-split-admin-user-management-module.md)) let an admin list
users, reset a password, change an email address, and manage group membership, but had no way
to add a new account or remove one — the admin had to do that directly in the database. Two
stored procedures for this already existed in `Fuchs_Database` but were completely unwired from
any controller: `fis_admin_createUserAccount` and `fis_admin_delUserAccount`. Neither followed
the module's established conventions:
- `fis_admin_createUserAccount` took no `@authaccount` and performed **no** `intranet_admin`
authorization check at all (every other write in this module checks
`fis_admin_getUserAuth(@authaccount) > 1`, per [0014](0014-admin-user-management.md)'s dual
auth axis), had no duplicate-email guard (`fis_admin_setUserAccount` does), and generated the
initial password inline in the `INSERT` without ever returning it — so nothing calling it
could email the new user their credentials.
- `fis_admin_delUserAccount` likewise had no `intranet_admin` check, and nothing stopped an
admin from deleting their own account.
## Decision
- **`create` and `delete` are new `id` cases in `Do_Process_AdminUsers`**, behind the same
`fds_sys > 4` gate as every other action in the module.
- **`fis_admin_createUserAccount` was brought in line with its siblings** rather than reused via
`fis_admin_setUserAccount`'s existing "insert when `@useraccount_id` is NULL" branch (which
would have been the smaller diff): it gained `@authaccount` + the standard
`fis_admin_getUserAuth(@authaccount) > 1` gate, the same duplicate-email check
`fis_admin_setUserAccount` already has, and — mirroring `fis_admin_resetUserPassword`'s
existing pattern — resolves `@password` (auto-generating one via `ocms_fn_generatePassword`
when the admin leaves it blank) **before** encrypting it, and returns it in the result set as
`newpassword` alongside the created row. `fis_admin_setUserAccount`'s insert branch was left
alone (still reachable in principle, but has no caller) since changing its shared behavior
risked the existing `email` action; a dedicated create proc with an explicit contract was
judged clearer than overloading an update proc's edge case.
- **New account creation always emails the credentials, never displays them** — identical
handling to password reset ([0014](0014-admin-user-management.md)): `HandleLogin`-style
`IComService.SendEmailAsync` (respecting `Fuchs:Email:OverrideRecipient`) sends name, email,
and password via `IntranetController.BuildWelcomeEmail`; on delivery failure the admin gets an
`IEventService.UserIssueAsync` notification per
[0003](0003-interrupting-exceptions-notify-the-user.md), matching `pwdreset`'s existing
`emailed` flag semantics.
- **`fis_admin_delUserAccount` gained the same `intranet_admin` gate** (previously entirely
missing — a real authorization hole once the proc is reachable from a controller) **and a
self-delete guard** (`@authaccount <> @useraccount_id`), enforced at both the SQL layer (belt)
and the controller (suspenders — returns 409 `cannot_delete_self` without a round-trip when the
caller targets their own account).
- **Frontend**: `fis.usr.js`'s base ribbon gained an always-visible "Neuer Benutzer" button
(`createDlg`, a `$ocms.dlgform` collecting Vorname/Nachname/E-Mail required, Titel/Anrede/Mobil
optional); the per-row ribbon gained "Benutzer löschen" (`deleteUser`), using the same
`confirm(...)` guard pattern Banking's `fis.bam.js` uses before destructive actions
(`smd`/`mbd`) rather than a modal dialog, since delete has no fields to collect.
## Consequences
- `fis_admin_createUserAccount`'s signature changed (`@authaccount` added as the first
parameter) and its result set gained a `newpassword` column; safe in-place change since it had
no existing caller.
- `fis_admin_delUserAccount` now silently no-ops (same "no-op instead of error" style as every
other `fis_admin_*` proc in this module) for a caller without `intranet_admin >= 2` **or** who
targets their own account — the controller's own `cannot_delete_self` check makes the
self-delete case surface as a clear error to the admin instead of a silent no-op, but a caller
lacking `intranet_admin` still only sees an unexplained failure, consistent with the accepted
gap from 0014.
- Delete is a soft-delete (`fis_admin_delUserAccount` already set `DateDeleted`/`UserDeleted`
before this change) — deleted accounts simply stop appearing in `fis_admin_getUserAccountList`
and can only be restored directly in the database; no "undo" is exposed in the UI.
- The list view (`fis.usr.scss`) was restyled from a plain bordered `<table>` to a card-style row
grid (avatar-initial badge, status pill, hover/selection highlighting) matching the visual
language `fis.admin.scss` already established (`adm_pill`, `adm_card`) — a pure presentation
change, the DOM is still built by the same `renderLst`/`$ocms.postXT('usr/list')` flow.
## Alternatives considered
- **Reuse `fis_admin_setUserAccount` for create** by calling it with `@useraccount_id = NULL`.
Rejected — its insert branch doesn't return the generated password (nothing to email to the
new user), and giving one endpoint two very different response shapes depending on a hidden
parameter was judged more surprising than a small, explicit `create` proc.
- **Hard-delete instead of soft-delete.** Not considered — `fis_admin_delUserAccount` already
implemented soft-delete before this change and every other user-facing list in the app
(`fis_admin_getUserAccountList` et al.) already filters on `DateDeleted is null`; changing to a
hard delete would be an unrelated, larger change.
- **Let an admin delete their own account and rely on another admin to recover.** Rejected —
trivially avoidable lockout risk with no upside.