Add per-user TOTP 2FA, client-level user assignment, self-service portal
- TOTP (RFC 6238, stdlib-only) enrollment in profile, login step-up, admin emergency reset. - Admins can grant a user visibility into individual clients (User.ClientIDs) in addition to whole-server access (User.ServerIDs). - New "My Access" page: non-admin users see only their assigned clients (view/QR/download only, no management), reachable from the main nav. - GetUser/GetUsers now redact TOTPSecret before returning JSON. No Go toolchain was available while writing this - not yet build-verified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvrfUytqd74H6WcQkRzFM4
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c29edfdcc3
commit
34bc8f76f9
@@ -59,6 +59,12 @@ Users Settings
|
||||
</select>
|
||||
<small class="form-text text-muted">Servers this user (if non-admin) may access. Admins always have access to all servers.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="_client_ids" class="control-label">Individual Client Access</label>
|
||||
<select multiple class="form-control" id="_client_ids" name="_client_ids">
|
||||
</select>
|
||||
<small class="form-text text-muted">Grants a non-admin user visibility into these specific clients, even without full server access.</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
@@ -179,7 +185,9 @@ Users Settings
|
||||
success: function (servers) {
|
||||
const select = modal.find("#_server_ids");
|
||||
select.empty();
|
||||
const serverNameById = {};
|
||||
$.each(servers, function (index, srv) {
|
||||
serverNameById[srv.id] = srv.name;
|
||||
select.append($('<option>').val(srv.id).text(srv.name + " (" + srv.id + ")"));
|
||||
});
|
||||
|
||||
@@ -188,6 +196,35 @@ Users Settings
|
||||
if (user_name !== "") {
|
||||
select.val(select.data('preselect') || []);
|
||||
}
|
||||
|
||||
// populate the individual client access select, labeling each option
|
||||
// with both the client's name/email and the server it belongs to so
|
||||
// admins aren't picking blind between same-named clients on different servers
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'GET',
|
||||
url: '{{.basePath}}/api/clients',
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
success: function (clientDataList) {
|
||||
const clientSelect = modal.find("#_client_ids");
|
||||
clientSelect.empty();
|
||||
$.each(clientDataList, function (index, clientData) {
|
||||
const client = clientData.Client;
|
||||
const serverName = serverNameById[client.server_id] || client.server_id || "unknown server";
|
||||
const label = (client.name || client.email || client.id) + " — " + serverName;
|
||||
clientSelect.append($('<option>').val(client.id).text(label));
|
||||
});
|
||||
|
||||
if (user_name !== "") {
|
||||
clientSelect.val(clientSelect.data('preselect') || []);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
toastr.error(responseJson['message']);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
@@ -216,6 +253,10 @@ Users Settings
|
||||
// once its options have been populated (see the servers ajax above)
|
||||
modal.find("#_server_ids").data('preselect', user.server_ids || []);
|
||||
modal.find("#_server_ids").val(user.server_ids || []);
|
||||
// remember the granted client ids so the select can pre-select them
|
||||
// once its options have been populated (see the clients ajax above)
|
||||
modal.find("#_client_ids").data('preselect', user.client_ids || []);
|
||||
modal.find("#_client_ids").val(user.client_ids || []);
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
@@ -230,6 +271,7 @@ Users Settings
|
||||
modal.find("#_user_password").prop("placeholder", "")
|
||||
modal.find("#_admin").prop("checked", false);
|
||||
modal.find("#_server_ids").data('preselect', []);
|
||||
modal.find("#_client_ids").data('preselect', []);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -243,12 +285,14 @@ Users Settings
|
||||
admin = true;
|
||||
}
|
||||
const server_ids = $("#_server_ids").val() || [];
|
||||
const client_ids = $("#_client_ids").val() || [];
|
||||
const data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"previous_username": previous_username,
|
||||
"admin": admin,
|
||||
"server_ids": server_ids
|
||||
"server_ids": server_ids,
|
||||
"client_ids": client_ids
|
||||
};
|
||||
|
||||
if (previous_username !== "") {
|
||||
|
||||
Reference in New Issue
Block a user