const express = require("express"); const router = express.Router(); const bcrypt = require("bcrypt"); const nodemailer = require("nodemailer"); const { pool } = require("../config/database"); const logger = require("../config/logger"); const rateLimit = require("express-rate-limit"); // SECURITY: Rate limiting for auth endpoints to prevent brute force const authRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 failed attempts per window before lockout skipSuccessfulRequests: true, // Only count failed attempts message: { success: false, message: "Too many failed login attempts, please try again after 15 minutes", }, standardHeaders: true, legacyHeaders: false, }); const signupRateLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 10, // 10 signups per hour per IP (increased for production) message: { success: false, message: "Too many signup attempts, please try again later", }, }); const resendCodeLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 3, // 3 resends per minute message: { success: false, message: "Please wait before requesting another code", }, }); // SECURITY: HTML escape function to prevent XSS in emails const escapeHtml = (str) => { if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }; // SECURITY: Use crypto for secure verification code generation const crypto = require("crypto"); // Email transporter configuration let transporter = null; if (process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS) { transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT) || 587, secure: process.env.SMTP_SECURE === "true", auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }); } // Generate 6-digit verification code using cryptographically secure random function generateVerificationCode() { // SECURITY: Use crypto.randomInt for secure random number generation return crypto.randomInt(100000, 999999).toString(); } // Send verification email async function sendVerificationEmail(email, code, firstName) { if (!transporter) { logger.warn("SMTP not configured - verification code logged instead"); logger.info(`🔐 Verification code for ${email}: ${code}`); return true; } // SECURITY: Escape user input to prevent XSS in emails const safeName = escapeHtml(firstName) || "there"; const safeCode = String(code).replace(/[^0-9]/g, ""); // Only allow digits const mailOptions = { from: process.env.SMTP_FROM || '"Sky Art Shop" ', to: email, subject: "Verify your Sky Art Shop account", html: `

🎨 Sky Art Shop

Welcome to our creative community!

Hi ${safeName}!

Thank you for creating an account with Sky Art Shop. Please use the verification code below to complete your registration:

${safeCode}

This code will expire in 15 minutes.

If you didn't create this account, please ignore this email.

`, }; try { await transporter.sendMail(mailOptions); logger.info(`Verification email sent to ${email}`); return true; } catch (error) { logger.error("Error sending verification email:", error); // Still log code as fallback logger.info(`🔐 Verification code for ${email}: ${code}`); return false; } } // Customer auth middleware - session based const requireCustomerAuth = (req, res, next) => { if (!req.session || !req.session.customerId) { return res .status(401) .json({ success: false, message: "Please login to continue" }); } next(); }; // =========================== // SIGNUP - Create new customer // =========================== router.post("/signup", signupRateLimiter, async (req, res) => { try { const { firstName, lastName, email, password, newsletterSubscribed = false, } = req.body; // Validation if (!firstName || !lastName || !email || !password) { return res.status(400).json({ success: false, message: "All fields are required (firstName, lastName, email, password)", }); } // SECURITY: Sanitize inputs to prevent XSS const sanitizedFirstName = firstName .replace(/[<>"'&]/g, "") .trim() .substring(0, 50); const sanitizedLastName = lastName .replace(/[<>"'&]/g, "") .trim() .substring(0, 50); // Email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ success: false, message: "Please enter a valid email address", }); } // Password strength validation if (password.length < 8) { return res.status(400).json({ success: false, message: "Password must be at least 8 characters long", }); } // Check if email already exists const existingCustomer = await pool.query( "SELECT id, email_verified FROM customers WHERE email = $1", [email.toLowerCase()], ); if (existingCustomer.rows.length > 0) { const customer = existingCustomer.rows[0]; // If email exists but not verified, allow re-registration if (!customer.email_verified) { // Generate new verification code const verificationCode = generateVerificationCode(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes const passwordHash = await bcrypt.hash(password, 12); await pool.query( `UPDATE customers SET first_name = $1, last_name = $2, password_hash = $3, verification_code = $4, verification_code_expires = $5, updated_at = CURRENT_TIMESTAMP WHERE email = $6`, [ firstName, lastName, passwordHash, verificationCode, expiresAt, email.toLowerCase(), ], ); // Send verification email await sendVerificationEmail(email, verificationCode, firstName); // Store email in session for verification step req.session.pendingVerificationEmail = email.toLowerCase(); return res.json({ success: true, message: "Verification code sent to your email. Please check your inbox.", requiresVerification: true, }); } return res.status(400).json({ success: false, message: "An account with this email already exists. Please login instead.", }); } // Hash password const passwordHash = await bcrypt.hash(password, 12); // Generate verification code const verificationCode = generateVerificationCode(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes // Create customer await pool.query( `INSERT INTO customers (first_name, last_name, email, password_hash, verification_code, verification_code_expires, newsletter_subscribed) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, email`, [ firstName, lastName, email.toLowerCase(), passwordHash, verificationCode, expiresAt, newsletterSubscribed, ], ); // Send verification email await sendVerificationEmail(email, verificationCode, firstName); // Store email in session for verification step req.session.pendingVerificationEmail = email.toLowerCase(); logger.info(`New customer signup: ${email}`); res.json({ success: true, message: "Account created! Please check your email for the verification code.", requiresVerification: true, }); } catch (error) { logger.error("Signup error:", error); res.status(500).json({ success: false, message: "Failed to create account. Please try again.", }); } }); // =========================== // VERIFY EMAIL // =========================== router.post("/verify-email", async (req, res) => { try { const { email, code } = req.body; const emailToVerify = email?.toLowerCase() || req.session.pendingVerificationEmail; if (!emailToVerify || !code) { return res.status(400).json({ success: false, message: "Email and verification code are required", }); } const result = await pool.query( `SELECT id, first_name, last_name, email_verified, verification_code, verification_code_expires FROM customers WHERE email = $1`, [emailToVerify], ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: "Account not found. Please sign up first.", }); } const customer = result.rows[0]; if (customer.email_verified) { return res.json({ success: true, message: "Email already verified. You can now login.", alreadyVerified: true, }); } if (customer.verification_code !== code) { return res.status(400).json({ success: false, message: "Invalid verification code. Please try again.", }); } if (new Date() > new Date(customer.verification_code_expires)) { return res.status(400).json({ success: false, message: "Verification code has expired. Please request a new one.", expired: true, }); } // Mark email as verified await pool.query( `UPDATE customers SET email_verified = TRUE, verification_code = NULL, verification_code_expires = NULL, last_login = CURRENT_TIMESTAMP, login_count = 1 WHERE id = $1`, [customer.id], ); // Set session - auto-login after verification req.session.customerId = customer.id; req.session.customerEmail = emailToVerify; req.session.customerName = customer.first_name; delete req.session.pendingVerificationEmail; logger.info(`Email verified for customer: ${emailToVerify}`); res.json({ success: true, message: "Email verified successfully! You are now logged in.", customer: { firstName: customer.first_name, lastName: customer.last_name, email: emailToVerify, }, }); } catch (error) { logger.error("Email verification error:", error); res.status(500).json({ success: false, message: "Verification failed. Please try again.", }); } }); // =========================== // RESEND VERIFICATION CODE // =========================== router.post("/resend-code", resendCodeLimiter, async (req, res) => { try { const { email } = req.body; const emailToVerify = email?.toLowerCase() || req.session.pendingVerificationEmail; if (!emailToVerify) { return res.status(400).json({ success: false, message: "Email is required", }); } const result = await pool.query( "SELECT id, first_name, email_verified FROM customers WHERE email = $1", [emailToVerify], ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: "Account not found. Please sign up first.", }); } const customer = result.rows[0]; if (customer.email_verified) { return res.json({ success: true, message: "Email already verified. You can now login.", alreadyVerified: true, }); } // Generate new verification code const verificationCode = generateVerificationCode(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); await pool.query( `UPDATE customers SET verification_code = $1, verification_code_expires = $2 WHERE id = $3`, [verificationCode, expiresAt, customer.id], ); // Send verification email await sendVerificationEmail( emailToVerify, verificationCode, customer.first_name, ); res.json({ success: true, message: "New verification code sent to your email.", }); } catch (error) { logger.error("Resend code error:", error); res.status(500).json({ success: false, message: "Failed to resend code. Please try again.", }); } }); // =========================== // LOGIN // =========================== router.post("/login", authRateLimiter, async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ success: false, message: "Email and password are required", }); } // SECURITY: Sanitize and validate email format const sanitizedEmail = email.toLowerCase().trim(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(sanitizedEmail)) { // SECURITY: Use consistent timing to prevent enumeration await bcrypt.hash("dummy-password", 12); return res.status(401).json({ success: false, message: "Invalid email or password", }); } const result = await pool.query( `SELECT id, first_name, last_name, email, password_hash, email_verified, is_active FROM customers WHERE email = $1`, [sanitizedEmail], ); if (result.rows.length === 0) { // SECURITY: Perform dummy hash to prevent timing attacks await bcrypt.hash("dummy-password", 12); return res.status(401).json({ success: false, message: "Invalid email or password", }); } const customer = result.rows[0]; if (!customer.is_active) { return res.status(403).json({ success: false, message: "This account has been deactivated. Please contact support.", }); } if (!customer.email_verified) { // Store email for verification flow req.session.pendingVerificationEmail = email.toLowerCase(); return res.status(403).json({ success: false, message: "Please verify your email before logging in.", requiresVerification: true, }); } // Verify password const isValidPassword = await bcrypt.compare( password, customer.password_hash, ); if (!isValidPassword) { return res.status(401).json({ success: false, message: "Invalid email or password", }); } // Update last login await pool.query( `UPDATE customers SET last_login = CURRENT_TIMESTAMP, login_count = login_count + 1 WHERE id = $1`, [customer.id], ); // Set session req.session.customerId = customer.id; req.session.customerEmail = customer.email; req.session.customerName = customer.first_name; logger.info(`Customer login: ${email}`); res.json({ success: true, message: "Login successful", customer: { id: customer.id, firstName: customer.first_name, lastName: customer.last_name, email: customer.email, }, }); } catch (error) { logger.error("Login error:", error); res.status(500).json({ success: false, message: "Login failed. Please try again.", }); } }); // =========================== // LOGOUT // =========================== router.post("/logout", (req, res) => { req.session.customerId = null; req.session.customerEmail = null; req.session.customerName = null; res.json({ success: true, message: "Logged out successfully" }); }); // =========================== // GET CURRENT SESSION // =========================== router.get("/session", async (req, res) => { try { if (!req.session.customerId) { return res.json({ success: true, loggedIn: false, }); } const result = await pool.query( `SELECT id, first_name, last_name, email, newsletter_subscribed, created_at FROM customers WHERE id = $1`, [req.session.customerId], ); if (result.rows.length === 0) { req.session.customerId = null; return res.json({ success: true, loggedIn: false, }); } const customer = result.rows[0]; res.json({ success: true, loggedIn: true, customer: { id: customer.id, firstName: customer.first_name, lastName: customer.last_name, email: customer.email, newsletterSubscribed: customer.newsletter_subscribed, memberSince: customer.created_at, }, }); } catch (error) { logger.error("Session error:", error); res.status(500).json({ success: false, message: "Failed to get session" }); } }); // =========================== // UPDATE PROFILE // =========================== router.put("/profile", requireCustomerAuth, async (req, res) => { try { const { firstName, lastName, newsletterSubscribed } = req.body; const updates = []; const values = []; let paramIndex = 1; if (firstName !== undefined) { updates.push(`first_name = $${paramIndex++}`); values.push(firstName); } if (lastName !== undefined) { updates.push(`last_name = $${paramIndex++}`); values.push(lastName); } if (newsletterSubscribed !== undefined) { updates.push(`newsletter_subscribed = $${paramIndex++}`); values.push(newsletterSubscribed); } if (updates.length === 0) { return res.status(400).json({ success: false, message: "No fields to update", }); } values.push(req.session.customerId); await pool.query( `UPDATE customers SET ${updates.join(", ")} WHERE id = $${paramIndex}`, values, ); res.json({ success: true, message: "Profile updated successfully" }); } catch (error) { logger.error("Profile update error:", error); res .status(500) .json({ success: false, message: "Failed to update profile" }); } }); module.exports = router; module.exports.requireCustomerAuth = requireCustomerAuth;