access-manager/public/login.html

255 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In — scottfelten.com</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0f172a; color: #e2e8f0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-card { background: #1e293b; border: 1px solid #334155; border-radius: 16px; width: 90%; max-width: 420px; padding: 32px; }
.logo { text-align: center; margin-bottom: 24px; }
.logo h1 { font-size: 22px; font-weight: 700; color: white; }
.logo p { font-size: 12px; color: #64748b; margin-top: 4px; }
/* Tabs */
.tabs { display: flex; gap: 4px; margin-bottom: 24px; background: #0f172a; border-radius: 8px; padding: 4px; }
.tab { flex: 1; padding: 8px 12px; text-align: center; font-size: 12px; font-weight: 600; color: #64748b; border-radius: 6px; cursor: pointer; transition: all 0.15s; border: none; background: none; }
.tab:hover { color: #94a3b8; }
.tab.active { background: #334155; color: white; }
/* Panels */
.panel { display: none; }
.panel.active { display: block; }
/* Form */
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
.input { width: 100%; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 12px 14px; font-size: 14px; color: #e2e8f0; outline: none; font-family: inherit; transition: border-color 0.15s; }
.input:focus { border-color: #3b82f6; }
.input::placeholder { color: #475569; }
.btn { width: 100%; padding: 12px; border: none; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.15s; }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-google { background: white; color: #1f2937; display: flex; align-items: center; justify-content: center; gap: 10px; }
.btn-google:hover { background: #f1f5f9; }
.btn-google svg { width: 18px; height: 18px; }
.divider { display: flex; align-items: center; gap: 12px; margin: 20px 0; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #334155; }
.divider span { font-size: 11px; color: #475569; text-transform: uppercase; letter-spacing: 0.05em; }
.message { padding: 12px 16px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; }
.message-success { background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.2); color: #4ade80; }
.message-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); color: #f87171; }
.message-info { background: rgba(59,130,246,0.1); border: 1px solid rgba(59,130,246,0.2); color: #60a5fa; }
.link { color: #3b82f6; cursor: pointer; font-size: 12px; text-decoration: none; }
.link:hover { text-decoration: underline; }
.password-req { font-size: 11px; color: #475569; margin-top: 4px; }
.footer { text-align: center; margin-top: 20px; padding-top: 16px; border-top: 1px solid #334155; }
.footer p { font-size: 11px; color: #475569; }
</style>
</head>
<body>
<div class="login-card">
<div class="logo">
<h1>scottfelten.com</h1>
<p>Sign in to access services</p>
</div>
<div id="msg"></div>
<!-- Reset password form (shown when ?reset=token) -->
<div id="reset-panel" style="display:none">
<h3 style="color:white;font-size:16px;margin-bottom:16px;">Set New Password</h3>
<div class="field">
<label>New Password</label>
<input type="password" id="reset-pw" class="input" placeholder="Minimum 12 characters" minlength="12">
<div class="password-req">At least 12 characters</div>
</div>
<div class="field">
<label>Confirm Password</label>
<input type="password" id="reset-pw2" class="input" placeholder="Confirm password">
</div>
<button class="btn btn-primary" onclick="doReset()">Set Password</button>
</div>
<!-- Main login form -->
<div id="login-panels">
<div class="tabs">
<button class="tab active" data-tab="google" onclick="switchTab('google')">Google</button>
<button class="tab" data-tab="magic" onclick="switchTab('magic')">Magic Link</button>
<button class="tab" data-tab="password" onclick="switchTab('password')">Password</button>
</div>
<!-- Google SSO -->
<div id="tab-google" class="panel active">
<p style="font-size:13px;color:#94a3b8;margin-bottom:16px;">Sign in with your Google account. Recommended for team members.</p>
<a href="/oauth2/start?rd=/" class="btn btn-google" style="text-decoration:none;">
<svg viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
Sign in with Google
</a>
</div>
<!-- Magic Link -->
<div id="tab-magic" class="panel">
<p style="font-size:13px;color:#94a3b8;margin-bottom:16px;">We'll email you a one-time login link. No password needed.</p>
<div class="field">
<label>Email</label>
<input type="email" id="magic-email" class="input" placeholder="you@example.com">
</div>
<button class="btn btn-primary" id="magic-btn" onclick="requestMagicLink()">Send Login Link</button>
</div>
<!-- Email + Password -->
<div id="tab-password" class="panel">
<div class="field">
<label>Email</label>
<input type="email" id="pw-email" class="input" placeholder="you@example.com">
</div>
<div class="field">
<label>Password</label>
<input type="password" id="pw-pass" class="input" placeholder="Your password">
</div>
<button class="btn btn-primary" onclick="doLogin()">Sign In</button>
<div style="text-align:center;margin-top:12px;">
<a class="link" onclick="requestPasswordReset()">Forgot password?</a>
</div>
</div>
</div>
<div class="footer">
<p>Access is by invitation only</p>
</div>
</div>
<script>
// Check for URL params
const params = new URLSearchParams(location.search);
const rd = params.get('rd') || '/';
const error = params.get('error');
const resetToken = params.get('reset');
if (error === 'expired') showMsg('Login link has expired. Please request a new one.', 'error');
if (error === 'not_registered') showMsg('Email not registered. Contact admin for access.', 'error');
if (resetToken) {
document.getElementById('login-panels').style.display = 'none';
document.getElementById('reset-panel').style.display = 'block';
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
document.getElementById('msg').innerHTML = '';
}
function showMsg(text, type) {
document.getElementById('msg').innerHTML = `<div class="message message-${type}">${text}</div>`;
}
async function requestMagicLink() {
const email = document.getElementById('magic-email').value.trim();
if (!email) return showMsg('Enter your email address', 'error');
const btn = document.getElementById('magic-btn');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const res = await fetch('/auth/magic/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, redirect: rd }),
});
const data = await res.json();
showMsg('Check your email for a login link. It expires in 15 minutes.', 'success');
btn.textContent = 'Link Sent ✓';
} catch (e) {
showMsg('Error sending link. Try again.', 'error');
btn.disabled = false;
btn.textContent = 'Send Login Link';
}
}
async function doLogin() {
const email = document.getElementById('pw-email').value.trim();
const password = document.getElementById('pw-pass').value;
if (!email || !password) return showMsg('Enter email and password', 'error');
try {
const res = await fetch('/auth/password/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (res.ok) {
location.href = rd;
} else {
let msg = data.error;
if (data.attemptsRemaining !== undefined && data.attemptsRemaining > 0) {
msg += ` (${data.attemptsRemaining} attempts remaining)`;
}
showMsg(msg, 'error');
}
} catch (e) {
showMsg('Connection error. Try again.', 'error');
}
}
async function requestPasswordReset() {
const email = document.getElementById('pw-email').value.trim();
if (!email) return showMsg('Enter your email first, then click Forgot Password', 'info');
try {
const res = await fetch('/auth/password/reset-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
showMsg('If that email is registered, a reset link has been sent.', 'success');
} catch {
showMsg('Error. Try again.', 'error');
}
}
async function doReset() {
const pw = document.getElementById('reset-pw').value;
const pw2 = document.getElementById('reset-pw2').value;
if (pw.length < 12) return showMsg('Password must be at least 12 characters', 'error');
if (pw !== pw2) return showMsg('Passwords don\'t match', 'error');
try {
const res = await fetch('/auth/password/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: resetToken, password: pw }),
});
const data = await res.json();
if (res.ok) {
showMsg('Password set! Redirecting to login...', 'success');
setTimeout(() => { location.href = '/login.html'; }, 2000);
} else {
showMsg(data.error, 'error');
}
} catch {
showMsg('Error. Try again.', 'error');
}
}
// Enter key handlers
document.getElementById('magic-email')?.addEventListener('keydown', e => { if (e.key === 'Enter') requestMagicLink(); });
document.getElementById('pw-pass')?.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
document.getElementById('reset-pw2')?.addEventListener('keydown', e => { if (e.key === 'Enter') doReset(); });
</script>
</body>
</html>
<!-- DEV PIPELINE TEST -->