feat: Add Admin Groups Management Module

- 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.
This commit is contained in:
Stefan
2026-09-16 09:35:31 +02:00
parent 1a5ac73f3c
commit 8bcec409c5
59 changed files with 1283 additions and 88 deletions
@@ -0,0 +1,93 @@
---
status: Active
lastUpdated: 2026-09-16
applyTo:
- "Fuchs/Controllers/IntranetController.AdminGroups.cs"
- "Fuchs/js/intranet/modules/fis.usrgrp*.js"
- "Fuchs/js/intranet/modules/fis.usrgrp.scss"
- "Fuchs/js/intranet/fis_main_menu.js"
- "Fuchs_Database/dbo/Functions/fis_admin_getUserGroupList.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_setUserGroup.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserGroup.sql"
relatedDecisions:
- "0021-user-group-management.md"
- "0014-admin-user-management.md"
- "0015-split-admin-user-management-module.md"
---
# Group Management module (`usrgrp`)
## Summary
The **Gruppenverwaltung** module lets a privileged operator see every non-deleted usergroup,
create a new one, rename it, and delete it. It's the catalog-management counterpart to the `usr`
module's per-user "Gruppen" dialog, which toggles an individual user's membership in these groups
but cannot create/rename/delete the groups themselves. Same sidemenu-visibility and server-side
authorization gate as `usr`/`admin` (`fds_sys > 4`), own `fn=usrgrp`, own frontend bundle, own
controller partial. See [0021](../Decisions/0021-user-group-management.md) for why this is a
separate module rather than folded into `usr`.
## How it works
### Authorization (two independent axes — same as `usr`, read [0014](../Decisions/0014-admin-user-management.md) first)
1. **Menu visibility + `fds_sys` gate** — `fis_main_menu.js#addAdminMenuButtonsIfAuthorized`
checks `fds_sys > 4` once and, if authorized, adds the "Administration" sidemenu dropdown with
three entries: Benutzerverwaltung (`init:usr`), Gruppenverwaltung (`init:usrgrp`) and
Systemüberwachung (`init:admin`). Every `/do/usrgrp/*` endpoint re-checks `fds_sys > 4`
server-side (`Do_Process_AdminGroups`, reusing `AdminMinAuthExclusive`/`GetSystemAdminAuthAsync`
from `IntranetController.Admin.cs`) and 401s otherwise.
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`. An
account with `fds_sys > 4` but no `intranet_admin` setting reaches every `usrgrp` endpoint but
gets empty results / silent no-op writes — not an error. This also applies to
`fis_admin_addGroupMember`/`remGroupMember` (used by the `usr` module's membership dialog,
documented there) — all five group-related `fis_admin_*` write/read objects now carry this
check as of [0021](../Decisions/0021-user-group-management.md), which closed a real gap where
three of them had none.
### Request flow
```
click "Gruppenverwaltung" → $ocms.init('usrgrp')
POST /do/usrgrp/auth → { manage, level } (manage>0 ⇒ authorized)
load /web/fis.usrgrp.de.js + /web/fis.usrgrp.css
$ocms.usrgrp.init2() → init3() → renderLst()
POST /do/usrgrp/list → { groups: [...] } every non-deleted group
base ribbon (eM) → "Anlegen" (always available)
POST /do/usrgrp/create {name} → { ok, usergroup_id, name }
row click → per-row ribbon (iMn): Umbenennen / Löschen
POST /do/usrgrp/rename {usergroup_id, name} → { ok, usergroup_id, name }
POST /do/usrgrp/delete {usergroup_id} → { ok }
```
### Backend (`IntranetController.AdminGroups.cs`)
- `list` → `fis_admin_getUserGroupList(@authaccount, NULL)`, all non-deleted groups with
decrypted names.
- `create`/`rename` → both call `fis_admin_setUserGroup(@authaccount, @usergroup_id, @name,
@enc_key)`; `create` passes `@usergroup_id = NULL` (the proc generates a new id via
`fis_fn_id`), `rename` passes the existing id.
- `delete` → `fis_admin_delUserGroup(@authaccount, @usergroup_id)` (soft-delete: sets
`UserDeleted`/`DateDeleted`). No check for remaining members — a deleted group's membership
rows simply stop being returned by `fis_admin_getUserGroupMembership` (filtered on the group's
own `DateDeleted`), see [0021](../Decisions/0021-user-group-management.md) Consequences.
### Frontend (`fis.usrgrp.js` + `fis.usrgrp_txt_de.js` + `fis.usrgrp.scss`)
Same list+ribbon pattern as `fis.usr.js`: `renderLst` builds a single-column card-style row list;
a row click toggles `.selected` and swaps the `#topbar` ribbon between the base ribbon (`eM`:
"Anlegen") and the per-row actions (`iMn`: Umbenennen/Löschen). `createDlg`/`renameDlg` use
`$ocms.dlgform`; `deleteGroup` follows the `confirm(...)`-before-destructive-action pattern (no
dialog, nothing to collect beyond the confirmation).
## Key files
- `Fuchs/Controllers/IntranetController.AdminGroups.cs` — `Do_Process_AdminGroups` dispatch.
- `Fuchs/Controllers/IntranetController.Admin.cs` — shared `AdminMinAuthExclusive`/
`GetSystemAdminAuthAsync`/`AdminJson` this module reuses.
- `Fuchs/js/intranet/modules/fis.usrgrp.js`, `fis.usrgrp_txt_de.js`, `fis.usrgrp.scss` — the
module, texts, styles (bundled via `bdlconfig.json`, `intranet:usrgrp` context).
- `Fuchs/js/intranet/fis_main_menu.js` (`addAdminMenuButtonsIfAuthorized`) — sidemenu wiring.
- `Fuchs_Database/dbo/Functions/fis_admin_getUserGroupList.sql` — read-side group query.
- `Fuchs_Database/dbo/Stored Procedures/fis_admin_setUserGroup.sql`,
`fis_admin_delUserGroup.sql` — write-side group procs.
## Related decisions
- [0021 — User group management (new module, closed the missing intranet_admin gate)](../Decisions/0021-user-group-management.md)
- [0014 — Admin user management (dual auth axis, original decisions)](../Decisions/0014-admin-user-management.md)
- [0015 — Split Admin into separate Status/User-Management modules](../Decisions/0015-split-admin-user-management-module.md)
+18 -6
View File
@@ -1,6 +1,6 @@
---
status: Active
lastUpdated: 2026-09-14
lastUpdated: 2026-09-16
applyTo:
- "Fuchs/Controllers/IntranetController.AdminUsers.cs"
- "Fuchs/js/intranet/modules/fis.usr*.js"
@@ -12,6 +12,7 @@ relatedDecisions:
- "0014-admin-user-management.md"
- "0015-split-admin-user-management-module.md"
- "0020-user-management-create-and-delete.md"
- "0021-user-group-management.md"
---
# User Management module (`usr`)
@@ -51,7 +52,8 @@ 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
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
@@ -63,8 +65,12 @@ click "Benutzerverwaltung" → $ocms.init('usr')
```
### Backend (`IntranetController.AdminUsers.cs`)
- `list` → `fis_admin_getUserAccountList(@authaccount, NULL, NULL, NULL)`, all non-deleted
accounts with decrypted name/email/disabled flag.
- `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
@@ -82,7 +88,9 @@ click "Benutzerverwaltung" → $ocms.init('usr')
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.
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](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,
@@ -101,7 +109,11 @@ click "Benutzerverwaltung" → $ocms.init('usr')
### 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
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).
@@ -0,0 +1,88 @@
---
status: Accepted
date: 2026-09-16
applyTo:
- "Fuchs/Controllers/IntranetController.AdminGroups.cs"
- "Fuchs/js/intranet/modules/fis.usrgrp*.js"
- "Fuchs/js/intranet/modules/fis.usrgrp.scss"
- "Fuchs/js/intranet/fis_main_menu.js"
- "Fuchs_Database/dbo/Functions/fis_admin_getUserGroupList.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_setUserGroup.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserGroup.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_addGroupMember.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_remGroupMember.sql"
supersededBy: ""
---
# 0021 — User group management: new `usrgrp` module, close the missing `intranet_admin` gate on group writes
## Context
The per-user "Gruppen" dialog in the `usr` module ([0014](0014-admin-user-management.md),
[0015](0015-split-admin-user-management-module.md)) lets an admin toggle an existing user's
membership in each usergroup, but there was no UI anywhere to create, rename or delete a
usergroup itself — `fis_usergroups` could only be populated by running a raw SQL script by hand
(`hlp/db/create user group.sql`). The DB-layer proc `fis_admin_setUserGroup` (create/rename) and
the read-side `fis_admin_getUserGroupList` already existed but were wired to no controller
endpoint, mirroring the exact gap [0020](0020-user-management-create-and-delete.md) closed for
user accounts.
While wiring this up, three of the four `fis_admin_*` group-write procedures were found missing
the module's standard `intranet_admin` authorization check entirely (`fis_admin_addGroupMember`,
`fis_admin_remGroupMember`, `fis_admin_delUserGroup`) — a caller with `fds_sys > 4` but no
`intranet_admin` setting could add/remove a user's group membership or delete a group outright,
the same real authorization hole 0020 fixed for `fis_admin_delUserAccount`. `fis_admin_setUserGroup`
already had the check.
## Decision
- **New standalone module, `fn=usrgrp`** (`IntranetController.AdminGroups.cs`,
`fis.usrgrp.js`/`fis.usrgrp_txt_de.js`/`fis.usrgrp.scss`), added as a third entry in the existing
"Administration" sidemenu dropdown alongside Benutzerverwaltung and Systemüberwachung — per
[0015](0015-split-admin-user-management-module.md)'s own stated consequence that any future
admin-only feature should default to its own module under that dropdown rather than be folded
into an existing one. The dropdown itself already stays visible while any of its children is
open, so switching between Gruppenverwaltung and Benutzerverwaltung needs no dedicated "back"
control.
- **Same dual-authorization gate as every other module under this dropdown**: `fds_sys > 4`
server-side (`AdminMinAuthExclusive`/`GetSystemAdminAuthAsync`, shared from
`IntranetController.Admin.cs`) gates whether the module is reachable/visible at all;
`fis_admin_getUserAuth(@authaccount) > 1` (the `intranet_admin` setting) independently gates
every read/write the `fis_admin_*` procs perform. Both together are what "only admins can see
and change groups" means in this codebase — see the Consequences below for why this is enforced
in four places, not one.
- **New `id` cases in `Do_Process_AdminGroups`**: `list` → `fis_admin_getUserGroupList`, `create`/
`rename` → `fis_admin_setUserGroup` (same proc; `rename` supplies an existing `@usergroup_id`),
`delete` → `fis_admin_delUserGroup`. No membership editing here — that stays in the `usr`
module's existing per-user checkbox dialog, unchanged.
- **Closed the missing `intranet_admin` gate** on `fis_admin_addGroupMember`,
`fis_admin_remGroupMember` and `fis_admin_delUserGroup` by adding the same
`fis_admin_getUserAuth(@authaccount) > 1` guard `fis_admin_setUserGroup` already had, following
the same "no-op instead of error" style 0020 used for `fis_admin_delUserAccount`: a caller
without `intranet_admin >= 2` gets an unmodified result set / a `false` success bit, not a SQL
error, consistent with every other `fis_admin_*` proc in this area.
- **Frontend** mirrors `fis.usr.js`'s list+ribbon pattern (`renderLst`/`eM`/`iMn`): a card-style
row list with a base ribbon button ("Anlegen") and a per-row ribbon ("Umbenennen"/"Löschen") on
row selection, using `$ocms.dlgform` for create/rename and a `confirm(...)` guard (no dialog) for
delete — the same choice 0020 made for deleting a user account.
## Consequences
- Deleting a group does **not** guard against it still having members — its `fis_usergroups_members`
rows simply stop being returned (the membership query already filters on the group's own
`DateDeleted`). No UI currently surfaces "this group has N members" before deletion; acceptable
since deletion is a soft-delete and the group row can be recreated with the same effective
membership restored via `fis_admin_addGroupMember` if this turns out to matter in practice.
- Four separate `fis_admin_*` procs/functions now each independently re-check
`intranet_admin > 1` (`getUserGroupList`, `setUserGroup`, `delUserGroup`, `addGroupMember`,
`remGroupMember` — five, in fact) rather than a single shared gate; this is the established
style in this codebase (every `fis_admin_*` object re-checks for itself, see 0014) and not
something this change revisits.
- Same operational note as 0014/0015/0020: an account needs **both** `fds_sys > 4` and
`intranet_admin >= 2` to actually manage groups, not obvious from the UI.
## Alternatives considered
- **Extend the existing per-user "Gruppen" dialog** with an inline "new group" input and a
delete icon per row, instead of a new module. Rejected per explicit user direction — the user
asked for a dedicated sidemenu entry for group management, separate from the per-user dialog.
- **Fold into the `usr` module** as a second ribbon button. Rejected for the same reason 0015
split `usr` off `admin` in the first place: two unrelated concerns (managing individual user
accounts vs. managing the group catalog) behind one button was already identified as a mistake
once it happened once; the "own module under the dropdown" default from 0015 applies directly.