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:
parent
7333839304
commit
171fd243de
@ -15,19 +15,20 @@ const generateToken = (id) => {
|
|||||||
exports.register = async (req, res) => {
|
exports.register = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { firstName, lastName, email, password, passwordConfirm } = req.body;
|
const { firstName, lastName, email, password, passwordConfirm } = req.body;
|
||||||
|
const normalizedEmail = email ? email.toLowerCase().trim() : '';
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!firstName || !lastName || !email || !password || !passwordConfirm) {
|
if (!firstName || !lastName || !email || !password || !passwordConfirm) {
|
||||||
return res.status(400).json({ message: 'All fields are required' });
|
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' });
|
return res.status(400).json({ message: 'Invalid email format' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validatePassword(password)) {
|
if (!validatePassword(password)) {
|
||||||
return res.status(400).json({
|
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
|
// Check if user already exists
|
||||||
let user = await User.findOne({ email });
|
let user = await User.findOne({ email: normalizedEmail });
|
||||||
if (user) {
|
if (user) {
|
||||||
return res.status(400).json({ message: 'Email already in use' });
|
return res.status(400).json({ message: 'Email already in use' });
|
||||||
}
|
}
|
||||||
@ -45,7 +46,7 @@ exports.register = async (req, res) => {
|
|||||||
user = await User.create({
|
user = await User.create({
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email: normalizedEmail,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,18 +70,18 @@ exports.register = async (req, res) => {
|
|||||||
exports.login = async (req, res) => {
|
exports.login = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
const normalizedEmail = email ? email.toLowerCase().trim() : '';
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
return res.status(400).json({ message: 'Email and password are required' });
|
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' });
|
return res.status(400).json({ message: 'Invalid email format' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user exists and get password field
|
const user = await User.findOne({ email: normalizedEmail });
|
||||||
const user = await User.findOne({ email });
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ message: 'Invalid email or password' });
|
return res.status(401).json({ message: 'Invalid email or password' });
|
||||||
}
|
}
|
||||||
@ -173,7 +174,7 @@ exports.refreshToken = async (req, res) => {
|
|||||||
// @access Private
|
// @access Private
|
||||||
exports.getMe = async (req, res) => {
|
exports.getMe = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = await User.findById(req.user.id);
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ exports.protect = async (req, res, next) => {
|
|||||||
return res.status(404).json({ message: 'User not found' });
|
return res.status(404).json({ message: 'User not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.id = user._id;
|
||||||
req.user = user;
|
req.user = user;
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -7,9 +7,7 @@ const validateEmail = (email) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validatePassword = (password) => {
|
const validatePassword = (password) => {
|
||||||
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number
|
return typeof password === 'string' && password.length >= 6;
|
||||||
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;
|
|
||||||
return regex.test(password);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validatePhone = (phone) => {
|
const validatePhone = (phone) => {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
// Simple file-based storage for demo purposes
|
// Simple file-based storage for demo purposes
|
||||||
const USERS_FILE = path.join(__dirname, '../data/users.json');
|
const USERS_FILE = path.join(__dirname, '../data/users.json');
|
||||||
@ -19,10 +20,11 @@ if (!fs.existsSync(USERS_FILE)) {
|
|||||||
// User Schema (simplified for file storage)
|
// User Schema (simplified for file storage)
|
||||||
class User {
|
class User {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
this._id = data._id || Date.now().toString();
|
this._id = data._id || crypto.randomUUID();
|
||||||
|
this.id = this._id;
|
||||||
this.firstName = data.firstName;
|
this.firstName = data.firstName;
|
||||||
this.lastName = data.lastName;
|
this.lastName = data.lastName;
|
||||||
this.email = data.email;
|
this.email = data.email ? data.email.toLowerCase().trim() : data.email;
|
||||||
this.password = data.password;
|
this.password = data.password;
|
||||||
this.phone = data.phone || null;
|
this.phone = data.phone || null;
|
||||||
this.address = data.address || {};
|
this.address = data.address || {};
|
||||||
@ -108,7 +110,8 @@ User.findOne = async (query) => {
|
|||||||
try {
|
try {
|
||||||
const users = await User.find();
|
const users = await User.find();
|
||||||
if (query.email) {
|
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;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -120,13 +123,13 @@ User.findOne = async (query) => {
|
|||||||
User.create = async (data) => {
|
User.create = async (data) => {
|
||||||
try {
|
try {
|
||||||
const users = await User.find();
|
const users = await User.find();
|
||||||
|
const email = data.email.toLowerCase().trim();
|
||||||
|
|
||||||
// Check for duplicate email
|
if (users.some(u => u.email === email)) {
|
||||||
if (users.some(u => u.email === data.email)) {
|
|
||||||
throw new Error('Email already in use');
|
throw new Error('Email already in use');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = new User(data);
|
const newUser = new User({ ...data, email });
|
||||||
await newUser.save();
|
await newUser.save();
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
|
|||||||
89
js/api.js
89
js/api.js
@ -6,6 +6,26 @@ const API_BASE_URL = (function() {
|
|||||||
return 'http://localhost:5000/api';
|
return 'http://localhost:5000/api';
|
||||||
})();
|
})();
|
||||||
const TOKEN_KEY = 'authToken';
|
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
|
// Ensure auth methods are available globally for inline scripts
|
||||||
window.login = async function(email, password) {
|
window.login = async function(email, password) {
|
||||||
@ -18,8 +38,8 @@ window.login = async function(email, password) {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem(TOKEN_KEY, data.token);
|
saveAuth(data);
|
||||||
console.log('Login successful');
|
updateAuthLinks();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +62,8 @@ window.register = async function(firstName, lastName, email, password, passwordC
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem(TOKEN_KEY, data.token);
|
saveAuth(data);
|
||||||
console.log('Registration successful');
|
updateAuthLinks();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,9 +82,12 @@ window.register = async function(firstName, lastName, email, password, passwordC
|
|||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
console.log('Logged out');
|
localStorage.removeItem(USER_KEY);
|
||||||
|
updateAuthLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.logout = logout;
|
||||||
|
|
||||||
function isLoggedIn() {
|
function isLoggedIn() {
|
||||||
return localStorage.getItem(TOKEN_KEY) !== null;
|
return localStorage.getItem(TOKEN_KEY) !== null;
|
||||||
}
|
}
|
||||||
@ -73,6 +96,62 @@ function getToken() {
|
|||||||
return localStorage.getItem(TOKEN_KEY);
|
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
|
// PRODUCT FUNCTIONS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
91
login.html
91
login.html
@ -17,45 +17,48 @@
|
|||||||
<button class="auth-tab" type="button" data-target="register">Register</button>
|
<button class="auth-tab" type="button" data-target="register">Register</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="auth-form" class="auth-form">
|
<form id="login-form" class="auth-form">
|
||||||
<div id="login-form">
|
<div>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="login-email">Email</label>
|
<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>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="login-password">Password</label>
|
<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>
|
</div>
|
||||||
<button class="auth-submit" type="submit">Login</button>
|
<button class="auth-submit" type="submit">Login</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div id="register-form" style="display:none;">
|
<form id="register-form" class="auth-form" style="display:none;">
|
||||||
|
<div>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="register-firstname">First name</label>
|
<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>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="register-lastname">Last name</label>
|
<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>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="register-email">Email</label>
|
<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>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="register-password">Password</label>
|
<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>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="register-confirm">Confirm password</label>
|
<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>
|
</div>
|
||||||
<button class="auth-submit" type="submit">Register</button>
|
<button class="auth-submit" type="submit">Register</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -64,7 +67,11 @@
|
|||||||
const tabs = document.querySelectorAll('.auth-tab');
|
const tabs = document.querySelectorAll('.auth-tab');
|
||||||
const loginForm = document.getElementById('login-form');
|
const loginForm = document.getElementById('login-form');
|
||||||
const registerForm = document.getElementById('register-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 => {
|
tabs.forEach(tab => {
|
||||||
tab.addEventListener('click', () => {
|
tab.addEventListener('click', () => {
|
||||||
@ -74,36 +81,54 @@
|
|||||||
if (target === 'login') {
|
if (target === 'login') {
|
||||||
loginForm.style.display = 'block';
|
loginForm.style.display = 'block';
|
||||||
registerForm.style.display = 'none';
|
registerForm.style.display = 'none';
|
||||||
|
setMessage('Use your email and password to access your account.');
|
||||||
} else {
|
} else {
|
||||||
loginForm.style.display = 'none';
|
loginForm.style.display = 'none';
|
||||||
registerForm.style.display = 'block';
|
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();
|
event.preventDefault();
|
||||||
const activeTab = document.querySelector('.auth-tab.active').dataset.target;
|
setMessage('Logging in...');
|
||||||
|
const email = document.getElementById('login-email').value.trim();
|
||||||
if (activeTab === 'login') {
|
const password = document.getElementById('login-password').value;
|
||||||
const email = document.getElementById('login-email').value.trim();
|
const result = await login(email, password);
|
||||||
const password = document.getElementById('login-password').value;
|
if (result && result.success) {
|
||||||
const result = await login(email, password);
|
setMessage('Login successful. Opening the order page...');
|
||||||
if (result && result.success) {
|
window.location.href = 'order.html';
|
||||||
alert('Login successful');
|
|
||||||
window.location.href = 'order.html';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const firstName = document.getElementById('register-firstname').value.trim();
|
setMessage('Login failed. Check your email and password.');
|
||||||
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;
|
registerForm.addEventListener('submit', async (event) => {
|
||||||
const result = await register(firstName, lastName, email, password, passwordConfirm);
|
event.preventDefault();
|
||||||
if (result && result.success) {
|
const firstName = document.getElementById('register-firstname').value.trim();
|
||||||
alert('Registration successful');
|
const lastName = document.getElementById('register-lastname').value.trim();
|
||||||
window.location.href = 'order.html';
|
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>
|
</script>
|
||||||
|
|||||||
@ -13,4 +13,5 @@
|
|||||||
## Important Notes
|
## Important Notes
|
||||||
- Run backend commands from the `backend/` directory so static files resolve correctly.
|
- 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.
|
- 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`.
|
||||||
Loading…
x
Reference in New Issue
Block a user