Files

167 lines
6.5 KiB
HTML
Raw Permalink Normal View History

2026-09-13 20:09:20 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Account · Infosec</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<script src="/js/app.js"></script>
<script>
(async () => {
const shell = await buildShell('account'); if (!shell) return;
const { me, content } = shell;
function render() {
content.innerHTML = `
<div class="page-head">
<h1>My Account</h1>
<p>Manage your sign-in security.</p>
</div>
<div class="card" style="max-width:640px">
<div class="card-head"><h2>Profile</h2></div>
<div class="card-body">
<div class="profile-row">
<div class="avatar-lg" id="avatarPreview">${avatarMarkup(me)}</div>
<div style="flex:1">
<p><strong>Username:</strong> ${esc(me.username)}</p>
<p><strong>Email:</strong> ${esc(me.email)}</p>
<p><strong>Role:</strong> <span class="badge badge-admin">${esc(me.role)}</span></p>
</div>
</div>
<div class="divider"></div>
<h3 style="margin:0 0 6px">Profile picture</h3>
<p class="hint" style="margin:0 0 12px">
Shown next to your name in the sidebar. PNG, JPEG, WebP or GIF, up to 4 MB.
</p>
<div class="row-actions" style="justify-content:flex-start">
<label class="btn btn-primary">
${me.avatar ? 'Change picture' : 'Upload picture'}
<input type="file" id="avatarFile" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
</label>
<button class="btn btn-danger" id="avatarRemove" ${me.avatar ? '' : 'disabled'}>Remove</button>
</div>
<div class="err-line" id="avatarErr"></div>
</div>
</div>
<div class="card" style="max-width:640px">
<div class="card-head">
<h2>Multi-factor authentication</h2>
<span class="badge ${me.mfa_enabled ? 'badge-approved' : 'badge-pending'}">${me.mfa_enabled ? 'Enabled' : 'Disabled'}</span>
</div>
<div class="card-body" id="mfaBody"></div>
</div>`;
renderMfa();
}
function renderMfa() {
const body = document.getElementById('mfaBody');
if (me.mfa_enabled) {
body.innerHTML = `
<p>Your account is protected with an authenticator app. You'll be asked for a code each time you sign in.</p>
<div class="field" style="max-width:320px">
<label>Confirm password to turn off MFA</label>
<input type="password" id="offpass">
</div>
<button class="btn btn-danger" id="disableBtn">Turn off MFA</button>`;
document.getElementById('disableBtn').onclick = async () => {
try {
await api('/api/auth/mfa/disable', { method: 'POST', body: { password: document.getElementById('offpass').value } });
me.mfa_enabled = false; toast('MFA disabled', 'ok'); render();
} catch (e) { toast(e.message, 'err'); }
};
} else {
body.innerHTML = `
<p>Add a second layer of security. You'll scan a QR code with an authenticator app (Google Authenticator, Authy, 1Password…) and enter a code to confirm.</p>
<button class="btn btn-primary" id="startBtn">${ICON.shield} Set up MFA</button>`;
document.getElementById('startBtn').onclick = startSetup;
}
}
async function startSetup() {
const r = await api('/api/auth/mfa/setup', { method: 'POST' });
modal('Set up multi-factor authentication', `
<div class="qr-box">
<img src="${r.qr}" alt="QR code">
<p class="hint">Scan with your authenticator app, or enter this key manually:</p>
<div class="secret">${esc(r.base32)}</div>
</div>
<div class="field" style="margin-top:20px">
<label>Enter the 6-digit code to confirm</label>
<input type="text" id="confirmCode" class="otp-input" inputmode="numeric" maxlength="6" placeholder="••••••">
</div>
<div class="err-line" id="mfaErr"></div>`, {
sticky: true,
buttons: [
{ label: 'Cancel' },
{ label: 'Enable MFA', className: 'btn-primary', onClick: async (back) => {
try {
await api('/api/auth/mfa/enable', { method: 'POST', body: { token: back.querySelector('#confirmCode').value.trim() } });
me.mfa_enabled = true; toast('MFA enabled', 'ok'); back.remove(); render();
} catch (e) { back.querySelector('#mfaErr').textContent = e.message; }
return false;
} },
],
});
}
// Picture, or the user's initials on a coloured disc as a fallback.
function avatarMarkup(u) {
if (u.avatar) return `<img src="${esc(u.avatar)}" alt="Your profile picture">`;
const initials = (u.username || '?').slice(0, 2).toUpperCase();
return `<span class="avatar-initials">${esc(initials)}</span>`;
}
function bindAvatar() {
const fileInput = document.getElementById('avatarFile');
const removeBtn = document.getElementById('avatarRemove');
const err = document.getElementById('avatarErr');
if (fileInput) fileInput.onchange = async () => {
err.textContent = '';
const file = fileInput.files && fileInput.files[0];
if (!file) return;
if (file.size > 4 * 1024 * 1024) { err.textContent = 'That image is larger than 4 MB.'; return; }
const fd = new FormData();
fd.append('avatar', file);
try {
const r = await api('/api/auth/avatar', { method: 'POST', body: fd });
me.avatar = r.avatar;
toast('Profile picture updated', 'ok');
render(); bindAvatar();
refreshSidebarAvatar();
} catch (e) { err.textContent = e.message; }
};
if (removeBtn) removeBtn.onclick = async () => {
err.textContent = '';
try {
await api('/api/auth/avatar', { method: 'DELETE' });
me.avatar = null;
toast('Profile picture removed', 'ok');
render(); bindAvatar();
refreshSidebarAvatar();
} catch (e) { err.textContent = e.message; }
};
}
// Update the sidebar avatar live, without a page reload.
function refreshSidebarAvatar() {
const holder = document.querySelector('.side-user .u-avatar');
if (!holder) return;
holder.innerHTML = me.avatar
? `<img src="${esc(me.avatar)}?t=${Date.now()}" alt="">`
: `<span class="avatar-initials">${esc((me.username || '?').slice(0, 2).toUpperCase())}</span>`;
}
const _origRender = render;
render = function () { _origRender(); bindAvatar(); };
render();
})();
</script>
</body>
</html>