Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0020-user-management-create-and-delete.md
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

6.2 KiB

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-09-14
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

0020 — User management: wire up create/delete, welcome email on create, guard delete

Context

The usr module (see 0015) 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'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): 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, 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.