From 4386a6336ad3b08ce942ab1f660e8a335a16cd16 Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 14 Sep 2026 14:51:59 +0200 Subject: [PATCH] Let admins create and delete users in Benutzerverwaltung, modernize list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire up two previously-unwired stored procedures for the usr module: - fis_admin_createUserAccount gained the missing @authaccount + intranet_admin auth gate, a duplicate-email check, and now returns the (possibly auto-generated) password so it can be emailed to the new user, mirroring fis_admin_resetUserPassword's pattern. - fis_admin_delUserAccount gained the same missing auth gate plus a guard against an admin deleting their own account. New /do/usr/create and /do/usr/delete endpoints in IntranetController.AdminUsers.cs, a "Neuer Benutzer" button and a "Benutzer löschen" ribbon action in fis.usr.js, and unit tests for the new welcome-email content (BuildWelcomeEmail). Also restyle the user list (fis.usr.scss) from a plain bordered table to a card-style row grid with avatar-initial badges and status pills, matching the Admin dashboard's visual language. Documented as ADR 0020 and in the admin-user-management concept doc. Co-Authored-By: Claude Sonnet 5 --- Fuchs.Tests/AdminUserManagementTests.cs | 45 ++++++- .../IntranetController.AdminUsers.cs | 117 ++++++++++++++++++ Fuchs/Docs/Concepts/admin-user-management.md | 58 ++++++--- .../0020-user-management-create-and-delete.md | 91 ++++++++++++++ Fuchs/js/intranet/modules/fis.usr.js | 82 +++++++++--- Fuchs/js/intranet/modules/fis.usr.scss | 104 ++++++++++++---- Fuchs/js/intranet/modules/fis.usr_txt_de.js | 19 ++- Fuchs/wwwroot/web/fis.usr.css | 115 ++++++++++++++--- Fuchs/wwwroot/web/fis.usr.de.js | 101 ++++++++++++--- Fuchs/wwwroot/web/fis.usr.de.min.js | 2 +- Fuchs/wwwroot/web/fis.usr.min.css | 2 +- Fuchs/wwwroot/web/tools.js | 5 +- .../fis_admin_createUserAccount.sql | 91 ++++++++------ .../fis_admin_delUserAccount.sql | 22 ++-- 14 files changed, 716 insertions(+), 138 deletions(-) create mode 100644 Fuchs/Docs/Decisions/0020-user-management-create-and-delete.md diff --git a/Fuchs.Tests/AdminUserManagementTests.cs b/Fuchs.Tests/AdminUserManagementTests.cs index e13caa9..71f4f36 100644 --- a/Fuchs.Tests/AdminUserManagementTests.cs +++ b/Fuchs.Tests/AdminUserManagementTests.cs @@ -5,9 +5,9 @@ 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. +/// the admin-initiated password-reset and new-account welcome notification content, since the +/// SQL-bound parts of IntranetController.AdminUsers.cs (fis_admin_* proc calls) can't be +/// unit-tested per repo convention and require a running database. /// public class AdminUserManagementTests { @@ -40,4 +40,43 @@ public class AdminUserManagementTests Assert.False(string.IsNullOrWhiteSpace(subject)); Assert.Contains("somepassword", html); } + + [Fact] + public void BuildWelcomeEmail_ValidInput_ContainsNameEmailAndPassword() + { + var (subject, html) = IntranetController.BuildWelcomeEmail("Max Mustermann", "max@example.com", "Ab12Cd34"); + + Assert.Contains("Zugang", subject); + Assert.Contains("Max Mustermann", html); + Assert.Contains("max@example.com", html); + Assert.Contains("Ab12Cd34", html); + } + + [Theory] + [InlineData("", "<script>alert(1)</script>")] + [InlineData("O'Brien&Co", "O'Brien&Co")] + public void BuildWelcomeEmail_PasswordWithHtmlSpecialChars_IsHtmlEncoded(string rawPassword, string encoded) + { + var (_, html) = IntranetController.BuildWelcomeEmail("Test User", "test@example.com", rawPassword); + + Assert.Contains(encoded, html); + Assert.DoesNotContain(rawPassword, html); + } + + [Fact] + public void BuildWelcomeEmail_EmailWithHtmlSpecialChars_IsHtmlEncoded() + { + var (_, html) = IntranetController.BuildWelcomeEmail("Test User", "a&b@example.com", "somepassword"); + + Assert.Contains("a&b@example.com", html); + } + + [Fact] + public void BuildWelcomeEmail_EmptyName_StillProducesValidContent() + { + var (subject, html) = IntranetController.BuildWelcomeEmail("", "test@example.com", "somepassword"); + + Assert.False(string.IsNullOrWhiteSpace(subject)); + Assert.Contains("somepassword", html); + } } diff --git a/Fuchs/Controllers/IntranetController.AdminUsers.cs b/Fuchs/Controllers/IntranetController.AdminUsers.cs index 1cf31fe..ad0a0ea 100644 --- a/Fuchs/Controllers/IntranetController.AdminUsers.cs +++ b/Fuchs/Controllers/IntranetController.AdminUsers.cs @@ -175,6 +175,113 @@ public partial class IntranetController return AdminJson(new { ok = true }); } + case "create": + { + if (!HasForm("name", "firstname", "email")) return BadRequest400(); + string name = Form("name"); + string firstname = Form("firstname"); + string email = Form("email"); + + var pl = StdParamlist( + SQL_VarChar("@authaccount", UserAccountID), + SQL_NVarChar("@name", name), + SQL_NVarChar("@firstname", firstname), + SQL_VarChar("@title", Form("title"), dbNull_IfEmpty: true), + SQL_VarChar("@gender", Form("gender"), dbNull_IfEmpty: true), + SQL_VarChar("@email", email), + SQL_VarChar("@mobile", Form("mobile"), dbNull_IfEmpty: true), + SQL_NVarChar("@password", null, dbNull_IfEmpty: true)); + + System.Data.DataRow? createdRow; + try + { + var res = await getSQLDatatable_async( + "EXECUTE [dbo].[fis_admin_createUserAccount] @authaccount, @name, @firstname, " + + "@title, @gender, @email, @mobile, @password, @enc_key;", + _intranet.Intranet__SQLConnectionString, pl, + Security: DbSec, options: SqlOpt(fn, id, code)); + createdRow = res.Count > 0 ? res.FirstRow : null; + } + catch (SqlException ex) when (ex.Number == 60001) + { + _logger.LogWarning("AdminUsers create: duplicate email rejected user={User}", UserAccountID); + return StatusCode(409, new { error = "duplicate_email" }); + } + catch (Exception ex) + { + await NotifyAdminUserActionFailedAsync("Benutzer anlegen fehlgeschlagen", + "Ein neues Benutzerkonto konnte nicht angelegt werden.", "", ex); + return StatusCode(500, new { error = "create_failed" }); + } + + if (createdRow == null) + { + _logger.LogWarning("AdminUsers create not successful user={User}", UserAccountID); + return StatusCode(500, new { error = "create_failed" }); + } + + string newId = createdRow.nz("useraccount_id"); + string newPassword = createdRow.nz("newpassword"); + string targetName = $"{firstname} {name}".Trim(); + + var (subject, html) = BuildWelcomeEmail(targetName, email, newPassword); + bool emailed = email.Length > 5 && + await _comService.SendEmailAsync("usrwelcome_" + email, subject, html, email, targetName, null); + + if (!emailed) + { + _logger.LogWarning("AdminUsers create: welcome mail not delivered target={Target} user={User}", + newId, UserAccountID); + await _events.UserIssueAsync("Neuer Benutzer: E-Mail-Versand fehlgeschlagen", + $"Die Zugangsdaten für das neue Benutzerkonto {newId} konnten nicht per E-Mail zugestellt werden.", + UserAccountID, new Dictionary { ["target"] = newId, ["email"] = email }); + } + + _logger.LogInformation("AdminUsers create succeeded target={Target} emailed={Emailed} user={User}", + newId, emailed, UserAccountID); + return AdminJson(new { ok = true, useraccount_id = newId, emailed }); + } + + case "delete": + { + if (!HasForm("useraccount_id")) return BadRequest400(); + string targetId = Form("useraccount_id"); + + if (targetId.Equals(UserAccountID, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning("AdminUsers delete: self-delete rejected user={User}", UserAccountID); + return StatusCode(409, new { error = "cannot_delete_self" }); + } + + bool success; + try + { + var pl = StdParamlist( + SQL_VarChar("@authaccount", UserAccountID), + SQL_VarChar("@useraccount_id", targetId)); + var res = await getSQLDatatable_async( + "EXECUTE [dbo].[fis_admin_delUserAccount] @authaccount, @useraccount_id;", + _intranet.Intranet__SQLConnectionString, pl, + Security: DbSec, options: SqlOpt(fn, id, code)); + success = res.Count > 0 && res.FirstRow[0] is true; + } + catch (Exception ex) + { + await NotifyAdminUserActionFailedAsync("Benutzer löschen fehlgeschlagen", + $"Das Benutzerkonto {targetId} konnte nicht gelöscht werden.", targetId, ex); + return StatusCode(500, new { error = "delete_failed" }); + } + + if (!success) + { + _logger.LogWarning("AdminUsers delete not successful target={Target} user={User}", targetId, UserAccountID); + return StatusCode(500, new { error = "delete_failed" }); + } + + _logger.LogInformation("AdminUsers delete succeeded target={Target} user={User}", targetId, UserAccountID); + return AdminJson(new { ok = true }); + } + case "groups": { if (!HasForm("useraccount_id")) return BadRequest400(); @@ -238,6 +345,16 @@ public partial class IntranetController $"

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

"); + /// + /// Builds the subject/HTML body for the "a new intranet account was created for you" + /// notification. Extracted as pure logic (no DB/HTTP), mirroring . + /// + internal static (string Subject, string Html) BuildWelcomeEmail(string targetName, string email, string newPassword) => ( + "sanitaerfuchs.de Intranet Zugang eingerichtet", + $"

Guten Tag {targetName},
für Sie wurde ein Zugang zum sanitaerfuchs.de Intranet eingerichtet.
" + + $"Ihre E-Mail-Adresse: {HttpUtility.HtmlEncode(email)}
" + + $"Ihr 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 diff --git a/Fuchs/Docs/Concepts/admin-user-management.md b/Fuchs/Docs/Concepts/admin-user-management.md index c35d2ed..29c668e 100644 --- a/Fuchs/Docs/Concepts/admin-user-management.md +++ b/Fuchs/Docs/Concepts/admin-user-management.md @@ -1,6 +1,6 @@ --- status: Active -lastUpdated: 2026-09-09 +lastUpdated: 2026-09-14 applyTo: - "Fuchs/Controllers/IntranetController.AdminUsers.cs" - "Fuchs/js/intranet/modules/fis.usr*.js" @@ -11,20 +11,22 @@ applyTo: relatedDecisions: - "0014-admin-user-management.md" - "0015-split-admin-user-management-module.md" + - "0020-user-management-create-and-delete.md" --- # User Management module (`usr`) ## Summary The **Benutzerverwaltung** module lets a privileged operator see every non-deleted intranet -user account (name + email + active/disabled), reset a password, change an email address, and -view/change group assignments. It shares its sidemenu-visibility and server-side authorization -gate with the separate Admin/status-dashboard module (`fds_sys > 4`) but is otherwise fully -independent — own `fn=usr`, own frontend bundle, own controller partial. See -[0015](../Decisions/0015-split-admin-user-management-module.md) for why these were split from -one module into two, and [0014](../Decisions/0014-admin-user-management.md) for the original -decisions this one carries forward (password always emailed, dual auth axis, reuse of -`fis_admin_setUserAccount` for email changes). +user account (name + email + active/disabled), create a new account, delete an existing one, +reset a password, change an email address, and view/change group assignments. It shares its +sidemenu-visibility and server-side authorization gate with the separate Admin/status-dashboard +module (`fds_sys > 4`) but is otherwise fully independent — own `fn=usr`, own frontend bundle, +own controller partial. See [0015](../Decisions/0015-split-admin-user-management-module.md) for +why these were split from one module into two, [0014](../Decisions/0014-admin-user-management.md) +for the original decisions this one carries forward (password always emailed, dual auth axis, +reuse of `fis_admin_setUserAccount` for email changes), and +[0020](../Decisions/0020-user-management-create-and-delete.md) for create/delete. ## How it works @@ -50,11 +52,14 @@ click "Benutzerverwaltung" → $ocms.init('usr') load /web/fis.usr.de.js + /web/fis.usr.css $ocms.usr.init2() → init3() → renderLst() POST /do/usr/list → { users: [...] } grid of every non-deleted account - row click → per-row ribbon (iMn): Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit + base ribbon (eM) → "Neuer Benutzer" (always available, not just when a row is selected) + POST /do/usr/create {name, firstname, email, title?, gender?, mobile?} → { ok, useraccount_id, emailed } | 409 duplicate_email + row click → per-row ribbon (iMn): Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit / Benutzer löschen POST /do/usr/pwdreset {useraccount_id, newpassword?} → { ok, emailed } POST /do/usr/email {useraccount_id, email} → { ok } | 409 duplicate_email POST /do/usr/groups {useraccount_id} → { groups: [...] } POST /do/usr/groupset {useraccount_id, usergroup_id, member} → { ok, member } + POST /do/usr/delete {useraccount_id} → { ok } | 409 cannot_delete_self ``` ### Backend (`IntranetController.AdminUsers.cs`) @@ -80,26 +85,43 @@ click "Benutzerverwaltung" → $ocms.init('usr') returns every group plus a `member` bit for that one user. - `groupset` → calls `fis_admin_addGroupMember`/`remGroupMember` depending on the posted `member` flag — applied immediately per checkbox toggle, no separate save step. +- `create` → `fis_admin_createUserAccount(@authaccount, @name, @firstname, @title, @gender, + @email, @mobile, @password, @enc_key)`. Only name/firstname/email are required from the + dialog; the admin may leave password blank (the proc auto-generates one and returns it as + `newpassword`, mirroring `fis_admin_resetUserPassword`'s pattern). The controller then emails + the new account's credentials via `BuildWelcomeEmail` — same "emailed, never shown on screen" + rule as `pwdreset` — and returns `emailed` so the UI can tell the admin whether delivery + succeeded. Duplicate email → 409 `duplicate_email`, same mapping as `email`. +- `delete` → `fis_admin_delUserAccount(@authaccount, @useraccount_id)` (soft-delete: sets + `UserDeleted`/`DateDeleted`). The controller rejects a self-delete with 409 + `cannot_delete_self` before even calling SQL; the proc enforces the same guard independently. + See [0020](../Decisions/0020-user-management-create-and-delete.md) for why both procs needed + the `intranet_admin` auth gate added when they were wired up. ### Frontend (`fis.usr.js` + `fis.usr_txt_de.js` + `fis.usr.scss`) A self-contained list+ribbon module modeled on Banking (`fis.bam.js`)'s `eM`/`iMn` pattern -rather than the Admin dashboard's card layout: `renderLst` builds the table via -`$fis.frm_list`/`$$.tblset`; a row click toggles `.selected` and swaps the `#topbar` ribbon -between the base "Neu Laden" button (`eM`) and the three per-row actions (`iMn`). Each action is -a dialog (`pwdResetDlg`/`emailDlg`/`groupsDlg`) using `$ocms.dlgform`/`$ocms.dlg` with a manual +rather than the Admin dashboard's card layout: `renderLst` builds a card-style row grid (avatar +initials badge, name/email columns, a status pill) rather than a plain ``; a row click +toggles `.selected` and swaps the `#topbar` ribbon between the base ribbon (`eM`: "Neuer +Benutzer" always available, plus "Neu Laden" once a row was selected) and the four per-row +actions (`iMn`: Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit / Benutzer löschen). +`pwdResetDlg`/`emailDlg`/`groupsDlg`/`createDlg` use `$ocms.dlgform`/`$ocms.dlg` with a manual `submit`/`postXT` (not the built-in `success`-URL wiring) so a 409's `error` code can be mapped -to a specific German message via `$uct.err`. +to a specific German message via `$uct.err`; `deleteUser` instead follows Banking's +`confirm(...)`-before-destructive-action pattern (`fis.bam.js`'s `smd`/`mbd`) since there's +nothing to collect beyond the confirmation itself. ## Key files -- `Fuchs/Controllers/IntranetController.AdminUsers.cs` — `Do_Process_AdminUsers` dispatch + `BuildPasswordResetEmail` + `NotifyAdminUserActionFailedAsync`. +- `Fuchs/Controllers/IntranetController.AdminUsers.cs` — `Do_Process_AdminUsers` dispatch + `BuildPasswordResetEmail`/`BuildWelcomeEmail` + `NotifyAdminUserActionFailedAsync`. - `Fuchs/Controllers/IntranetController.Admin.cs` — shared `AdminMinAuthExclusive`/`GetSystemAdminAuthAsync`/`AdminJson` this module reuses. - `Fuchs/js/intranet/modules/fis.usr.js`, `fis.usr_txt_de.js`, `fis.usr.scss` — the module, texts, styles (bundled via `bdlconfig.json`, `intranet:usr` context). - `Fuchs/js/intranet/fis_main_menu.js` (`addAdminMenuButtonsIfAuthorized`) + `fis_main_go.js` — conditional menu buttons (this module and `admin`). - `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/AdminUserManagementTests.cs` — password-reset email content tests. +- `Fuchs_Database/dbo/Stored Procedures/fis_admin_resetUserPassword.sql`, `fis_admin_setUserAccount.sql`, `fis_admin_addGroupMember.sql`, `fis_admin_remGroupMember.sql`, `fis_admin_createUserAccount.sql`, `fis_admin_delUserAccount.sql` — write-side user/group procs. +- `Fuchs.Tests/AdminUserManagementTests.cs` — password-reset and welcome-email content tests. ## Related decisions - [0014 — Admin user management (original decisions, still in force)](../Decisions/0014-admin-user-management.md) +- [0020 — Wire up create/delete, welcome email on create, guard delete](../Decisions/0020-user-management-create-and-delete.md) - [0015 — Split Admin into separate Status/User-Management modules](../Decisions/0015-split-admin-user-management-module.md) - [0003 — Interrupting exceptions notify the user](../Decisions/0003-interrupting-exceptions-notify-the-user.md) diff --git a/Fuchs/Docs/Decisions/0020-user-management-create-and-delete.md b/Fuchs/Docs/Decisions/0020-user-management-create-and-delete.md new file mode 100644 index 0000000..9245557 --- /dev/null +++ b/Fuchs/Docs/Decisions/0020-user-management-create-and-delete.md @@ -0,0 +1,91 @@ +--- +status: Accepted +date: 2026-09-14 +applyTo: + - "Fuchs/Controllers/IntranetController.AdminUsers.cs" + - "Fuchs/js/intranet/modules/fis.usr*.js" + - "Fuchs/js/intranet/modules/fis.usr.scss" + - "Fuchs_Database/dbo/Stored Procedures/fis_admin_createUserAccount.sql" + - "Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserAccount.sql" +supersededBy: "" +--- + +# 0020 — User management: wire up create/delete, welcome email on create, guard delete + +## Context +The `usr` module (see [0015](0015-split-admin-user-management-module.md)) let an admin list +users, reset a password, change an email address, and manage group membership, but had no way +to add a new account or remove one — the admin had to do that directly in the database. Two +stored procedures for this already existed in `Fuchs_Database` but were completely unwired from +any controller: `fis_admin_createUserAccount` and `fis_admin_delUserAccount`. Neither followed +the module's established conventions: +- `fis_admin_createUserAccount` took no `@authaccount` and performed **no** `intranet_admin` + authorization check at all (every other write in this module checks + `fis_admin_getUserAuth(@authaccount) > 1`, per [0014](0014-admin-user-management.md)'s dual + auth axis), had no duplicate-email guard (`fis_admin_setUserAccount` does), and generated the + initial password inline in the `INSERT` without ever returning it — so nothing calling it + could email the new user their credentials. +- `fis_admin_delUserAccount` likewise had no `intranet_admin` check, and nothing stopped an + admin from deleting their own account. + +## Decision +- **`create` and `delete` are new `id` cases in `Do_Process_AdminUsers`**, behind the same + `fds_sys > 4` gate as every other action in the module. +- **`fis_admin_createUserAccount` was brought in line with its siblings** rather than reused via + `fis_admin_setUserAccount`'s existing "insert when `@useraccount_id` is NULL" branch (which + would have been the smaller diff): it gained `@authaccount` + the standard + `fis_admin_getUserAuth(@authaccount) > 1` gate, the same duplicate-email check + `fis_admin_setUserAccount` already has, and — mirroring `fis_admin_resetUserPassword`'s + existing pattern — resolves `@password` (auto-generating one via `ocms_fn_generatePassword` + when the admin leaves it blank) **before** encrypting it, and returns it in the result set as + `newpassword` alongside the created row. `fis_admin_setUserAccount`'s insert branch was left + alone (still reachable in principle, but has no caller) since changing its shared behavior + risked the existing `email` action; a dedicated create proc with an explicit contract was + judged clearer than overloading an update proc's edge case. +- **New account creation always emails the credentials, never displays them** — identical + handling to password reset ([0014](0014-admin-user-management.md)): `HandleLogin`-style + `IComService.SendEmailAsync` (respecting `Fuchs:Email:OverrideRecipient`) sends name, email, + and password via `IntranetController.BuildWelcomeEmail`; on delivery failure the admin gets an + `IEventService.UserIssueAsync` notification per + [0003](0003-interrupting-exceptions-notify-the-user.md), matching `pwdreset`'s existing + `emailed` flag semantics. +- **`fis_admin_delUserAccount` gained the same `intranet_admin` gate** (previously entirely + missing — a real authorization hole once the proc is reachable from a controller) **and a + self-delete guard** (`@authaccount <> @useraccount_id`), enforced at both the SQL layer (belt) + and the controller (suspenders — returns 409 `cannot_delete_self` without a round-trip when the + caller targets their own account). +- **Frontend**: `fis.usr.js`'s base ribbon gained an always-visible "Neuer Benutzer" button + (`createDlg`, a `$ocms.dlgform` collecting Vorname/Nachname/E-Mail required, Titel/Anrede/Mobil + optional); the per-row ribbon gained "Benutzer löschen" (`deleteUser`), using the same + `confirm(...)` guard pattern Banking's `fis.bam.js` uses before destructive actions + (`smd`/`mbd`) rather than a modal dialog, since delete has no fields to collect. + +## Consequences +- `fis_admin_createUserAccount`'s signature changed (`@authaccount` added as the first + parameter) and its result set gained a `newpassword` column; safe in-place change since it had + no existing caller. +- `fis_admin_delUserAccount` now silently no-ops (same "no-op instead of error" style as every + other `fis_admin_*` proc in this module) for a caller without `intranet_admin >= 2` **or** who + targets their own account — the controller's own `cannot_delete_self` check makes the + self-delete case surface as a clear error to the admin instead of a silent no-op, but a caller + lacking `intranet_admin` still only sees an unexplained failure, consistent with the accepted + gap from 0014. +- Delete is a soft-delete (`fis_admin_delUserAccount` already set `DateDeleted`/`UserDeleted` + before this change) — deleted accounts simply stop appearing in `fis_admin_getUserAccountList` + and can only be restored directly in the database; no "undo" is exposed in the UI. +- The list view (`fis.usr.scss`) was restyled from a plain bordered `
` to a card-style row + grid (avatar-initial badge, status pill, hover/selection highlighting) matching the visual + language `fis.admin.scss` already established (`adm_pill`, `adm_card`) — a pure presentation + change, the DOM is still built by the same `renderLst`/`$ocms.postXT('usr/list')` flow. + +## Alternatives considered +- **Reuse `fis_admin_setUserAccount` for create** by calling it with `@useraccount_id = NULL`. + Rejected — its insert branch doesn't return the generated password (nothing to email to the + new user), and giving one endpoint two very different response shapes depending on a hidden + parameter was judged more surprising than a small, explicit `create` proc. +- **Hard-delete instead of soft-delete.** Not considered — `fis_admin_delUserAccount` already + implemented soft-delete before this change and every other user-facing list in the app + (`fis_admin_getUserAccountList` et al.) already filters on `DateDeleted is null`; changing to a + hard delete would be an unrelated, larger change. +- **Let an admin delete their own account and rely on another admin to recover.** Rejected — + trivially avoidable lockout risk with no upside. diff --git a/Fuchs/js/intranet/modules/fis.usr.js b/Fuchs/js/intranet/modules/fis.usr.js index 5c4476c..fc736d1 100644 --- a/Fuchs/js/intranet/modules/fis.usr.js +++ b/Fuchs/js/intranet/modules/fis.usr.js @@ -12,7 +12,7 @@ let $usr = { }, /* Default ribbon (nothing selected). */ eM: function (r) { - let m = []; + let m = [{ lbl: $uct.newuser_btn, fnc: $usr.createDlg }]; if (bool(r, false) === true) { m.push({ lbl: $uct.rel, fnc: $usr.renderLst }); } return $('#topbar').ocmsmenu(m); }, @@ -21,28 +21,43 @@ let $usr = { return $('#topbar').ocmsmenu([ { lbl: $uct.pwdreset, fnc: function () { $usr.pwdResetDlg(rw); } }, { lbl: $uct.email, fnc: function () { $usr.emailDlg(rw); } }, - { lbl: $uct.groups, fnc: function () { $usr.groupsDlg(rw); } } + { lbl: $uct.groups, fnc: function () { $usr.groupsDlg(rw); } }, + { lbl: $uct.delete, fnc: function () { $usr.deleteUser(rw); } } ]); }, + /* initials for the avatar badge, e.g. "Nadine Beckmann" -> "NB" */ + initials: function (firstname, name) { + let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0); + return (f + n).toUpperCase() || '?'; + }, renderLst: function () { let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst); $usr.eM(true); $ocms.postXT({ url: $ocms.url('usr/list'), success: function (r) { usrfrm.empty(); - let ts = $$.tblset({}, usrfrm); - let thr = $$.tr(ts.hd); - $.each([$uct.col_name, $uct.col_email, $uct.col_status], function (i, lbl) { - $$.th(thr).text(lbl); - }); + let grid = $$.dc('usr_grid', usrfrm); + let hdr = $$.dc('usr_row usr_row_hd', grid); + $$.dc('usr_col_avatar', hdr); + $$.dc('usr_col_name', hdr).text($uct.col_name); + $$.dc('usr_col_email', hdr).text($uct.col_email); + $$.dc('usr_col_status', hdr).text($uct.col_status); + if ((r.users || []).length === 0) { + $$.dc('usr_empty', grid).text($uct.empty); + return; + } $.each(r.users || [], function (ri, rw) { - let tr = $$.tr(ts.bdy, { id: 'usr' + rw.useraccount_id }); - $$.td(tr).text(`${rw.firstname || ''} ${rw.name || ''}`.trim()); - $$.td(tr).text(rw.email || ''); - $$.td(tr).text(bool(rw.disabled, false) === true ? $uct.disabled : $uct.active); - tr.click(function () { - tr.toggleClass('selected').siblings().rC('selected'); - if (tr.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); } + let disabled = bool(rw.disabled, false) === true; + let row = $$.dc('usr_row', grid, { id: 'usr' + rw.useraccount_id }); + $$.dc('usr_avatar', row).text($usr.initials(rw.firstname, rw.name)); + $$.dc('usr_col_name', row).text(`${rw.firstname || ''} ${rw.name || ''}`.trim()); + $$.dc('usr_col_email', row).text(rw.email || ''); + $$.dc('usr_col_status', row).append( + $$.dc(disabled ? 'usr_pill usr_pill_off' : 'usr_pill usr_pill_on').text(disabled ? $uct.disabled : $uct.active) + ); + row.click(function () { + row.toggleClass('selected').siblings().rC('selected'); + if (row.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); } }); }); }, error: function () { @@ -51,6 +66,45 @@ let $usr = { }, complete: function () { usrlst.ldng(0); } }); }, + createDlg: function () { + $ocms.dlgform([ + { name: 'firstname', label: $uct.firstname_field, type: 'string', required: true }, + { name: 'name', label: $uct.name_field, type: 'string', required: true }, + { name: 'email', label: $uct.email_field, type: 'email', required: true }, + { name: 'title', label: $uct.title_field, type: 'string' }, + { name: 'gender', label: $uct.gender_field, type: 'select', url: [['', ''], ['m', $uct.gender_m], ['f', $uct.gender_f]] }, + { name: 'mobile', label: $uct.mobile_field, type: 'string' } + ], { + title: $uct.newuser, + button: $uct.newuser_btn, + submit: function (e) { + let c = $(this).ldng(1); + let qs = c.serializeObject(true, { typedvalues: true }); + $ocms.postXT({ + url: $ocms.url('usr/create'), data: qs, success: function (response) { + alert(response.emailed === true ? $uct.create_sent : $uct.create_noemail); + c.trigger('modal_close'); + }, error: function (jqXHR) { + let code = ((jqXHR || {}).responseJSON || {}).error; + alert($uct.err[code] || $uct.err_generic); + }, complete: function () { c.ldng(0); $usr.renderLst(); } + }); + } + }); + }, + deleteUser: function (rw) { + let displayName = `${rw.firstname || ''} ${rw.name || ''}`.trim(); + if (confirm(string($uct.delete_confirm, [displayName]))) { + $ocms.postXT({ + url: $ocms.url('usr/delete'), data: { useraccount_id: rw.useraccount_id }, success: function () { + $usr.eM(true); + }, error: function (jqXHR) { + let code = ((jqXHR || {}).responseJSON || {}).error; + alert($uct.err[code] || $uct.err_generic); + }, complete: function () { $usr.renderLst(); } + }); + } + }, pwdResetDlg: function (rw) { $ocms.dlgform([ { name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id }, diff --git a/Fuchs/js/intranet/modules/fis.usr.scss b/Fuchs/js/intranet/modules/fis.usr.scss index 23991b9..c2525b3 100644 --- a/Fuchs/js/intranet/modules/fis.usr.scss +++ b/Fuchs/js/intranet/modules/fis.usr.scss @@ -1,33 +1,78 @@ .usrfrm { - > table { - border-collapse: collapse; - margin: 2rem; + padding: 1.5rem 2rem; - th { - padding: 0.2rem 0.35rem; - border: 1px solid #CCC; + .usr_grid { + border: 1px solid #e2e2e2; + border-radius: 0.5rem; + background: #fff; + box-shadow: 0 1px 3px rgba(50, 50, 50, 0.1); + overflow: hidden; + } + + .usr_row { + display: grid; + grid-template-columns: 2.75rem 1.6fr 1.8fr 8rem; + align-items: center; + gap: 0.75rem; + padding: 0.55rem 1rem; + border-bottom: 1px solid #eee; + + &:last-child { border-bottom: none; } + } + + .usr_row_hd { + background: $fuchs_blau; + color: #fff; + font-weight: bold; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.02em; + padding: 0.5rem 1rem; + } + + .usr_row:not(.usr_row_hd) { + cursor: pointer; + transition: background-color 0.12s ease-in-out; + + &:hover { background-color: #f3f6fb; } + + &.selected { + background-color: rgba($fuchs_akzent, 0.12); + box-shadow: inset 3px 0 0 $fuchs_akzent; } + } - td { - padding: 0.2rem 0.35rem; - border: 1px solid #CCC; - } + .usr_avatar { + width: 2.1rem; + height: 2.1rem; + border-radius: 50%; + background: $fuchs_blau; + color: #fff; + font-size: 0.78rem; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + } - tr { - cursor: pointer; + .usr_col_name { font-weight: 600; color: #222; } + .usr_col_email { color: #555; overflow-wrap: anywhere; } - &:nth-child(2n+1) td { - background-color: #EEE; - } + .usr_pill { + display: inline-block; + padding: 0.1rem 0.6rem; + border-radius: 1rem; + font-size: 0.78rem; + font-weight: bold; + white-space: nowrap; + } + .usr_pill_on { background: #e2f3da; color: #2f6d18; } + .usr_pill_off { background: #ececec; color: #777; } - &:hover td { - background-color: #f3f6fb; - } - - &.selected td { - background-color: lightblue; - } - } + .usr_empty { + padding: 1.5rem 1rem; + text-align: center; + color: #777; } .usr_err { @@ -37,6 +82,19 @@ } } +@media (max-width: 640px) { + .usrfrm { + padding: 0.75rem; + + .usr_row { grid-template-columns: 2.25rem 1fr; grid-template-areas: "avatar name" "avatar email" "avatar status"; } + .usr_row_hd { display: none; } + .usr_avatar { grid-area: avatar; } + .usr_col_name { grid-area: name; } + .usr_col_email { grid-area: email; font-size: 0.85rem; } + .usr_col_status { grid-area: status; } + } +} + .usr_groups_dlg { padding: 0.25rem 0; diff --git a/Fuchs/js/intranet/modules/fis.usr_txt_de.js b/Fuchs/js/intranet/modules/fis.usr_txt_de.js index 5185532..fb0e96a 100644 --- a/Fuchs/js/intranet/modules/fis.usr_txt_de.js +++ b/Fuchs/js/intranet/modules/fis.usr_txt_de.js @@ -7,6 +7,20 @@ let $uct = { , active: 'Aktiv' , disabled: 'Deaktiviert' , loadfail: 'Die Benutzerliste konnte nicht geladen werden.' + , empty: 'Es sind keine Benutzerkonten vorhanden.' + , newuser: 'Neuer Benutzer' + , newuser_btn: 'Neuer Benutzer' + , firstname_field: 'Vorname' + , name_field: 'Nachname' + , title_field: 'Titel' + , gender_field: 'Anrede' + , gender_m: 'Herr' + , gender_f: 'Frau' + , mobile_field: 'Mobil' + , create_sent: 'Das Benutzerkonto wurde angelegt, die Zugangsdaten wurden per E-Mail versendet.' + , create_noemail: 'Das Benutzerkonto wurde angelegt, die Zugangsdaten konnten aber nicht per E-Mail zugestellt werden.' + , delete: 'Benutzer löschen' + , delete_confirm: 'Soll der Benutzer "{0}" wirklich gelöscht werden?' , pwdreset: 'Passwort zurücksetzen' , pwdreset_btn: 'Zurücksetzen' , pwd_field: 'Neues Passwort' @@ -21,7 +35,10 @@ let $uct = { 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.' + change_failed: 'Die Änderung konnte nicht gespeichert werden.', + create_failed: 'Das Benutzerkonto konnte nicht angelegt werden.', + delete_failed: 'Der Benutzer konnte nicht gelöscht werden.', + cannot_delete_self: 'Das eigene Benutzerkonto kann nicht gelöscht werden.' } , err_generic: 'Die Änderung konnte nicht gespeichert werden.' }; diff --git a/Fuchs/wwwroot/web/fis.usr.css b/Fuchs/wwwroot/web/fis.usr.css index e7c4966..ba81c5f 100644 --- a/Fuchs/wwwroot/web/fis.usr.css +++ b/Fuchs/wwwroot/web/fis.usr.css @@ -16,29 +16,87 @@ /* #1b4379 #0033b3 */ /*neu: #56a532 ; alt: rgb(32,144,119);*/ /* 10% sw */ -.usrfrm > table { - border-collapse: collapse; - margin: 2rem; +.usrfrm { + padding: 1.5rem 2rem; } -.usrfrm > table th { - padding: 0.2rem 0.35rem; - border: 1px solid #CCC; +.usrfrm .usr_grid { + border: 1px solid #e2e2e2; + border-radius: 0.5rem; + background: #fff; + box-shadow: 0 1px 3px rgba(50, 50, 50, 0.1); + overflow: hidden; } -.usrfrm > table td { - padding: 0.2rem 0.35rem; - border: 1px solid #CCC; +.usrfrm .usr_row { + display: grid; + grid-template-columns: 2.75rem 1.6fr 1.8fr 8rem; + align-items: center; + gap: 0.75rem; + padding: 0.55rem 1rem; + border-bottom: 1px solid #eee; } -.usrfrm > table tr { +.usrfrm .usr_row:last-child { + border-bottom: none; +} +.usrfrm .usr_row_hd { + background: rgb(27, 67, 121); + color: #fff; + font-weight: bold; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.02em; + padding: 0.5rem 1rem; +} +.usrfrm .usr_row:not(.usr_row_hd) { cursor: pointer; + transition: background-color 0.12s ease-in-out; } -.usrfrm > table tr:nth-child(2n+1) td { - background-color: #EEE; -} -.usrfrm > table tr:hover td { +.usrfrm .usr_row:not(.usr_row_hd):hover { background-color: #f3f6fb; } -.usrfrm > table tr.selected td { - background-color: lightblue; +.usrfrm .usr_row:not(.usr_row_hd).selected { + background-color: rgba(86, 165, 50, 0.12); + box-shadow: inset 3px 0 0 rgb(86, 165, 50); +} +.usrfrm .usr_avatar { + width: 2.1rem; + height: 2.1rem; + border-radius: 50%; + background: rgb(27, 67, 121); + color: #fff; + font-size: 0.78rem; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} +.usrfrm .usr_col_name { + font-weight: 600; + color: #222; +} +.usrfrm .usr_col_email { + color: #555; + overflow-wrap: anywhere; +} +.usrfrm .usr_pill { + display: inline-block; + padding: 0.1rem 0.6rem; + border-radius: 1rem; + font-size: 0.78rem; + font-weight: bold; + white-space: nowrap; +} +.usrfrm .usr_pill_on { + background: #e2f3da; + color: #2f6d18; +} +.usrfrm .usr_pill_off { + background: #ececec; + color: #777; +} +.usrfrm .usr_empty { + padding: 1.5rem 1rem; + text-align: center; + color: #777; } .usrfrm .usr_err { padding: 1rem 2rem; @@ -46,6 +104,31 @@ font-weight: bold; } +@media (max-width: 640px) { + .usrfrm { + padding: 0.75rem; + } + .usrfrm .usr_row { + grid-template-columns: 2.25rem 1fr; + grid-template-areas: "avatar name" "avatar email" "avatar status"; + } + .usrfrm .usr_row_hd { + display: none; + } + .usrfrm .usr_avatar { + grid-area: avatar; + } + .usrfrm .usr_col_name { + grid-area: name; + } + .usrfrm .usr_col_email { + grid-area: email; + font-size: 0.85rem; + } + .usrfrm .usr_col_status { + grid-area: status; + } +} .usr_groups_dlg { padding: 0.25rem 0; } diff --git a/Fuchs/wwwroot/web/fis.usr.de.js b/Fuchs/wwwroot/web/fis.usr.de.js index f127415..58ee979 100644 --- a/Fuchs/wwwroot/web/fis.usr.de.js +++ b/Fuchs/wwwroot/web/fis.usr.de.js @@ -7,6 +7,20 @@ let $uct = { , active: 'Aktiv' , disabled: 'Deaktiviert' , loadfail: 'Die Benutzerliste konnte nicht geladen werden.' + , empty: 'Es sind keine Benutzerkonten vorhanden.' + , newuser: 'Neuer Benutzer' + , newuser_btn: 'Neuer Benutzer' + , firstname_field: 'Vorname' + , name_field: 'Nachname' + , title_field: 'Titel' + , gender_field: 'Anrede' + , gender_m: 'Herr' + , gender_f: 'Frau' + , mobile_field: 'Mobil' + , create_sent: 'Das Benutzerkonto wurde angelegt, die Zugangsdaten wurden per E-Mail versendet.' + , create_noemail: 'Das Benutzerkonto wurde angelegt, die Zugangsdaten konnten aber nicht per E-Mail zugestellt werden.' + , delete: 'Benutzer löschen' + , delete_confirm: 'Soll der Benutzer "{0}" wirklich gelöscht werden?' , pwdreset: 'Passwort zurücksetzen' , pwdreset_btn: 'Zurücksetzen' , pwd_field: 'Neues Passwort' @@ -21,7 +35,10 @@ let $uct = { 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.' + change_failed: 'Die Änderung konnte nicht gespeichert werden.', + create_failed: 'Das Benutzerkonto konnte nicht angelegt werden.', + delete_failed: 'Der Benutzer konnte nicht gelöscht werden.', + cannot_delete_self: 'Das eigene Benutzerkonto kann nicht gelöscht werden.' } , err_generic: 'Die Änderung konnte nicht gespeichert werden.' }; @@ -40,7 +57,7 @@ let $usr = { }, /* Default ribbon (nothing selected). */ eM: function (r) { - let m = []; + let m = [{ lbl: $uct.newuser_btn, fnc: $usr.createDlg }]; if (bool(r, false) === true) { m.push({ lbl: $uct.rel, fnc: $usr.renderLst }); } return $('#topbar').ocmsmenu(m); }, @@ -49,28 +66,43 @@ let $usr = { return $('#topbar').ocmsmenu([ { lbl: $uct.pwdreset, fnc: function () { $usr.pwdResetDlg(rw); } }, { lbl: $uct.email, fnc: function () { $usr.emailDlg(rw); } }, - { lbl: $uct.groups, fnc: function () { $usr.groupsDlg(rw); } } + { lbl: $uct.groups, fnc: function () { $usr.groupsDlg(rw); } }, + { lbl: $uct.delete, fnc: function () { $usr.deleteUser(rw); } } ]); }, + /* initials for the avatar badge, e.g. "Nadine Beckmann" -> "NB" */ + initials: function (firstname, name) { + let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0); + return (f + n).toUpperCase() || '?'; + }, renderLst: function () { let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst); $usr.eM(true); $ocms.postXT({ url: $ocms.url('usr/list'), success: function (r) { usrfrm.empty(); - let ts = $$.tblset({}, usrfrm); - let thr = $$.tr(ts.hd); - $.each([$uct.col_name, $uct.col_email, $uct.col_status], function (i, lbl) { - $$.th(thr).text(lbl); - }); + let grid = $$.dc('usr_grid', usrfrm); + let hdr = $$.dc('usr_row usr_row_hd', grid); + $$.dc('usr_col_avatar', hdr); + $$.dc('usr_col_name', hdr).text($uct.col_name); + $$.dc('usr_col_email', hdr).text($uct.col_email); + $$.dc('usr_col_status', hdr).text($uct.col_status); + if ((r.users || []).length === 0) { + $$.dc('usr_empty', grid).text($uct.empty); + return; + } $.each(r.users || [], function (ri, rw) { - let tr = $$.tr(ts.bdy, { id: 'usr' + rw.useraccount_id }); - $$.td(tr).text(`${rw.firstname || ''} ${rw.name || ''}`.trim()); - $$.td(tr).text(rw.email || ''); - $$.td(tr).text(bool(rw.disabled, false) === true ? $uct.disabled : $uct.active); - tr.click(function () { - tr.toggleClass('selected').siblings().rC('selected'); - if (tr.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); } + let disabled = bool(rw.disabled, false) === true; + let row = $$.dc('usr_row', grid, { id: 'usr' + rw.useraccount_id }); + $$.dc('usr_avatar', row).text($usr.initials(rw.firstname, rw.name)); + $$.dc('usr_col_name', row).text(`${rw.firstname || ''} ${rw.name || ''}`.trim()); + $$.dc('usr_col_email', row).text(rw.email || ''); + $$.dc('usr_col_status', row).append( + $$.dc(disabled ? 'usr_pill usr_pill_off' : 'usr_pill usr_pill_on').text(disabled ? $uct.disabled : $uct.active) + ); + row.click(function () { + row.toggleClass('selected').siblings().rC('selected'); + if (row.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); } }); }); }, error: function () { @@ -79,6 +111,45 @@ let $usr = { }, complete: function () { usrlst.ldng(0); } }); }, + createDlg: function () { + $ocms.dlgform([ + { name: 'firstname', label: $uct.firstname_field, type: 'string', required: true }, + { name: 'name', label: $uct.name_field, type: 'string', required: true }, + { name: 'email', label: $uct.email_field, type: 'email', required: true }, + { name: 'title', label: $uct.title_field, type: 'string' }, + { name: 'gender', label: $uct.gender_field, type: 'select', url: [['', ''], ['m', $uct.gender_m], ['f', $uct.gender_f]] }, + { name: 'mobile', label: $uct.mobile_field, type: 'string' } + ], { + title: $uct.newuser, + button: $uct.newuser_btn, + submit: function (e) { + let c = $(this).ldng(1); + let qs = c.serializeObject(true, { typedvalues: true }); + $ocms.postXT({ + url: $ocms.url('usr/create'), data: qs, success: function (response) { + alert(response.emailed === true ? $uct.create_sent : $uct.create_noemail); + c.trigger('modal_close'); + }, error: function (jqXHR) { + let code = ((jqXHR || {}).responseJSON || {}).error; + alert($uct.err[code] || $uct.err_generic); + }, complete: function () { c.ldng(0); $usr.renderLst(); } + }); + } + }); + }, + deleteUser: function (rw) { + let displayName = `${rw.firstname || ''} ${rw.name || ''}`.trim(); + if (confirm(string($uct.delete_confirm, [displayName]))) { + $ocms.postXT({ + url: $ocms.url('usr/delete'), data: { useraccount_id: rw.useraccount_id }, success: function () { + $usr.eM(true); + }, error: function (jqXHR) { + let code = ((jqXHR || {}).responseJSON || {}).error; + alert($uct.err[code] || $uct.err_generic); + }, complete: function () { $usr.renderLst(); } + }); + } + }, pwdResetDlg: function (rw) { $ocms.dlgform([ { name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id }, diff --git a/Fuchs/wwwroot/web/fis.usr.de.min.js b/Fuchs/wwwroot/web/fis.usr.de.min.js index 1c66d47..030f907 100644 --- a/Fuchs/wwwroot/web/fis.usr.de.min.js +++ b/Fuchs/wwwroot/web/fis.usr.de.min.js @@ -1 +1 @@ -let $uct={mdl:"Benutzerverwaltung",rel:"Neu Laden",col_name:"Name",col_email:"E-Mail",col_status:"Status",active:"Aktiv",disabled:"Deaktiviert",loadfail:"Die Benutzerliste konnte nicht geladen werden.",pwdreset:"Passwort zurücksetzen",pwdreset_btn:"Zurücksetzen",pwd_field:"Neues Passwort",pwd_hint:"Leer lassen, um automatisch ein Passwort zu generieren. In jedem Fall wird das neue Passwort per E-Mail an den Benutzer gesendet.",pwdreset_sent:"Das neue Passwort wurde per E-Mail an den Benutzer gesendet.",pwdreset_noemail:"Das Passwort wurde zurückgesetzt, konnte aber nicht per E-Mail zugestellt werden.",email:"E-Mail-Adresse ändern",email_field:"E-Mail-Adresse",groups:"Gruppenzugehörigkeit",groups_title:"Gruppen – {0}",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."},err_generic:"Die Änderung konnte nicht gespeichert werden."},$usr={init2:function(){$ocms.getScript([],(function(){$usr.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($uct.mdl),$usr.eM(),$usr.renderLst()},eM:function(e){let t=[];return!0===bool(e,!1)&&t.push({lbl:$uct.rel,fnc:$usr.renderLst}),$("#topbar").ocmsmenu(t)},iMn:function(e){return $("#topbar").ocmsmenu([{lbl:$uct.pwdreset,fnc:function(){$usr.pwdResetDlg(e)}},{lbl:$uct.email,fnc:function(){$usr.emailDlg(e)}},{lbl:$uct.groups,fnc:function(){$usr.groupsDlg(e)}}])},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),t=$$.dc("usrfrm",e);$usr.eM(!0),$ocms.postXT({url:$ocms.url("usr/list"),success:function(e){t.empty();let r=$$.tblset({},t),n=$$.tr(r.hd);$.each([$uct.col_name,$uct.col_email,$uct.col_status],(function(e,t){$$.th(n).text(t)})),$.each(e.users||[],(function(e,t){let n=$$.tr(r.bdy,{id:"usr"+t.useraccount_id});$$.td(n).text(`${t.firstname||""} ${t.name||""}`.trim()),$$.td(n).text(t.email||""),$$.td(n).text(!0===bool(t.disabled,!1)?$uct.disabled:$uct.active),n.click((function(){n.toggleClass("selected").siblings().rC("selected"),!0===n.is(".selected")?$usr.iMn(t):$usr.eM(!0)}))}))},error:function(){t.empty(),$$.dc("usr_err",t).text($uct.loadfail)},complete:function(){e.ldng(0)}})},pwdResetDlg:function(e){$ocms.dlgform([{name:"useraccount_id",type:"hidden",value:e.useraccount_id},{name:"newpassword",label:$uct.pwd_field,type:"string",note:$uct.pwd_hint}],{title:$uct.pwdreset,button:$uct.pwdreset_btn,submit:function(e){let t=$(this).ldng(1),r=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usr/pwdreset"),data:r,success:function(e){alert(!0===e.emailed?$uct.pwdreset_sent:$uct.pwdreset_noemail),t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($uct.err[t]||$uct.err_generic)},complete:function(){t.ldng(0),$usr.renderLst()}})}})},emailDlg:function(e){$ocms.dlgform([{name:"useraccount_id",type:"hidden",value:e.useraccount_id},{name:"email",label:$uct.email_field,type:"email",required:!0,value:e.email}],{title:$uct.email,submit:function(e){let t=$(this).ldng(1),r=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usr/email"),data:r,success:function(){t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($uct.err[t]||$uct.err_generic)},complete:function(){t.ldng(0),$usr.renderLst()}})}})},groupsDlg:function(e){let t=$$.dc("usr_groups_dlg");$ocms.postXT({url:$ocms.url("usr/groups"),data:{useraccount_id:e.useraccount_id},success:function(r){$.each(r.groups||[],(function(r,n){let s=$$.dc("usr_group_row",t),u="usrg"+n.usergroup_id,i=$$.i({type:"checkbox",id:u}).prop("checked",bool(n.member,!1)).appendTo(s);$$.lbl(n.name,{for:u}).appendTo(s),i.change((function(){let t=i.prop("checked");$ocms.postXT({url:$ocms.url("usr/groupset"),data:{useraccount_id:e.useraccount_id,usergroup_id:n.usergroup_id,member:t},error:function(){i.prop("checked",!t),alert($uct.err_generic)}})}))}))}}),$ocms.dlg(t,{title:string($uct.groups_title,[`${e.firstname||""} ${e.name||""}`.trim()]),size:[420,380]})}},$$usr={init2:$usr.init2,auth:{}};export default $$usr; \ No newline at end of file +let $uct={mdl:"Benutzerverwaltung",rel:"Neu Laden",col_name:"Name",col_email:"E-Mail",col_status:"Status",active:"Aktiv",disabled:"Deaktiviert",loadfail:"Die Benutzerliste konnte nicht geladen werden.",empty:"Es sind keine Benutzerkonten vorhanden.",newuser:"Neuer Benutzer",newuser_btn:"Neuer Benutzer",firstname_field:"Vorname",name_field:"Nachname",title_field:"Titel",gender_field:"Anrede",gender_m:"Herr",gender_f:"Frau",mobile_field:"Mobil",create_sent:"Das Benutzerkonto wurde angelegt, die Zugangsdaten wurden per E-Mail versendet.",create_noemail:"Das Benutzerkonto wurde angelegt, die Zugangsdaten konnten aber nicht per E-Mail zugestellt werden.",delete:"Benutzer löschen",delete_confirm:'Soll der Benutzer "{0}" wirklich gelöscht werden?',pwdreset:"Passwort zurücksetzen",pwdreset_btn:"Zurücksetzen",pwd_field:"Neues Passwort",pwd_hint:"Leer lassen, um automatisch ein Passwort zu generieren. In jedem Fall wird das neue Passwort per E-Mail an den Benutzer gesendet.",pwdreset_sent:"Das neue Passwort wurde per E-Mail an den Benutzer gesendet.",pwdreset_noemail:"Das Passwort wurde zurückgesetzt, konnte aber nicht per E-Mail zugestellt werden.",email:"E-Mail-Adresse ändern",email_field:"E-Mail-Adresse",groups:"Gruppenzugehörigkeit",groups_title:"Gruppen – {0}",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.",create_failed:"Das Benutzerkonto konnte nicht angelegt werden.",delete_failed:"Der Benutzer konnte nicht gelöscht werden.",cannot_delete_self:"Das eigene Benutzerkonto kann nicht gelöscht werden."},err_generic:"Die Änderung konnte nicht gespeichert werden."},$usr={init2:function(){$ocms.getScript([],(function(){$usr.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($uct.mdl),$usr.eM(),$usr.renderLst()},eM:function(e){let t=[{lbl:$uct.newuser_btn,fnc:$usr.createDlg}];return!0===bool(e,!1)&&t.push({lbl:$uct.rel,fnc:$usr.renderLst}),$("#topbar").ocmsmenu(t)},iMn:function(e){return $("#topbar").ocmsmenu([{lbl:$uct.pwdreset,fnc:function(){$usr.pwdResetDlg(e)}},{lbl:$uct.email,fnc:function(){$usr.emailDlg(e)}},{lbl:$uct.groups,fnc:function(){$usr.groupsDlg(e)}},{lbl:$uct.delete,fnc:function(){$usr.deleteUser(e)}}])},initials:function(e,t){return((e||"").trim().charAt(0)+(t||"").trim().charAt(0)).toUpperCase()||"?"},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),t=$$.dc("usrfrm",e);$usr.eM(!0),$ocms.postXT({url:$ocms.url("usr/list"),success:function(e){t.empty();let r=$$.dc("usr_grid",t),n=$$.dc("usr_row usr_row_hd",r);$$.dc("usr_col_avatar",n),$$.dc("usr_col_name",n).text($uct.col_name),$$.dc("usr_col_email",n).text($uct.col_email),$$.dc("usr_col_status",n).text($uct.col_status),0!==(e.users||[]).length?$.each(e.users||[],(function(e,t){let n=!0===bool(t.disabled,!1),s=$$.dc("usr_row",r,{id:"usr"+t.useraccount_id});$$.dc("usr_avatar",s).text($usr.initials(t.firstname,t.name)),$$.dc("usr_col_name",s).text(`${t.firstname||""} ${t.name||""}`.trim()),$$.dc("usr_col_email",s).text(t.email||""),$$.dc("usr_col_status",s).append($$.dc(n?"usr_pill usr_pill_off":"usr_pill usr_pill_on").text(n?$uct.disabled:$uct.active)),s.click((function(){s.toggleClass("selected").siblings().rC("selected"),!0===s.is(".selected")?$usr.iMn(t):$usr.eM(!0)}))})):$$.dc("usr_empty",r).text($uct.empty)},error:function(){t.empty(),$$.dc("usr_err",t).text($uct.loadfail)},complete:function(){e.ldng(0)}})},createDlg:function(){$ocms.dlgform([{name:"firstname",label:$uct.firstname_field,type:"string",required:!0},{name:"name",label:$uct.name_field,type:"string",required:!0},{name:"email",label:$uct.email_field,type:"email",required:!0},{name:"title",label:$uct.title_field,type:"string"},{name:"gender",label:$uct.gender_field,type:"select",url:[["",""],["m",$uct.gender_m],["f",$uct.gender_f]]},{name:"mobile",label:$uct.mobile_field,type:"string"}],{title:$uct.newuser,button:$uct.newuser_btn,submit:function(e){let t=$(this).ldng(1),r=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usr/create"),data:r,success:function(e){alert(!0===e.emailed?$uct.create_sent:$uct.create_noemail),t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($uct.err[t]||$uct.err_generic)},complete:function(){t.ldng(0),$usr.renderLst()}})}})},deleteUser:function(e){let t=`${e.firstname||""} ${e.name||""}`.trim();confirm(string($uct.delete_confirm,[t]))&&$ocms.postXT({url:$ocms.url("usr/delete"),data:{useraccount_id:e.useraccount_id},success:function(){$usr.eM(!0)},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($uct.err[t]||$uct.err_generic)},complete:function(){$usr.renderLst()}})},pwdResetDlg:function(e){$ocms.dlgform([{name:"useraccount_id",type:"hidden",value:e.useraccount_id},{name:"newpassword",label:$uct.pwd_field,type:"string",note:$uct.pwd_hint}],{title:$uct.pwdreset,button:$uct.pwdreset_btn,submit:function(e){let t=$(this).ldng(1),r=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usr/pwdreset"),data:r,success:function(e){alert(!0===e.emailed?$uct.pwdreset_sent:$uct.pwdreset_noemail),t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($uct.err[t]||$uct.err_generic)},complete:function(){t.ldng(0),$usr.renderLst()}})}})},emailDlg:function(e){$ocms.dlgform([{name:"useraccount_id",type:"hidden",value:e.useraccount_id},{name:"email",label:$uct.email_field,type:"email",required:!0,value:e.email}],{title:$uct.email,submit:function(e){let t=$(this).ldng(1),r=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usr/email"),data:r,success:function(){t.trigger("modal_close")},error:function(e){let t=((e||{}).responseJSON||{}).error;alert($uct.err[t]||$uct.err_generic)},complete:function(){t.ldng(0),$usr.renderLst()}})}})},groupsDlg:function(e){let t=$$.dc("usr_groups_dlg");$ocms.postXT({url:$ocms.url("usr/groups"),data:{useraccount_id:e.useraccount_id},success:function(r){$.each(r.groups||[],(function(r,n){let s=$$.dc("usr_group_row",t),u="usrg"+n.usergroup_id,l=$$.i({type:"checkbox",id:u}).prop("checked",bool(n.member,!1)).appendTo(s);$$.lbl(n.name,{for:u}).appendTo(s),l.change((function(){let t=l.prop("checked");$ocms.postXT({url:$ocms.url("usr/groupset"),data:{useraccount_id:e.useraccount_id,usergroup_id:n.usergroup_id,member:t},error:function(){l.prop("checked",!t),alert($uct.err_generic)}})}))}))}}),$ocms.dlg(t,{title:string($uct.groups_title,[`${e.firstname||""} ${e.name||""}`.trim()]),size:[420,380]})}},$$usr={init2:$usr.init2,auth:{}};export default $$usr; \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.usr.min.css b/Fuchs/wwwroot/web/fis.usr.min.css index dee8ad5..2c333b3 100644 --- a/Fuchs/wwwroot/web/fis.usr.min.css +++ b/Fuchs/wwwroot/web/fis.usr.min.css @@ -1 +1 @@ -.usrfrm>table{border-collapse:collapse;margin:2rem}.usrfrm>table td,.usrfrm>table th{padding:.2rem .35rem;border:1px solid #ccc}.usrfrm>table tr{cursor:pointer}.usrfrm>table tr:nth-child(2n+1) td{background-color:#eee}.usrfrm>table tr:hover td{background-color:#f3f6fb}.usrfrm>table tr.selected td{background-color:#add8e6}.usrfrm .usr_err{padding:1rem 2rem;color:#b00020;font-weight:700}.usr_groups_dlg{padding:.25rem 0}.usr_groups_dlg .usr_group_row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid #eee}.usr_groups_dlg .usr_group_row:last-child{border-bottom:none}.usr_groups_dlg .usr_group_row input[type=checkbox]{margin:0}.usr_groups_dlg .usr_group_row label{margin:0;font-weight:400} \ No newline at end of file +.usrfrm{padding:1.5rem 2rem}.usrfrm .usr_grid{border:1px solid #e2e2e2;border-radius:.5rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.1);overflow:hidden}.usrfrm .usr_row{display:grid;grid-template-columns:2.75rem 1.6fr 1.8fr 8rem;align-items:center;gap:.75rem;padding:.55rem 1rem;border-bottom:1px solid #eee}.usrfrm .usr_row:last-child{border-bottom:none}.usrfrm .usr_row_hd{background:#1b4379;color:#fff;font-weight:700;font-size:.85rem;text-transform:uppercase;letter-spacing:.02em;padding:.5rem 1rem}.usrfrm .usr_row:not(.usr_row_hd){cursor:pointer;transition:background-color .12s ease-in-out}.usrfrm .usr_row:not(.usr_row_hd):hover{background-color:#f3f6fb}.usrfrm .usr_row:not(.usr_row_hd).selected{background-color:rgba(86,165,50,.12);box-shadow:inset 3px 0 0 #56a532}.usrfrm .usr_avatar{width:2.1rem;height:2.1rem;border-radius:50%;background:#1b4379;color:#fff;font-size:.78rem;font-weight:700;display:flex;align-items:center;justify-content:center}.usrfrm .usr_col_name{font-weight:600;color:#222}.usrfrm .usr_col_email{color:#555;overflow-wrap:anywhere}.usrfrm .usr_pill{display:inline-block;padding:.1rem .6rem;border-radius:1rem;font-size:.78rem;font-weight:700;white-space:nowrap}.usrfrm .usr_pill_on{background:#e2f3da;color:#2f6d18}.usrfrm .usr_pill_off{background:#ececec;color:#777}.usrfrm .usr_empty{padding:1.5rem 1rem;text-align:center;color:#777}.usrfrm .usr_err{padding:1rem 2rem;color:#b00020;font-weight:700}@media (max-width:640px){.usrfrm{padding:.75rem}.usrfrm .usr_row{grid-template-columns:2.25rem 1fr;grid-template-areas:"avatar name" "avatar email" "avatar status"}.usrfrm .usr_row_hd{display:none}.usrfrm .usr_avatar{grid-area:avatar}.usrfrm .usr_col_name{grid-area:name}.usrfrm .usr_col_email{grid-area:email;font-size:.85rem}.usrfrm .usr_col_status{grid-area:status}}.usr_groups_dlg{padding:.25rem 0}.usr_groups_dlg .usr_group_row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid #eee}.usr_groups_dlg .usr_group_row:last-child{border-bottom:none}.usr_groups_dlg .usr_group_row input[type=checkbox]{margin:0}.usr_groups_dlg .usr_group_row label{margin:0;font-weight:400} \ No newline at end of file diff --git a/Fuchs/wwwroot/web/tools.js b/Fuchs/wwwroot/web/tools.js index ceb47a5..f36c9c0 100644 --- a/Fuchs/wwwroot/web/tools.js +++ b/Fuchs/wwwroot/web/tools.js @@ -1,4 +1,5 @@ -/*! js-cookie v3.0.1 | MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"
","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0 -- ============================================= CREATE PROCEDURE [dbo].[fis_admin_createUserAccount] - @name nvarchar(100) + @authaccount varchar(5) + ,@name nvarchar(100) ,@firstname nvarchar(100) ,@title varchar(50) ,@gender varchar(1) @@ -20,41 +21,59 @@ BEGIN SET NOCOUNT ON; DECLARE @utcnow datetime = GETUTCDATE(); + DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount); + DECLARE @newid varchar(5); - INSERT INTO [dbo].[fis_useraccounts] - ([useraccount_id] - ,[name_enc] - ,[firstname_enc] - ,[title_enc] - ,[gender_enc] - ,[email_enc] - ,[mobile_enc] - ,[password_enc] - ,[UserDisabled] - ,[DateDisabled] - ,[UserCreated] - ,[DateCreated] - ,[UserModified] - ,[DateModified] - ,[UserDeleted] - ,[DateDeleted]) - OUTPUT inserted.[useraccount_id] - VALUES - ([dbo].[fis_fn_useraccount_id] () - ,ENCRYPTBYKEY(@enc_key, @name) - ,ENCRYPTBYKEY(@enc_key, @firstname) - ,ENCRYPTBYKEY(@enc_key, @title) - ,ENCRYPTBYKEY(@enc_key, @gender) - ,ENCRYPTBYKEY(@enc_key, @email) - ,ENCRYPTBYKEY(@enc_key, @mobile) - ,CASE WHEN @password is null then ENCRYPTBYKEY(@enc_key, CAST([dbo].[ocms_fn_generatePassword] (8, 0, 1, 1) AS nvarchar(20))) ELSE ENCRYPTBYKEY(@enc_key, @password) END - ,NULL --[UserDisabled] - ,NULL --[DateDisabled] - ,'sa' - ,@utcnow - ,'sa' - ,@utcnow - ,NULL - ,NULL); + IF @auth > 1 + BEGIN + + IF EXISTS (SELECT * FROM [dbo].[fis_useraccounts] WHERE [DateDeleted] is null AND ISNULL(@email,'') <> '' AND LTRIM(RTRIM(DECRYPTBYKEY([email_enc]))) like LTRIM(RTRIM(@email))) + THROW 60001, 'email is already assigned to another account', 1; + + IF @password IS NULL + BEGIN + SET @password = [dbo].[ocms_fn_generatePassword] (8, 0, 1, 1); + END + + SET @newid = [dbo].[fis_fn_useraccount_id] (); + + INSERT INTO [dbo].[fis_useraccounts] + ([useraccount_id] + ,[name_enc] + ,[firstname_enc] + ,[title_enc] + ,[gender_enc] + ,[email_enc] + ,[mobile_enc] + ,[password_enc] + ,[UserDisabled] + ,[DateDisabled] + ,[UserCreated] + ,[DateCreated] + ,[UserModified] + ,[DateModified] + ,[UserDeleted] + ,[DateDeleted]) + VALUES + (@newid + ,ENCRYPTBYKEY(@enc_key, @name) + ,ENCRYPTBYKEY(@enc_key, @firstname) + ,ENCRYPTBYKEY(@enc_key, @title) + ,ENCRYPTBYKEY(@enc_key, @gender) + ,ENCRYPTBYKEY(@enc_key, @email) + ,ENCRYPTBYKEY(@enc_key, @mobile) + ,ENCRYPTBYKEY(@enc_key, @password) + ,NULL --[UserDisabled] + ,NULL --[DateDisabled] + ,@authaccount + ,@utcnow + ,@authaccount + ,@utcnow + ,NULL + ,NULL); + + SELECT TOP(1) *, @password as [newpassword] FROM [dbo].[fis_admin_getUserAccountList](@authaccount, @newid, null, null); + + END END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserAccount.sql b/Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserAccount.sql index 2febd1b..1eb9387 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserAccount.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserAccount.sql @@ -12,17 +12,23 @@ AS BEGIN SET NOCOUNT ON; DECLARE @utcnow datetime = GETUTCDATE(); + DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount); - IF EXISTS (SELECT * FROM [dbo].[fis_useraccounts] WHERE [useraccount_id] = @useraccount_id and [DateDeleted] is null) - BEGIN + IF @auth > 1 AND @authaccount <> @useraccount_id + BEGIN - UPDATE [dbo].[fis_useraccounts] - SET [UserDeleted] = @authaccount - ,[DateDeleted] = @utcnow - WHERE - [useraccount_id] = @useraccount_id; + IF EXISTS (SELECT * FROM [dbo].[fis_useraccounts] WHERE [useraccount_id] = @useraccount_id and [DateDeleted] is null) + BEGIN - END + UPDATE [dbo].[fis_useraccounts] + SET [UserDeleted] = @authaccount + ,[DateDeleted] = @utcnow + WHERE + [useraccount_id] = @useraccount_id; + + END + + END SELECT CAST((CASE WHEN NOT EXISTS (SELECT * FROM [dbo].[fis_useraccounts] WHERE [useraccount_id] = @useraccount_id and [DateDeleted] is null) THEN 1 ELSE 0 END) as bit);