Implement a fully functional user login and registration system

Refactor the authentication system to separate login and registration forms, implement password hashing, manage JWT tokens, and enable user data persistence in `backend/data/users.json`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 375ec6d3-d5af-4f82-ab81-5c60fd4a86a3
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8d427c3d-aa60-488b-82c4-6cef148ba5d7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/147e665c-8c0d-48ec-b0ad-fdc89cd4460f/375ec6d3-d5af-4f82-ab81-5c60fd4a86a3/e238nM8
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
tornikegerantia 2026-04-15 00:19:58 +00:00
parent 7333839304
commit 171fd243de
7 changed files with 164 additions and 56 deletions

View File

@ -15,19 +15,20 @@ const generateToken = (id) => {
exports.register = async (req, res) => {
try {
const { firstName, lastName, email, password, passwordConfirm } = req.body;
const normalizedEmail = email ? email.toLowerCase().trim() : '';
// Validation
if (!firstName || !lastName || !email || !password || !passwordConfirm) {
return res.status(400).json({ message: 'All fields are required' });
}
if (!validateEmail(email)) {
if (!validateEmail(normalizedEmail)) {
return res.status(400).json({ message: 'Invalid email format' });
}
if (!validatePassword(password)) {
return res.status(400).json({
message: 'Password must be at least 8 characters with uppercase, lowercase, and number',
message: 'Password must be at least 6 characters',
});
}
@ -36,7 +37,7 @@ exports.register = async (req, res) => {
}
// Check if user already exists
let user = await User.findOne({ email });
let user = await User.findOne({ email: normalizedEmail });
if (user) {
return res.status(400).json({ message: 'Email already in use' });
}
@ -45,7 +46,7 @@ exports.register = async (req, res) => {
user = await User.create({
firstName,
lastName,
email,
email: normalizedEmail,
password,
});
@ -69,18 +70,18 @@ exports.register = async (req, res) => {
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
const normalizedEmail = email ? email.toLowerCase().trim() : '';
// Validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
if (!validateEmail(email)) {
if (!validateEmail(normalizedEmail)) {
return res.status(400).json({ message: 'Invalid email format' });
}
// Check if user exists and get password field
const user = await User.findOne({ email });
const user = await User.findOne({ email: normalizedEmail });
if (!user) {
return res.status(401).json({ message: 'Invalid email or password' });
}
@ -173,7 +174,7 @@ exports.refreshToken = async (req, res) => {
// @access Private
exports.getMe = async (req, res) => {
try {
const user = await User.findById(req.user.id);
const user = await User.findById(req.user._id);
res.status(200).json({
success: true,

View File

@ -29,6 +29,7 @@ exports.protect = async (req, res, next) => {
return res.status(404).json({ message: 'User not found' });
}
user.id = user._id;
req.user = user;
next();
} catch (error) {

View File

@ -7,9 +7,7 @@ const validateEmail = (email) => {
};
const validatePassword = (password) => {
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;
return regex.test(password);
return typeof password === 'string' && password.length >= 6;
};
const validatePhone = (phone) => {

View File

@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
// Simple file-based storage for demo purposes
const USERS_FILE = path.join(__dirname, '../data/users.json');
@ -19,10 +20,11 @@ if (!fs.existsSync(USERS_FILE)) {
// User Schema (simplified for file storage)
class User {
constructor(data) {
this._id = data._id || Date.now().toString();
this._id = data._id || crypto.randomUUID();
this.id = this._id;
this.firstName = data.firstName;
this.lastName = data.lastName;
this.email = data.email;
this.email = data.email ? data.email.toLowerCase().trim() : data.email;
this.password = data.password;
this.phone = data.phone || null;
this.address = data.address || {};
@ -108,7 +110,8 @@ User.findOne = async (query) => {
try {
const users = await User.find();
if (query.email) {
return users.find(u => u.email === query.email) || null;
const email = query.email.toLowerCase().trim();
return users.find(u => u.email === email) || null;
}
return null;
} catch (error) {
@ -120,13 +123,13 @@ User.findOne = async (query) => {
User.create = async (data) => {
try {
const users = await User.find();
const email = data.email.toLowerCase().trim();
// Check for duplicate email
if (users.some(u => u.email === data.email)) {
if (users.some(u => u.email === email)) {
throw new Error('Email already in use');
}
const newUser = new User(data);
const newUser = new User({ ...data, email });
await newUser.save();
return newUser;

View File

@ -6,6 +6,26 @@ const API_BASE_URL = (function() {
return 'http://localhost:5000/api';
})();
const TOKEN_KEY = 'authToken';
const USER_KEY = 'authUser';
function saveAuth(data) {
if (data && data.token) {
localStorage.setItem(TOKEN_KEY, data.token);
}
if (data && data.user) {
localStorage.setItem(USER_KEY, JSON.stringify(data.user));
}
}
function getStoredUser() {
try {
const user = localStorage.getItem(USER_KEY);
return user ? JSON.parse(user) : null;
} catch (error) {
localStorage.removeItem(USER_KEY);
return null;
}
}
// Ensure auth methods are available globally for inline scripts
window.login = async function(email, password) {
@ -18,8 +38,8 @@ window.login = async function(email, password) {
const data = await response.json();
if (data.success) {
localStorage.setItem(TOKEN_KEY, data.token);
console.log('Login successful');
saveAuth(data);
updateAuthLinks();
return data;
}
@ -42,8 +62,8 @@ window.register = async function(firstName, lastName, email, password, passwordC
const data = await response.json();
if (data.success) {
localStorage.setItem(TOKEN_KEY, data.token);
console.log('Registration successful');
saveAuth(data);
updateAuthLinks();
return data;
}
@ -62,9 +82,12 @@ window.register = async function(firstName, lastName, email, password, passwordC
function logout() {
localStorage.removeItem(TOKEN_KEY);
console.log('Logged out');
localStorage.removeItem(USER_KEY);
updateAuthLinks();
}
window.logout = logout;
function isLoggedIn() {
return localStorage.getItem(TOKEN_KEY) !== null;
}
@ -73,6 +96,62 @@ function getToken() {
return localStorage.getItem(TOKEN_KEY);
}
async function getCurrentUser() {
const token = getToken();
if (!token) return null;
try {
const response = await fetch(`${API_BASE_URL}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await response.json();
if (data.success) {
localStorage.setItem(USER_KEY, JSON.stringify(data.user));
updateAuthLinks();
return data.user;
}
logout();
return null;
} catch (error) {
return getStoredUser();
}
}
window.getCurrentUser = getCurrentUser;
function updateAuthLinks() {
if (typeof document === 'undefined') return;
const user = getStoredUser();
document.querySelectorAll('.auth-link').forEach(link => {
if (isLoggedIn()) {
const name = user && user.firstName ? user.firstName : 'Account';
link.textContent = `Logout (${name})`;
link.href = '#';
link.onclick = event => {
event.preventDefault();
logout();
window.location.href = 'login.html';
};
} else {
link.textContent = 'Login / Register';
link.href = 'login.html';
link.onclick = null;
}
});
}
window.updateAuthLinks = updateAuthLinks;
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
updateAuthLinks();
if (isLoggedIn()) {
getCurrentUser();
}
});
}
// ============================================
// PRODUCT FUNCTIONS
// ============================================

View File

@ -17,45 +17,48 @@
<button class="auth-tab" type="button" data-target="register">Register</button>
</div>
<form id="auth-form" class="auth-form">
<div id="login-form">
<form id="login-form" class="auth-form">
<div>
<div class="auth-field">
<label for="login-email">Email</label>
<input class="auth-input" id="login-email" type="email" placeholder="you@example.com" required />
<input class="auth-input" id="login-email" type="email" placeholder="you@example.com" autocomplete="email" required />
</div>
<div class="auth-field">
<label for="login-password">Password</label>
<input class="auth-input" id="login-password" type="password" placeholder="••••••••" required />
<input class="auth-input" id="login-password" type="password" placeholder="••••••••" autocomplete="current-password" required />
</div>
<button class="auth-submit" type="submit">Login</button>
</div>
</form>
<div id="register-form" style="display:none;">
<form id="register-form" class="auth-form" style="display:none;">
<div>
<div class="auth-field">
<label for="register-firstname">First name</label>
<input class="auth-input" id="register-firstname" type="text" placeholder="First name" required />
<input class="auth-input" id="register-firstname" type="text" placeholder="First name" autocomplete="given-name" required />
</div>
<div class="auth-field">
<label for="register-lastname">Last name</label>
<input class="auth-input" id="register-lastname" type="text" placeholder="Last name" required />
<input class="auth-input" id="register-lastname" type="text" placeholder="Last name" autocomplete="family-name" required />
</div>
<div class="auth-field">
<label for="register-email">Email</label>
<input class="auth-input" id="register-email" type="email" placeholder="you@example.com" required />
<input class="auth-input" id="register-email" type="email" placeholder="you@example.com" autocomplete="email" required />
</div>
<div class="auth-field">
<label for="register-password">Password</label>
<input class="auth-input" id="register-password" type="password" placeholder="••••••••" required />
<input class="auth-input" id="register-password" type="password" placeholder="••••••••" autocomplete="new-password" required />
</div>
<div class="auth-field">
<label for="register-confirm">Confirm password</label>
<input class="auth-input" id="register-confirm" type="password" placeholder="••••••••" required />
<input class="auth-input" id="register-confirm" type="password" placeholder="••••••••" autocomplete="new-password" required />
</div>
<button class="auth-submit" type="submit">Register</button>
</div>
</form>
<p class="auth-note">Note: This page is connected to the backend API and will log in or register users using your current server.</p>
<p id="auth-message" class="auth-note">Use your email and password to access your account.</p>
<p class="auth-note"><a href="index.html">Back to home</a></p>
</div>
</div>
@ -64,7 +67,11 @@
const tabs = document.querySelectorAll('.auth-tab');
const loginForm = document.getElementById('login-form');
const registerForm = document.getElementById('register-form');
const authForm = document.getElementById('auth-form');
const authMessage = document.getElementById('auth-message');
function setMessage(message) {
authMessage.textContent = message;
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
@ -74,36 +81,54 @@
if (target === 'login') {
loginForm.style.display = 'block';
registerForm.style.display = 'none';
setMessage('Use your email and password to access your account.');
} else {
loginForm.style.display = 'none';
registerForm.style.display = 'block';
setMessage('Create an account with a password of at least 6 characters.');
}
});
});
authForm.addEventListener('submit', async (event) => {
loginForm.addEventListener('submit', async (event) => {
event.preventDefault();
const activeTab = document.querySelector('.auth-tab.active').dataset.target;
if (activeTab === 'login') {
const email = document.getElementById('login-email').value.trim();
const password = document.getElementById('login-password').value;
const result = await login(email, password);
if (result && result.success) {
alert('Login successful');
window.location.href = 'order.html';
}
setMessage('Logging in...');
const email = document.getElementById('login-email').value.trim();
const password = document.getElementById('login-password').value;
const result = await login(email, password);
if (result && result.success) {
setMessage('Login successful. Opening the order page...');
window.location.href = 'order.html';
} else {
const firstName = document.getElementById('register-firstname').value.trim();
const lastName = document.getElementById('register-lastname').value.trim();
const email = document.getElementById('register-email').value.trim();
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-confirm').value;
const result = await register(firstName, lastName, email, password, passwordConfirm);
if (result && result.success) {
alert('Registration successful');
window.location.href = 'order.html';
}
setMessage('Login failed. Check your email and password.');
}
});
registerForm.addEventListener('submit', async (event) => {
event.preventDefault();
const firstName = document.getElementById('register-firstname').value.trim();
const lastName = document.getElementById('register-lastname').value.trim();
const email = document.getElementById('register-email').value.trim();
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-confirm').value;
if (password !== passwordConfirm) {
setMessage('Passwords do not match.');
return;
}
if (password.length < 6) {
setMessage('Password must be at least 6 characters.');
return;
}
setMessage('Creating your account...');
const result = await register(firstName, lastName, email, password, passwordConfirm);
if (result && result.success) {
setMessage('Registration successful. Opening the order page...');
window.location.href = 'order.html';
} else {
setMessage('Registration failed. Try a different email or password.');
}
});
</script>

View File

@ -13,4 +13,5 @@
## Important Notes
- Run backend commands from the `backend/` directory so static files resolve correctly.
- The frontend API helper uses relative same-origin `/api` URLs when served over HTTP.
- User preference: never change the site's colors unless explicitly requested; only change objects/content/layout.
- User preference: never change the site's colors unless explicitly requested; only change objects/content/layout.
- Login/register uses `/api/auth/*`, bcrypt-hashed passwords, JWT tokens in browser local storage, and file-based users in `backend/data/users.json`.