112 lines
6.0 KiB
HTML
112 lines
6.0 KiB
HTML
{{define "webmail_mfa_setup_required.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>Set up two-factor authentication - 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; }
|
|
.setup-card { max-width: 480px; margin: 0 auto; width: 100%; }
|
|
.card { background-color: #2d2d2d; border: 1px solid #404040; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1090;">
|
|
{{range .flashes}}
|
|
<div class="toast align-items-center text-bg-{{if eq .Category "error"}}danger{{else}}{{.Category}}{{end}} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="bi bi-{{if eq .Category "error"}}exclamation-triangle{{else if eq .Category "success"}}check-circle{{else}}info-circle{{end}} me-2"></i>
|
|
{{.Message}}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="container setup-card">
|
|
<div class="text-center mb-4">
|
|
<i class="bi bi-shield-lock-fill" style="font-size: 2.5rem;"></i>
|
|
<h4 class="mt-2">Two-factor authentication required</h4>
|
|
<p class="text-muted">Your administrator requires MFA for this mailbox (<strong>{{.email}}</strong>). Set up one of the options below to continue — nothing else is accessible until then. Any app passwords you already have keep working for email as normal.</p>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header"><h5 class="mb-0"><i class="bi bi-fingerprint me-2"></i>Passkey <span class="badge bg-primary ms-1">Recommended</span></h5></div>
|
|
<div class="card-body">
|
|
<p class="text-muted">Use your device's built-in security (fingerprint, face, or a hardware security key).</p>
|
|
<button type="button" class="btn btn-primary" id="passkey-add-btn"><i class="bi bi-fingerprint me-1"></i>Set up a Passkey</button>
|
|
<div id="passkey-error" class="alert alert-danger d-none mt-2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h5 class="mb-0"><i class="bi bi-qr-code me-2"></i>Authenticator App</h5></div>
|
|
<div class="card-body">
|
|
<p class="text-muted">Use Google Authenticator, 1Password, or any TOTP app.</p>
|
|
<form method="post" action="/webmail/account/totp/setup">
|
|
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-qr-code me-1"></i>Set up Authenticator App</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.toast').forEach(function(el) { new bootstrap.Toast(el, {delay: 6000}).show(); });
|
|
});
|
|
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(/=+$/, '');
|
|
}
|
|
document.getElementById('passkey-add-btn').addEventListener('click', async function() {
|
|
const errEl = document.getElementById('passkey-error');
|
|
errEl.classList.add('d-none');
|
|
try {
|
|
const beginResp = await fetch('/webmail/account/passkey/begin', { method: 'POST' });
|
|
if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey registration');
|
|
const options = await beginResp.json();
|
|
const publicKey = options.publicKey;
|
|
publicKey.challenge = b64urlToBuf(publicKey.challenge);
|
|
publicKey.user.id = b64urlToBuf(publicKey.user.id);
|
|
if (publicKey.excludeCredentials) {
|
|
publicKey.excludeCredentials = publicKey.excludeCredentials.map(c => ({ ...c, id: b64urlToBuf(c.id) }));
|
|
}
|
|
const cred = await navigator.credentials.create({ publicKey });
|
|
const body = {
|
|
id: cred.id, rawId: bufToB64url(cred.rawId), type: cred.type,
|
|
response: {
|
|
attestationObject: bufToB64url(cred.response.attestationObject),
|
|
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
|
|
},
|
|
};
|
|
const finishResp = await fetch('/webmail/account/passkey/finish', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
});
|
|
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Could not save passkey');
|
|
window.location.href = '/webmail/';
|
|
} catch (e) {
|
|
errEl.textContent = e.message || 'Passkey registration failed';
|
|
errEl.classList.remove('d-none');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|