- 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
266 lines
10 KiB
HTML
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 }}
|