Let admins create and delete users in Benutzerverwaltung, modernize list
Playwright Tests / test (push) Canceled after 0s
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:
co-authored by
Claude Sonnet 5
parent
38145a917e
commit
4386a6336a
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: Active
|
||||
lastUpdated: 2026-09-09
|
||||
lastUpdated: 2026-09-14
|
||||
applyTo:
|
||||
- "Fuchs/Controllers/IntranetController.AdminUsers.cs"
|
||||
- "Fuchs/js/intranet/modules/fis.usr*.js"
|
||||
@@ -11,20 +11,22 @@ applyTo:
|
||||
relatedDecisions:
|
||||
- "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), 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](../Decisions/0015-split-admin-user-management-module.md) for why these were split from
|
||||
one module into two, and [0014](../Decisions/0014-admin-user-management.md) for the original
|
||||
decisions this one carries forward (password always emailed, dual auth axis, reuse of
|
||||
`fis_admin_setUserAccount` for email changes).
|
||||
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](../Decisions/0015-split-admin-user-management-module.md) for
|
||||
why these were split from one module into two, [0014](../Decisions/0014-admin-user-management.md)
|
||||
for the original decisions this one carries forward (password always emailed, dual auth axis,
|
||||
reuse of `fis_admin_setUserAccount` for email changes), and
|
||||
[0020](../Decisions/0020-user-management-create-and-delete.md) for create/delete.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -50,11 +52,14 @@ click "Benutzerverwaltung" → $ocms.init('usr')
|
||||
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
|
||||
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`)
|
||||
@@ -80,26 +85,43 @@ click "Benutzerverwaltung" → $ocms.init('usr')
|
||||
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](../Decisions/0020-user-management-create-and-delete.md) 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 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
|
||||
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`.
|
||||
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` + `NotifyAdminUserActionFailedAsync`.
|
||||
- `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` — write-side user/group procs.
|
||||
- `Fuchs.Tests/AdminUserManagementTests.cs` — password-reset email content tests.
|
||||
- `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.
|
||||
|
||||
## Related decisions
|
||||
- [0014 — Admin user management (original decisions, still in force)](../Decisions/0014-admin-user-management.md)
|
||||
- [0020 — Wire up create/delete, welcome email on create, guard delete](../Decisions/0020-user-management-create-and-delete.md)
|
||||
- [0015 — Split Admin into separate Status/User-Management modules](../Decisions/0015-split-admin-user-management-module.md)
|
||||
- [0003 — Interrupting exceptions notify the user](../Decisions/0003-interrupting-exceptions-notify-the-user.md)
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user