100 lines
3.1 KiB
Cheetah
100 lines
3.1 KiB
Cheetah
{{define "page"}}
|
|
<div class="login-wrapper">
|
|
<div class="login-card glass fade-in">
|
|
<div class="login-header">
|
|
<div class="brand">clavitor</div>
|
|
<p>Sign in to manage your vaults</p>
|
|
</div>
|
|
|
|
<button class="btn btn-passkey btn-block btn-lg" onclick="passkeyLogin()">
|
|
<svg class="passkey-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z"/>
|
|
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>
|
|
</svg>
|
|
Sign in with passkey
|
|
</button>
|
|
|
|
<div class="login-divider">or</div>
|
|
|
|
<form id="email-form" onsubmit="return emailLogin(event)">
|
|
<div class="field">
|
|
<label for="email">Email address</label>
|
|
<input type="email" id="email" class="input" placeholder="you@example.com" required autocomplete="email" autofocus>
|
|
</div>
|
|
<button type="submit" class="btn btn-ghost btn-block">
|
|
Continue with email
|
|
</button>
|
|
</form>
|
|
|
|
<div class="login-footer">
|
|
No account? <a href="{{.Base}}/checkout">Get started</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script>
|
|
async function passkeyLogin() {
|
|
const btn = document.querySelector('.btn-passkey');
|
|
const orig = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span style="opacity:0.6">Waiting for authenticator…</span>';
|
|
|
|
try {
|
|
// WebAuthn authentication
|
|
const resp = await fetch('{{.Base}}/api/auth/passkey/options', { method: 'POST' });
|
|
if (!resp.ok) throw new Error('Server error');
|
|
const options = await resp.json();
|
|
|
|
// TODO: navigator.credentials.get() with options
|
|
// For now, simulate
|
|
await new Promise(r => setTimeout(r, 800));
|
|
|
|
// On success, redirect to dashboard
|
|
window.location.href = '{{.Base}}/dashboard';
|
|
} catch (err) {
|
|
showToast(err.message || 'Authentication failed', 'error');
|
|
btn.disabled = false;
|
|
btn.innerHTML = orig;
|
|
}
|
|
}
|
|
|
|
async function emailLogin(e) {
|
|
e.preventDefault();
|
|
const email = document.getElementById('email').value;
|
|
const btn = e.target.querySelector('button');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Sending link…';
|
|
|
|
try {
|
|
const resp = await fetch('{{.Base}}/api/auth/email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email })
|
|
});
|
|
if (!resp.ok) throw new Error('Could not send login code');
|
|
// Redirect to code entry page
|
|
window.location.href = '{{.Base}}/verify?email=' + encodeURIComponent(email);
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Continue with email';
|
|
}
|
|
}
|
|
|
|
function showToast(msg, type) {
|
|
let t = document.querySelector('.toast');
|
|
if (!t) {
|
|
t = document.createElement('div');
|
|
t.className = 'toast';
|
|
document.body.appendChild(t);
|
|
}
|
|
t.textContent = msg;
|
|
t.className = 'toast ' + type;
|
|
requestAnimationFrame(() => t.classList.add('show'));
|
|
setTimeout(() => t.classList.remove('show'), type === 'error' ? 6000 : 3000);
|
|
}
|
|
</script>
|
|
{{end}}
|