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
@@ -175,6 +175,113 @@ public partial class IntranetController
return AdminJson(new { ok = true });
}
case "create":
{
if (!HasForm("name", "firstname", "email")) return BadRequest400();
string name = Form("name");
string firstname = Form("firstname");
string email = Form("email");
var pl = StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_NVarChar("@name", name),
SQL_NVarChar("@firstname", firstname),
SQL_VarChar("@title", Form("title"), dbNull_IfEmpty: true),
SQL_VarChar("@gender", Form("gender"), dbNull_IfEmpty: true),
SQL_VarChar("@email", email),
SQL_VarChar("@mobile", Form("mobile"), dbNull_IfEmpty: true),
SQL_NVarChar("@password", null, dbNull_IfEmpty: true));
System.Data.DataRow? createdRow;
try
{
var res = await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_createUserAccount] @authaccount, @name, @firstname, " +
"@title, @gender, @email, @mobile, @password, @enc_key;",
_intranet.Intranet__SQLConnectionString, pl,
Security: DbSec, options: SqlOpt(fn, id, code));
createdRow = res.Count > 0 ? res.FirstRow : null;
}
catch (SqlException ex) when (ex.Number == 60001)
{
_logger.LogWarning("AdminUsers create: duplicate email rejected user={User}", UserAccountID);
return StatusCode(409, new { error = "duplicate_email" });
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("Benutzer anlegen fehlgeschlagen",
"Ein neues Benutzerkonto konnte nicht angelegt werden.", "", ex);
return StatusCode(500, new { error = "create_failed" });
}
if (createdRow == null)
{
_logger.LogWarning("AdminUsers create not successful user={User}", UserAccountID);
return StatusCode(500, new { error = "create_failed" });
}
string newId = createdRow.nz("useraccount_id");
string newPassword = createdRow.nz("newpassword");
string targetName = $"{firstname} {name}".Trim();
var (subject, html) = BuildWelcomeEmail(targetName, email, newPassword);
bool emailed = email.Length > 5 &&
await _comService.SendEmailAsync("usrwelcome_" + email, subject, html, email, targetName, null);
if (!emailed)
{
_logger.LogWarning("AdminUsers create: welcome mail not delivered target={Target} user={User}",
newId, UserAccountID);
await _events.UserIssueAsync("Neuer Benutzer: E-Mail-Versand fehlgeschlagen",
$"Die Zugangsdaten für das neue Benutzerkonto {newId} konnten nicht per E-Mail zugestellt werden.",
UserAccountID, new Dictionary<string, object?> { ["target"] = newId, ["email"] = email });
}
_logger.LogInformation("AdminUsers create succeeded target={Target} emailed={Emailed} user={User}",
newId, emailed, UserAccountID);
return AdminJson(new { ok = true, useraccount_id = newId, emailed });
}
case "delete":
{
if (!HasForm("useraccount_id")) return BadRequest400();
string targetId = Form("useraccount_id");
if (targetId.Equals(UserAccountID, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("AdminUsers delete: self-delete rejected user={User}", UserAccountID);
return StatusCode(409, new { error = "cannot_delete_self" });
}
bool success;
try
{
var pl = StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@useraccount_id", targetId));
var res = await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_delUserAccount] @authaccount, @useraccount_id;",
_intranet.Intranet__SQLConnectionString, pl,
Security: DbSec, options: SqlOpt(fn, id, code));
success = res.Count > 0 && res.FirstRow[0] is true;
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("Benutzer löschen fehlgeschlagen",
$"Das Benutzerkonto {targetId} konnte nicht gelöscht werden.", targetId, ex);
return StatusCode(500, new { error = "delete_failed" });
}
if (!success)
{
_logger.LogWarning("AdminUsers delete not successful target={Target} user={User}", targetId, UserAccountID);
return StatusCode(500, new { error = "delete_failed" });
}
_logger.LogInformation("AdminUsers delete succeeded target={Target} user={User}", targetId, UserAccountID);
return AdminJson(new { ok = true });
}
case "groups":
{
if (!HasForm("useraccount_id")) return BadRequest400();
@@ -238,6 +345,16 @@ public partial class IntranetController
$"<p>Guten Tag {targetName},<br/>Ihr Passwort wurde von einem Administrator zurückgesetzt.<br/>" +
$"Ihr neues Passwort: {HttpUtility.HtmlEncode(newPassword)}</p>");
/// <summary>
/// Builds the subject/HTML body for the "a new intranet account was created for you"
/// notification. Extracted as pure logic (no DB/HTTP), mirroring <see cref="BuildPasswordResetEmail"/>.
/// </summary>
internal static (string Subject, string Html) BuildWelcomeEmail(string targetName, string email, string newPassword) => (
"sanitaerfuchs.de Intranet Zugang eingerichtet",
$"<p>Guten Tag {targetName},<br/>für Sie wurde ein Zugang zum sanitaerfuchs.de Intranet eingerichtet.<br/>" +
$"Ihre E-Mail-Adresse: {HttpUtility.HtmlEncode(email)}<br/>" +
$"Ihr Passwort: {HttpUtility.HtmlEncode(newPassword)}</p>");
/// <summary>
/// Logs and raises a user-facing notification for an admin user-management action that
/// threw unexpectedly, per the "interrupting exceptions notify the user" rule