255 lines
12 KiB
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 -->
|