Add Administration user management: list, password reset, email change, group assignments
Playwright Tests / test (push) Canceled after 0s
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:
@@ -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;
|
||||
|
||||
@@ -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 ─────────────────────────────────────────── */
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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}
|
||||
Reference in New Issue
Block a user