TreeTrack / static /login.html
RoyAalekh's picture
Security: remove auto-login demo endpoint, require password for demo_user
2a49dd7
raw
history blame
18.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TreeTrack - Sign In</title>
<link rel="icon" type="image/png" href="/static/image/icons8-tree-96.png">
<link rel="apple-touch-icon" href="/static/image/icons8-tree-96.png">
<link rel="stylesheet" href="/static/css/design-system.css">
<style>
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
position: relative;
overflow: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Static Background Image */
.forest-background {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
background-image: url('https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop&ixlib=rb-4.0.3');
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
filter: brightness(0.9);
}
/* Elegant Login Container */
.tt-login-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: var(--radius-3xl);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1);
padding: var(--space-8) var(--space-10);
width: 100%;
max-width: 420px;
position: relative;
/* Remove animation to prevent upward drift on refresh */
opacity: 1;
transform: translateY(0);
}
/* Optional: Add animation only on first visit */
.tt-login-container.animate-in {
animation: slideUp 0.6s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Logo Section */
.logo-section {
text-align: center;
margin-bottom: var(--space-8);
}
.logo {
font-size: var(--text-4xl);
font-weight: var(--font-bold);
background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: var(--space-2);
letter-spacing: -0.02em;
}
.logo-subtitle {
color: var(--gray-600);
font-size: var(--text-sm);
font-weight: var(--font-medium);
margin-bottom: var(--space-2);
}
/* Form Styling */
.login-form {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.tt-form-input {
padding: var(--space-4) var(--space-5);
font-size: var(--text-base);
border: 2px solid var(--gray-200);
border-radius: var(--radius-xl);
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.8);
}
.tt-form-input:focus {
border-color: var(--primary-500);
box-shadow: 0 0 0 4px rgba(138, 176, 112, 0.1);
background: white;
}
.tt-form-label {
font-weight: var(--font-semibold);
color: var(--gray-700);
margin-bottom: var(--space-2);
}
/* Button Styling */
.login-button {
margin-top: var(--space-2);
position: relative;
overflow: hidden;
border-radius: var(--radius-xl);
padding: var(--space-4) var(--space-6);
font-size: var(--text-base);
font-weight: var(--font-semibold);
background: var(--gradient-primary);
border: none;
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.login-button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.login-button.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid transparent;
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Demo Accounts Section - Minimized */
.demo-accounts {
margin-top: var(--space-6);
position: relative;
}
.demo-toggle {
background: none;
border: none;
color: var(--primary-600);
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
padding: var(--space-3) 0;
width: 100%;
text-align: center;
border-radius: var(--radius-lg);
transition: all 0.2s ease;
border: 1px solid transparent;
}
.demo-toggle:hover {
background: var(--primary-50);
border-color: var(--primary-200);
}
.accounts-dropdown {
position: absolute;
bottom: 100%; /* open upwards to avoid being cut off at the bottom */
left: 0;
right: 0;
background: white;
border: 1px solid var(--gray-200);
border-radius: var(--radius-xl);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
z-index: 20;
overflow: hidden;
opacity: 0;
transform: translateY(10px); /* animate from below upwards */
transition: all 0.2s ease;
pointer-events: none;
}
.accounts-dropdown.show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.account-item {
background: transparent;
padding: var(--space-4) var(--space-5);
border: none;
width: 100%;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
border-bottom: 1px solid var(--gray-100);
}
.account-item:last-child {
border-bottom: none;
}
.account-item:hover {
background: var(--primary-50);
}
.account-role {
font-weight: var(--font-semibold);
color: var(--gray-900);
font-size: var(--text-sm);
margin-bottom: var(--space-1);
}
.account-username {
color: var(--gray-600);
font-size: var(--text-xs);
}
/* Messages */
.tt-message {
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-lg);
font-size: var(--text-sm);
font-weight: var(--font-medium);
margin-bottom: var(--space-4);
}
.tt-message-error {
background: var(--error-50);
color: var(--error-700);
border: 1px solid var(--error-200);
}
.tt-message-success {
background: var(--success-50);
color: var(--success-700);
border: 1px solid var(--success-200);
}
/* Footer */
.footer {
text-align: center;
margin-top: var(--space-6);
color: var(--gray-500);
font-size: var(--text-xs);
}
/* Mobile Responsiveness */
@media (max-width: 640px) {
body {
padding: var(--space-2);
}
.tt-login-container {
padding: var(--space-6) var(--space-6);
max-width: 100%;
margin: 0;
}
.logo {
font-size: var(--text-3xl);
}
}
@media (max-width: 480px) {
.tt-login-container {
padding: var(--space-5);
}
.logo {
font-size: var(--text-2xl);
}
}
/* Demo Account Styling */
.account-item.demo-account {
background: linear-gradient(135deg, var(--primary-50), var(--primary-100));
border: 2px solid var(--primary-200);
margin-bottom: var(--space-2);
}
.account-item.demo-account:hover {
background: linear-gradient(135deg, var(--primary-100), var(--primary-150));
border-color: var(--primary-300);
transform: translateY(-1px);
}
.account-item.demo-account .account-role {
color: var(--primary-700);
font-weight: var(--font-semibold);
}
.account-item.demo-account .account-username {
color: var(--primary-600);
}
/* Focus improvements */
.demo-toggle:focus,
.account-item:focus {
outline: 2px solid var(--primary-500);
outline-offset: 2px;
}
</style>
</head>
<body>
<!-- Forest Background -->
<div class="forest-background"></div>
<div class="tt-login-container">
<div class="logo-section">
<div class="logo">TreeTrack</div>
<div class="logo-subtitle">Secure Field Research Access</div>
</div>
<form class="login-form" id="loginForm">
<div id="message" class="tt-message" style="display: none;"></div>
<div class="tt-form-group">
<label class="tt-form-label" for="username">Username</label>
<input class="tt-form-input" type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="tt-form-group">
<label class="tt-form-label" for="password">Password</label>
<input class="tt-form-input" type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button class="tt-btn tt-btn-primary tt-btn-lg login-button" type="submit" id="loginButton">
<span id="buttonText">Sign In to TreeTrack</span>
</button>
</form>
<div class="demo-accounts">
<button class="demo-toggle" id="demoToggle" onclick="toggleDemoAccounts()">
Choose Account ↓
</button>
<div class="accounts-dropdown" id="accountsDropdown">
<button class="account-item demo-account" onclick="selectAccount('demo_user', event)">
<div class="account-role">Demo Account</div>
<div class="account-username">demo_user (requires password)</div>
</button>
<button class="account-item" onclick="selectAccount('aalekh', event)">
<div class="account-role">Aalekh (Admin)</div>
<div class="account-username">Full system access</div>
</button>
<button class="account-item" onclick="selectAccount('admin', event)">
<div class="account-role">System Admin</div>
<div class="account-username">Administrative access</div>
</button>
<button class="account-item" onclick="selectAccount('ishita', event)">
<div class="account-role">Ishita</div>
<div class="account-username">Tree research & documentation</div>
</button>
<button class="account-item" onclick="selectAccount('jeeb', event)">
<div class="account-role">Jeeb</div>
<div class="account-username">Tree research & documentation</div>
</button>
</div>
</div>
<div class="footer">
© 2025 TreeTrack - Secure Field Research Platform
</div>
</div>
<script>
function toggleDemoAccounts() {
const dropdown = document.getElementById('accountsDropdown');
const toggle = document.getElementById('demoToggle');
if (dropdown.classList.contains('show')) {
dropdown.classList.remove('show');
toggle.textContent = 'Choose Account ↓';
} else {
dropdown.classList.add('show');
toggle.textContent = 'Hide Accounts ↑';
}
}
function selectAccount(username, event) {
document.getElementById('username').value = username;
document.getElementById('password').value = '';
document.getElementById('password').focus();
// Hide dropdown after selection
const dropdown = document.getElementById('accountsDropdown');
const toggle = document.getElementById('demoToggle');
dropdown.classList.remove('show');
toggle.textContent = 'Choose Account ↓';
// Visual feedback on selection
const button = event.target.closest('.account-item');
if (button) {
button.style.background = 'var(--primary-100)';
setTimeout(() => {
button.style.background = 'transparent';
}, 300);
}
}
// Legacy function for backward compatibility
function fillCredentials(username, event = null) {
selectAccount(username, event || { target: document.body });
}
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const demoSection = document.querySelector('.demo-accounts');
const dropdown = document.getElementById('accountsDropdown');
if (!demoSection.contains(event.target) && dropdown.classList.contains('show')) {
dropdown.classList.remove('show');
document.getElementById('demoToggle').textContent = 'Choose Account ↓';
}
});
function showMessage(message, type = 'error') {
const messageEl = document.getElementById('message');
messageEl.textContent = message;
messageEl.className = `tt-message tt-message-${type} tt-fade-in`;
messageEl.style.display = 'block';
if (type === 'success') {
setTimeout(() => {
messageEl.style.display = 'none';
}, 3000);
}
}
function setLoading(loading) {
const button = document.getElementById('loginButton');
const buttonText = document.getElementById('buttonText');
if (loading) {
button.disabled = true;
button.classList.add('loading');
buttonText.textContent = 'Signing In...';
} else {
button.disabled = false;
button.classList.remove('loading');
buttonText.textContent = 'Sign In to TreeTrack';
}
}
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) {
showMessage('Please enter both username and password');
return;
}
setLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (response.ok) {
// Store authentication token
localStorage.setItem('auth_token', result.token);
localStorage.setItem('user_info', JSON.stringify(result.user));
showMessage('Login successful! Redirecting...', 'success');
// Redirect to main application
setTimeout(() => {
window.location.href = '/';
}, 1500);
} else {
showMessage(result.detail || 'Login failed. Please check your credentials.');
}
} catch (error) {
console.error('Login error:', error);
showMessage('Network error. Please try again.');
} finally {
setLoading(false);
}
});
// Check if already logged in
document.addEventListener('DOMContentLoaded', () => {
const token = localStorage.getItem('auth_token');
if (token) {
// Validate token
fetch('/api/auth/validate', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => response.ok ? window.location.href = '/' : null)
.catch(() => {
// Token invalid, remove it
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
});
}
});
// Auto-fill demo username on page load for development
// Auto-select ishita account for easy testing (password still needs to be entered)
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
fillCredentials('ishita');
}, 1000);
});
</script>
</body>
</html>