89 lines
3.9 KiB
HTML
89 lines
3.9 KiB
HTML
{{define "login"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Login - LLM Gateway</title>
|
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
.auth-box { background: #1e293b; border-radius: 12px; padding: 32px; width: 100%; max-width: 400px; }
|
|
.auth-box h1 { text-align: center; margin-bottom: 24px; font-size: 1.5rem; color: #f8fafc; }
|
|
.form-group { margin-bottom: 16px; }
|
|
.form-group label { display: block; font-size: 0.85rem; color: #94a3b8; margin-bottom: 4px; }
|
|
.form-group input { width: 100%; padding: 10px 12px; background: #0f172a; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; font-size: 0.95rem; }
|
|
.form-group input:focus { outline: none; border-color: #3b82f6; }
|
|
.btn-primary { display: block; width: 100%; padding: 10px 20px; border-radius: 6px; border: none; cursor: pointer; font-size: 0.9rem; font-weight: 500; background: #3b82f6; color: #fff; }
|
|
.btn-primary:hover { background: #2563eb; }
|
|
.error-msg { background: #7f1d1d40; border: 1px solid #991b1b; color: #fca5a5; padding: 10px; border-radius: 6px; margin-bottom: 16px; font-size: 0.85rem; }
|
|
.hidden { display: none !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="auth-box">
|
|
<h1>LLM Gateway</h1>
|
|
<div id="login-error"></div>
|
|
<form id="login-form" onsubmit="doLogin(event)">
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input type="text" id="login-username" required autocomplete="username">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" id="login-password" required autocomplete="current-password">
|
|
</div>
|
|
<button type="submit" class="btn-primary">Sign In</button>
|
|
</form>
|
|
<form id="totp-form" class="hidden" onsubmit="doLoginTOTP(event)">
|
|
<div class="form-group">
|
|
<label>Enter your 6-digit authenticator code</label>
|
|
<input type="text" id="login-totp-code" required pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" inputmode="numeric" style="text-align:center; font-size:1.5rem; letter-spacing:0.3em;">
|
|
</div>
|
|
<button type="submit" class="btn-primary">Verify</button>
|
|
</form>
|
|
</div>
|
|
<script>
|
|
function showError(msg) {
|
|
document.getElementById('login-error').innerHTML = '<div class="error-msg">' + msg + '</div>';
|
|
}
|
|
async function doLogin(e) {
|
|
e.preventDefault();
|
|
try {
|
|
const resp = await fetch('/api/auth/login', {
|
|
method: 'POST', credentials: 'same-origin',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
username: document.getElementById('login-username').value,
|
|
password: document.getElementById('login-password').value
|
|
})
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok) { showError(data.error || 'Login failed'); return; }
|
|
if (data.require_totp) {
|
|
document.getElementById('login-form').classList.add('hidden');
|
|
document.getElementById('totp-form').classList.remove('hidden');
|
|
document.getElementById('login-totp-code').focus();
|
|
return;
|
|
}
|
|
window.location.href = '/dashboard';
|
|
} catch (e) { showError(e.message); }
|
|
}
|
|
async function doLoginTOTP(e) {
|
|
e.preventDefault();
|
|
try {
|
|
const resp = await fetch('/api/auth/login/totp', {
|
|
method: 'POST', credentials: 'same-origin',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ code: document.getElementById('login-totp-code').value })
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok) { showError(data.error || 'Invalid code'); return; }
|
|
window.location.href = '/dashboard';
|
|
} catch (e) { showError(e.message); }
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|