Spaces:
Running
Running
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Đăng Nhập - Hệ thống QLVB</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| body { | |
| background: linear-gradient(135deg, #f0f4f8 0%, #d9e2ec 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: 'Segoe UI', sans-serif; | |
| } | |
| </style> | |
| </head> | |
| <body class="p-4"> | |
| <div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md border border-slate-200 relative"> | |
| <!-- Loading Overlay --> | |
| <div id="loading" class="hidden absolute inset-0 bg-white/80 z-10 flex items-center justify-center rounded-2xl"> | |
| <i class="fas fa-spinner fa-spin text-blue-600 text-3xl"></i> | |
| </div> | |
| <div class="text-center mb-8"> | |
| <div class="inline-block p-3 rounded-full bg-blue-600 text-white mb-3"> | |
| <i class="fas fa-file-contract text-3xl"></i> | |
| </div> | |
| <h2 class="text-2xl font-bold text-slate-800">Hệ Thống QLVB</h2> | |
| <p class="text-slate-500 text-sm">Vui lòng đăng nhập để truy cập dữ liệu</p> | |
| </div> | |
| <!-- TABS --> | |
| <div class="flex mb-6 border-b border-gray-200"> | |
| <button id="tabLogin" onclick="switchTab('login')" class="flex-1 py-2 text-blue-600 font-bold border-b-2 border-blue-600">Đăng Nhập</button> | |
| <button id="tabRegister" onclick="switchTab('register')" class="flex-1 py-2 text-slate-500 font-medium hover:text-blue-500">Đăng Ký</button> | |
| </div> | |
| <!-- FORM AUTH --> | |
| <form id="authForm" onsubmit="handleAuth(event)"> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">Email</label> | |
| <div class="relative"> | |
| <span class="absolute left-3 top-2.5 text-slate-400"><i class="fas fa-envelope"></i></span> | |
| <input type="email" id="email" required class="pl-10 w-full p-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" placeholder="email@example.com"> | |
| </div> | |
| </div> | |
| <div class="mb-2"> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">Mật khẩu</label> | |
| <div class="relative"> | |
| <span class="absolute left-3 top-2.5 text-slate-400"><i class="fas fa-lock"></i></span> | |
| <input type="password" id="password" required class="pl-10 w-full p-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" placeholder="••••••••"> | |
| </div> | |
| </div> | |
| <!-- Link Quên mật khẩu (Chỉ hiện khi ở tab Login) --> | |
| <div id="forgotPasswordLink" class="text-right mb-6"> | |
| <a href="#" onclick="showResetModal(event)" class="text-xs text-blue-600 hover:text-blue-800 font-medium">Quên mật khẩu?</a> | |
| </div> | |
| <!-- Spacer cho tab Register để layout không nhảy --> | |
| <div id="registerSpacer" class="mb-6 hidden"></div> | |
| <button type="submit" id="submitBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 rounded shadow transition flex justify-center items-center"> | |
| <span>Đăng Nhập</span> | |
| </button> | |
| <p id="message" class="text-center text-sm mt-4 hidden font-medium px-2 py-2 rounded border"></p> | |
| </form> | |
| <!-- FORM RESET PASSWORD (MODAL ẨN) --> | |
| <div id="resetModal" class="hidden absolute inset-0 bg-white z-20 p-8 rounded-2xl flex flex-col justify-center"> | |
| <h3 class="text-xl font-bold text-center mb-4 text-slate-800">Đặt lại mật khẩu</h3> | |
| <p class="text-sm text-center text-slate-500 mb-6">Nhập email của bạn để nhận liên kết đặt lại mật khẩu.</p> | |
| <form onsubmit="handleResetPassword(event)"> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">Email đăng ký</label> | |
| <div class="relative"> | |
| <span class="absolute left-3 top-2.5 text-slate-400"><i class="fas fa-envelope"></i></span> | |
| <input type="email" id="resetEmail" required class="pl-10 w-full p-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none"> | |
| </div> | |
| </div> | |
| <button type="submit" class="w-full bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2.5 rounded shadow transition mb-3"> | |
| Gửi yêu cầu | |
| </button> | |
| <button type="button" onclick="hideResetModal()" class="w-full bg-slate-100 hover:bg-slate-200 text-slate-600 font-medium py-2.5 rounded transition"> | |
| Quay lại | |
| </button> | |
| </form> | |
| <p id="resetMessage" class="text-center text-sm mt-4 hidden font-medium px-2 py-2 rounded border"></p> | |
| </div> | |
| </div> | |
| <!-- Import Config (Cùng cấp) --> | |
| <script src="config.js"></script> | |
| <!-- Firebase SDK --> | |
| <script type="module"> | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js"; | |
| import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, sendPasswordResetEmail } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js"; | |
| import { getFirestore, doc, setDoc, getDoc, serverTimestamp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js"; | |
| const app = initializeApp(CONFIG.FIREBASE_CONFIG); | |
| const auth = getAuth(app); | |
| const db = getFirestore(app); | |
| const USERS_COL = CONFIG.USERS_COLLECTION || "users"; | |
| window.isLoginMode = true; | |
| window.switchTab = function(mode) { | |
| window.isLoginMode = (mode === 'login'); | |
| const tabLogin = document.getElementById('tabLogin'); | |
| const tabRegister = document.getElementById('tabRegister'); | |
| const btnSpan = document.querySelector('#submitBtn span'); | |
| const msg = document.getElementById('message'); | |
| const forgotLink = document.getElementById('forgotPasswordLink'); | |
| const spacer = document.getElementById('registerSpacer'); | |
| msg.classList.add('hidden'); | |
| document.getElementById('authForm').reset(); | |
| if(window.isLoginMode) { | |
| tabLogin.className = "flex-1 py-2 text-blue-600 font-bold border-b-2 border-blue-600"; | |
| tabRegister.className = "flex-1 py-2 text-slate-500 font-medium hover:text-blue-500"; | |
| if(btnSpan) btnSpan.textContent = "Đăng Nhập"; | |
| forgotLink.classList.remove('hidden'); | |
| spacer.classList.add('hidden'); | |
| } else { | |
| tabRegister.className = "flex-1 py-2 text-blue-600 font-bold border-b-2 border-blue-600"; | |
| tabLogin.className = "flex-1 py-2 text-slate-500 font-medium hover:text-blue-500"; | |
| if(btnSpan) btnSpan.textContent = "Đăng Ký Tài Khoản"; | |
| forgotLink.classList.add('hidden'); | |
| spacer.classList.remove('hidden'); | |
| } | |
| } | |
| // --- AUTH MAIN LOGIC --- | |
| window.handleAuth = async function(e) { | |
| e.preventDefault(); | |
| const email = document.getElementById('email').value; | |
| const password = document.getElementById('password').value; | |
| const msg = document.getElementById('message'); | |
| const loading = document.getElementById('loading'); | |
| loading.classList.remove('hidden'); | |
| msg.classList.add('hidden'); | |
| try { | |
| if (window.isLoginMode) { | |
| // --- XỬ LÝ ĐĂNG NHẬP --- | |
| const userCredential = await signInWithEmailAndPassword(auth, email, password); | |
| const user = userCredential.user; | |
| // Kiểm tra Firestore | |
| const userRef = doc(db, USERS_COL, user.uid); | |
| const userSnap = await getDoc(userRef); | |
| if (userSnap.exists()) { | |
| const userData = userSnap.data(); | |
| if (userData.status === 'pending') { | |
| await signOut(auth); | |
| throw new Error("Tài khoản đang chờ Admin duyệt!"); | |
| } else if (userData.status === 'blocked') { | |
| await signOut(auth); | |
| throw new Error("Tài khoản đã bị khóa!"); | |
| } | |
| } else { | |
| // User tồn tại trong Auth nhưng mất trong Firestore -> Tự động khôi phục | |
| await setDoc(userRef, { | |
| email: user.email, | |
| status: 'pending', | |
| created_at: serverTimestamp() | |
| }); | |
| await signOut(auth); | |
| throw new Error("Đã khôi phục tài khoản cũ. Vui lòng chờ Admin duyệt lại."); | |
| } | |
| window.location.href = 'index.html'; | |
| } else { | |
| // --- XỬ LÝ ĐĂNG KÝ --- | |
| try { | |
| const userCredential = await createUserWithEmailAndPassword(auth, email, password); | |
| const user = userCredential.user; | |
| await setDoc(doc(db, USERS_COL, user.uid), { | |
| email: user.email, | |
| status: 'pending', | |
| created_at: serverTimestamp() | |
| }); | |
| await signOut(auth); | |
| showMessage("Đăng ký thành công! Vui lòng chờ Admin kích hoạt.", "success"); | |
| setTimeout(() => window.switchTab('login'), 2000); | |
| } catch (regError) { | |
| if (regError.code === 'auth/email-already-in-use') { | |
| // Thử đăng nhập ngầm để check pass | |
| try { | |
| const userCredential = await signInWithEmailAndPassword(auth, email, password); | |
| const user = userCredential.user; | |
| const userRef = doc(db, USERS_COL, user.uid); | |
| const userSnap = await getDoc(userRef); | |
| if (!userSnap.exists()) { | |
| await setDoc(userRef, { | |
| email: user.email, | |
| status: 'pending', | |
| created_at: serverTimestamp() | |
| }); | |
| await signOut(auth); | |
| showMessage("Tài khoản cũ đã được khôi phục. Vui lòng chờ Admin.", "success"); | |
| setTimeout(() => window.switchTab('login'), 3000); | |
| return; | |
| } else { | |
| await signOut(auth); | |
| throw new Error("Email này đã được sử dụng."); | |
| } | |
| } catch (loginErr) { | |
| if(loginErr.code === 'auth/wrong-password' || loginErr.code === 'auth/invalid-credential') { | |
| // Gợi ý reset pass | |
| throw new Error("Email đã tồn tại & sai mật khẩu. Vui lòng dùng 'Quên mật khẩu?'."); | |
| } else { | |
| throw loginErr; | |
| } | |
| } | |
| } else { | |
| throw regError; | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| console.error(error); | |
| let text = error.message; | |
| if(error.code === 'auth/invalid-credential' || error.code === 'auth/wrong-password') text = "Email hoặc mật khẩu không đúng."; | |
| if(error.code === 'auth/email-already-in-use') text = "Email này đã được đăng ký."; | |
| if(error.code === 'auth/weak-password') text = "Mật khẩu quá yếu (tối thiểu 6 ký tự)."; | |
| if(error.code === 'auth/too-many-requests') text = "Quá nhiều lần thử sai. Vui lòng thử lại sau."; | |
| showMessage(text, "error"); | |
| } finally { | |
| loading.classList.add('hidden'); | |
| } | |
| } | |
| // --- RESET PASSWORD LOGIC --- | |
| window.showResetModal = function(e) { | |
| e.preventDefault(); | |
| // Tự điền email nếu đã nhập ở form chính | |
| document.getElementById('resetEmail').value = document.getElementById('email').value; | |
| document.getElementById('resetModal').classList.remove('hidden'); | |
| document.getElementById('resetMessage').classList.add('hidden'); | |
| } | |
| window.hideResetModal = function() { | |
| document.getElementById('resetModal').classList.add('hidden'); | |
| } | |
| window.handleResetPassword = async function(e) { | |
| e.preventDefault(); | |
| const email = document.getElementById('resetEmail').value; | |
| const msgBox = document.getElementById('resetMessage'); | |
| const loading = document.getElementById('loading'); | |
| if(!email) return; | |
| loading.classList.remove('hidden'); | |
| msgBox.classList.add('hidden'); | |
| try { | |
| await sendPasswordResetEmail(auth, email); | |
| msgBox.textContent = "Đã gửi email! Hãy kiểm tra hộp thư (cả mục Spam) để đặt lại mật khẩu."; | |
| msgBox.className = "text-center text-sm mt-4 text-green-600 bg-green-50 px-2 py-2 rounded border border-green-200 block"; | |
| } catch (error) { | |
| console.error(error); | |
| let text = error.message; | |
| if(error.code === 'auth/user-not-found') text = "Email này chưa được đăng ký trong hệ thống."; | |
| if(error.code === 'auth/invalid-email') text = "Email không hợp lệ."; | |
| msgBox.textContent = text; | |
| msgBox.className = "text-center text-sm mt-4 text-red-600 bg-red-50 px-2 py-2 rounded border border-red-200 block"; | |
| } finally { | |
| loading.classList.add('hidden'); | |
| } | |
| } | |
| function showMessage(text, type) { | |
| const msg = document.getElementById('message'); | |
| msg.textContent = text; | |
| msg.className = "text-center text-sm mt-4 font-medium px-2 py-2 rounded border block"; | |
| if (type === 'error') { | |
| msg.classList.add('text-red-600', 'bg-red-50', 'border-red-200'); | |
| } else if (type === 'success') { | |
| msg.classList.add('text-green-600', 'bg-green-50', 'border-green-200'); | |
| } else { | |
| msg.classList.add('text-yellow-600', 'bg-yellow-50', 'border-yellow-200'); | |
| } | |
| } | |
| auth.onAuthStateChanged(async (user) => { | |
| if (user) { | |
| const userSnap = await getDoc(doc(db, USERS_COL, user.uid)); | |
| if (userSnap.exists() && userSnap.data().status === 'active') { | |
| window.location.href = 'index.html'; | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |