Files
wireguard-ui-multi/templates/login.html
T
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

182 lines
7.6 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>WireGuard UI</title>
<!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Favicon -->
<link rel="icon" href="{{.basePath}}/favicon">
<!-- Font Awesome -->
<link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css">
<!-- Ionicons -->
<link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<!-- icheck bootstrap -->
<link rel="stylesheet" href="{{.basePath}}/static/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="{{.basePath}}/static/dist/css/adminlte.min.css">
<!-- Google Font: Source Sans Pro -->
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">
</head>
<body class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<a href="https://github.com/ngoduykhanh/wireguard-ui">WireGuard UI</a>
</div>
<!-- /.login-logo -->
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg">Sign in to start your session</p>
<form action="" method="post">
<div class="input-group mb-3">
<input id="username" type="text" class="form-control" placeholder="Username">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
</div>
<div class="input-group mb-3">
<input id="password" type="password" class="form-control" placeholder="Password">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-8">
<div class="icheck-primary">
<input type="checkbox" id="remember">
<label for="remember">
Remember Me
</label>
</div>
</div>
<!-- /.col -->
<div class="col-4">
<button id="btn_login" type="submit" class="btn btn-primary btn-block">Sign In</button>
</div>
<!-- /.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>
</div>
<!-- /.login-card-body -->
</div>
</div>
<!-- /.login-box -->
<!-- jQuery -->
<script src="{{.basePath}}/static/plugins/jquery/jquery.min.js"></script>
<!-- Bootstrap 4 -->
<script src="{{.basePath}}/static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="{{.basePath}}/static/dist/js/adminlte.min.js"></script>
</body>
<script>
function redirectNext() {
const urlParams = new URLSearchParams(window.location.search);
const nextURL = urlParams.get('next');
if (nextURL && /(?:^\/[a-zA-Z_])|(?:^\/$)/.test(nextURL.trim())) {
window.location.href = nextURL;
} else {
window.location.href = '/{{.basePath}}';
}
}
</script>
<script>
$(document).ready(function () {
$('#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();
let rememberMe = false;
if ($("#remember").is(':checked')){
rememberMe = true;
}
const data = {"username": username, "password": password, "rememberMe": rememberMe}
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/login',
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
redirectNext();
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
document.getElementById("message").innerHTML = `<p style="color:#ff0000">${responseJson['message']}</p>`;
}
});
});
});
</script>
</html>