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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user