first commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
{{define "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 - 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; }
|
||||
.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-outline-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}}
|
||||
{{end}}
|
||||
|
||||
{{if .totp_enabled}}
|
||||
<form method="POST" action="/pymta-manager/login/mfa">
|
||||
<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>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary"><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 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('/pymta-manager/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('/pymta-manager/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 = {{if .next}}'{{.next}}'{{else}}'/pymta-manager/'{{end}};
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message || 'Passkey login failed';
|
||||
errEl.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user