fix(auth+bugs): optional auth bypass, login routes, conform column name, panel metadata fields, login page: login.html

This commit is contained in:
Zac Gaetano 2026-05-15 23:40:15 -04:00
parent 72c4a7f136
commit be8e0bda41

View file

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wild Dragon MAM &mdash; Login</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0f14;
--surface: #161921;
--border: #262c3a;
--accent: #e8a020;
--text: #e8eaf0;
--text-dim: #6b7280;
--error: #e05555;
--success: #3ecf6a;
--radius: 6px;
--input-h: 40px;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
.card {
width: 360px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 36px 32px 32px;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 28px;
}
.logo-icon {
width: 32px;
height: 32px;
background: var(--accent);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.logo-text {
line-height: 1.2;
}
.logo-name {
font-weight: 700;
font-size: 15px;
color: var(--text);
}
.logo-sub {
font-size: 11px;
color: var(--text-dim);
letter-spacing: 0.5px;
text-transform: uppercase;
}
h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 6px;
}
.subtitle {
color: var(--text-dim);
font-size: 13px;
margin-bottom: 24px;
}
.field {
margin-bottom: 16px;
}
label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
input {
width: 100%;
height: var(--input-h);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 14px;
padding: 0 12px;
outline: none;
transition: border-color 0.15s;
}
input:focus {
border-color: var(--accent);
}
.btn {
width: 100%;
height: 40px;
background: var(--accent);
border: none;
border-radius: var(--radius);
color: #0d0f14;
font-weight: 700;
font-size: 14px;
cursor: pointer;
margin-top: 8px;
transition: opacity 0.15s;
}
.btn:hover { opacity: 0.88; }
.btn:active { opacity: 0.76; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.flash {
border-radius: var(--radius);
padding: 10px 12px;
font-size: 13px;
margin-bottom: 16px;
display: none;
}
.flash.error { background: rgba(224,85,85,.12); border: 1px solid var(--error); color: var(--error); display: block; }
.flash.success { background: rgba(62,207,106,.12); border: 1px solid var(--success); color: var(--success); display: block; }
.setup-link {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: var(--text-dim);
}
.setup-link a {
color: var(--accent);
text-decoration: none;
}
.setup-link a:hover { text-decoration: underline; }
/* Setup panel (hidden by default) */
#setup-panel { display: none; }
</style>
</head>
<body>
<div class="card">
<div class="logo">
<div class="logo-icon">&#127922;</div>
<div class="logo-text">
<div class="logo-name">Wild Dragon</div>
<div class="logo-sub">Media Asset Management</div>
</div>
</div>
<!-- Flash message -->
<div id="flash" class="flash"></div>
<!-- Login form -->
<div id="login-panel">
<h1>Sign in</h1>
<p class="subtitle">Enter your credentials to continue</p>
<form id="login-form" autocomplete="on">
<div class="field">
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username" required placeholder="username">
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;">
</div>
<button type="submit" class="btn" id="login-btn">Sign in</button>
</form>
<div class="setup-link">
First time? <a href="#" id="show-setup">Create admin account</a>
</div>
</div>
<!-- First-run setup form (shown only if no users exist) -->
<div id="setup-panel">
<h1>Create admin</h1>
<p class="subtitle">No accounts exist yet &mdash; create the first admin</p>
<form id="setup-form" autocomplete="off">
<div class="field">
<label for="su-username">Username</label>
<input id="su-username" name="username" type="text" required placeholder="admin">
</div>
<div class="field">
<label for="su-password">Password (min 8 chars)</label>
<input id="su-password" name="password" type="password" required minlength="8" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;">
</div>
<div class="field">
<label for="su-display">Display name (optional)</label>
<input id="su-display" name="display_name" type="text" placeholder="Admin">
</div>
<button type="submit" class="btn" id="setup-btn">Create account</button>
</form>
<div class="setup-link">
<a href="#" id="show-login">&larr; Back to login</a>
</div>
</div>
</div>
<script>
const API = '/api/v1/auth';
const flash = document.getElementById('flash');
const loginPanel = document.getElementById('login-panel');
const setupPanel = document.getElementById('setup-panel');
const loginForm = document.getElementById('login-form');
const setupForm = document.getElementById('setup-form');
const loginBtn = document.getElementById('login-btn');
const setupBtn = document.getElementById('setup-btn');
function showFlash(msg, type) {
flash.textContent = msg;
flash.className = 'flash ' + type;
}
function clearFlash() {
flash.className = 'flash';
flash.textContent = '';
}
// Toggle between login and setup panels
document.getElementById('show-setup').addEventListener('click', (e) => {
e.preventDefault();
clearFlash();
loginPanel.style.display = 'none';
setupPanel.style.display = 'block';
});
document.getElementById('show-login').addEventListener('click', (e) => {
e.preventDefault();
clearFlash();
setupPanel.style.display = 'none';
loginPanel.style.display = 'block';
});
// Login
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
clearFlash();
loginBtn.disabled = true;
loginBtn.textContent = 'Signing in…';
try {
const res = await fetch(`${API}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
username: document.getElementById('username').value.trim(),
password: document.getElementById('password').value,
}),
});
if (res.ok) {
showFlash('Signed in — redirecting…', 'success');
setTimeout(() => { window.location.href = '/'; }, 600);
} else {
const data = await res.json().catch(() => ({}));
showFlash(data.error || 'Login failed', 'error');
}
} catch (err) {
showFlash('Network error: ' + err.message, 'error');
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Sign in';
}
});
// First-run setup
setupForm.addEventListener('submit', async (e) => {
e.preventDefault();
clearFlash();
setupBtn.disabled = true;
setupBtn.textContent = 'Creating…';
try {
const res = await fetch(`${API}/setup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
username: document.getElementById('su-username').value.trim(),
password: document.getElementById('su-password').value,
display_name: document.getElementById('su-display').value.trim(),
}),
});
if (res.ok) {
showFlash('Admin account created — you can now log in', 'success');
setTimeout(() => {
setupPanel.style.display = 'none';
loginPanel.style.display = 'block';
}, 1200);
} else {
const data = await res.json().catch(() => ({}));
showFlash(data.error || 'Setup failed', 'error');
}
} catch (err) {
showFlash('Network error: ' + err.message, 'error');
} finally {
setupBtn.disabled = false;
setupBtn.textContent = 'Create account';
}
});
</script>
</body>
</html>