Files
sysopsandClaude Sonnet 5 34bc8f76f9 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
2026-07-25 00:42:05 +02:00

266 lines
10 KiB
HTML

{{ define "title"}}
Profile
{{ end }}
{{ define "top_css"}}
{{ end }}
{{ define "username"}}
{{ .username }}
{{ end }}
{{ define "page_title"}}
Profile
{{ end }}
{{ define "page_content"}}
<section class="content">
<div class="container-fluid">
<!-- <h5 class="mt-4 mb-2">Global Settings</h5> -->
<div class="row">
<!-- left column -->
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Update user information</h3>
</div>
<!-- /.card-header -->
<!-- form start -->
<form role="form" id="frm_profile" name="frm_profile">
<div class="card-body">
<div class="form-group">
<label for="username" class="control-label">Username</label>
<input type="text" class="form-control" name="username" id="username"
value="">
</div>
<div class="form-group">
<label for="password" class="control-label">Password</label>
<input type="password" class="form-control" name="password" id="password"
value="" placeholder="Leave empty to keep the password unchanged">
</div>
<!-- /.card-body -->
<div class="card-footer">
<button type="submit" class="btn btn-success" id="update">Update</button>
</div>
</div>
</form>
</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>
</section>
{{ end }}
{{ define "bottom_js"}}
<script>
{
var previous_username;
var admin;
}
$(document).ready(function () {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}',
dataType: 'json',
contentType: "application/json",
success: function (resp) {
const user = resp;
$("#username").val(user.username);
previous_username = user.username;
admin = user.admin;
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
function updateUserInfo() {
const username = $("#username").val();
const password = $("#password").val();
const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
updateUserInfo();
}
});
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
});
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 }}