Add Administration user management: list, password reset, email change, group assignments
Playwright Tests / test (push) Canceled after 0s

Wires the previously-unused fis_admin_* user/group stored procs into the Admin
module (fds_sys-gated), adds a users grid with a per-row ribbon (reset
password, change email, manage group membership), and always emails a
password reset result to the user rather than showing it to the admin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 10:34:04 +02:00
co-authored by Claude Sonnet 5
parent 6c38ee3d55
commit a5f497090e
14 changed files with 740 additions and 8 deletions
+43
View File
@@ -0,0 +1,43 @@
using Fuchs.Controllers;
using Xunit;
namespace Fuchs.Tests;
/// <summary>
/// Covers the pure (non-DB) logic behind the Admin module's user-management endpoints —
/// specifically the admin-initiated password-reset notification content, since the SQL-bound
/// parts of IntranetController.Admin.cs (fis_admin_* proc calls) can't be unit-tested per
/// repo convention and require a running database.
/// </summary>
public class AdminUserManagementTests
{
[Fact]
public void BuildPasswordResetEmail_ValidInput_ContainsNameAndPassword()
{
var (subject, html) = IntranetController.BuildPasswordResetEmail("Max Mustermann", "Ab12Cd34");
Assert.Contains("Passwort", subject);
Assert.Contains("Max Mustermann", html);
Assert.Contains("Ab12Cd34", html);
}
[Theory]
[InlineData("<script>alert(1)</script>", "&lt;script&gt;alert(1)&lt;/script&gt;")]
[InlineData("O'Brien&Co", "O&#39;Brien&amp;Co")]
public void BuildPasswordResetEmail_PasswordWithHtmlSpecialChars_IsHtmlEncoded(string rawPassword, string encoded)
{
var (_, html) = IntranetController.BuildPasswordResetEmail("Test User", rawPassword);
Assert.Contains(encoded, html);
Assert.DoesNotContain(rawPassword, html);
}
[Fact]
public void BuildPasswordResetEmail_EmptyName_StillProducesValidContent()
{
var (subject, html) = IntranetController.BuildPasswordResetEmail("", "somepassword");
Assert.False(string.IsNullOrWhiteSpace(subject));
Assert.Contains("somepassword", html);
}
}
@@ -1,8 +1,12 @@
using System.Web;
using Fuchs.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
using static OCORE.web.mvc_helper_async;
@@ -14,6 +18,13 @@ namespace Fuchs.Controllers;
// The menu button is only shown, and the module script only loaded, for such users (frontend),
// but every data endpoint here ALSO enforces the level server-side (defense in depth) — the
// passive/probe data is diagnostic and must never be reachable by a lower-privileged session.
//
// The user-management endpoints ("users"/"userpwdreset"/"useremail"/"usergroups"/"usergroupset")
// below share that same fds_sys > 4 gate, but the fis_admin_* stored procs/functions they call
// enforce a SEPARATE, independent authorization axis: 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. See
// Docs/Decisions for the ADR covering this — accounts doing user management need BOTH.
public partial class IntranetController
{
// fds_sys authorization must exceed this to use the Admin module.
@@ -72,6 +83,183 @@ public partial class IntranetController
return AdminJson(new { result });
}
case "users":
{
var res = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, NULL, NULL, NULL) ORDER BY [order];",
_intranet.Intranet__SQLConnectionString,
StdParamlist(SQL_VarChar("@authaccount", UserAccountID)),
Security: DbSec, options: SqlOpt(fn, id, code));
return AdminJson(new { users = res.DataTable.toArrayofObjectDictionaries() });
}
case "userpwdreset":
{
if (!HasForm("useraccount_id")) return BadRequest400();
string targetId = Form("useraccount_id");
string newPassword = Form("newpassword");
if (!string.IsNullOrWhiteSpace(newPassword) && !newPassword.ValidatePassword(minLength: 6, numSpecial: 0))
{
_logger.LogWarning("Admin userpwdreset: password requirements not met, target={Target} user={User}",
targetId, UserAccountID);
return StatusCode(409, new { error = "requirements" });
}
bool success;
string generatedPassword;
try
{
var pl = StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@useraccount_id", targetId),
SQL_NVarChar("@newpassword", newPassword, dbNull_IfEmpty: true));
var res = await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_resetUserPassword] @authaccount, @useraccount_id, @enc_key, @newpassword;",
_intranet.Intranet__SQLConnectionString, pl,
Security: DbSec, options: SqlOpt(fn, id, code));
var row = res.FirstRow;
success = row["success"] is true;
generatedPassword = row.nz("newpassword");
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("Passwort-Reset fehlgeschlagen",
$"Der Passwort-Reset für Benutzerkonto {targetId} konnte nicht durchgeführt werden.",
targetId, ex);
return StatusCode(500, new { error = "reset_failed" });
}
if (!success)
{
_logger.LogWarning("Admin userpwdreset not successful target={Target} user={User}", targetId, UserAccountID);
return StatusCode(500, new { error = "reset_failed" });
}
var userRow = (await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, @useraccount_id, NULL, NULL);",
_intranet.Intranet__SQLConnectionString,
StdParamlist(SQL_VarChar("@authaccount", UserAccountID), SQL_VarChar("@useraccount_id", targetId)),
Security: DbSec, options: SqlOpt(fn, id, code))).FirstRow;
string targetEmail = userRow.nz("email");
string targetName = $"{userRow.nz("firstname")} {userRow.nz("name")}".Trim();
var (subject, html) = BuildPasswordResetEmail(targetName, generatedPassword);
bool emailed = targetEmail.Length > 5 &&
await _comService.SendEmailAsync("pwreset_" + targetEmail, subject, html, targetEmail, targetName, null);
if (!emailed)
{
_logger.LogWarning(
"Admin userpwdreset: password not delivered target={Target} hasEmail={HasEmail} user={User}",
targetId, targetEmail.Length > 5, UserAccountID);
await _events.UserIssueAsync("Passwort-Reset: E-Mail-Versand fehlgeschlagen",
$"Das neue Passwort für Benutzerkonto {targetId} konnte nicht per E-Mail zugestellt werden.",
UserAccountID, new Dictionary<string, object?> { ["target"] = targetId, ["email"] = targetEmail });
}
_logger.LogInformation("Admin userpwdreset succeeded target={Target} emailed={Emailed} user={User}",
targetId, emailed, UserAccountID);
return AdminJson(new { ok = true, emailed });
}
case "useremail":
{
if (!HasForm("useraccount_id", "email")) return BadRequest400();
string targetId = Form("useraccount_id");
string newEmail = Form("email");
var currentRow = (await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, @useraccount_id, NULL, NULL);",
_intranet.Intranet__SQLConnectionString,
StdParamlist(SQL_VarChar("@authaccount", UserAccountID), SQL_VarChar("@useraccount_id", targetId)),
Security: DbSec, options: SqlOpt(fn, id, code))).FirstRow;
if (string.IsNullOrEmpty(currentRow.nz("useraccount_id")))
return BadRequest400();
var pl = StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@useraccount_id", targetId),
SQL_NVarChar("@name", currentRow.nz("name")),
SQL_NVarChar("@firstname", currentRow.nz("firstname"), dbNull_IfEmpty: true),
SQL_VarChar("@title", currentRow.nz("title"), dbNull_IfEmpty: true),
SQL_VarChar("@gender", currentRow.nz("gender"), dbNull_IfEmpty: true),
SQL_VarChar("@email", newEmail),
SQL_VarChar("@mobile", currentRow.nz("mobile"), dbNull_IfEmpty: true),
SQL_NVarChar("@password", null, dbNull_IfEmpty: true));
try
{
await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_setUserAccount] @authaccount, @useraccount_id, @name, @firstname, " +
"@title, @gender, @email, @mobile, @password, @enc_key;",
_intranet.Intranet__SQLConnectionString, pl,
Security: DbSec, options: SqlOpt(fn, id, code));
}
catch (SqlException ex) when (ex.Number == 60001)
{
_logger.LogWarning("Admin useremail: duplicate email rejected target={Target} user={User}",
targetId, UserAccountID);
return StatusCode(409, new { error = "duplicate_email" });
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("E-Mail-Änderung fehlgeschlagen",
$"Die E-Mail-Adresse für Benutzerkonto {targetId} konnte nicht geändert werden.",
targetId, ex);
return StatusCode(500, new { error = "change_failed" });
}
_logger.LogInformation("Admin useremail changed target={Target} user={User}", targetId, UserAccountID);
return AdminJson(new { ok = true });
}
case "usergroups":
{
if (!HasForm("useraccount_id")) return BadRequest400();
var res = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_getUserGroupMembership](@authaccount, @useraccount_id) ORDER BY [order];",
_intranet.Intranet__SQLConnectionString,
StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@useraccount_id", Form("useraccount_id"))),
Security: DbSec, options: SqlOpt(fn, id, code));
return AdminJson(new { groups = res.DataTable.toArrayofObjectDictionaries() });
}
case "usergroupset":
{
if (!HasForm("useraccount_id", "usergroup_id", "member")) return BadRequest400();
string targetId = Form("useraccount_id");
string groupId = Form("usergroup_id");
bool member = Form("member").Equals("true", StringComparison.OrdinalIgnoreCase) || Form("member") == "1";
var pl = StdParamlist(
SQL_VarChar("@authaccount", UserAccountID),
SQL_VarChar("@usergroup_id", groupId),
SQL_VarChar("@useraccount_id", targetId));
try
{
var res = member
? await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_addGroupMember] @authaccount, @usergroup_id, @useraccount_id;",
_intranet.Intranet__SQLConnectionString, pl, Security: DbSec, options: SqlOpt(fn, id, code))
: await getSQLDatatable_async(
"EXECUTE [dbo].[fis_admin_remGroupMember] @authaccount, @usergroup_id, @useraccount_id;",
_intranet.Intranet__SQLConnectionString, pl, Security: DbSec, options: SqlOpt(fn, id, code));
_logger.LogInformation("Admin usergroupset target={Target} group={Group} member={Member} user={User}",
targetId, groupId, member, UserAccountID);
return AdminJson(new { ok = true, member = res.FirstRow[0] is true });
}
catch (Exception ex)
{
await NotifyAdminUserActionFailedAsync("Gruppenzuordnung fehlgeschlagen",
$"Die Gruppenzuordnung für Benutzerkonto {targetId} konnte nicht geändert werden.",
targetId, ex);
return StatusCode(500, new { error = "change_failed" });
}
}
default:
_logger.LogWarning("Admin: no handler for action={Action}, user={User}", id, UserAccountID);
return BadRequest400();
@@ -90,6 +278,29 @@ public partial class IntranetController
private ContentResult AdminJson(object payload) =>
Content(JsonConvert.SerializeObject(payload, CamelCaseJson), "application/json; charset=utf-8");
/// <summary>
/// Builds the subject/HTML body for the "your password was reset by an admin" notification.
/// Extracted as pure logic (no DB/HTTP) so the content — including HTML-encoding of the
/// password — is unit-testable without instantiating the controller.
/// </summary>
internal static (string Subject, string Html) BuildPasswordResetEmail(string targetName, string newPassword) => (
"sanitaerfuchs.de Intranet Passwort zurückgesetzt",
$"<p>Guten Tag {targetName},<br/>Ihr Passwort wurde von einem Administrator zurückgesetzt.<br/>" +
$"Ihr neues 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
/// (<see href="../Docs/Decisions/0003-interrupting-exceptions-notify-the-user.md"/>) —
/// the admin performing the action must never be left believing it silently succeeded.
/// </summary>
private async Task NotifyAdminUserActionFailedAsync(string title, string message, string targetId, Exception ex)
{
_logger.LogError(ex, "{Title} target={Target} user={User}", title, targetId, UserAccountID);
await _events.UserIssueAsync(title, message, UserAccountID,
new Dictionary<string, object?> { ["target"] = targetId });
}
/// <summary>
/// Resolves the calling user's <c>fds_sys</c> module authorization level via the
/// <c>fis_getModuleAuth</c> SQL function (same mechanism as <see cref="HandleAuth"/>).
+37 -2
View File
@@ -1,6 +1,6 @@
---
status: Active
lastUpdated: 2026-07-16
lastUpdated: 2026-09-09
applyTo:
- "Fuchs/Controllers/IntranetController.Admin.cs"
- "Fuchs/Services/SystemStatusService.cs"
@@ -9,8 +9,11 @@ applyTo:
- "Fuchs/js/intranet/modules/fis.admin*.js"
- "Fuchs/js/intranet/modules/fis.admin.scss"
- "Fuchs/js/intranet/fis_main_menu.js"
- "Fuchs_Database/dbo/Functions/fis_admin_*.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_*.sql"
relatedDecisions:
- "0011-admin-module-system-status.md"
- "0014-admin-user-management.md"
---
# Admin / System-Status module
@@ -84,14 +87,46 @@ refresh button, the email card carries the test-email dialog. The topbar has an
button that reloads the whole snapshot. Admin responses are serialized **camelCase** (the DTOs are
PascalCase) so the JS reads `probe.status`, `info.database.server`, etc.
## User management (`users`/`userpwdreset`/`useremail`/`usergroups`/`usergroupset`)
See [0014](../Decisions/0014-admin-user-management.md) for why this lives here and for the
dual-authorization caveat (`fds_sys > 4` gates the endpoints; the SQL layer separately requires
the caller's `intranet_admin` setting to be `> 1`).
A card below the status grid (`$adm.usersCard`) renders a table of every non-deleted user
account (name, email, active/disabled) via `POST admin/users` →
`fis_admin_getUserAccountList(@authaccount, NULL, NULL, NULL)`. Clicking a row toggles a
per-row ribbon (`$adm.uMn`, replacing the topbar exactly like Banking's `iMn`/`eM` swap) with
three actions:
- **Passwort zurücksetzen** — a dialog with one optional password field ("leave blank to
auto-generate"). `POST admin/userpwdreset` calls `fis_admin_resetUserPassword` (now takes an
optional `@newpassword`; `NULL` ⇒ the proc auto-generates one as before). On success the
controller re-fetches the target's row (server-trusted email — never client-supplied) and
emails the plaintext password via `IComService.SendEmailAsync` — the admin never sees it. The
email subject/body is built by `IntranetController.BuildPasswordResetEmail` (pure, unit-tested
in `Fuchs.Tests/AdminUserManagementTests.cs`). Failure to reset, or to deliver the email, calls
`IEventService.UserIssueAsync` so the admin gets a visible notification either way.
- **E-Mail ändern** — a dialog with the email field prefilled. `POST admin/useremail` fetches
the current row and calls `fis_admin_setUserAccount` with only the email changed (other fields
passed through unchanged, `@password = NULL` so the update path leaves the password alone). A
duplicate email (`THROW 60001` in the proc) surfaces as an HTTP 409 `{ error: "duplicate_email" }`.
- **Gruppenzugehörigkeit** — a dialog (`$adm.groupsDlg`) listing every group as a checkbox,
populated via `POST admin/usergroups` → the new `fis_admin_getUserGroupMembership` function.
Each checkbox toggle immediately posts `admin/usergroupset`, which calls
`fis_admin_addGroupMember`/`remGroupMember` depending on the new state — no separate save step.
## Key files
- `Fuchs/Controllers/IntranetController.Admin.cs` — `Do_Process_Admin` dispatch + auth gate + camelCase JSON helper.
- `Fuchs/Controllers/IntranetController.Admin.cs` — `Do_Process_Admin` dispatch + auth gate + camelCase JSON helper + user-management endpoints.
- `Fuchs/Services/ISystemStatusService.cs`, `SystemStatusService.cs`, `SystemStatusModels.cs` — diagnostics service + DTOs.
- `Fuchs/Services/StartupCheckReport.cs` (`StartupCheckReporter` singleton) + `StartupSelfTestService.cs` — boot self-test result captured for the non-refreshable widget.
- `Fuchs/Services/IBlobStorageService.cs` / `AzureBlobStorageService.cs` — `CheckConnectivityAsync` + `BlobConnectivity`.
- `Fuchs/js/intranet/modules/fis.admin*.js`, `fis.admin.scss` — the module, texts, styles (bundled via `bdlconfig.json`).
- `Fuchs/js/intranet/fis_main_menu.js` (`addAdminMenuIfAuthorized`) + `fis_main_go.js` — conditional menu button.
- `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/SystemStatusServiceTests.cs` — service tests.
- `Fuchs.Tests/AdminUserManagementTests.cs` — password-reset email content tests.
## Related decisions
- [0011 — Admin module gated on `fds_sys` > 4](../Decisions/0011-admin-module-system-status.md)
- [0014 — Admin user management: folded into the `admin` module, dual auth axis, password always emailed](../Decisions/0014-admin-user-management.md)
@@ -0,0 +1,82 @@
---
status: Accepted
date: 2026-09-09
applyTo:
- "Fuchs/Controllers/IntranetController.Admin.cs"
- "Fuchs/js/intranet/modules/fis.admin*.js"
- "Fuchs/js/intranet/modules/fis.admin.scss"
- "Fuchs_Database/dbo/Functions/fis_admin_*.sql"
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_*.sql"
supersededBy: ""
---
# 0014 — Admin user management: folded into the `admin` module, dual auth axis, password always emailed
## Context
An operator needs to see the list of non-deleted intranet user accounts (name + email), and
per account reset the password, change the email address, and view/change group assignments.
The database layer for this already existed (`fis_admin_getUserAccountList`,
`fis_admin_getUserGroupList`, `fis_admin_setUserAccount`, `fis_admin_resetUserPassword`,
`fis_admin_addGroupMember`/`remGroupMember`) but was completely unwired from any controller.
Those procs gate on a **different** authorization mechanism than the rest of the app: a
per-user `intranet_admin` setting read via `fis_admin_getUserAuth(@authaccount) > 1`, entirely
independent of the `fis_getModuleAuth`/`fis_module_auth` mechanism the rest of the app
(including the existing Admin module, see
[0011](0011-admin-module-system-status.md)) uses.
## Decision
- **Fold user management into the existing `admin` module** (`fn=admin`) rather than create a
new top-level module. New `id` cases (`users`, `userpwdreset`, `useremail`, `usergroups`,
`usergroupset`) sit in `Do_Process_Admin` alongside the system-status diagnostics, behind the
**same** `fds_sys > 4` server-side gate as the rest of that module.
- **Known, accepted gap: two independent authorization axes.** `fds_sys > 4` controls whether
the endpoints are reachable at all; the underlying `fis_admin_*` SQL objects separately
require the caller's `intranet_admin` setting to be `> 1` (`fis_admin_getUserAuth`). An
account with `fds_sys > 4` but no `intranet_admin` setting will reach these endpoints but get
empty result sets / no-op writes from SQL — not an error, just silently nothing. **Operational
consequence:** any account meant to do user management must be provisioned with both
`fds_sys > 4` and `intranet_admin >= 2` (via `fis_useraccounts_settings`). This was a deliberate
simplicity trade-off (see Alternatives) rather than unifying the two permission systems.
- **Password reset always emails the result; never displays it.** The ribbon's "Passwort
zurücksetzen" action lets the admin either type a specific new password or leave the field
blank to auto-generate one (`fis_admin_resetUserPassword` gained an optional `@newpassword`
parameter for this). Either way, the resulting plaintext password is sent only to the target
user's stored email address via `IComService` (respecting the `Fuchs:Email:OverrideRecipient`
dev safety net) — the admin never sees it on screen.
- **Change email reuses `fis_admin_setUserAccount`** (prefilling the other fields from the
existing row, `@password = NULL` so the update path leaves the password untouched) instead of
adding a narrower proc — its `WHEN MATCHED` branch never touches `password_enc`, and its
existing duplicate-email `THROW 60001` is mapped to an HTTP 409 in the controller.
- **New `fis_admin_getUserGroupMembership` function** returns every group plus a `member` bit
for one target user (mirrors `fis_admin_getUserGroupList`'s auth-gate style) — needed for the
per-user group checkbox dialog; toggling a checkbox calls `fis_admin_addGroupMember`/
`remGroupMember` immediately (no separate save step, consistent with the rest of the module's
ribbon actions).
- Per the [exception-user-notification rule](0003-interrupting-exceptions-notify-the-user.md),
a failed password reset, a failed reset-email delivery, or a target with no usable email all
call `IEventService.UserIssueAsync` so the admin is never left believing an operation
succeeded silently.
## Consequences
- Deploying/onboarding an admin for user management is now a two-step provisioning task
(`fds_sys` **and** `intranet_admin`), not obvious from the UI — document this whenever
granting the permission.
- `fis_admin_resetUserPassword`'s signature changed (added optional `@newpassword`) and its
result set gained a `newpassword` column; it was previously unwired from any caller, so this
is a safe in-place change.
- The Admin module's frontend now mixes two concerns (system diagnostics + user management)
under one ribbon/menu entry; if user management grows substantially, revisit splitting it into
its own module (see Alternatives).
## Alternatives considered
- **Unify the two authorization axes** (e.g. make `fis_admin_*` procs check `fis_getModuleAuth`
instead of `fis_admin_getUserAuth`, or vice versa). Rejected for this change — it would touch
every existing `fis_admin_*` proc/function (all already gate on `fis_admin_getUserAuth`) for a
feature that only needed to wire up what already existed; revisit if a real conflict surfaces
in practice.
- **New standalone top-level module** (`fn=usr`) with its own menu button and its own
`fis_admin_getUserAuth`-based gate. Rejected per explicit user direction — folding into the
existing Administration module was preferred over a second admin-ish menu entry.
- **Show the new/reset password on screen instead of emailing it.** Rejected per explicit user
direction — the password must only ever reach the user's own inbox, never be visible to the
admin performing the reset.
+111
View File
@@ -16,6 +16,14 @@ let $adm = {
{ lbl: $adt.refreshAll, id: 'adm_refresh_all', ico: 'glyphicon glyphicon-refresh', fnc: function () { $adm.init3(); } }
]);
},
/* Per-row ribbon shown while a user row is selected (mirrors the Banking module's iMn/eM swap). */
uMn: function (rw) {
return $('#topbar').ocmsmenu([
{ lbl: $adt.u_pwdreset, fnc: function () { $adm.pwdResetDlg(rw); } },
{ lbl: $adt.u_email, fnc: function () { $adm.emailDlg(rw); } },
{ lbl: $adt.u_groups, fnc: function () { $adm.groupsDlg(rw); } }
]);
},
load: function (frm) {
$ocms.postXT({
url: $ocms.url('admin/status'), success: function (r) {
@@ -42,6 +50,109 @@ let $adm = {
$adm.blobCard(grid, info.blob || {}, pmap.blob);
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
$$.dc('adm_sec', frm).text($adt.sub_users);
$adm.usersCard(frm);
},
/* ── user management ─────────────────────────────────────────────────── */
usersCard: function (frm) {
let card = $$.dc('adm_card adm_card_wide', frm);
let hd = $$.dc('adm_card_hd', card);
$$.s($adt.u_title).appendTo(hd);
let bd = $$.dc('adm_card_bd adm_users_bd', card);
$adm.renderUsers(bd);
},
renderUsers: function (bd) {
bd.empty();
let ts = $$.tblset({}, bd);
let thr = $$.tr(ts.hd);
$.each([$adt.u_col_name, $adt.u_col_email, $adt.u_col_status], function (i, lbl) {
$$.th(thr).text(lbl);
});
$ocms.postXT({
url: $ocms.url('admin/users'), success: function (r) {
$.each(r.users || [], function (ri, rw) {
let tr = $$.tr(ts.bdy, { id: 'adu' + rw.useraccount_id });
$$.td(tr).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
$$.td(tr).text(rw.email || '');
$$.td(tr).text(bool(rw.disabled, false) === true ? $adt.u_disabled : $adt.u_active);
tr.click(function () {
tr.toggleClass('selected').siblings().rC('selected');
if (tr.is('.selected') === true) { $adm.uMn(rw); } else { $adm.topbar(); }
});
});
}, error: function () {
$$.dc('adm_err', bd).text($adt.loadfail);
}
});
},
pwdResetDlg: function (rw) {
$ocms.dlgform([
{ name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id },
{ name: 'newpassword', label: $adt.u_pwd_field, type: 'string', note: $adt.u_pwd_hint }
], {
title: $adt.u_pwdreset,
button: $adt.u_pwdreset_btn,
submit: function (e) {
let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true });
$ocms.postXT({
url: $ocms.url('admin/userpwdreset'), data: qs, success: function (response) {
alert(response.emailed === true ? $adt.u_pwdreset_sent : $adt.u_pwdreset_noemail);
c.trigger('modal_close');
}, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error;
alert($adt.u_err[code] || $adt.u_err_generic);
}, complete: function () { c.ldng(0); $adm.renderUsers($('.adm_users_bd')); $adm.topbar(); }
});
}
});
},
emailDlg: function (rw) {
$ocms.dlgform([
{ name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id },
{ name: 'email', label: $adt.u_email_field, type: 'email', required: true, value: rw.email }
], {
title: $adt.u_email,
submit: function (e) {
let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true });
$ocms.postXT({
url: $ocms.url('admin/useremail'), data: qs, success: function () {
c.trigger('modal_close');
}, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error;
alert($adt.u_err[code] || $adt.u_err_generic);
}, complete: function () { c.ldng(0); $adm.renderUsers($('.adm_users_bd')); $adm.topbar(); }
});
}
});
},
groupsDlg: function (rw) {
let f = $$.dc('adm_groups_dlg');
$ocms.postXT({
url: $ocms.url('admin/usergroups'), data: { useraccount_id: rw.useraccount_id }, success: function (r) {
$.each(r.groups || [], function (gi, g) {
let row = $$.dc('adm_group_row', f);
let cid = 'adg' + g.usergroup_id;
let cb = $$.i({ type: 'checkbox', id: cid }).prop('checked', bool(g.member, false)).appendTo(row);
$$.lbl(g.name, { for: cid }).appendTo(row);
cb.change(function () {
let checked = cb.prop('checked');
$ocms.postXT({
url: $ocms.url('admin/usergroupset'),
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked },
error: function () {
cb.prop('checked', !checked);
alert($adt.u_err_generic);
}
});
});
});
}
});
$ocms.dlg(f, { title: string($adt.u_groups_title, [`${rw.firstname || ''} ${rw.name || ''}`.trim()]), size: [420, 380] });
},
/* ── generic building blocks ─────────────────────────────────────────── */
+23
View File
@@ -188,6 +188,29 @@
color: #fff;
border-color: rgba(255, 255, 255, 0.4);
}
.adm_users_bd table tbody tr {
cursor: pointer;
&:hover td { background-color: #f3f6fb; }
}
}
.adm_groups_dlg {
padding: 0.25rem 0;
.adm_group_row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0;
border-bottom: 1px solid #eee;
&:last-child { border-bottom: none; }
input[type="checkbox"] { margin: 0; }
label { margin: 0; font-weight: normal; }
}
}
.adm_note {
@@ -81,4 +81,29 @@ let $adt = {
, tm_default_subject: 'Fuchs Intranet – Test-E-Mail'
, tm_default_body: 'Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.'
, loadfail: 'Der Systemstatus konnte nicht geladen werden.'
// user management
, sub_users: 'Benutzerverwaltung'
, u_title: 'Benutzerkonten'
, u_col_name: 'Name'
, u_col_email: 'E-Mail'
, u_col_status: 'Status'
, u_active: 'Aktiv'
, u_disabled: 'Deaktiviert'
, u_pwdreset: 'Passwort zurücksetzen'
, u_pwdreset_btn: 'Zurücksetzen'
, u_pwd_field: 'Neues Passwort'
, u_pwd_hint: 'Leer lassen, um automatisch ein Passwort zu generieren. In jedem Fall wird das neue Passwort per E-Mail an den Benutzer gesendet.'
, u_pwdreset_sent: 'Das neue Passwort wurde per E-Mail an den Benutzer gesendet.'
, u_pwdreset_noemail: 'Das Passwort wurde zurückgesetzt, konnte aber nicht per E-Mail zugestellt werden.'
, u_email: 'E-Mail-Adresse ändern'
, u_email_field: 'E-Mail-Adresse'
, u_groups: 'Gruppenzugehörigkeit'
, u_groups_title: 'Gruppen – {0}'
, u_err: {
requirements: 'Das Passwort erfüllt nicht die Mindestanforderungen (mind. 6 Zeichen).',
duplicate_email: 'Diese E-Mail-Adresse ist bereits einem anderen Benutzerkonto zugeordnet.',
reset_failed: 'Das Passwort konnte nicht zurückgesetzt werden.',
change_failed: 'Die Änderung konnte nicht gespeichert werden.'
}
, u_err_generic: 'Die Änderung konnte nicht gespeichert werden.'
};
+27
View File
@@ -245,6 +245,33 @@
color: #fff;
border-color: rgba(255, 255, 255, 0.4);
}
.admfrm .adm_users_bd table tbody tr {
cursor: pointer;
}
.admfrm .adm_users_bd table tbody tr:hover td {
background-color: #f3f6fb;
}
.adm_groups_dlg {
padding: 0.25rem 0;
}
.adm_groups_dlg .adm_group_row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0;
border-bottom: 1px solid #eee;
}
.adm_groups_dlg .adm_group_row:last-child {
border-bottom: none;
}
.adm_groups_dlg .adm_group_row input[type=checkbox] {
margin: 0;
}
.adm_groups_dlg .adm_group_row label {
margin: 0;
font-weight: normal;
}
.adm_note {
margin-top: 0.75rem;
+136
View File
@@ -81,6 +81,31 @@ let $adt = {
, tm_default_subject: 'Fuchs Intranet – Test-E-Mail'
, tm_default_body: 'Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.'
, loadfail: 'Der Systemstatus konnte nicht geladen werden.'
// user management
, sub_users: 'Benutzerverwaltung'
, u_title: 'Benutzerkonten'
, u_col_name: 'Name'
, u_col_email: 'E-Mail'
, u_col_status: 'Status'
, u_active: 'Aktiv'
, u_disabled: 'Deaktiviert'
, u_pwdreset: 'Passwort zurücksetzen'
, u_pwdreset_btn: 'Zurücksetzen'
, u_pwd_field: 'Neues Passwort'
, u_pwd_hint: 'Leer lassen, um automatisch ein Passwort zu generieren. In jedem Fall wird das neue Passwort per E-Mail an den Benutzer gesendet.'
, u_pwdreset_sent: 'Das neue Passwort wurde per E-Mail an den Benutzer gesendet.'
, u_pwdreset_noemail: 'Das Passwort wurde zurückgesetzt, konnte aber nicht per E-Mail zugestellt werden.'
, u_email: 'E-Mail-Adresse ändern'
, u_email_field: 'E-Mail-Adresse'
, u_groups: 'Gruppenzugehörigkeit'
, u_groups_title: 'Gruppen – {0}'
, u_err: {
requirements: 'Das Passwort erfüllt nicht die Mindestanforderungen (mind. 6 Zeichen).',
duplicate_email: 'Diese E-Mail-Adresse ist bereits einem anderen Benutzerkonto zugeordnet.',
reset_failed: 'Das Passwort konnte nicht zurückgesetzt werden.',
change_failed: 'Die Änderung konnte nicht gespeichert werden.'
}
, u_err_generic: 'Die Änderung konnte nicht gespeichert werden.'
};
let $adm = {
@@ -101,6 +126,14 @@ let $adm = {
{ lbl: $adt.refreshAll, id: 'adm_refresh_all', ico: 'glyphicon glyphicon-refresh', fnc: function () { $adm.init3(); } }
]);
},
/* Per-row ribbon shown while a user row is selected (mirrors the Banking module's iMn/eM swap). */
uMn: function (rw) {
return $('#topbar').ocmsmenu([
{ lbl: $adt.u_pwdreset, fnc: function () { $adm.pwdResetDlg(rw); } },
{ lbl: $adt.u_email, fnc: function () { $adm.emailDlg(rw); } },
{ lbl: $adt.u_groups, fnc: function () { $adm.groupsDlg(rw); } }
]);
},
load: function (frm) {
$ocms.postXT({
url: $ocms.url('admin/status'), success: function (r) {
@@ -127,6 +160,109 @@ let $adm = {
$adm.blobCard(grid, info.blob || {}, pmap.blob);
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
$$.dc('adm_sec', frm).text($adt.sub_users);
$adm.usersCard(frm);
},
/* ── user management ─────────────────────────────────────────────────── */
usersCard: function (frm) {
let card = $$.dc('adm_card adm_card_wide', frm);
let hd = $$.dc('adm_card_hd', card);
$$.s($adt.u_title).appendTo(hd);
let bd = $$.dc('adm_card_bd adm_users_bd', card);
$adm.renderUsers(bd);
},
renderUsers: function (bd) {
bd.empty();
let ts = $$.tblset({}, bd);
let thr = $$.tr(ts.hd);
$.each([$adt.u_col_name, $adt.u_col_email, $adt.u_col_status], function (i, lbl) {
$$.th(thr).text(lbl);
});
$ocms.postXT({
url: $ocms.url('admin/users'), success: function (r) {
$.each(r.users || [], function (ri, rw) {
let tr = $$.tr(ts.bdy, { id: 'adu' + rw.useraccount_id });
$$.td(tr).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
$$.td(tr).text(rw.email || '');
$$.td(tr).text(bool(rw.disabled, false) === true ? $adt.u_disabled : $adt.u_active);
tr.click(function () {
tr.toggleClass('selected').siblings().rC('selected');
if (tr.is('.selected') === true) { $adm.uMn(rw); } else { $adm.topbar(); }
});
});
}, error: function () {
$$.dc('adm_err', bd).text($adt.loadfail);
}
});
},
pwdResetDlg: function (rw) {
$ocms.dlgform([
{ name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id },
{ name: 'newpassword', label: $adt.u_pwd_field, type: 'string', note: $adt.u_pwd_hint }
], {
title: $adt.u_pwdreset,
button: $adt.u_pwdreset_btn,
submit: function (e) {
let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true });
$ocms.postXT({
url: $ocms.url('admin/userpwdreset'), data: qs, success: function (response) {
alert(response.emailed === true ? $adt.u_pwdreset_sent : $adt.u_pwdreset_noemail);
c.trigger('modal_close');
}, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error;
alert($adt.u_err[code] || $adt.u_err_generic);
}, complete: function () { c.ldng(0); $adm.renderUsers($('.adm_users_bd')); $adm.topbar(); }
});
}
});
},
emailDlg: function (rw) {
$ocms.dlgform([
{ name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id },
{ name: 'email', label: $adt.u_email_field, type: 'email', required: true, value: rw.email }
], {
title: $adt.u_email,
submit: function (e) {
let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true });
$ocms.postXT({
url: $ocms.url('admin/useremail'), data: qs, success: function () {
c.trigger('modal_close');
}, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error;
alert($adt.u_err[code] || $adt.u_err_generic);
}, complete: function () { c.ldng(0); $adm.renderUsers($('.adm_users_bd')); $adm.topbar(); }
});
}
});
},
groupsDlg: function (rw) {
let f = $$.dc('adm_groups_dlg');
$ocms.postXT({
url: $ocms.url('admin/usergroups'), data: { useraccount_id: rw.useraccount_id }, success: function (r) {
$.each(r.groups || [], function (gi, g) {
let row = $$.dc('adm_group_row', f);
let cid = 'adg' + g.usergroup_id;
let cb = $$.i({ type: 'checkbox', id: cid }).prop('checked', bool(g.member, false)).appendTo(row);
$$.lbl(g.name, { for: cid }).appendTo(row);
cb.change(function () {
let checked = cb.prop('checked');
$ocms.postXT({
url: $ocms.url('admin/usergroupset'),
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked },
error: function () {
cb.prop('checked', !checked);
alert($adt.u_err_generic);
}
});
});
});
}
});
$ocms.dlg(f, { title: string($adt.u_groups_title, [`${rw.firstname || ''} ${rw.name || ''}`.trim()]), size: [420, 380] });
},
/* ── generic building blocks ─────────────────────────────────────────── */
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.admfrm{padding:1.5rem}.admfrm .adm_sec{font-size:1.2rem;font-weight:700;color:#1b4379;margin:1.5rem 0 .75rem;border-bottom:2px solid #1b4379;padding-bottom:.25rem}.admfrm .adm_sec:first-child{margin-top:0}.admfrm .adm_err{padding:1rem;color:#b00020;font-weight:700}.admfrm .adm_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(22rem,1fr));gap:1rem;align-items:start}.admfrm .adm_card{border:1px solid #d5d5d5;border-radius:.4rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.12);overflow:hidden}.admfrm .adm_card.adm_card_wide{grid-column:1/-1}.admfrm .adm_card_hd{display:flex;align-items:center;gap:.5rem;padding:.6rem .85rem;background:#1b4379;color:#fff;font-weight:700}.admfrm .adm_card_hd>span:first-child{flex:1 1 auto}.admfrm .adm_card_bd{padding:.6rem .85rem}.admfrm .adm_card_ft{padding:.5rem .85rem .75rem;text-align:right}.admfrm table.adm_kv{border-collapse:collapse;width:100%}.admfrm table.adm_kv td{padding:.25rem .35rem;border-bottom:1px solid #eee;vertical-align:top;font-size:.92rem}.admfrm table.adm_kv td.k{color:#666;white-space:nowrap;width:40%}.admfrm table.adm_kv td.v{word-break:break-word}.admfrm table.adm_kv tr:last-child td{border-bottom:none}.admfrm .adm_probe{margin-top:.6rem;padding-top:.5rem;border-top:1px dashed #ddd;font-size:.88rem}.admfrm .adm_probe .adm_probe_msg{font-weight:600}.admfrm .adm_probe .adm_probe_det{color:#555;word-break:break-word;margin-top:.15rem}.admfrm .adm_probe .adm_probe_dur{color:#999;margin-top:.15rem;font-size:.8rem}.admfrm .adm_probe .adm_probe_metrics{margin-top:.35rem}.admfrm .adm_probe .adm_probe_metrics .adm_metric{display:flex;justify-content:space-between;gap:.5rem;padding:.1rem 0;border-bottom:1px dotted #eee}.admfrm .adm_probe .adm_probe_metrics .adm_metric:last-child{border-bottom:none}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_k{color:#555;word-break:break-word}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_v{font-weight:600;white-space:nowrap}.admfrm .adm_checks{margin-top:.5rem}.admfrm .adm_checks .adm_check{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.25rem 0;border-bottom:1px solid #eee}.admfrm .adm_checks .adm_check:last-child{border-bottom:none}.admfrm .adm_checks .adm_check .adm_check_k{color:#333}.admfrm .adm_checks .adm_check .adm_check_v{font-weight:700;font-size:.8rem;padding:.05rem .5rem;border-radius:.25rem;white-space:nowrap}.admfrm .adm_checks .adm_check .adm_check_ok{background:#e2f3da;color:#2f6d18}.admfrm .adm_checks .adm_check .adm_check_fail{background:#f3ded9;color:#c0341d}.admfrm .adm_checks .adm_check .adm_check_skipped{background:#ececec;color:#777}.admfrm .adm_pill{display:inline-block;padding:.1rem .55rem;border-radius:1rem;font-size:.78rem;font-weight:700;color:#fff;background:#999;white-space:nowrap}.admfrm .adm_pill_ok{background:#56a532}.admfrm .adm_pill_error{background:#c0341d}.admfrm .adm_pill_disabled{background:#888}.admfrm .adm_pill_unconfigured{background:#d69100}.admfrm .adm_pill_checking{background:#2a72c4}.admfrm .adm_bool{display:inline-block;padding:0 .4rem;border-radius:.25rem;font-size:.8rem;font-weight:700}.admfrm .adm_bool_y{background:#e2f3da;color:#2f6d18}.admfrm .adm_bool_n{background:#f3e0dd;color:#9c2a17}.admfrm .adm_env{font-size:.8rem;font-weight:400;padding:.1rem .5rem;border-radius:.25rem;background:rgba(255,255,255,.25)}.admfrm .adm_env.adm_env_test{background:#13a143}.admfrm .adm_btn{cursor:pointer;border-radius:.28rem;padding:.2rem .6rem;font-size:.85rem;border:1px solid #ababab;background-color:#fff;color:#333}.admfrm .adm_btn.adm_btn_primary{background-color:#56a532;border-color:rgb(69.68,133.688372093,40.511627907);color:#fff}.admfrm .adm_card_hd .adm_btn{background-color:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.4)}.adm_note{margin-top:.75rem;padding:.5rem .6rem;background:#f3f6fb;border-left:3px solid #1b4379;font-size:.85rem;color:#444}
.admfrm{padding:1.5rem}.admfrm .adm_sec{font-size:1.2rem;font-weight:700;color:#1b4379;margin:1.5rem 0 .75rem;border-bottom:2px solid #1b4379;padding-bottom:.25rem}.admfrm .adm_sec:first-child{margin-top:0}.admfrm .adm_err{padding:1rem;color:#b00020;font-weight:700}.admfrm .adm_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(22rem,1fr));gap:1rem;align-items:start}.admfrm .adm_card{border:1px solid #d5d5d5;border-radius:.4rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.12);overflow:hidden}.admfrm .adm_card.adm_card_wide{grid-column:1/-1}.admfrm .adm_card_hd{display:flex;align-items:center;gap:.5rem;padding:.6rem .85rem;background:#1b4379;color:#fff;font-weight:700}.admfrm .adm_card_hd>span:first-child{flex:1 1 auto}.admfrm .adm_card_bd{padding:.6rem .85rem}.admfrm .adm_card_ft{padding:.5rem .85rem .75rem;text-align:right}.admfrm table.adm_kv{border-collapse:collapse;width:100%}.admfrm table.adm_kv td{padding:.25rem .35rem;border-bottom:1px solid #eee;vertical-align:top;font-size:.92rem}.admfrm table.adm_kv td.k{color:#666;white-space:nowrap;width:40%}.admfrm table.adm_kv td.v{word-break:break-word}.admfrm table.adm_kv tr:last-child td{border-bottom:none}.admfrm .adm_probe{margin-top:.6rem;padding-top:.5rem;border-top:1px dashed #ddd;font-size:.88rem}.admfrm .adm_probe .adm_probe_msg{font-weight:600}.admfrm .adm_probe .adm_probe_det{color:#555;word-break:break-word;margin-top:.15rem}.admfrm .adm_probe .adm_probe_dur{color:#999;margin-top:.15rem;font-size:.8rem}.admfrm .adm_probe .adm_probe_metrics{margin-top:.35rem}.admfrm .adm_probe .adm_probe_metrics .adm_metric{display:flex;justify-content:space-between;gap:.5rem;padding:.1rem 0;border-bottom:1px dotted #eee}.admfrm .adm_probe .adm_probe_metrics .adm_metric:last-child{border-bottom:none}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_k{color:#555;word-break:break-word}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_v{font-weight:600;white-space:nowrap}.admfrm .adm_checks{margin-top:.5rem}.admfrm .adm_checks .adm_check{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.25rem 0;border-bottom:1px solid #eee}.admfrm .adm_checks .adm_check:last-child{border-bottom:none}.admfrm .adm_checks .adm_check .adm_check_k{color:#333}.admfrm .adm_checks .adm_check .adm_check_v{font-weight:700;font-size:.8rem;padding:.05rem .5rem;border-radius:.25rem;white-space:nowrap}.admfrm .adm_checks .adm_check .adm_check_ok{background:#e2f3da;color:#2f6d18}.admfrm .adm_checks .adm_check .adm_check_fail{background:#f3ded9;color:#c0341d}.admfrm .adm_checks .adm_check .adm_check_skipped{background:#ececec;color:#777}.admfrm .adm_pill{display:inline-block;padding:.1rem .55rem;border-radius:1rem;font-size:.78rem;font-weight:700;color:#fff;background:#999;white-space:nowrap}.admfrm .adm_pill_ok{background:#56a532}.admfrm .adm_pill_error{background:#c0341d}.admfrm .adm_pill_disabled{background:#888}.admfrm .adm_pill_unconfigured{background:#d69100}.admfrm .adm_pill_checking{background:#2a72c4}.admfrm .adm_bool{display:inline-block;padding:0 .4rem;border-radius:.25rem;font-size:.8rem;font-weight:700}.admfrm .adm_bool_y{background:#e2f3da;color:#2f6d18}.admfrm .adm_bool_n{background:#f3e0dd;color:#9c2a17}.admfrm .adm_env{font-size:.8rem;font-weight:400;padding:.1rem .5rem;border-radius:.25rem;background:rgba(255,255,255,.25)}.admfrm .adm_env.adm_env_test{background:#13a143}.admfrm .adm_btn{cursor:pointer;border-radius:.28rem;padding:.2rem .6rem;font-size:.85rem;border:1px solid #ababab;background-color:#fff;color:#333}.admfrm .adm_btn.adm_btn_primary{background-color:#56a532;border-color:rgb(69.68,133.688372093,40.511627907);color:#fff}.admfrm .adm_card_hd .adm_btn{background-color:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.4)}.admfrm .adm_users_bd table tbody tr{cursor:pointer}.admfrm .adm_users_bd table tbody tr:hover td{background-color:#f3f6fb}.adm_groups_dlg{padding:.25rem 0}.adm_groups_dlg .adm_group_row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid #eee}.adm_groups_dlg .adm_group_row:last-child{border-bottom:none}.adm_groups_dlg .adm_group_row input[type=checkbox]{margin:0}.adm_groups_dlg .adm_group_row label{margin:0;font-weight:400}.adm_note{margin-top:.75rem;padding:.5rem .6rem;background:#f3f6fb;border-left:3px solid #1b4379;font-size:.85rem;color:#444}
+1
View File
@@ -223,6 +223,7 @@
<Build Include="dbo\Functions\json_diffObjectArrays.sql" />
<Build Include="dbo\Functions\json_compareObjectArrays.sql" />
<Build Include="dbo\Functions\fis_getONEPersonWidgets.sql" />
<Build Include="dbo\Functions\fis_admin_getUserGroupMembership.sql" />
<Build Include="dbo\Functions\fis_admin_getUserGroupList.sql" />
<Build Include="dbo\Functions\fis_admin_getUserAccountList.sql" />
<Build Include="dbo\Functions\fis_admin_getUserAccount_Namelist.sql" />
@@ -0,0 +1,33 @@
-- =============================================
-- Author: <Author,,Name>
-- Create date: <Create Date, ,>
-- Description: <Description, ,>
-- =============================================
CREATE FUNCTION [dbo].[fis_admin_getUserGroupMembership]
(
@authaccount varchar(5)
, @useraccount_id varchar(5)
)
RETURNS @RET TABLE ( [usergroup_id] varchar(5), [name] nvarchar(255) null, [member] bit, [order] int)
AS
BEGIN
DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount);
IF @auth > 1
BEGIN
INSERT INTO @RET
SELECT ug.[usergroup_id]
,CAST(DECRYPTBYKEY(ug.[name_enc]) as nvarchar(255))
,'member' = CASE WHEN ugm.[useraccount_id] is not null THEN 1 ELSE 0 END
,'order' = ROW_NUMBER() OVER (ORDER BY CAST(DECRYPTBYKEY(ug.[name_enc]) as nvarchar(255)))
FROM [dbo].[fis_usergroups] as ug
LEFT JOIN [dbo].[fis_usergroups_members] as ugm ON ugm.[usergroup_id] = ug.[usergroup_id]
AND ugm.[useraccount_id] = @useraccount_id AND ugm.[DateDeleted] is null
WHERE ug.[DateDeleted] is null;
END
RETURN;
END
@@ -10,6 +10,7 @@ CREATE PROCEDURE [dbo].[fis_admin_resetUserPassword]
@authaccount varchar(10)
,@useraccount_id varchar(10)
,@enc_key uniqueidentifier
,@newpassword nvarchar(20) = NULL /* admin-supplied password; when NULL a random one is generated */
AS
BEGIN
SET NOCOUNT ON;
@@ -17,19 +18,23 @@ BEGIN
DECLARE @utcnow datetime = GETUTCDATE();
DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount);
DECLARE @newpassword nvarchar(20) = [dbo].[ocms_fn_generatePassword] (8, 0, 1, 1);
IF @newpassword IS NULL
BEGIN
SET @newpassword = [dbo].[ocms_fn_generatePassword] (8, 0, 1, 1);
END
IF @auth > 1 OR @authaccount = @useraccount_id
BEGIN
UPDATE [dbo].[fis_useraccounts]
SET [password_enc] = ENCRYPTBYKEY(@enc_key, @newpassword)
, [DateModified] = GETUTCDATE()
, [UserModified] = @authaccount
WHERE [useraccount_id] = @useraccount_id AND [dateDeleted] is null;
SELECT CAST(CASE WHEN DECRYPTBYKEY((SELECT TOP(1) [password_enc] FROM [dbo].[fis_useraccounts] WHERE [useraccount_id] = @useraccount_id)) = @newpassword THEN 1 ELSE 0 END as bit);
SELECT CAST(CASE WHEN DECRYPTBYKEY((SELECT TOP(1) [password_enc] FROM [dbo].[fis_useraccounts] WHERE [useraccount_id] = @useraccount_id)) = @newpassword THEN 1 ELSE 0 END as bit) as [success]
, @newpassword as [newpassword];
END