mfa fixing
This commit is contained in:
@@ -2,12 +2,6 @@
|
||||
{{define "page_title"}}Account Settings{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
{{if .mfa_required}}
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-shield-exclamation me-2"></i>
|
||||
Your administrator requires two-factor authentication for all admin accounts. Set up an authenticator app or a passkey below to continue using the dashboard.
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card">
|
||||
|
||||
@@ -27,22 +27,26 @@
|
||||
|
||||
{{if .has_passkeys}}
|
||||
<div class="d-grid mb-3">
|
||||
<button type="button" class="btn btn-outline-primary" id="passkey-btn">
|
||||
<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</div>{{end}}
|
||||
{{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="/pymta-manager/login/mfa">
|
||||
<form method="POST" action="/pymta-manager/login/mfa" id="totp-form" {{if .has_passkeys}}class="d-none"{{end}}>
|
||||
<input type="hidden" name="next" value="{{.next}}">
|
||||
<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 autofocus>
|
||||
<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 btn-primary"><i class="bi bi-shield-check me-1"></i>Verify</button>
|
||||
<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}}
|
||||
@@ -66,6 +70,16 @@
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
{{define "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 - mailgoserver</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 every account (signed in as <strong>{{.username}}</strong>). Set up one of the options below to continue — nothing else is accessible until then.</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="/pymta-manager/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('/pymta-manager/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('/pymta-manager/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 = '/pymta-manager/';
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message || 'Passkey registration failed';
|
||||
errEl.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -1,30 +1,45 @@
|
||||
{{define "title"}}Set up authenticator app{{end}}
|
||||
{{define "page_title"}}Set up authenticator app{{end}}
|
||||
{{define "totp_setup.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 authenticator app - mailgoserver</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; }
|
||||
.card { background-color: #2d2d2d; border: 1px solid #404040; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h5 class="mb-0"><i class="bi bi-qr-code me-2"></i>Scan with your authenticator app</h5></div>
|
||||
<div class="card-body text-center">
|
||||
{{if .qr_data_uri}}
|
||||
<img src="{{.qr_data_uri}}" alt="TOTP QR code" class="img-fluid mb-3" style="max-width: 256px; background: white; padding: 8px; border-radius: 8px;">
|
||||
{{end}}
|
||||
<p class="text-muted">Can't scan? Enter this key manually:</p>
|
||||
<code class="d-block mb-4" style="word-break: break-all;">{{.secret}}</code>
|
||||
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h5 class="mb-0"><i class="bi bi-qr-code me-2"></i>Scan with your authenticator app</h5></div>
|
||||
<div class="card-body text-center">
|
||||
{{if .qr_data_uri}}
|
||||
<img src="{{.qr_data_uri}}" alt="TOTP QR code" class="img-fluid mb-3" style="max-width: 256px; background: white; padding: 8px; border-radius: 8px;">
|
||||
{{end}}
|
||||
<p class="text-muted">Can't scan? Enter this key manually:</p>
|
||||
<code class="d-block mb-4" style="word-break: break-all;">{{.secret}}</code>
|
||||
|
||||
<form method="POST" action="/pymta-manager/account/totp/confirm" class="text-start">
|
||||
<div class="mb-3">
|
||||
<label for="code" class="form-label">Enter the 6-digit code from your app to confirm</label>
|
||||
<input type="text" class="form-control" id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autofocus>
|
||||
<form method="POST" action="/pymta-manager/account/totp/confirm" class="text-start">
|
||||
<div class="mb-3">
|
||||
<label for="code" class="form-label">Enter the 6-digit code from your app to confirm</label>
|
||||
<input type="text" class="form-control" id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autofocus>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="/pymta-manager/account" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Confirm and enable</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="/pymta-manager/account" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Confirm and enable</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
@@ -27,21 +27,25 @@
|
||||
|
||||
{{if .has_passkeys}}
|
||||
<div class="d-grid mb-3">
|
||||
<button type="button" class="btn btn-outline-primary" id="passkey-btn">
|
||||
<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</div>{{end}}
|
||||
{{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">
|
||||
<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 autofocus>
|
||||
<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 btn-primary"><i class="bi bi-shield-check me-1"></i>Verify</button>
|
||||
<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}}
|
||||
@@ -65,6 +69,16 @@
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
{{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}}
|
||||
Reference in New Issue
Block a user