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:
sysops
2026-07-25 00:42:05 +02:00
co-authored by Claude Sonnet 5
parent c29edfdcc3
commit 34bc8f76f9
12 changed files with 1213 additions and 36 deletions
+129
View File
@@ -47,6 +47,43 @@ Profile
</div>
<!-- /.card -->
</div>
<div class="col-md-6">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Two-Factor Authentication (TOTP)</h3>
</div>
<!-- /.card-header -->
<div class="card-body">
<div id="totp-disabled-panel">
<p id="totp-status-text">Two-factor authentication is not enabled.</p>
<button type="button" class="btn btn-primary" id="btn_totp_setup">Set up 2FA</button>
<div id="totp-enroll-panel" style="display:none; margin-top: 15px;">
<p>Scan this QR code with your authenticator app, or enter the secret manually:</p>
<div class="text-center mb-3">
<img id="totp-qrcode" src="" alt="TOTP QR code" style="max-width:200px;">
</div>
<div class="form-group">
<label for="totp-secret" class="control-label">Secret</label>
<input type="text" class="form-control" id="totp-secret" readonly>
</div>
<div class="form-group">
<label for="totp-confirm-code" class="control-label">Enter the 6-digit code to confirm</label>
<input type="text" inputmode="numeric" maxlength="6" class="form-control" id="totp-confirm-code" placeholder="123456">
</div>
<button type="button" class="btn btn-success" id="btn_totp_confirm">Confirm</button>
</div>
</div>
<div id="totp-enabled-panel" style="display:none;">
<p><span class="badge badge-success">Enabled</span> Two-factor authentication is enabled on your account.</p>
<button type="button" class="btn btn-danger" id="btn_totp_disable">Disable 2FA</button>
</div>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
</div>
<!-- /.row -->
</div>
@@ -132,5 +169,97 @@ Profile
}
});
});
function refreshTotpUiState(enabled) {
if (enabled) {
$("#totp-disabled-panel").hide();
$("#totp-enroll-panel").hide();
$("#totp-enabled-panel").show();
} else {
$("#totp-enabled-panel").hide();
$("#totp-disabled-panel").show();
}
}
$(document).ready(function () {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}',
dataType: 'json',
contentType: "application/json",
success: function (resp) {
refreshTotpUiState(!!resp.totp_enabled);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
$("#btn_totp_setup").click(function () {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/profile/totp/enroll',
dataType: 'json',
contentType: "application/json",
success: function (resp) {
$("#totp-qrcode").attr("src", resp.qrcode);
$("#totp-secret").val(resp.secret);
$("#totp-enroll-panel").show();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
$("#btn_totp_confirm").click(function () {
const code = $("#totp-confirm-code").val();
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/profile/totp/confirm',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify({"code": code}),
success: function (resp) {
toastr.success("Two-factor authentication enabled");
refreshTotpUiState(true);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
$("#btn_totp_disable").click(function () {
if (!confirm("Disable two-factor authentication?")) {
return;
}
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/profile/totp/disable',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify({}),
success: function (resp) {
toastr.success("Two-factor authentication disabled");
$("#totp-secret").val("");
$("#totp-confirm-code").val("");
$("#totp-qrcode").attr("src", "");
refreshTotpUiState(false);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
});
</script>
{{ end }}