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
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user