diff --git a/Assest/butterfly.mp4 b/Assets/butterfly.mp4 similarity index 100% rename from Assest/butterfly.mp4 rename to Assets/butterfly.mp4 diff --git a/Assest/butterflygif.mp4 b/Assets/butterflygif.mp4 similarity index 100% rename from Assest/butterflygif.mp4 rename to Assets/butterflygif.mp4 diff --git a/README.md b/README.md index d3d232c..0f0c3a3 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Every contribution helps others learn, grow, and inspire creativity. Addminiproject/

-├── Assest/ # Contains images, cursor gifs, and other media
+├── Assets/ # Contains images, cursor gifs, and other media
│ └── butterfly.gif # Example cursor animation
├── projectforcontributor/ # Folder for mini projects submitted by contributors
│ ├── calculator.html # Example pastel pink calculator project
diff --git a/about.html b/about.html index 1446ee1..df7f623 100644 --- a/about.html +++ b/about.html @@ -1,12 +1,12 @@ - - - About | Add Mini Project - - - + + + About | Add Mini Project + + +
@@ -61,19 +61,19 @@

About Thi
- +
diff --git a/auth.js b/auth.js index f42ba70..0b4385a 100644 --- a/auth.js +++ b/auth.js @@ -4,6 +4,196 @@ function getMainPagePath(fileName) { return isGuidelinesPage ? `../${fileName}` : fileName; } +const USER_STORAGE_KEY = "user"; +const AUTH_SESSION_KEY = "authSession"; +const SESSION_DURATION_MS = 2 * 60 * 60 * 1000; +const HASH_ITERATIONS = 120000; + +function normalizeEmail(email) { + return String(email || "").trim().toLowerCase(); +} + +function sanitizeUsername(username) { + return String(username || "").replace(/[^a-zA-Z0-9 _-]/g, "").trim(); +} + +function isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +function validatePasswordStrength(password) { + if (password.length < 10) { + return "Password must be at least 10 characters long."; + } + if (!/[A-Z]/.test(password)) { + return "Password must include at least one uppercase letter."; + } + if (!/[a-z]/.test(password)) { + return "Password must include at least one lowercase letter."; + } + if (!/[0-9]/.test(password)) { + return "Password must include at least one number."; + } + if (!/[^a-zA-Z0-9]/.test(password)) { + return "Password must include at least one special character."; + } + return ""; +} + +function bufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ""; + for (let i = 0; i < bytes.length; i += 1) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +function base64ToBytes(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +function safeParseJSON(value) { + if (!value) { + return null; + } + + try { + return JSON.parse(value); + } catch (_error) { + return null; + } +} + +function getStoredUser() { + return safeParseJSON(localStorage.getItem(USER_STORAGE_KEY)); +} + +function storeUser(userRecord) { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userRecord)); +} + +async function derivePasswordHash(password, saltBytes, iterations) { + const encoder = new TextEncoder(); + const passwordKey = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveBits"] + ); + + const bits = await crypto.subtle.deriveBits( + { + name: "PBKDF2", + hash: "SHA-256", + salt: saltBytes, + iterations, + }, + passwordKey, + 256 + ); + + return bufferToBase64(bits); +} + +async function hashPassword(password, existingSalt, existingIterations) { + if (!window.crypto || !window.crypto.subtle) { + throw new Error("Secure crypto APIs are unavailable in this browser."); + } + + const iterations = existingIterations || HASH_ITERATIONS; + let saltBytes; + + if (existingSalt) { + saltBytes = base64ToBytes(existingSalt); + } else { + saltBytes = crypto.getRandomValues(new Uint8Array(16)); + } + + const hash = await derivePasswordHash(password, saltBytes, iterations); + return { + hash, + salt: bufferToBase64(saltBytes), + iterations, + }; +} + +function createSession(userRecord) { + const session = { + username: userRecord.username, + email: userRecord.email, + expiresAt: Date.now() + SESSION_DURATION_MS, + }; + sessionStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(session)); +} + +function clearSession() { + sessionStorage.removeItem(AUTH_SESSION_KEY); +} + +function getSession() { + const session = safeParseJSON(sessionStorage.getItem(AUTH_SESSION_KEY)); + + if (!session || !session.expiresAt) { + return null; + } + + if (Date.now() > Number(session.expiresAt)) { + clearSession(); + return null; + } + + return session; +} + +function hasActiveSession() { + return Boolean(getSession()); +} + +async function verifyHashedPassword(password, userRecord) { + if (!userRecord || !userRecord.passwordHash || !userRecord.salt) { + return false; + } + + const result = await hashPassword( + password, + userRecord.salt, + Number(userRecord.iterations) || HASH_ITERATIONS + ); + + return result.hash === userRecord.passwordHash; +} + +async function migrateLegacyUserIfNeeded(userRecord, passwordAttempt) { + if (!userRecord || !userRecord.password || userRecord.passwordHash) { + return userRecord; + } + + if (userRecord.password !== passwordAttempt) { + return null; + } + + const hashed = await hashPassword(passwordAttempt); + const migrated = { + version: 2, + username: userRecord.username, + email: normalizeEmail(userRecord.email), + passwordHash: hashed.hash, + salt: hashed.salt, + iterations: hashed.iterations, + createdAt: userRecord.createdAt || new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + storeUser(migrated); + return migrated; +} + /* Password toggle */ function togglePassword(id, icon) { const input = document.getElementById(id); @@ -25,24 +215,59 @@ function togglePassword(id, icon) { const signupForm = document.getElementById("signupForm"); if (signupForm) { - signupForm.addEventListener("submit", (event) => { + signupForm.addEventListener("submit", async (event) => { event.preventDefault(); - const username = document.getElementById("username").value; - const email = document.getElementById("email").value; + const usernameInput = document.getElementById("username").value; + const emailInput = document.getElementById("email").value; const password = document.getElementById("password").value; const confirmPassword = document.getElementById("confirmPassword").value; const error = document.getElementById("signupError"); error.textContent = ""; + const username = sanitizeUsername(usernameInput); + const email = normalizeEmail(emailInput); + + if (!/^[a-zA-Z0-9 _-]{3,30}$/.test(username)) { + error.textContent = + "Username must be 3-30 characters and use only letters, numbers, spaces, underscore, or hyphen."; + return; + } + + if (!isValidEmail(email)) { + error.textContent = "Please enter a valid email address."; + return; + } + + const passwordMessage = validatePasswordStrength(password); + if (passwordMessage) { + error.textContent = passwordMessage; + return; + } + if (password !== confirmPassword) { error.textContent = "Passwords do not match!"; return; } - const user = { username, email, password }; - localStorage.setItem("user", JSON.stringify(user)); + try { + const hashed = await hashPassword(password); + const user = { + version: 2, + username, + email, + passwordHash: hashed.hash, + salt: hashed.salt, + iterations: hashed.iterations, + createdAt: new Date().toISOString(), + }; + storeUser(user); + } catch (_error) { + error.textContent = + "Unable to create account securely in this browser. Please try another browser."; + return; + } window.location.href = getMainPagePath("signin.html"); }); @@ -52,48 +277,73 @@ if (signupForm) { const signinForm = document.getElementById("signinForm"); if (signinForm) { - signinForm.addEventListener("submit", (event) => { + signinForm.addEventListener("submit", async (event) => { event.preventDefault(); - const email = document.getElementById("loginEmail").value; + const email = normalizeEmail(document.getElementById("loginEmail").value); const password = document.getElementById("loginPassword").value; const error = document.getElementById("loginError"); - const storedUser = JSON.parse(localStorage.getItem("user")); - - if ( - storedUser && - storedUser.email === email && - storedUser.password === password - ) { - localStorage.setItem("loggedIn", "true"); - localStorage.setItem("loggedInUser", storedUser.username); - window.location.href = getMainPagePath("index.html"); - } else { + error.textContent = ""; + + if (!isValidEmail(email)) { + error.textContent = "Please enter a valid email address."; + return; + } + + const storedUser = getStoredUser(); + if (!storedUser) { error.textContent = "Invalid email or password"; + return; + } + + try { + const migratedUser = await migrateLegacyUserIfNeeded(storedUser, password); + const userToVerify = migratedUser || storedUser; + const isEmailMatch = normalizeEmail(userToVerify.email) === email; + const isPasswordMatch = await verifyHashedPassword(password, userToVerify); + + if (isEmailMatch && isPasswordMatch) { + createSession(userToVerify); + window.location.href = getMainPagePath("index.html"); + return; + } + + error.textContent = "Invalid email or password"; + } catch (_error) { + error.textContent = "Login failed. Please try again."; } }); } /* Session check */ function checkAuth() { - if (!localStorage.getItem("loggedIn")) { + if (!hasActiveSession()) { window.location.href = getMainPagePath("signin.html"); } } /* Show username in navbar */ function showLoggedInUser() { - const username = localStorage.getItem("loggedInUser"); + const session = getSession(); const userElement = document.getElementById("navUsername"); - if (userElement && username) { - userElement.textContent = username; + if (userElement && session && session.username) { + userElement.textContent = session.username; + } +} + +function redirectIfAuthenticated() { + if (hasActiveSession()) { + window.location.href = getMainPagePath("index.html"); } } /* Logout */ function logout() { - localStorage.removeItem("loggedIn"); - localStorage.removeItem("loggedInUser"); + clearSession(); window.location.href = getMainPagePath("signin.html"); } + +// Remove outdated keys from older auth versions. +localStorage.removeItem("loggedIn"); +localStorage.removeItem("loggedInUser"); diff --git a/contact.html b/contact.html index b74eb3d..7c72e76 100644 --- a/contact.html +++ b/contact.html @@ -4,6 +4,7 @@ + Contact | Add Mini Project @@ -143,7 +144,7 @@

Send a Me
- +
diff --git a/contribute.html b/contribute.html index 3cfaf3d..4ee5526 100644 --- a/contribute.html +++ b/contribute.html @@ -4,6 +4,7 @@ + How to Contribute @@ -160,7 +161,7 @@

Mini Project Rules Confirmation

- +
diff --git a/index.html b/index.html index 6415c58..e6d9bf2 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + Open Source Mini Projects @@ -160,7 +161,7 @@

Tic Tac Toe

- +
diff --git a/mini-project-guidelines/index.html b/mini-project-guidelines/index.html index e50ada1..c07977d 100644 --- a/mini-project-guidelines/index.html +++ b/mini-project-guidelines/index.html @@ -3,6 +3,7 @@ + Mini Project Submission Guidelines @@ -160,7 +161,7 @@

FAQ

- +
diff --git a/projects.html b/projects.html index 0e3b43d..4f801b7 100644 --- a/projects.html +++ b/projects.html @@ -3,6 +3,7 @@ + Projects | Add Mini Project @@ -119,7 +120,7 @@

Tic Tac Toe

- +
diff --git a/signin.html b/signin.html index eb4608a..3ae8c29 100644 --- a/signin.html +++ b/signin.html @@ -3,6 +3,7 @@ + Sign In @@ -32,7 +33,7 @@

Login

diff --git a/signup.html b/signup.html index 9764c0d..1a5a9e2 100644 --- a/signup.html +++ b/signup.html @@ -3,6 +3,7 @@ + Sign Up @@ -41,7 +42,7 @@

Create Account