414 lines
22 KiB
HTML
414 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html class="light" lang="en">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
|
<title>Robotics Account - Login</title>
|
|
<!-- Fonts -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
|
|
<!-- Material Symbols -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
|
|
<link rel="stylesheet" href="../css/main.css" />
|
|
<style>
|
|
.material-symbols-outlined {
|
|
font-family: 'Material Symbols Outlined';
|
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
|
|
font-size: 1.25rem;
|
|
line-height: 1;
|
|
letter-spacing: normal;
|
|
text-transform: none;
|
|
display: inline-flex;
|
|
white-space: nowrap;
|
|
word-wrap: normal;
|
|
direction: ltr;
|
|
}
|
|
body { font-family: 'Inter', sans-serif; min-height: 100vh; }
|
|
h1, h2, h3 { font-family: 'Manrope', sans-serif; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gradient-to-br from-blue-50 via-background to-purple-50 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 antialiased">
|
|
<div class="min-h-screen flex items-center justify-center px-4">
|
|
<div class="w-full max-w-md">
|
|
<!-- Login Card -->
|
|
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-lg border border-outline-variant/10 p-8">
|
|
<!-- Header -->
|
|
<div class="text-center mb-8">
|
|
<div class="flex justify-center mb-4">
|
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center">
|
|
<span class="material-symbols-outlined text-primary text-4xl">security</span>
|
|
</div>
|
|
</div>
|
|
<h1 class="text-2xl font-black text-slate-900 dark:text-slate-50 tracking-tight">Robotics Account Manager</h1>
|
|
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Account Management System</p>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
|
|
<button id="loginTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-primary text-on-primary shadow-sm">Đăng nhập</button>
|
|
<button id="registerTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-surface-container-low text-on-surface-variant">Đăng ký</button>
|
|
</div>
|
|
|
|
<!-- Login Form -->
|
|
<form id="loginForm" class="space-y-5">
|
|
<!-- Username/Email Input -->
|
|
<div>
|
|
<label for="username" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username or Email</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
|
<span class="material-symbols-outlined text-base">person</span>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
name="username"
|
|
placeholder="Enter your username"
|
|
required
|
|
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password Input -->
|
|
<div>
|
|
<label for="password" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Password</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
|
<span class="material-symbols-outlined text-base">lock</span>
|
|
</span>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
name="password"
|
|
placeholder="Enter your password"
|
|
required
|
|
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remember Me Checkbox -->
|
|
<div class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="remember"
|
|
name="remember"
|
|
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
|
|
/>
|
|
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
|
|
</div>
|
|
|
|
<!-- Login Button -->
|
|
<button
|
|
type="submit"
|
|
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-6"
|
|
>
|
|
<span class="material-symbols-outlined text-sm">login</span>
|
|
<span>Sign In</span>
|
|
</button>
|
|
|
|
<!-- Error Message -->
|
|
<div id="errorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
|
|
|
|
<div id="verifyNotice" class="hidden bg-amber-50 text-amber-800 border border-amber-200 rounded-lg px-4 py-3 text-xs font-medium space-y-2">
|
|
<p id="verifyNoticeText">Please confirm your email before signing in.</p>
|
|
<button id="resendVerifyBtn" type="button" class="inline-flex items-center gap-1 px-3 py-1.5 rounded-md bg-amber-100 hover:bg-amber-200 text-amber-900 font-semibold transition-colors">
|
|
<span class="material-symbols-outlined text-sm">forward_to_inbox</span>
|
|
<span>Resend confirmation email</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Register Form -->
|
|
<form id="registerForm" class="space-y-5 hidden">
|
|
<div>
|
|
<label for="regFullname" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Full Name</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
|
<span class="material-symbols-outlined text-base">badge</span>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="regFullname"
|
|
name="fullname"
|
|
placeholder="Enter your full name"
|
|
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="regEmail" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Email</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
|
<span class="material-symbols-outlined text-base">mail</span>
|
|
</span>
|
|
<input
|
|
type="email"
|
|
id="regEmail"
|
|
name="email"
|
|
placeholder="Enter your email"
|
|
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="regUsername" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
|
<span class="material-symbols-outlined text-base">person_add</span>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="regUsername"
|
|
name="username"
|
|
placeholder="Choose a username"
|
|
required
|
|
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="regPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Password</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
|
<span class="material-symbols-outlined text-base">lock</span>
|
|
</span>
|
|
<input
|
|
type="password"
|
|
id="regPassword"
|
|
name="password"
|
|
placeholder="Create a password"
|
|
required
|
|
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
|
|
>
|
|
<span class="material-symbols-outlined text-sm">how_to_reg</span>
|
|
<span>Tạo tài khoản</span>
|
|
</button>
|
|
|
|
<div id="registerErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
|
|
<div id="registerSuccessMessage" class="hidden bg-green-50 text-green-800 border border-green-200 rounded-lg px-4 py-3 text-xs font-medium"></div>
|
|
</form>
|
|
|
|
<!-- Footer -->
|
|
<!-- <div class="mt-8 pt-6 border-t border-outline-variant/10 text-center">
|
|
<p class="text-[10px] text-on-surface-variant/60">Default credentials for demo: admin / admin</p>
|
|
</div> -->
|
|
</div>
|
|
|
|
<!-- Bottom info -->
|
|
<div class="text-center mt-6">
|
|
<p class="text-xs text-on-surface-variant/60">v1.0.0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Simple login functionality
|
|
const loginForm = document.getElementById('loginForm');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const rememberCheckbox = document.getElementById('remember');
|
|
const registerForm = document.getElementById('registerForm');
|
|
const registerErrorMessage = document.getElementById('registerErrorMessage');
|
|
const registerSuccessMessage = document.getElementById('registerSuccessMessage');
|
|
const verifyNotice = document.getElementById('verifyNotice');
|
|
const verifyNoticeText = document.getElementById('verifyNoticeText');
|
|
const resendVerifyBtn = document.getElementById('resendVerifyBtn');
|
|
const regFullnameInput = document.getElementById('regFullname');
|
|
const regEmailInput = document.getElementById('regEmail');
|
|
const regUsernameInput = document.getElementById('regUsername');
|
|
const regPasswordInput = document.getElementById('regPassword');
|
|
const loginTab = document.getElementById('loginTab');
|
|
const registerTab = document.getElementById('registerTab');
|
|
let pendingVerificationIdentifier = '';
|
|
let currentMode = 'login';
|
|
|
|
const setMode = (mode) => {
|
|
currentMode = mode;
|
|
const isLogin = mode === 'login';
|
|
|
|
loginForm.classList.toggle('hidden', !isLogin);
|
|
registerForm.classList.toggle('hidden', isLogin);
|
|
errorMessage.classList.add('hidden');
|
|
registerErrorMessage.classList.add('hidden');
|
|
registerSuccessMessage.classList.add('hidden');
|
|
verifyNotice.classList.add('hidden');
|
|
|
|
const activate = (btn, active) => {
|
|
btn.classList.toggle('bg-primary', active);
|
|
btn.classList.toggle('text-on-primary', active);
|
|
btn.classList.toggle('shadow-sm', active);
|
|
btn.classList.toggle('bg-surface-container-low', !active);
|
|
btn.classList.toggle('text-on-surface-variant', !active);
|
|
};
|
|
|
|
activate(loginTab, isLogin);
|
|
activate(registerTab, !isLogin);
|
|
};
|
|
|
|
// Check if already logged in
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const currentUser = localStorage.getItem('currentUser');
|
|
if (currentUser) {
|
|
window.location.href = './index.html';
|
|
}
|
|
|
|
setMode('login');
|
|
|
|
// Restore remembered username
|
|
const rememberedUsername = localStorage.getItem('rememberedUsername');
|
|
if (rememberedUsername) {
|
|
usernameInput.value = rememberedUsername;
|
|
rememberCheckbox.checked = true;
|
|
}
|
|
});
|
|
|
|
loginTab.addEventListener('click', () => setMode('login'));
|
|
registerTab.addEventListener('click', () => setMode('register'));
|
|
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
errorMessage.classList.add('hidden');
|
|
verifyNotice.classList.add('hidden');
|
|
|
|
const username = usernameInput.value.trim();
|
|
const password = passwordInput.value;
|
|
|
|
try {
|
|
// Call backend login API
|
|
const response = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.user) {
|
|
// Store user info from backend
|
|
localStorage.setItem('currentUser', JSON.stringify(data.user));
|
|
|
|
// Handle remember me
|
|
if (rememberCheckbox.checked) {
|
|
localStorage.setItem('rememberedUsername', username);
|
|
} else {
|
|
localStorage.removeItem('rememberedUsername');
|
|
}
|
|
|
|
// Redirect to dashboard
|
|
window.location.href = './index.html';
|
|
} else if (data.requiresEmailVerification) {
|
|
pendingVerificationIdentifier = data.username || data.email || username;
|
|
verifyNoticeText.textContent = data.message || 'Please confirm your email before signing in';
|
|
verifyNotice.classList.remove('hidden');
|
|
} else {
|
|
// Show error
|
|
errorMessage.textContent = data.message || 'Invalid username or password';
|
|
errorMessage.classList.remove('hidden');
|
|
passwordInput.value = '';
|
|
passwordInput.focus();
|
|
}
|
|
} catch (error) {
|
|
errorMessage.textContent = 'Connection error. Try admin / admin';
|
|
errorMessage.classList.remove('hidden');
|
|
console.error('Login error:', error);
|
|
}
|
|
});
|
|
|
|
registerForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
registerErrorMessage.classList.add('hidden');
|
|
registerSuccessMessage.classList.add('hidden');
|
|
|
|
const payload = {
|
|
fullname: regFullnameInput.value.trim(),
|
|
email: regEmailInput.value.trim(),
|
|
username: regUsernameInput.value.trim(),
|
|
password: regPasswordInput.value
|
|
};
|
|
|
|
if (!payload.username || !payload.password || !payload.email) {
|
|
registerErrorMessage.textContent = 'Username, password and email are required';
|
|
registerErrorMessage.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/register', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const isJson = response.headers.get('content-type')?.includes('application/json');
|
|
const data = isJson ? await response.json() : null;
|
|
|
|
if (response.ok && data?.success) {
|
|
const lines = [data.message || 'Registration successful. Please check your email to confirm account.'];
|
|
if (data.verificationPreviewUrl) {
|
|
lines.push(`Development verification link: ${data.verificationPreviewUrl}`);
|
|
}
|
|
|
|
regPasswordInput.value = '';
|
|
setMode('login');
|
|
usernameInput.value = payload.username;
|
|
passwordInput.value = '';
|
|
pendingVerificationIdentifier = payload.email || payload.username;
|
|
verifyNoticeText.textContent = lines.join(' ');
|
|
verifyNotice.classList.remove('hidden');
|
|
} else {
|
|
const fallback = isJson ? (data?.message || 'Registration failed') : 'Server error (HTML response)';
|
|
registerErrorMessage.textContent = fallback;
|
|
registerErrorMessage.classList.remove('hidden');
|
|
}
|
|
} catch (error) {
|
|
registerErrorMessage.textContent = 'Connection error. Please try again.';
|
|
registerErrorMessage.classList.remove('hidden');
|
|
console.error('Register error:', error);
|
|
}
|
|
});
|
|
|
|
resendVerifyBtn.addEventListener('click', async () => {
|
|
const identifier = pendingVerificationIdentifier || usernameInput.value.trim();
|
|
if (!identifier) {
|
|
verifyNoticeText.textContent = 'Enter username/email first, then try resending verification.';
|
|
verifyNotice.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/resend-verification', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ identifier })
|
|
});
|
|
const data = await response.json();
|
|
verifyNoticeText.textContent = data.message || 'Verification email processed.';
|
|
verifyNotice.classList.remove('hidden');
|
|
if (data.verificationPreviewUrl) {
|
|
verifyNoticeText.textContent += ` Development link: ${data.verificationPreviewUrl}`;
|
|
}
|
|
} catch (error) {
|
|
verifyNoticeText.textContent = 'Cannot resend verification email right now.';
|
|
verifyNotice.classList.remove('hidden');
|
|
console.error('Resend verification error:', error);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|