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,132 @@
using Microsoft.AspNetCore.Mvc;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
using static OCORE.web.mvc_helper_async;
namespace Fuchs.Controllers;
// Partial class: Administration user-group management module (fn=usrgrp).
//
// Split out as its own module (rather than folded into the usr module) per
// Docs/Decisions/0021-user-group-management.md, following the pattern
// Docs/Decisions/0015-split-admin-user-management-module.md already established for adding a new
// admin-only capability. Shares the Admin/usr modules' fds_sys > 4 gate (AdminMinAuthExclusive,
// GetSystemAdminAuthAsync, AdminJson — all in IntranetController.Admin.cs).
//
// The fis_admin_* stored procs/functions called below enforce the SAME SEPARATE, independent
// authorization axis as the usr module: the per-user "intranet_admin" setting via
// fis_admin_getUserAuth(@authaccount) > 1. An account with fds_sys > 4 but no intranet_admin
// setting will reach these endpoints yet get empty results / no-op writes from SQL — accounts
// doing group management need BOTH.
public partial class IntranetController
{
private async Task<IActionResult> Do_Process_AdminGroups(string fn, string id, string code)
{
_logger.LogDebug("Do_Process_AdminGroups action={Action} code={Code} user={User}", id, code, UserAccountID);
int authLevel = await GetSystemAdminAuthAsync(fn, id, code);
bool authorized = authLevel > AdminMinAuthExclusive;
if (id.Equals("auth", StringComparison.OrdinalIgnoreCase))
return await JSONAsync(new { manage = authorized ? 1 : 0, level = authLevel });
if (!authorized)
{
_logger.LogWarning("AdminGroups access denied for user={User} (fds_sys={Level}) action={Action}",
UserAccountID, authLevel, id);
return Unauthorized401();
}
switch (id.ToLowerInvariant())
{
case "list":
{
var res = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_getUserGroupList](@authaccount, NULL) ORDER BY [order];",
_intranet.Intranet__SQLConnectionString,
StdParamlist(SQL_VarChar("@authaccount", UserAccountID)),
Security: DbSec, options: SqlOpt(fn, id, code));
return AdminJson(new { groups = res.DataTable.toArrayofObjectDictionaries() });
}
case "create":
case "rename":
{
if (!HasForm("name")) return BadRequest400();
string name = Form("name");
string? groupId = id.Equals("rename", StringComparison.OrdinalIgnoreCase) ? Form("usergroup_id") : null;
if (id.Equals("rename", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(groupId))
return BadRequest400();
var pl = StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@usergroup_id", groupId, dbNull_IfEmpty: true),
SQL_NVarChar("@name", name));
System.Data.DataRow? savedRow;
try
{
var res = await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_setUserGroup] @authaccount, @usergroup_id, @name, @enc_key;",
_intranet.Intranet__SQLConnectionString, pl,
Security: DbSec, options: SqlOpt(fn, id, code));
savedRow = res.Count > 0 ? res.FirstRow : null;
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("Gruppe speichern fehlgeschlagen",
"Eine Benutzergruppe konnte nicht gespeichert werden.", groupId ?? "", ex);
return StatusCode(500, new { error = "save_failed" });
}
if (savedRow == null)
{
_logger.LogWarning("AdminGroups {Action} not successful group={Group} user={User}", id, groupId, UserAccountID);
return StatusCode(500, new { error = "save_failed" });
}
_logger.LogInformation("AdminGroups {Action} succeeded group={Group} user={User}",
id, savedRow.nz("usergroup_id"), UserAccountID);
return AdminJson(new { ok = true, usergroup_id = savedRow.nz("usergroup_id"), name = savedRow.nz("name") });
}
case "delete":
{
if (!HasForm("usergroup_id")) return BadRequest400();
string groupId = Form("usergroup_id");
bool success;
try
{
var res = await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_delUserGroup] @authaccount, @usergroup_id;",
_intranet.Intranet__SQLConnectionString,
StdParamlist(SQL_VarChar("@authaccount", UserAccountID), SQL_VarChar("@usergroup_id", groupId)),
Security: DbSec, options: SqlOpt(fn, id, code));
success = res.Count > 0 && res.FirstRow[0] is true;
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("Gruppe löschen fehlgeschlagen",
$"Die Benutzergruppe {groupId} konnte nicht gelöscht werden.", groupId, ex);
return StatusCode(500, new { error = "delete_failed" });
}
if (!success)
{
_logger.LogWarning("AdminGroups delete not successful group={Group} user={User}", groupId, UserAccountID);
return StatusCode(500, new { error = "delete_failed" });
}
_logger.LogInformation("AdminGroups delete succeeded group={Group} user={User}", groupId, UserAccountID);
return AdminJson(new { ok = true });
}
default:
_logger.LogWarning("AdminGroups: no handler for action={Action}, user={User}", id, UserAccountID);
return BadRequest400();
}
}
}
@@ -1,4 +1,4 @@
using System.Web;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using OCORE.SQL;
@@ -44,10 +44,16 @@ public partial class IntranetController
{
case "list":
{
// Optional useraccount_id narrows the result to a single account so the frontend can
// refresh one row in place (e.g. after an email or group-membership change) instead
// of re-rendering the whole grid and losing the current selection.
string? filterId = HasForm("useraccount_id") ? Form("useraccount_id") : null;
var res = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, NULL, NULL, NULL) ORDER BY [order];",
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, @useraccount_id, NULL, NULL) ORDER BY [order];",
_intranet.Intranet__SQLConnectionString,
StdParamlist(SQL_VarChar("@authaccount", UserAccountID)),
StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@useraccount_id", filterId, dbNull_IfEmpty: true)),
Security: DbSec, options: SqlOpt(fn, id, code));
return AdminJson(new { users = res.DataTable.toArrayofObjectDictionaries() });
}
+1
View File
@@ -168,6 +168,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
"bam" => await Do_Process_Bankings(fn, id, code),
"admin" => await Do_Process_Admin(fn, id, code),
"usr" => await Do_Process_AdminUsers(fn, id, code),
"usrgrp" => await Do_Process_AdminGroups(fn, id, code),
"auth" => await HandleAuth(fn, id, code),
"spwc" => await HandleSendPasswordCode(fn, id, code),
"spw" => await HandleSendPassword(fn, id, code),