ai-servers/llm-gateway/internal/dashboard/templates/partials/settings.html

195 lines
8.1 KiB
HTML

{{define "content"}}
<div class="page-header">
<h1>Settings</h1>
</div>
<div class="section">
<h2>Profile</h2>
<div id="profile-msg"></div>
<form onsubmit="changeUsername(event)" style="max-width:400px;margin-bottom:16px;">
<div class="form-group">
<label>Username</label>
<div style="display:flex;gap:8px;">
<input type="text" id="settings-username" value="{{.User.Username}}" required>
<button type="submit" class="btn btn-sm btn-primary" style="white-space:nowrap;">Update</button>
</div>
</div>
</form>
<form onsubmit="changeEmail(event)" style="max-width:400px;">
<div class="form-group">
<label>Email</label>
<div style="display:flex;gap:8px;">
<input type="email" id="settings-email" value="{{.User.Email}}" placeholder="optional">
<button type="submit" class="btn btn-sm btn-primary" style="white-space:nowrap;">Update</button>
</div>
</div>
</form>
</div>
<div class="section">
<h2>Change Password</h2>
<div id="password-msg"></div>
<form onsubmit="changePassword(event)" style="max-width:400px;">
<div class="form-group">
<label>Current Password</label>
<input type="password" id="current-password" required autocomplete="current-password">
</div>
<div class="form-group">
<label>New Password (min 8 characters)</label>
<input type="password" id="new-password" required minlength="8" autocomplete="new-password">
</div>
<div class="form-group">
<label>Confirm New Password</label>
<input type="password" id="new-password2" required minlength="8" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary" style="width:auto;">Change Password</button>
</form>
</div>
<div class="section">
<h2>Two-Factor Authentication</h2>
<div id="totp-status">
{{if .User.TOTPEnabled}}
<p style="color:#4ade80;margin-bottom:12px;">Two-factor authentication is <strong>enabled</strong>.</p>
<button class="btn btn-sm btn-danger" onclick="disableTOTP()">Disable 2FA</button>
{{else}}
<p style="color:#94a3b8;margin-bottom:12px;">Two-factor authentication is <strong>not enabled</strong>.</p>
<button class="btn btn-sm btn-primary" onclick="setupTOTP()">Enable 2FA</button>
{{end}}
</div>
<div id="totp-setup-area" style="display:none;">
<p style="color:#94a3b8;font-size:0.85rem;margin-bottom:12px;">Scan this QR code with your authenticator app, then enter the code below to verify.</p>
<div id="totp-qr" style="text-align:center;margin:16px 0;"></div>
<div id="totp-secret-display" style="text-align:center;margin:8px 0;font-family:monospace;color:#94a3b8;font-size:0.8rem;"></div>
<form onsubmit="verifyTOTP(event)" style="max-width:300px;margin:0 auto;">
<div class="form-group">
<input type="text" id="totp-verify-code" required pattern="[0-9]{6}" maxlength="6" placeholder="Enter 6-digit code" autocomplete="one-time-code" inputmode="numeric" style="text-align:center;font-size:1.2rem;letter-spacing:0.2em;">
</div>
<button type="submit" class="btn btn-primary">Verify & Enable</button>
</form>
</div>
</div>
{{if .User.IsAdmin}}
<div class="section">
<h2>Config Validation</h2>
<p style="color:#94a3b8;font-size:0.85rem;margin-bottom:12px;">Validate the current gateway configuration file for errors.</p>
<button class="btn btn-sm btn-primary" onclick="validateConfig()">Validate Config</button>
<div id="config-validation-result" style="margin-top:12px;"></div>
</div>
{{end}}
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
<script>
function showMsg(id, msg, isError) {
document.getElementById(id).innerHTML = '<div class="' + (isError ? 'error-msg' : 'success-msg') + '">' + msg + '</div>';
setTimeout(function() { document.getElementById(id).innerHTML = ''; }, 5000);
}
async function changeUsername(e) {
e.preventDefault();
try {
var resp = await fetch('/api/auth/me/username', {
method: 'PUT', credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ new_username: document.getElementById('settings-username').value })
});
var data = await resp.json();
if (!resp.ok) { showMsg('profile-msg', data.error||'Failed', true); return; }
showMsg('profile-msg', 'Username updated', false);
} catch (e) { showMsg('profile-msg', e.message, true); }
}
async function changeEmail(e) {
e.preventDefault();
try {
var resp = await fetch('/api/auth/me/email', {
method: 'PUT', credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ email: document.getElementById('settings-email').value })
});
var data = await resp.json();
if (!resp.ok) { showMsg('profile-msg', data.error||'Failed', true); return; }
showMsg('profile-msg', 'Email updated', false);
} catch (e) { showMsg('profile-msg', e.message, true); }
}
async function changePassword(e) {
e.preventDefault();
var np = document.getElementById('new-password').value;
if (np !== document.getElementById('new-password2').value) {
showMsg('password-msg', 'Passwords do not match', true);
return;
}
try {
var resp = await fetch('/api/auth/me/password', {
method: 'PUT', credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
current_password: document.getElementById('current-password').value,
new_password: np
})
});
var data = await resp.json();
if (!resp.ok) { showMsg('password-msg', data.error||'Failed', true); return; }
showMsg('password-msg', 'Password updated', false);
document.getElementById('current-password').value = '';
document.getElementById('new-password').value = '';
document.getElementById('new-password2').value = '';
} catch (e) { showMsg('password-msg', e.message, true); }
}
async function setupTOTP() {
try {
var resp = await fetch('/api/auth/totp/setup', { method: 'POST', credentials: 'same-origin' });
var data = await resp.json();
if (!resp.ok) { alert(data.error||'Failed'); return; }
document.getElementById('totp-setup-area').style.display = 'block';
document.getElementById('totp-secret-display').textContent = 'Secret: ' + data.secret;
var qrDiv = document.getElementById('totp-qr');
qrDiv.innerHTML = '';
var canvas = document.createElement('canvas');
new QRious({ element: canvas, value: data.uri, size: 200, level: 'M' });
qrDiv.appendChild(canvas);
} catch (e) { alert(e.message); }
}
async function verifyTOTP(e) {
e.preventDefault();
try {
var resp = await fetch('/api/auth/totp/verify', {
method: 'POST', credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ code: document.getElementById('totp-verify-code').value })
});
var data = await resp.json();
if (!resp.ok) { alert(data.error||'Invalid code'); return; }
htmx.ajax('GET', '/settings', {target: '#content', swap: 'innerHTML'});
} catch (e) { alert(e.message); }
}
async function disableTOTP() {
if (!confirm('Disable two-factor authentication?')) return;
try {
var resp = await fetch('/api/auth/totp', { method: 'DELETE', credentials: 'same-origin' });
if (!resp.ok) { var d = await resp.json(); alert(d.error||'Failed'); return; }
htmx.ajax('GET', '/settings', {target: '#content', swap: 'innerHTML'});
} catch (e) { alert(e.message); }
}
async function validateConfig() {
var el = document.getElementById('config-validation-result');
el.innerHTML = '<span style="color:#94a3b8;">Validating...</span>';
try {
var resp = await fetch('/api/config/validate', { credentials: 'same-origin' });
var data = await resp.json();
if (data.valid) {
el.innerHTML = '<div class="success-msg">Configuration is valid.</div>';
} else {
var errs = (data.errors||[]).map(function(e) { return '<li>' + e + '</li>'; }).join('');
el.innerHTML = '<div class="error-msg">Configuration errors:<ul style="margin:4px 0 0 16px;">' + errs + '</ul></div>';
}
} catch (e) { el.innerHTML = '<div class="error-msg">' + e.message + '</div>'; }
}
</script>
{{end}}