171 lines
7 KiB
HTML
171 lines
7 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>
|
|
|
|
<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); }
|
|
}
|
|
</script>
|
|
{{end}}
|