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
@@ -0,0 +1,122 @@
|
||||
{{define "title"}}
|
||||
My Access
|
||||
{{end}}
|
||||
|
||||
{{define "top_css"}}
|
||||
{{end}}
|
||||
|
||||
{{define "username"}}
|
||||
{{ .username }}
|
||||
{{end}}
|
||||
|
||||
{{define "page_title"}}
|
||||
My Access
|
||||
{{end}}
|
||||
|
||||
{{define "page_content"}}
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-2 flex-wrap">
|
||||
<h5 class="mb-2">My Access</h5>
|
||||
</div>
|
||||
|
||||
{{if not .clients}}
|
||||
<div class="alert alert-info">
|
||||
No VPN access has been assigned to your account yet — contact an administrator.
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row" id="my-access-list">
|
||||
{{range .clients}}
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{.Name}}</h3>
|
||||
<div class="card-tools">
|
||||
{{if .Enabled}}
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
{{else}}
|
||||
<span class="badge badge-secondary">Disabled</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-1"><strong>Email:</strong> {{if .Email}}{{.Email}}{{else}}—{{end}}</p>
|
||||
<p class="mb-1"><strong>Server:</strong> {{.ServerName}} ({{.Interface}})</p>
|
||||
<p class="mb-1"><strong>Allocated IP:</strong> {{ StringsJoin .AllocatedIPs ", " }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm btn-show-qr" data-clientid="{{.ID}}" data-clientname="{{.Name}}" data-toggle="modal" data-target="#modal_qr_client">
|
||||
<i class="fas fa-qrcode"></i> Show QR
|
||||
</button>
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{$.basePath}}/my-access/client/{{.ID}}/download">
|
||||
<i class="fas fa-download"></i> Download config
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="modal fade" id="modal_qr_client">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">QR Code</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
<!-- /.modal -->
|
||||
{{end}}
|
||||
|
||||
{{define "bottom_js"}}
|
||||
<script>
|
||||
$("#modal_qr_client").on('show.bs.modal', function (event) {
|
||||
const button = $(event.relatedTarget);
|
||||
const clientId = button.data('clientid');
|
||||
const clientName = button.data('clientname');
|
||||
const modal = $(this);
|
||||
const qrImg = $("#qr_code");
|
||||
|
||||
modal.find(".modal-title").text("Scan QR Code for " + clientName);
|
||||
qrImg.hide();
|
||||
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'GET',
|
||||
url: '{{.basePath}}/my-access/client/' + clientId + '/qrcode',
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
success: function (resp) {
|
||||
if (resp.QRCode) {
|
||||
qrImg.attr('src', resp.QRCode).show();
|
||||
} else {
|
||||
toastr.error('No QR code available for this client');
|
||||
}
|
||||
},
|
||||
error: function (jqXHR) {
|
||||
try {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
toastr.error(responseJson['message']);
|
||||
} catch (e) {
|
||||
toastr.error('Failed to load QR code');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user