diff --git a/js/auth.js b/js/auth.js index a035cf2..31b9803 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,7 +1,9 @@ (function () { - const USERS_KEY = "users"; - const SESSION_KEY = "loggedInUser"; - const THEME_KEY = "theme"; + const USERS_KEY = "users"; + const SESSION_KEY = "loggedInUser"; + const SESSION_CHK_KEY = "loggedInUser_check"; + const THEME_KEY = "theme"; + const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; function readJSON(key, fallback) { try { @@ -21,16 +23,58 @@ return readJSON(USERS_KEY, []); } - function getSessionUser() { - return readJSON(SESSION_KEY, null); + async function hashPassword(password) { + const encoder = new TextEncoder(); + const data = encoder.encode(password); + const hashBuf = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(hashBuf)) + .map(function (b) { return b.toString(16).padStart(2, "0"); }) + .join(""); + } + + function computeChecksum(payload) { + return btoa(encodeURIComponent(payload)).slice(0, 24); } function setSessionUser(user) { - writeJSON(SESSION_KEY, user); + const payload = JSON.stringify(user); + const checksum = computeChecksum(payload); + localStorage.setItem(SESSION_KEY, payload); + localStorage.setItem(SESSION_CHK_KEY, checksum); + } + + function getSessionUser() { + const payload = localStorage.getItem(SESSION_KEY); + const checksum = localStorage.getItem(SESSION_CHK_KEY); + + if (!payload) return null; + if (computeChecksum(payload) !== checksum) { + clearSessionUser(); + console.warn("Session integrity check failed. Session cleared."); + return null; + } + + let user; + try { + user = JSON.parse(payload); + } catch { + clearSessionUser(); + return null; + } + if (user.loginAt) { + const age = Date.now() - new Date(user.loginAt).getTime(); + if (age > SESSION_TTL_MS) { + clearSessionUser(); + return null; + } + } + + return user; } function clearSessionUser() { localStorage.removeItem(SESSION_KEY); + localStorage.removeItem(SESSION_CHK_KEY); } function normalizeEmail(email) { @@ -38,7 +82,7 @@ } function escapeHTML(value) { - return String(value || "").replace(/[&<>"'`\/]/g, function (character) { + return String(value || "").replace(/[&<>"'`\/]/g, function (ch) { return { "&": "&", "<": "<", @@ -47,7 +91,7 @@ "'": "'", "`": "`", "/": "/" - }[character] || character; + }[ch] || ch; }); } @@ -56,13 +100,27 @@ } function validatePhone(phone) { - return /^\d{10}$/.test(String(phone || "").trim()); + const digits = String(phone || "").replace(/\D/g, ""); + return digits.length === 10 || + (digits.length === 12 && digits.startsWith("91")); + } + + function validatePassword(password) { + if (password.length < 8) { + return "Password must be at least 8 characters."; + } + if (!/[A-Z]/.test(password)) { + return "Password must include at least one uppercase letter."; + } + if (!/[0-9]/.test(password)) { + return "Password must include at least one number."; + } + return null; } function setFieldError(inputId, message) { const errorEl = document.getElementById(`${inputId}-error`); if (!errorEl) return; - errorEl.textContent = message; errorEl.classList.toggle("active", Boolean(message)); } @@ -70,22 +128,41 @@ function setFormMessage(formId, message, type) { const form = document.getElementById(formId); if (!form) return; - const messageEl = form.querySelector(".form-message"); if (!messageEl) return; - messageEl.textContent = message; messageEl.classList.remove("success", "error"); - if (type) { - messageEl.classList.add(type); - } + if (type) messageEl.classList.add(type); } function clearFormErrors(fieldIds, formId) { - fieldIds.forEach((fieldId) => setFieldError(fieldId, "")); + fieldIds.forEach(function (id) { setFieldError(id, ""); }); setFormMessage(formId, "", ""); } + function setSubmitLoading(form, loading, originalLabel) { + const btn = form ? form.querySelector('button[type="submit"]') : null; + if (!btn) return; + btn.disabled = loading; + btn.textContent = loading ? "Please wait…" : (originalLabel || "Submit"); + } + + function setupPasswordToggle(inputId, toggleId) { + const input = document.getElementById(inputId); + const toggle = document.getElementById(toggleId); + if (!input || !toggle) return; + + toggle.addEventListener("click", function () { + const isPassword = input.type === "password"; + input.type = isPassword ? "text" : "password"; + toggle.setAttribute( + "aria-label", + isPassword ? "Hide password" : "Show password" + ); + toggle.textContent = isPassword ? "Hide" : "Show"; + }); + } + function getThemePreference() { return localStorage.getItem(THEME_KEY) === "dark" ? "dark" : "light"; } @@ -93,7 +170,6 @@ function updateThemeToggleState(theme) { const themeToggle = document.getElementById("theme-toggle"); if (!themeToggle) return; - const isDark = theme === "dark"; themeToggle.setAttribute("aria-pressed", String(isDark)); themeToggle.setAttribute( @@ -110,10 +186,8 @@ function setupThemeToggle() { applyTheme(getThemePreference()); - const themeToggle = document.getElementById("theme-toggle"); if (!themeToggle) return; - themeToggle.addEventListener("click", function () { const nextTheme = document.body.classList.contains("dark") ? "light" : "dark"; localStorage.setItem(THEME_KEY, nextTheme); @@ -122,23 +196,18 @@ } function closeProfileDropdown(options) { - const profileMenu = document.querySelector("[data-profile-menu]"); + const profileMenu = document.querySelector("[data-profile-menu]"); const profileToggle = document.getElementById("profileToggle"); - if (!profileMenu || !profileToggle) return; - profileMenu.classList.remove("open"); profileToggle.setAttribute("aria-expanded", "false"); - - if (options && options.focusToggle) { - profileToggle.focus(); - } + if (options && options.focusToggle) profileToggle.focus(); } function setupProfileDropdown() { - const profileMenu = document.querySelector("[data-profile-menu]"); + const profileMenu = document.querySelector("[data-profile-menu]"); const profileToggle = document.getElementById("profileToggle"); - const logoutBtn = document.getElementById("logoutBtn"); + const logoutBtn = document.getElementById("logoutBtn"); if (!profileMenu || !profileToggle) return; @@ -147,30 +216,20 @@ profileToggle.setAttribute("aria-expanded", "true"); }; - const handleDocumentClick = function (event) { - if (!profileMenu.contains(event.target)) { - closeProfileDropdown(); - } - }; - - const handleEscapeKey = function (event) { - if (event.key === "Escape") { - closeProfileDropdown({ focusToggle: true }); - } - }; - profileToggle.addEventListener("click", function (event) { event.stopPropagation(); + profileMenu.classList.contains("open") + ? closeProfileDropdown() + : openProfileDropdown(); + }); - if (profileMenu.classList.contains("open")) { - closeProfileDropdown(); - } else { - openProfileDropdown(); - } + document.addEventListener("click", function (event) { + if (!profileMenu.contains(event.target)) closeProfileDropdown(); }); - document.addEventListener("click", handleDocumentClick); - document.addEventListener("keydown", handleEscapeKey); + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") closeProfileDropdown({ focusToggle: true }); + }); if (logoutBtn) { logoutBtn.addEventListener("click", function () { @@ -180,19 +239,18 @@ } } - // Navbar session rendering. - // Why: every page needs the same logged-out/login and logged-in/profile state. function renderAuthNav() { const authNavItem = document.getElementById("authNavItem"); if (!authNavItem) return; const loggedInUser = getSessionUser(); if (!loggedInUser || !loggedInUser.name) { - authNavItem.innerHTML = 'Login'; + authNavItem.innerHTML = + 'Login'; return; } - - const safeName = escapeHTML(String(loggedInUser.name).trim()); + + const safeName = escapeHTML(String(loggedInUser.name).trim()); const safeEmail = escapeHTML(String(loggedInUser.email || "").trim()); authNavItem.innerHTML = ` @@ -231,19 +289,19 @@ function handleLoginPage() { const loginForm = document.getElementById("login-form"); if (!loginForm) return; + setupPasswordToggle("login-password", "login-password-toggle"); - loginForm.addEventListener("submit", function (event) { + loginForm.addEventListener("submit", async function (event) { event.preventDefault(); - const emailInput = document.getElementById("login-email"); + const emailInput = document.getElementById("login-email"); const passwordInput = document.getElementById("login-password"); const email = normalizeEmail(emailInput ? emailInput.value : ""); - const password = String(passwordInput ? passwordInput.value : "").trim(); + const password = String(passwordInput ? passwordInput.value : ""); clearFormErrors(["login-email", "login-password"], "login-form"); let hasError = false; - if (!validateEmail(email)) { setFieldError("login-email", "Please enter a valid email address."); hasError = true; @@ -259,26 +317,47 @@ return; } - const users = getUsers(); - const matchedUser = users.find( - (user) => normalizeEmail(user.email) === email && String(user.password || "") === password - ); - - if (!matchedUser) { - setFormMessage("login-form", "Invalid email or password. Please try again.", "error"); - return; + setSubmitLoading(loginForm, true); + + try { + const hashedInput = await hashPassword(password); + const users = getUsers(); + + const matchedUser = users.find(function (user) { + return ( + normalizeEmail(user.email) === email && + String(user.password || "") === hashedInput + ); + }); + + if (!matchedUser) { + setFormMessage( + "login-form", + "Invalid email or password. Please try again.", + "error" + ); + setSubmitLoading(loginForm, false, "Login"); + return; + } + + setSessionUser({ + name: matchedUser.name, + email: matchedUser.email, + phone: matchedUser.phone, + location: matchedUser.location, + loginAt: new Date().toISOString() + }); + + setFormMessage("login-form", "Login successful. Redirecting…", "success"); + setTimeout(function () { + window.location.href = "index.html"; + }, 800); + + } catch (err) { + console.error("Login error:", err); + setFormMessage("login-form", "Something went wrong. Please try again.", "error"); + setSubmitLoading(loginForm, false, "Login"); } - - setSessionUser({ - name: matchedUser.name, - email: matchedUser.email, - phone: matchedUser.phone, - location: matchedUser.location, - loginAt: new Date().toISOString() - }); - - setFormMessage("login-form", "Login successful. Redirecting...", "success"); - window.location.href = "index.html"; }); } @@ -286,19 +365,21 @@ const signupForm = document.getElementById("signup-form"); if (!signupForm) return; - signupForm.addEventListener("submit", function (event) { + setupPasswordToggle("signup-password", "signup-password-toggle"); + + signupForm.addEventListener("submit", async function (event) { event.preventDefault(); - const nameInput = document.getElementById("signup-name"); - const emailInput = document.getElementById("signup-email"); + const nameInput = document.getElementById("signup-name"); + const emailInput = document.getElementById("signup-email"); const passwordInput = document.getElementById("signup-password"); - const phoneInput = document.getElementById("signup-phone"); + const phoneInput = document.getElementById("signup-phone"); const locationInput = document.getElementById("signup-location"); - const name = String(nameInput ? nameInput.value : "").trim(); - const email = normalizeEmail(emailInput ? emailInput.value : ""); - const password = String(passwordInput ? passwordInput.value : "").trim(); - const phone = String(phoneInput ? phoneInput.value : "").trim(); + const name = String(nameInput ? nameInput.value : "").trim(); + const email = normalizeEmail(emailInput ? emailInput.value : ""); + const password = String(passwordInput ? passwordInput.value : ""); + const phone = String(phoneInput ? phoneInput.value : "").trim(); const location = String(locationInput ? locationInput.value : "").trim(); clearFormErrors( @@ -312,62 +393,70 @@ setFieldError("signup-name", "Name is required."); hasError = true; } - + + const users = getUsers(); if (!validateEmail(email)) { setFieldError("signup-email", "Please enter a valid email address."); hasError = true; + } else if (users.some(function (u) { return normalizeEmail(u.email) === email; })) { + setFieldError("signup-email", "This email is already registered. Please log in."); + hasError = true; } - if (password.length < 6) { - setFieldError("signup-password", "Password must be at least 6 characters."); + const pwError = validatePassword(password); + if (pwError) { + setFieldError("signup-password", pwError); hasError = true; } - if (!validatePhone(phone)) { - setFieldError("signup-phone", "Phone number must contain exactly 10 digits."); + setFieldError("signup-phone", "Enter a valid 10-digit phone number."); hasError = true; } - if (!location) { setFieldError("signup-location", "Location is required."); hasError = true; } - const users = getUsers(); - const emailAlreadyExists = users.some((user) => normalizeEmail(user.email) === email); - if (emailAlreadyExists) { - setFieldError("signup-email", "This email is already registered. Please log in."); - hasError = true; - } - if (hasError) { setFormMessage("signup-form", "Please fix the highlighted fields.", "error"); return; } - const newUser = { - name, - email, - password, - phone, - location, - createdAt: new Date().toISOString() - }; - - users.push(newUser); - writeJSON(USERS_KEY, users); - - // Auto-login after successful registration as requested. - setSessionUser({ - name: newUser.name, - email: newUser.email, - phone: newUser.phone, - location: newUser.location, - loginAt: new Date().toISOString() - }); - - setFormMessage("signup-form", "Account created successfully. Redirecting...", "success"); - window.location.href = "index.html"; + setSubmitLoading(signupForm, true); + + try { + const hashedPassword = await hashPassword(password); + + const newUser = { + name, + email, + password: hashedPassword, + phone, + location, + createdAt: new Date().toISOString() + }; + + users.push(newUser); + writeJSON(USERS_KEY, users); + + setSessionUser({ + name: newUser.name, + email: newUser.email, + phone: newUser.phone, + location: newUser.location, + loginAt: new Date().toISOString() + }); + + setFormMessage("signup-form", "Account created! Redirecting…", "success"); + setTimeout(function () { + window.location.href = "index.html"; + }, 800); + + } catch (err) { + console.error("Signup error:", err); + setFormMessage("signup-form", "Something went wrong. Please try again.", "error"); + setSubmitLoading(signupForm, false, "Create Account"); + } }); } @@ -376,24 +465,22 @@ const loggedInUser = getSessionUser(); if (!loggedInUser) return; - const profileMap = { - "profile-name-value": loggedInUser.name || "", - "profile-email-value": loggedInUser.email || "", - "profile-phone-value": loggedInUser.phone || "", + "profile-name-value": loggedInUser.name || "", + "profile-email-value": loggedInUser.email || "", + "profile-phone-value": loggedInUser.phone || "", "profile-location-value": loggedInUser.location || "" }; - Object.keys(profileMap).forEach((elementId) => { - const element = document.getElementById(elementId); - if (element) { - element.textContent = profileMap[elementId]; - } + Object.keys(profileMap).forEach(function (elementId) { + const el = document.getElementById(elementId); + if (el) el.textContent = profileMap[elementId]; }); - + const profileInitial = document.getElementById("profile-avatar-initial"); if (profileInitial) { - profileInitial.textContent = String(loggedInUser.name || "U").trim().charAt(0).toUpperCase() || "U"; + const safeName = escapeHTML(String(loggedInUser.name || "U").trim()); + profileInitial.textContent = safeName.charAt(0).toUpperCase() || "U"; } } @@ -410,7 +497,7 @@ } function redirectIfAuthenticationStateMismatchesPage() { - const pageType = document.body.dataset.pageType || ""; + const pageType = document.body.dataset.pageType || ""; const loggedInUser = getSessionUser(); if ((pageType === "profile" || pageType === "dashboard") && !loggedInUser) { @@ -422,15 +509,11 @@ window.location.href = "index.html"; return true; } - return false; } - + function init() { - if (redirectIfAuthenticationStateMismatchesPage()) { - return; - } - + if (redirectIfAuthenticationStateMismatchesPage()) return; setupThemeToggle(); renderAuthNav(); handleLoginPage();