- db.py: beauty_users + beauty_sessions tables; PBKDF2-SHA256 password hashing; init_beauty_auth seeds default Admin user; full CRUD helpers - beauty_main.py: AuthMiddleware blocks all routes except /api/auth/* and /login.html; auth routes: login (sets HttpOnly 30-day cookie), logout, /me, change-password (invalidates sessions), list/add/delete users - login.html: standalone dark-themed sign-in page matching app palette - index.html: auth check in init() → redirects to login.html if 401; header shows username + settings gear + sign-out; settings modal with change-password form and admin user management (add/delete users) Default credentials: Admin / Asdpsd9012!HAP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
3.8 KiB
HTML
131 lines
3.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BeautyLeads — Sign In</title>
|
|
<style>
|
|
:root{
|
|
--bg:#0f0f13;--surface:#18181f;--card:#1e1e28;--border:#2a2a38;
|
|
--text:#e2e0f0;--muted:#7c7a96;--accent:#e879a0;--accent2:#c026d3;
|
|
--danger:#f43f5e;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{
|
|
background:var(--bg);color:var(--text);
|
|
font-family:'Segoe UI',system-ui,sans-serif;font-size:14px;
|
|
min-height:100vh;display:flex;align-items:center;justify-content:center;
|
|
}
|
|
.wrap{width:100%;max-width:360px;padding:24px}
|
|
|
|
.logo{
|
|
text-align:center;margin-bottom:32px;
|
|
}
|
|
.logo-name{
|
|
font-size:26px;font-weight:800;
|
|
background:linear-gradient(135deg,var(--accent),var(--accent2));
|
|
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
|
|
}
|
|
.logo-sub{color:var(--muted);font-size:12px;margin-top:4px}
|
|
|
|
.card{
|
|
background:var(--card);border:1px solid var(--border);border-radius:14px;
|
|
padding:28px 24px;
|
|
box-shadow:0 8px 40px rgba(0,0,0,.45);
|
|
}
|
|
h2{font-size:16px;font-weight:700;margin-bottom:20px;text-align:center}
|
|
|
|
label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;
|
|
letter-spacing:.06em;margin-bottom:5px}
|
|
input{
|
|
display:block;width:100%;background:var(--surface);color:var(--text);
|
|
border:1px solid var(--border);border-radius:8px;
|
|
padding:10px 12px;font-size:13px;outline:none;
|
|
transition:border-color .15s;
|
|
}
|
|
input:focus{border-color:var(--accent)}
|
|
.field{margin-bottom:16px}
|
|
|
|
.btn{
|
|
display:block;width:100%;padding:11px;
|
|
background:linear-gradient(135deg,var(--accent),var(--accent2));
|
|
color:#fff;border:none;border-radius:8px;
|
|
font-size:14px;font-weight:700;cursor:pointer;
|
|
transition:opacity .15s;margin-top:8px;
|
|
}
|
|
.btn:hover{opacity:.88}
|
|
.btn:disabled{opacity:.5;cursor:default}
|
|
|
|
.error{
|
|
background:rgba(244,63,94,.12);border:1px solid rgba(244,63,94,.35);
|
|
color:var(--danger);border-radius:8px;padding:9px 12px;
|
|
font-size:12px;margin-bottom:14px;display:none;
|
|
}
|
|
.error.show{display:block}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<div class="logo">
|
|
<div class="logo-name">BeautyLeads</div>
|
|
<div class="logo-sub">Cosmetics B2B Intelligence</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Sign In</h2>
|
|
<div class="error" id="err"></div>
|
|
|
|
<form id="loginForm">
|
|
<div class="field">
|
|
<label for="username">Username</label>
|
|
<input id="username" type="text" autocomplete="username" autofocus required>
|
|
</div>
|
|
<div class="field">
|
|
<label for="password">Password</label>
|
|
<input id="password" type="password" autocomplete="current-password" required>
|
|
</div>
|
|
<button class="btn" type="submit" id="submitBtn">Sign In</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('loginForm');
|
|
const err = document.getElementById('err');
|
|
const btn = document.getElementById('submitBtn');
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
err.className = 'error';
|
|
btn.disabled = true;
|
|
btn.textContent = 'Signing in…';
|
|
|
|
try {
|
|
const r = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
username: document.getElementById('username').value,
|
|
password: document.getElementById('password').value,
|
|
}),
|
|
});
|
|
if (r.ok) {
|
|
window.location.replace('/');
|
|
} else {
|
|
const d = await r.json().catch(() => ({}));
|
|
err.textContent = d.detail || 'Login failed';
|
|
err.className = 'error show';
|
|
btn.disabled = false;
|
|
btn.textContent = 'Sign In';
|
|
}
|
|
} catch (ex) {
|
|
err.textContent = 'Network error — please try again';
|
|
err.className = 'error show';
|
|
btn.disabled = false;
|
|
btn.textContent = 'Sign In';
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|