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) => {
|
||||
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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
89
js/api.js
89
js/api.js
@ -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
|
||||
// ============================================
|
||||
|
||||
91
login.html
91
login.html
@ -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>
|
||||
|
||||
@ -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`.
|
||||
Loading…
x
Reference in New Issue
Block a user