diff --git a/Fuchs.Tests/AdminUserManagementTests.cs b/Fuchs.Tests/AdminUserManagementTests.cs new file mode 100644 index 0000000..e13caa9 --- /dev/null +++ b/Fuchs.Tests/AdminUserManagementTests.cs @@ -0,0 +1,43 @@ +using Fuchs.Controllers; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// 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. +/// +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>")] + [InlineData("O'Brien&Co", "O'Brien&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); + } +} diff --git a/Fuchs/Controllers/IntranetController.Admin.cs b/Fuchs/Controllers/IntranetController.Admin.cs index 6fbea3e..0eff37d 100644 --- a/Fuchs/Controllers/IntranetController.Admin.cs +++ b/Fuchs/Controllers/IntranetController.Admin.cs @@ -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 { ["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"); + /// + /// 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. + /// + internal static (string Subject, string Html) BuildPasswordResetEmail(string targetName, string newPassword) => ( + "sanitaerfuchs.de Intranet Passwort zurückgesetzt", + $"

Guten Tag {targetName},
Ihr Passwort wurde von einem Administrator zurückgesetzt.
" + + $"Ihr neues Passwort: {HttpUtility.HtmlEncode(newPassword)}

"); + + /// + /// Logs and raises a user-facing notification for an admin user-management action that + /// threw unexpectedly, per the "interrupting exceptions notify the user" rule + /// () — + /// the admin performing the action must never be left believing it silently succeeded. + /// + 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 { ["target"] = targetId }); + } + /// /// Resolves the calling user's fds_sys module authorization level via the /// fis_getModuleAuth SQL function (same mechanism as ). diff --git a/Fuchs/Docs/Concepts/admin-system-status.md b/Fuchs/Docs/Concepts/admin-system-status.md index 4d29354..c7dfd37 100644 --- a/Fuchs/Docs/Concepts/admin-system-status.md +++ b/Fuchs/Docs/Concepts/admin-system-status.md @@ -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) diff --git a/Fuchs/Docs/Decisions/0014-admin-user-management.md b/Fuchs/Docs/Decisions/0014-admin-user-management.md new file mode 100644 index 0000000..955a794 --- /dev/null +++ b/Fuchs/Docs/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. diff --git a/Fuchs/js/intranet/modules/fis.admin.js b/Fuchs/js/intranet/modules/fis.admin.js index 5690598..ce011d5 100644 --- a/Fuchs/js/intranet/modules/fis.admin.js +++ b/Fuchs/js/intranet/modules/fis.admin.js @@ -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 ─────────────────────────────────────────── */ diff --git a/Fuchs/js/intranet/modules/fis.admin.scss b/Fuchs/js/intranet/modules/fis.admin.scss index 2616245..c7f1cff 100644 --- a/Fuchs/js/intranet/modules/fis.admin.scss +++ b/Fuchs/js/intranet/modules/fis.admin.scss @@ -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 { diff --git a/Fuchs/js/intranet/modules/fis.admin_txt_de.js b/Fuchs/js/intranet/modules/fis.admin_txt_de.js index 7a8fcf3..d1327d5 100644 --- a/Fuchs/js/intranet/modules/fis.admin_txt_de.js +++ b/Fuchs/js/intranet/modules/fis.admin_txt_de.js @@ -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.' }; diff --git a/Fuchs/wwwroot/web/fis.admin.css b/Fuchs/wwwroot/web/fis.admin.css index f672fe6..e458918 100644 --- a/Fuchs/wwwroot/web/fis.admin.css +++ b/Fuchs/wwwroot/web/fis.admin.css @@ -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; diff --git a/Fuchs/wwwroot/web/fis.admin.de.js b/Fuchs/wwwroot/web/fis.admin.de.js index 03d4c85..4d285ec 100644 --- a/Fuchs/wwwroot/web/fis.admin.de.js +++ b/Fuchs/wwwroot/web/fis.admin.de.js @@ -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 ─────────────────────────────────────────── */ diff --git a/Fuchs/wwwroot/web/fis.admin.de.min.js b/Fuchs/wwwroot/web/fis.admin.de.min.js index 9219582..81b6a86 100644 --- a/Fuchs/wwwroot/web/fis.admin.de.min.js +++ b/Fuchs/wwwroot/web/fis.admin.de.min.js @@ -1 +1 @@ -let $adt={mdl:"Administration",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",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."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let r=$$.dc("adm_grid",e);$adm.dbCard(r,t.database||{},d.database),$adm.mailCard(r,t.email||{}),$adm.blobCard(r,t.blob||{},d.blob),$adm.mfrCard(r,t.mfr||{},d.mfr),$adm.kvCard(r,t.keyVault||{},d.keyvault)},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let r=$$.td(d).addClass("v");t[1]instanceof jQuery?r.append(t[1]):r.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,r){let n=$$.dc("adm_card",e),i=$$.dc("adm_card_hd",n);$$.s(t).appendTo(i);let s=$$.sc("adm_pill").appendTo(i),o=$$.bbtn($adt.refresh,"adm_btn").appendTo(i),c=$$.dc("adm_card_bd",n);$adm.kvTable(c,d);let l=$$.dc("adm_probe",c),m=$$.dc("adm_probe_msg",l),u=$$.dc("adm_probe_metrics",l),_=$$.dc("adm_probe_det",l),p=$$.dc("adm_probe_dur",l),b=function(e){e=e||{},$adm.setPill(s,e),m.text(e.message||""),u.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",u);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),_.text(e.detail||""),p.text(null!=e.durationMs?e.durationMs+" ms":"")};return b(r),o.click((function(){$adm.setPill(s,{status:"checking"}),m.text(""),u.empty(),_.text(""),p.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){b(e.probe)},error:function(){$adm.setPill(s,{status:"error"}),m.text($adt.st_error)}})})),n},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let r=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&r.aC("adm_env_test"),r.appendTo(d);let n=$$.dc("adm_card_bd",a);$adm.kvTable(n,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let r=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",r).text($adt.startup_notrun);let n=t.items||[],i=!1;$.each(n,(function(e,t){!0===t.enabled&&!0!==t.ok&&(i=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:i?"error":"ok"}),$adm.kvTable(r,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let s=$$.dc("adm_checks",r);$.each(n,(function(e,t){let a=$$.dc("adm_check",s);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",r={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(r).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t){let a=$$.dc("adm_card",e),d=$$.dc("adm_card_hd",a);$$.s($adt.mail).appendTo(d),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:!0===t.mailerEnabled?"ok":"disabled"});let r=$$.dc("adm_card_bd",a),n=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&n.push([$adt.override,t.overrideRecipient]),$adm.kvTable(r,n);let i=$$.dc("adm_card_ft",a);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(i).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),url:$ocms.url("admin/testmail"),typedvalues:!0,success:function(e){let t=e&&e.result||{};alert(t.message||(!0===t.sent?$adt.tm_sent:$adt.tm_failed))}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file +let $adt={mdl:"Administration",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",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.",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."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},uMn:function(e){return $("#topbar").ocmsmenu([{lbl:$adt.u_pwdreset,fnc:function(){$adm.pwdResetDlg(e)}},{lbl:$adt.u_email,fnc:function(){$adm.emailDlg(e)}},{lbl:$adt.u_groups,fnc:function(){$adm.groupsDlg(e)}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let r=$$.dc("adm_grid",e);$adm.dbCard(r,t.database||{},d.database),$adm.mailCard(r,t.email||{}),$adm.blobCard(r,t.blob||{},d.blob),$adm.mfrCard(r,t.mfr||{},d.mfr),$adm.kvCard(r,t.keyVault||{},d.keyvault),$$.dc("adm_sec",e).text($adt.sub_users),$adm.usersCard(e)},usersCard:function(e){let t=$$.dc("adm_card adm_card_wide",e),a=$$.dc("adm_card_hd",t);$$.s($adt.u_title).appendTo(a);let d=$$.dc("adm_card_bd adm_users_bd",t);$adm.renderUsers(d)},renderUsers:function(e){e.empty();let t=$$.tblset({},e),a=$$.tr(t.hd);$.each([$adt.u_col_name,$adt.u_col_email,$adt.u_col_status],(function(e,t){$$.th(a).text(t)})),$ocms.postXT({url:$ocms.url("admin/users"),success:function(e){$.each(e.users||[],(function(e,a){let d=$$.tr(t.bdy,{id:"adu"+a.useraccount_id});$$.td(d).text(`${a.firstname||""} ${a.name||""}`.trim()),$$.td(d).text(a.email||""),$$.td(d).text(!0===bool(a.disabled,!1)?$adt.u_disabled:$adt.u_active),d.click((function(){d.toggleClass("selected").siblings().rC("selected"),!0===d.is(".selected")?$adm.uMn(a):$adm.topbar()}))}))},error:function(){$$.dc("adm_err",e).text($adt.loadfail)}})},pwdResetDlg:function(e){$ocms.dlgform([{name:"useraccount_id",type:"hidden",value:e.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 t=$(this).ldng(1),a=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("admin/userpwdreset"),data:a,success:function(e){alert(!0===e.emailed?$adt.u_pwdreset_sent:$adt.u_pwdreset_noemail),t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($adt.u_err[t]||$adt.u_err_generic)},complete:function(){t.ldng(0),$adm.renderUsers($(".adm_users_bd")),$adm.topbar()}})}})},emailDlg:function(e){$ocms.dlgform([{name:"useraccount_id",type:"hidden",value:e.useraccount_id},{name:"email",label:$adt.u_email_field,type:"email",required:!0,value:e.email}],{title:$adt.u_email,submit:function(e){let t=$(this).ldng(1),a=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("admin/useremail"),data:a,success:function(){t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($adt.u_err[t]||$adt.u_err_generic)},complete:function(){t.ldng(0),$adm.renderUsers($(".adm_users_bd")),$adm.topbar()}})}})},groupsDlg:function(e){let t=$$.dc("adm_groups_dlg");$ocms.postXT({url:$ocms.url("admin/usergroups"),data:{useraccount_id:e.useraccount_id},success:function(a){$.each(a.groups||[],(function(a,d){let r=$$.dc("adm_group_row",t),n="adg"+d.usergroup_id,s=$$.i({type:"checkbox",id:n}).prop("checked",bool(d.member,!1)).appendTo(r);$$.lbl(d.name,{for:n}).appendTo(r),s.change((function(){let t=s.prop("checked");$ocms.postXT({url:$ocms.url("admin/usergroupset"),data:{useraccount_id:e.useraccount_id,usergroup_id:d.usergroup_id,member:t},error:function(){s.prop("checked",!t),alert($adt.u_err_generic)}})}))}))}}),$ocms.dlg(t,{title:string($adt.u_groups_title,[`${e.firstname||""} ${e.name||""}`.trim()]),size:[420,380]})},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let r=$$.td(d).addClass("v");t[1]instanceof jQuery?r.append(t[1]):r.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,r){let n=$$.dc("adm_card",e),s=$$.dc("adm_card_hd",n);$$.s(t).appendTo(s);let i=$$.sc("adm_pill").appendTo(s),o=$$.bbtn($adt.refresh,"adm_btn").appendTo(s),c=$$.dc("adm_card_bd",n);$adm.kvTable(c,d);let l=$$.dc("adm_probe",c),u=$$.dc("adm_probe_msg",l),m=$$.dc("adm_probe_metrics",l),_=$$.dc("adm_probe_det",l),p=$$.dc("adm_probe_dur",l),f=function(e){e=e||{},$adm.setPill(i,e),u.text(e.message||""),m.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",m);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),_.text(e.detail||""),p.text(null!=e.durationMs?e.durationMs+" ms":"")};return f(r),o.click((function(){$adm.setPill(i,{status:"checking"}),u.text(""),m.empty(),_.text(""),p.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){f(e.probe)},error:function(){$adm.setPill(i,{status:"error"}),u.text($adt.st_error)}})})),n},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let r=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&r.aC("adm_env_test"),r.appendTo(d);let n=$$.dc("adm_card_bd",a);$adm.kvTable(n,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let r=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",r).text($adt.startup_notrun);let n=t.items||[],s=!1;$.each(n,(function(e,t){!0===t.enabled&&!0!==t.ok&&(s=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:s?"error":"ok"}),$adm.kvTable(r,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let i=$$.dc("adm_checks",r);$.each(n,(function(e,t){let a=$$.dc("adm_check",i);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",r={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(r).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t){let a=$$.dc("adm_card",e),d=$$.dc("adm_card_hd",a);$$.s($adt.mail).appendTo(d),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:!0===t.mailerEnabled?"ok":"disabled"});let r=$$.dc("adm_card_bd",a),n=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&n.push([$adt.override,t.overrideRecipient]),$adm.kvTable(r,n);let s=$$.dc("adm_card_ft",a);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(s).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),url:$ocms.url("admin/testmail"),typedvalues:!0,success:function(e){let t=e&&e.result||{};alert(t.message||(!0===t.sent?$adt.tm_sent:$adt.tm_failed))}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.admin.min.css b/Fuchs/wwwroot/web/fis.admin.min.css index ba7f0c9..59c8c88 100644 --- a/Fuchs/wwwroot/web/fis.admin.min.css +++ b/Fuchs/wwwroot/web/fis.admin.min.css @@ -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} \ No newline at end of file +.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} \ No newline at end of file diff --git a/Fuchs_Database/FuchsDatabase.sqlproj b/Fuchs_Database/FuchsDatabase.sqlproj index d5b509d..2c0891c 100644 --- a/Fuchs_Database/FuchsDatabase.sqlproj +++ b/Fuchs_Database/FuchsDatabase.sqlproj @@ -223,6 +223,7 @@ + diff --git a/Fuchs_Database/dbo/Functions/fis_admin_getUserGroupMembership.sql b/Fuchs_Database/dbo/Functions/fis_admin_getUserGroupMembership.sql new file mode 100644 index 0000000..084fc67 --- /dev/null +++ b/Fuchs_Database/dbo/Functions/fis_admin_getUserGroupMembership.sql @@ -0,0 +1,33 @@ + + +-- ============================================= +-- Author: +-- Create date: +-- 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 diff --git a/Fuchs_Database/dbo/Stored Procedures/fis_admin_resetUserPassword.sql b/Fuchs_Database/dbo/Stored Procedures/fis_admin_resetUserPassword.sql index 84f05b5..26d4a48 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fis_admin_resetUserPassword.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fis_admin_resetUserPassword.sql @@ -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