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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 10:34:04 +02:00
co-authored by Claude Sonnet 5
parent 6c38ee3d55
commit a5f497090e
14 changed files with 740 additions and 8 deletions
+27
View File
@@ -245,6 +245,33 @@
color: #fff;
border-color: rgba(255, 255, 255, 0.4);
}
.admfrm .adm_users_bd table tbody tr {
cursor: pointer;
}
.admfrm .adm_users_bd table tbody tr:hover td {
background-color: #f3f6fb;
}
.adm_groups_dlg {
padding: 0.25rem 0;
}
.adm_groups_dlg .adm_group_row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0;
border-bottom: 1px solid #eee;
}
.adm_groups_dlg .adm_group_row:last-child {
border-bottom: none;
}
.adm_groups_dlg .adm_group_row input[type=checkbox] {
margin: 0;
}
.adm_groups_dlg .adm_group_row label {
margin: 0;
font-weight: normal;
}
.adm_note {
margin-top: 0.75rem;
+136
View File
@@ -81,6 +81,31 @@ let $adt = {
, tm_default_subject: 'Fuchs Intranet – Test-E-Mail'
, tm_default_body: 'Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.'
, loadfail: 'Der Systemstatus konnte nicht geladen werden.'
// user management
, sub_users: 'Benutzerverwaltung'
, u_title: 'Benutzerkonten'
, u_col_name: 'Name'
, u_col_email: 'E-Mail'
, u_col_status: 'Status'
, u_active: 'Aktiv'
, u_disabled: 'Deaktiviert'
, u_pwdreset: 'Passwort zurücksetzen'
, u_pwdreset_btn: 'Zurücksetzen'
, u_pwd_field: 'Neues Passwort'
, u_pwd_hint: 'Leer lassen, um automatisch ein Passwort zu generieren. In jedem Fall wird das neue Passwort per E-Mail an den Benutzer gesendet.'
, u_pwdreset_sent: 'Das neue Passwort wurde per E-Mail an den Benutzer gesendet.'
, u_pwdreset_noemail: 'Das Passwort wurde zurückgesetzt, konnte aber nicht per E-Mail zugestellt werden.'
, u_email: 'E-Mail-Adresse ändern'
, u_email_field: 'E-Mail-Adresse'
, u_groups: 'Gruppenzugehörigkeit'
, u_groups_title: 'Gruppen – {0}'
, u_err: {
requirements: 'Das Passwort erfüllt nicht die Mindestanforderungen (mind. 6 Zeichen).',
duplicate_email: 'Diese E-Mail-Adresse ist bereits einem anderen Benutzerkonto zugeordnet.',
reset_failed: 'Das Passwort konnte nicht zurückgesetzt werden.',
change_failed: 'Die Änderung konnte nicht gespeichert werden.'
}
, u_err_generic: 'Die Änderung konnte nicht gespeichert werden.'
};
let $adm = {
@@ -101,6 +126,14 @@ let $adm = {
{ lbl: $adt.refreshAll, id: 'adm_refresh_all', ico: 'glyphicon glyphicon-refresh', fnc: function () { $adm.init3(); } }
]);
},
/* Per-row ribbon shown while a user row is selected (mirrors the Banking module's iMn/eM swap). */
uMn: function (rw) {
return $('#topbar').ocmsmenu([
{ lbl: $adt.u_pwdreset, fnc: function () { $adm.pwdResetDlg(rw); } },
{ lbl: $adt.u_email, fnc: function () { $adm.emailDlg(rw); } },
{ lbl: $adt.u_groups, fnc: function () { $adm.groupsDlg(rw); } }
]);
},
load: function (frm) {
$ocms.postXT({
url: $ocms.url('admin/status'), success: function (r) {
@@ -127,6 +160,109 @@ let $adm = {
$adm.blobCard(grid, info.blob || {}, pmap.blob);
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
$$.dc('adm_sec', frm).text($adt.sub_users);
$adm.usersCard(frm);
},
/* ── user management ─────────────────────────────────────────────────── */
usersCard: function (frm) {
let card = $$.dc('adm_card adm_card_wide', frm);
let hd = $$.dc('adm_card_hd', card);
$$.s($adt.u_title).appendTo(hd);
let bd = $$.dc('adm_card_bd adm_users_bd', card);
$adm.renderUsers(bd);
},
renderUsers: function (bd) {
bd.empty();
let ts = $$.tblset({}, bd);
let thr = $$.tr(ts.hd);
$.each([$adt.u_col_name, $adt.u_col_email, $adt.u_col_status], function (i, lbl) {
$$.th(thr).text(lbl);
});
$ocms.postXT({
url: $ocms.url('admin/users'), success: function (r) {
$.each(r.users || [], function (ri, rw) {
let tr = $$.tr(ts.bdy, { id: 'adu' + rw.useraccount_id });
$$.td(tr).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
$$.td(tr).text(rw.email || '');
$$.td(tr).text(bool(rw.disabled, false) === true ? $adt.u_disabled : $adt.u_active);
tr.click(function () {
tr.toggleClass('selected').siblings().rC('selected');
if (tr.is('.selected') === true) { $adm.uMn(rw); } else { $adm.topbar(); }
});
});
}, error: function () {
$$.dc('adm_err', bd).text($adt.loadfail);
}
});
},
pwdResetDlg: function (rw) {
$ocms.dlgform([
{ name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id },
{ name: 'newpassword', label: $adt.u_pwd_field, type: 'string', note: $adt.u_pwd_hint }
], {
title: $adt.u_pwdreset,
button: $adt.u_pwdreset_btn,
submit: function (e) {
let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true });
$ocms.postXT({
url: $ocms.url('admin/userpwdreset'), data: qs, success: function (response) {
alert(response.emailed === true ? $adt.u_pwdreset_sent : $adt.u_pwdreset_noemail);
c.trigger('modal_close');
}, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error;
alert($adt.u_err[code] || $adt.u_err_generic);
}, complete: function () { c.ldng(0); $adm.renderUsers($('.adm_users_bd')); $adm.topbar(); }
});
}
});
},
emailDlg: function (rw) {
$ocms.dlgform([
{ name: 'useraccount_id', type: 'hidden', value: rw.useraccount_id },
{ name: 'email', label: $adt.u_email_field, type: 'email', required: true, value: rw.email }
], {
title: $adt.u_email,
submit: function (e) {
let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true });
$ocms.postXT({
url: $ocms.url('admin/useremail'), data: qs, success: function () {
c.trigger('modal_close');
}, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error;
alert($adt.u_err[code] || $adt.u_err_generic);
}, complete: function () { c.ldng(0); $adm.renderUsers($('.adm_users_bd')); $adm.topbar(); }
});
}
});
},
groupsDlg: function (rw) {
let f = $$.dc('adm_groups_dlg');
$ocms.postXT({
url: $ocms.url('admin/usergroups'), data: { useraccount_id: rw.useraccount_id }, success: function (r) {
$.each(r.groups || [], function (gi, g) {
let row = $$.dc('adm_group_row', f);
let cid = 'adg' + g.usergroup_id;
let cb = $$.i({ type: 'checkbox', id: cid }).prop('checked', bool(g.member, false)).appendTo(row);
$$.lbl(g.name, { for: cid }).appendTo(row);
cb.change(function () {
let checked = cb.prop('checked');
$ocms.postXT({
url: $ocms.url('admin/usergroupset'),
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked },
error: function () {
cb.prop('checked', !checked);
alert($adt.u_err_generic);
}
});
});
});
}
});
$ocms.dlg(f, { title: string($adt.u_groups_title, [`${rw.firstname || ''} ${rw.name || ''}`.trim()]), size: [420, 380] });
},
/* ── generic building blocks ─────────────────────────────────────────── */
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.admfrm{padding:1.5rem}.admfrm .adm_sec{font-size:1.2rem;font-weight:700;color:#1b4379;margin:1.5rem 0 .75rem;border-bottom:2px solid #1b4379;padding-bottom:.25rem}.admfrm .adm_sec:first-child{margin-top:0}.admfrm .adm_err{padding:1rem;color:#b00020;font-weight:700}.admfrm .adm_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(22rem,1fr));gap:1rem;align-items:start}.admfrm .adm_card{border:1px solid #d5d5d5;border-radius:.4rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.12);overflow:hidden}.admfrm .adm_card.adm_card_wide{grid-column:1/-1}.admfrm .adm_card_hd{display:flex;align-items:center;gap:.5rem;padding:.6rem .85rem;background:#1b4379;color:#fff;font-weight:700}.admfrm .adm_card_hd>span:first-child{flex:1 1 auto}.admfrm .adm_card_bd{padding:.6rem .85rem}.admfrm .adm_card_ft{padding:.5rem .85rem .75rem;text-align:right}.admfrm table.adm_kv{border-collapse:collapse;width:100%}.admfrm table.adm_kv td{padding:.25rem .35rem;border-bottom:1px solid #eee;vertical-align:top;font-size:.92rem}.admfrm table.adm_kv td.k{color:#666;white-space:nowrap;width:40%}.admfrm table.adm_kv td.v{word-break:break-word}.admfrm table.adm_kv tr:last-child td{border-bottom:none}.admfrm .adm_probe{margin-top:.6rem;padding-top:.5rem;border-top:1px dashed #ddd;font-size:.88rem}.admfrm .adm_probe .adm_probe_msg{font-weight:600}.admfrm .adm_probe .adm_probe_det{color:#555;word-break:break-word;margin-top:.15rem}.admfrm .adm_probe .adm_probe_dur{color:#999;margin-top:.15rem;font-size:.8rem}.admfrm .adm_probe .adm_probe_metrics{margin-top:.35rem}.admfrm .adm_probe .adm_probe_metrics .adm_metric{display:flex;justify-content:space-between;gap:.5rem;padding:.1rem 0;border-bottom:1px dotted #eee}.admfrm .adm_probe .adm_probe_metrics .adm_metric:last-child{border-bottom:none}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_k{color:#555;word-break:break-word}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_v{font-weight:600;white-space:nowrap}.admfrm .adm_checks{margin-top:.5rem}.admfrm .adm_checks .adm_check{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.25rem 0;border-bottom:1px solid #eee}.admfrm .adm_checks .adm_check:last-child{border-bottom:none}.admfrm .adm_checks .adm_check .adm_check_k{color:#333}.admfrm .adm_checks .adm_check .adm_check_v{font-weight:700;font-size:.8rem;padding:.05rem .5rem;border-radius:.25rem;white-space:nowrap}.admfrm .adm_checks .adm_check .adm_check_ok{background:#e2f3da;color:#2f6d18}.admfrm .adm_checks .adm_check .adm_check_fail{background:#f3ded9;color:#c0341d}.admfrm .adm_checks .adm_check .adm_check_skipped{background:#ececec;color:#777}.admfrm .adm_pill{display:inline-block;padding:.1rem .55rem;border-radius:1rem;font-size:.78rem;font-weight:700;color:#fff;background:#999;white-space:nowrap}.admfrm .adm_pill_ok{background:#56a532}.admfrm .adm_pill_error{background:#c0341d}.admfrm .adm_pill_disabled{background:#888}.admfrm .adm_pill_unconfigured{background:#d69100}.admfrm .adm_pill_checking{background:#2a72c4}.admfrm .adm_bool{display:inline-block;padding:0 .4rem;border-radius:.25rem;font-size:.8rem;font-weight:700}.admfrm .adm_bool_y{background:#e2f3da;color:#2f6d18}.admfrm .adm_bool_n{background:#f3e0dd;color:#9c2a17}.admfrm .adm_env{font-size:.8rem;font-weight:400;padding:.1rem .5rem;border-radius:.25rem;background:rgba(255,255,255,.25)}.admfrm .adm_env.adm_env_test{background:#13a143}.admfrm .adm_btn{cursor:pointer;border-radius:.28rem;padding:.2rem .6rem;font-size:.85rem;border:1px solid #ababab;background-color:#fff;color:#333}.admfrm .adm_btn.adm_btn_primary{background-color:#56a532;border-color:rgb(69.68,133.688372093,40.511627907);color:#fff}.admfrm .adm_card_hd .adm_btn{background-color:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.4)}.adm_note{margin-top:.75rem;padding:.5rem .6rem;background:#f3f6fb;border-left:3px solid #1b4379;font-size:.85rem;color:#444}
.admfrm{padding:1.5rem}.admfrm .adm_sec{font-size:1.2rem;font-weight:700;color:#1b4379;margin:1.5rem 0 .75rem;border-bottom:2px solid #1b4379;padding-bottom:.25rem}.admfrm .adm_sec:first-child{margin-top:0}.admfrm .adm_err{padding:1rem;color:#b00020;font-weight:700}.admfrm .adm_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(22rem,1fr));gap:1rem;align-items:start}.admfrm .adm_card{border:1px solid #d5d5d5;border-radius:.4rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.12);overflow:hidden}.admfrm .adm_card.adm_card_wide{grid-column:1/-1}.admfrm .adm_card_hd{display:flex;align-items:center;gap:.5rem;padding:.6rem .85rem;background:#1b4379;color:#fff;font-weight:700}.admfrm .adm_card_hd>span:first-child{flex:1 1 auto}.admfrm .adm_card_bd{padding:.6rem .85rem}.admfrm .adm_card_ft{padding:.5rem .85rem .75rem;text-align:right}.admfrm table.adm_kv{border-collapse:collapse;width:100%}.admfrm table.adm_kv td{padding:.25rem .35rem;border-bottom:1px solid #eee;vertical-align:top;font-size:.92rem}.admfrm table.adm_kv td.k{color:#666;white-space:nowrap;width:40%}.admfrm table.adm_kv td.v{word-break:break-word}.admfrm table.adm_kv tr:last-child td{border-bottom:none}.admfrm .adm_probe{margin-top:.6rem;padding-top:.5rem;border-top:1px dashed #ddd;font-size:.88rem}.admfrm .adm_probe .adm_probe_msg{font-weight:600}.admfrm .adm_probe .adm_probe_det{color:#555;word-break:break-word;margin-top:.15rem}.admfrm .adm_probe .adm_probe_dur{color:#999;margin-top:.15rem;font-size:.8rem}.admfrm .adm_probe .adm_probe_metrics{margin-top:.35rem}.admfrm .adm_probe .adm_probe_metrics .adm_metric{display:flex;justify-content:space-between;gap:.5rem;padding:.1rem 0;border-bottom:1px dotted #eee}.admfrm .adm_probe .adm_probe_metrics .adm_metric:last-child{border-bottom:none}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_k{color:#555;word-break:break-word}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_v{font-weight:600;white-space:nowrap}.admfrm .adm_checks{margin-top:.5rem}.admfrm .adm_checks .adm_check{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.25rem 0;border-bottom:1px solid #eee}.admfrm .adm_checks .adm_check:last-child{border-bottom:none}.admfrm .adm_checks .adm_check .adm_check_k{color:#333}.admfrm .adm_checks .adm_check .adm_check_v{font-weight:700;font-size:.8rem;padding:.05rem .5rem;border-radius:.25rem;white-space:nowrap}.admfrm .adm_checks .adm_check .adm_check_ok{background:#e2f3da;color:#2f6d18}.admfrm .adm_checks .adm_check .adm_check_fail{background:#f3ded9;color:#c0341d}.admfrm .adm_checks .adm_check .adm_check_skipped{background:#ececec;color:#777}.admfrm .adm_pill{display:inline-block;padding:.1rem .55rem;border-radius:1rem;font-size:.78rem;font-weight:700;color:#fff;background:#999;white-space:nowrap}.admfrm .adm_pill_ok{background:#56a532}.admfrm .adm_pill_error{background:#c0341d}.admfrm .adm_pill_disabled{background:#888}.admfrm .adm_pill_unconfigured{background:#d69100}.admfrm .adm_pill_checking{background:#2a72c4}.admfrm .adm_bool{display:inline-block;padding:0 .4rem;border-radius:.25rem;font-size:.8rem;font-weight:700}.admfrm .adm_bool_y{background:#e2f3da;color:#2f6d18}.admfrm .adm_bool_n{background:#f3e0dd;color:#9c2a17}.admfrm .adm_env{font-size:.8rem;font-weight:400;padding:.1rem .5rem;border-radius:.25rem;background:rgba(255,255,255,.25)}.admfrm .adm_env.adm_env_test{background:#13a143}.admfrm .adm_btn{cursor:pointer;border-radius:.28rem;padding:.2rem .6rem;font-size:.85rem;border:1px solid #ababab;background-color:#fff;color:#333}.admfrm .adm_btn.adm_btn_primary{background-color:#56a532;border-color:rgb(69.68,133.688372093,40.511627907);color:#fff}.admfrm .adm_card_hd .adm_btn{background-color:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.4)}.admfrm .adm_users_bd table tbody tr{cursor:pointer}.admfrm .adm_users_bd table tbody tr:hover td{background-color:#f3f6fb}.adm_groups_dlg{padding:.25rem 0}.adm_groups_dlg .adm_group_row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid #eee}.adm_groups_dlg .adm_group_row:last-child{border-bottom:none}.adm_groups_dlg .adm_group_row input[type=checkbox]{margin:0}.adm_groups_dlg .adm_group_row label{margin:0;font-weight:400}.adm_note{margin-top:.75rem;padding:.5rem .6rem;background:#f3f6fb;border-left:3px solid #1b4379;font-size:.85rem;color:#444}