Files
mailgoserver/internal/webui/templates/webmail_login_mfa.html
T
2026-08-13 10:40:27 +01:00

128 lines
5.9 KiB
HTML

{{define "webmail_login_mfa.html"}}
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify it's you - Webmail</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.login-card { max-width: 420px; margin: 0 auto; width: 100%; }
.card { background-color: #2d2d2d; border: 1px solid #404040; }
</style>
</head>
<body>
<div class="container login-card">
<div class="text-center mb-4">
<i class="bi bi-shield-lock-fill" style="font-size: 2.5rem;"></i>
<h4 class="mt-2">Verify it's you</h4>
<p class="text-muted">One more step to finish signing in</p>
</div>
<div class="card">
<div class="card-body p-4">
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
<div id="passkey-error" class="alert alert-danger d-none"></div>
{{if .has_passkeys}}
<div class="d-grid mb-3">
<button type="button" class="btn btn-primary" id="passkey-btn">
<i class="bi bi-fingerprint me-1"></i>Use a passkey / security key
</button>
</div>
{{if .totp_enabled}}
<div class="text-center text-muted mb-3">
or <a href="#" id="show-totp-link">use an authenticator app code instead</a>
</div>
{{end}}
{{end}}
{{if .totp_enabled}}
<form method="POST" action="/webmail/login/mfa" id="totp-form" {{if .has_passkeys}}class="d-none"{{end}}>
<div class="mb-3">
<label for="code" class="form-label">6-digit authenticator code</label>
<input type="text" class="form-control" id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required {{if not .has_passkeys}}autofocus{{end}}>
</div>
<div class="d-grid">
<button type="submit" class="btn {{if .has_passkeys}}btn-outline-primary{{else}}btn-primary{{end}}"><i class="bi bi-shield-check me-1"></i>Verify</button>
</div>
</form>
{{end}}
</div>
</div>
</div>
<script>
function b64urlToBuf(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const bin = atob(s);
const buf = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
return buf.buffer;
}
function bufToB64url(buf) {
const bytes = new Uint8Array(buf);
let bin = '';
bytes.forEach(b => bin += String.fromCharCode(b));
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const showTotpLink = document.getElementById('show-totp-link');
if (showTotpLink) {
showTotpLink.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('totp-form').classList.remove('d-none');
document.getElementById('code').focus();
this.parentElement.classList.add('d-none');
});
}
const passkeyBtn = document.getElementById('passkey-btn');
if (passkeyBtn) {
passkeyBtn.addEventListener('click', async function() {
const errEl = document.getElementById('passkey-error');
errEl.classList.add('d-none');
try {
const beginResp = await fetch('/webmail/login/passkey/begin');
if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey login');
const options = await beginResp.json();
const publicKey = options.publicKey;
publicKey.challenge = b64urlToBuf(publicKey.challenge);
if (publicKey.allowCredentials) {
publicKey.allowCredentials = publicKey.allowCredentials.map(c => ({ ...c, id: b64urlToBuf(c.id) }));
}
const assertion = await navigator.credentials.get({ publicKey });
const body = {
id: assertion.id,
rawId: bufToB64url(assertion.rawId),
type: assertion.type,
response: {
authenticatorData: bufToB64url(assertion.response.authenticatorData),
clientDataJSON: bufToB64url(assertion.response.clientDataJSON),
signature: bufToB64url(assertion.response.signature),
userHandle: assertion.response.userHandle ? bufToB64url(assertion.response.userHandle) : null,
},
};
const finishResp = await fetch('/webmail/login/passkey/finish', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
});
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Passkey verification failed');
window.location.href = '/webmail/';
} catch (e) {
errEl.textContent = e.message || 'Passkey login failed';
errEl.classList.remove('d-none');
}
});
}
</script>
</body>
</html>
{{end}}