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
+52 -1
View File
@@ -64,6 +64,22 @@
<!-- /.col -->
</div>
</form>
<form id="totp-form" action="" method="post" style="display:none;">
<p class="login-box-msg">Enter the 6-digit code from your authenticator app</p>
<div class="input-group mb-3">
<input id="totp_code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="form-control" placeholder="123456">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-shield-alt"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button id="btn_totp" type="submit" class="btn btn-primary btn-block">Verify</button>
</div>
</div>
</form>
<div class="text-center mb-3">
<p id="message"></p>
</div>
@@ -93,11 +109,16 @@
</script>
<script>
$(document).ready(function () {
$('form').on('submit', function(e) {
$('#username, #password').closest('form').on('submit', function(e) {
e.preventDefault();
$("#btn_login").trigger('click');
});
$('#totp-form').on('submit', function(e) {
e.preventDefault();
$("#btn_totp").trigger('click');
});
$("#btn_login").click(function () {
const username = $("#username").val();
const password = $("#password").val();
@@ -114,6 +135,36 @@
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function(data) {
if (data['totp_required']) {
document.getElementById("message").innerHTML = "";
$('#username').closest('form').hide();
$('#totp-form').show();
$('#totp_code').focus();
return;
}
document.getElementById("message").innerHTML = `<p style="color:green">${data['message']}</p>`;
// redirect after logging in successfully
redirectNext();
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
document.getElementById("message").innerHTML = `<p style="color:#ff0000">${responseJson['message']}</p>`;
}
});
});
$("#btn_totp").click(function () {
const code = $("#totp_code").val();
const data = {"code": code}
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/login/totp',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function(data) {
document.getElementById("message").innerHTML = `<p style="color:green">${data['message']}</p>`;
// redirect after logging in successfully