295 lines
8.5 KiB
HTML
295 lines
8.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Login - Clickduck Control Center</title>
|
|
<link rel="icon" type="image/png" href="/static/clickduck_logo.png">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
rel="stylesheet">
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
<style>
|
|
.login-wrapper {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.login-card {
|
|
width: 100%;
|
|
max-width: 440px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-xl);
|
|
padding: 2.75rem 2.25rem;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.75), 0 0 0 1px var(--border-subtle);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.login-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: var(--primary-gradient);
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(12px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.login-brand {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-bottom: 2rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.login-logo-container {
|
|
position: relative;
|
|
width: 120px;
|
|
height: 120px;
|
|
margin-bottom: 1.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.login-logo-glow {
|
|
position: absolute;
|
|
inset: -6px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(56, 189, 248, 0.3) 0%, rgba(99, 102, 241, 0.15) 70%, transparent 100%);
|
|
filter: blur(8px);
|
|
z-index: 0;
|
|
animation: pulseGlow 4s ease-in-out infinite alternate;
|
|
}
|
|
|
|
@keyframes pulseGlow {
|
|
from { opacity: 0.5; transform: scale(0.96); }
|
|
to { opacity: 0.9; transform: scale(1.04); }
|
|
}
|
|
|
|
.login-logo-img {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
clip-path: circle(49% at 50% 50%);
|
|
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.6), 0 0 0 2px rgba(56, 189, 248, 0.3);
|
|
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.login-logo-container:hover .login-logo-img {
|
|
transform: scale(1.05) rotate(2deg);
|
|
box-shadow: 0 15px 35px -5px rgba(0, 0, 0, 0.7), 0 0 0 3px var(--primary);
|
|
}
|
|
|
|
.login-title {
|
|
font-size: 1.6rem;
|
|
font-weight: 700;
|
|
color: var(--text-main);
|
|
letter-spacing: -0.02em;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.login-subtitle {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
max-width: 320px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.login-form .form-group {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.login-form label {
|
|
display: block;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
|
|
.login-form input[type="text"],
|
|
.login-form input[type="password"] {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-main);
|
|
font-family: inherit;
|
|
font-size: 0.95rem;
|
|
outline: none;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.login-form input:focus {
|
|
border-color: var(--border-focus);
|
|
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2);
|
|
}
|
|
|
|
.login-btn {
|
|
width: 100%;
|
|
padding: 0.85rem;
|
|
margin-top: 0.75rem;
|
|
background: var(--primary-gradient);
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
box-shadow: 0 4px 15px var(--primary-glow);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.login-btn:hover {
|
|
opacity: 0.95;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px var(--primary-glow);
|
|
}
|
|
|
|
.login-btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.login-error {
|
|
background: rgba(244, 63, 94, 0.15);
|
|
border: 1px solid var(--danger);
|
|
color: #fecdd3;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.85rem;
|
|
margin-bottom: 1.25rem;
|
|
display: none;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="dark-theme">
|
|
<div class="login-wrapper">
|
|
<div class="login-card">
|
|
<div class="login-brand">
|
|
<div class="login-logo-container">
|
|
<div class="login-logo-glow"></div>
|
|
<img
|
|
src="/static/clickduck_logo.png"
|
|
alt="Clickduck Logo"
|
|
class="login-logo-img"
|
|
onerror="this.src='/static/clickduck_logo_small.jpg'"
|
|
>
|
|
</div>
|
|
<h1 class="login-title">Clickduck Portal</h1>
|
|
<p class="login-subtitle">Autonomous Web Crawler & Broken Link Detector</p>
|
|
</div>
|
|
|
|
<div id="loginError" class="login-error">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
</svg>
|
|
<span id="errorMessage">Invalid username or password</span>
|
|
</div>
|
|
|
|
<form id="loginForm" class="login-form">
|
|
<div class="form-group">
|
|
<label for="username">Username</label>
|
|
<input type="text" id="username" name="username" required autocomplete="username" placeholder="admin" autofocus>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password</label>
|
|
<input type="password" id="password" name="password" required autocomplete="current-password" placeholder="••••••••••••">
|
|
</div>
|
|
|
|
<button type="submit" id="btnSubmit" class="login-btn">
|
|
<span>Sign In</span>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M5 12h14"></path>
|
|
<path d="M12 5l7 7-7 7"></path>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('loginForm');
|
|
const errBox = document.getElementById('loginError');
|
|
const errMsg = document.getElementById('errorMessage');
|
|
const btn = document.getElementById('btnSubmit');
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
errBox.style.display = 'none';
|
|
btn.disabled = true;
|
|
btn.querySelector('span').textContent = 'Authenticating...';
|
|
|
|
const username = document.getElementById('username').value.trim();
|
|
const password = document.getElementById('password').value;
|
|
|
|
try {
|
|
const resp = await fetch('/api/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const data = await resp.json();
|
|
|
|
if (resp.ok && data.success) {
|
|
btn.querySelector('span').textContent = 'Success! Redirecting...';
|
|
const redirectUrl = new URLSearchParams(window.location.search).get('redirect') || '/';
|
|
window.location.href = redirectUrl;
|
|
} else {
|
|
errMsg.textContent = data.detail || data.message || 'Invalid username or password';
|
|
errBox.style.display = 'flex';
|
|
btn.disabled = false;
|
|
btn.querySelector('span').textContent = 'Sign In';
|
|
}
|
|
} catch (err) {
|
|
errMsg.textContent = 'Connection error. Please try again.';
|
|
errBox.style.display = 'flex';
|
|
btn.disabled = false;
|
|
btn.querySelector('span').textContent = 'Sign In';
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|