const express = require("express"); const jwt = require("jsonwebtoken"); const { OAuth2Client } = require("google-auth-library"); const { User } = require("../models"); // Import from models/index.js to get models with associations const logger = require("../utils/logger"); const emailService = require("../services/emailService"); const { sanitizeInput, validateRegistration, validateLogin, validateGoogleAuth, } = require("../middleware/validation"); const { csrfProtection, getCSRFToken } = require("../middleware/csrf"); const { loginLimiter, registerLimiter } = require("../middleware/rateLimiter"); const router = express.Router(); const googleClient = new OAuth2Client( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/auth/google/callback" ); // Get CSRF token endpoint router.get("/csrf-token", (req, res) => { getCSRFToken(req, res); }); router.post( "/register", registerLimiter, csrfProtection, sanitizeInput, validateRegistration, async (req, res) => { try { const { username, email, password, firstName, lastName, phone } = req.body; const existingUser = await User.findOne({ where: { [require("sequelize").Op.or]: [{ email }, { username }], }, }); if (existingUser) { return res.status(400).json({ error: "Registration failed", details: [ { field: "email", message: "An account with this email already exists", }, ], }); } const user = await User.create({ username, email, password, firstName, lastName, phone, }); // Generate verification token and send email await user.generateVerificationToken(); // Send verification email (don't block registration if email fails) let verificationEmailSent = false; try { await emailService.sendVerificationEmail(user, user.verificationToken); verificationEmailSent = true; } catch (emailError) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send verification email", { error: emailError.message, userId: user.id, email: user.email }); // Continue with registration even if email fails } const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: "15m", // Short-lived access token }); const refreshToken = jwt.sign( { id: user.id, type: "refresh" }, process.env.JWT_SECRET, { expiresIn: "7d" } ); // Set tokens as httpOnly cookies res.cookie("accessToken", token, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 15 * 60 * 1000, // 15 minutes }); res.cookie("refreshToken", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("User registration successful", { userId: user.id, username: user.username, email: user.email }); res.status(201).json({ user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName, isVerified: user.isVerified, }, verificationEmailSent, // Don't send token in response body for security }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Registration error", { error: error.message, stack: error.stack, email: req.body.email, username: req.body.username }); res.status(500).json({ error: "Registration failed. Please try again." }); } } ); router.post( "/login", loginLimiter, csrfProtection, sanitizeInput, validateLogin, async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ where: { email } }); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); } // Check if account is locked if (user.isLocked()) { return res.status(423).json({ error: "Account is temporarily locked due to too many failed login attempts. Please try again later.", }); } // Verify password const isPasswordValid = await user.comparePassword(password); if (!isPasswordValid) { // Increment login attempts await user.incLoginAttempts(); return res.status(401).json({ error: "Invalid credentials" }); } // Reset login attempts on successful login await user.resetLoginAttempts(); const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: "15m", // Short-lived access token }); const refreshToken = jwt.sign( { id: user.id, type: "refresh" }, process.env.JWT_SECRET, { expiresIn: "7d" } ); // Set tokens as httpOnly cookies res.cookie("accessToken", token, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 15 * 60 * 1000, // 15 minutes }); res.cookie("refreshToken", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("User login successful", { userId: user.id, email: user.email }); res.json({ user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName, isVerified: user.isVerified, }, // Don't send token in response body for security }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Login error", { error: error.message, stack: error.stack, email: req.body.email }); res.status(500).json({ error: "Login failed. Please try again." }); } } ); router.post( "/google", loginLimiter, csrfProtection, sanitizeInput, validateGoogleAuth, async (req, res) => { try { const { code } = req.body; if (!code) { return res.status(400).json({ error: "Authorization code is required" }); } // Exchange authorization code for tokens const { tokens } = await googleClient.getToken({ code, redirect_uri: process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/auth/google/callback", }); // Verify the ID token from the token response const ticket = await googleClient.verifyIdToken({ idToken: tokens.id_token, audience: process.env.GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); const { sub: googleId, email, given_name: firstName, family_name: lastName, picture, } = payload; if (!email || !firstName || !lastName) { return res .status(400) .json({ error: "Required user information not provided by Google" }); } // Check if user exists by Google ID first let user = await User.findOne({ where: { providerId: googleId, authProvider: "google" }, }); if (!user) { // Check if user exists with same email but different auth provider const existingUser = await User.findOne({ where: { email } }); if (existingUser) { return res.status(409).json({ error: "An account with this email already exists. Please use email/password login.", }); } // Create new user (Google OAuth users are auto-verified) user = await User.create({ email, firstName, lastName, authProvider: "google", providerId: googleId, profileImage: picture, username: email.split("@")[0] + "_" + googleId.slice(-6), // Generate unique username isVerified: true, verifiedAt: new Date(), }); } // Generate JWT tokens const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: "15m", }); const refreshToken = jwt.sign( { id: user.id, type: "refresh" }, process.env.JWT_SECRET, { expiresIn: "7d" } ); // Set tokens as httpOnly cookies res.cookie("accessToken", token, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 15 * 60 * 1000, }); res.cookie("refreshToken", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 7 * 24 * 60 * 60 * 1000, }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Google authentication successful", { userId: user.id, email: user.email, isNewUser: !user.createdAt || (Date.now() - new Date(user.createdAt).getTime()) < 1000 }); res.json({ user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName, profileImage: user.profileImage, isVerified: user.isVerified, }, // Don't send token in response body for security }); } catch (error) { if (error.message && error.message.includes("invalid_grant")) { return res .status(401) .json({ error: "Invalid or expired authorization code. Please try again." }); } if (error.message && error.message.includes("redirect_uri_mismatch")) { return res .status(400) .json({ error: "Redirect URI mismatch. Please contact support." }); } const reqLogger = logger.withRequestId(req.id); reqLogger.error("Google OAuth error", { error: error.message, stack: error.stack, codePresent: !!req.body.code }); res .status(500) .json({ error: "Google authentication failed. Please try again." }); } } ); // Email verification endpoint router.post("/verify-email", sanitizeInput, async (req, res) => { try { const { token } = req.body; if (!token) { return res.status(400).json({ error: "Verification token required", code: "TOKEN_REQUIRED", }); } // Find user with this verification token const user = await User.findOne({ where: { verificationToken: token }, }); if (!user) { return res.status(400).json({ error: "Invalid verification token", code: "VERIFICATION_TOKEN_INVALID", }); } // Check if already verified if (user.isVerified) { return res.status(400).json({ error: "Email already verified", code: "ALREADY_VERIFIED", }); } // Check if token is valid (not expired) if (!user.isVerificationTokenValid(token)) { return res.status(400).json({ error: "Verification token has expired. Please request a new one.", code: "VERIFICATION_TOKEN_EXPIRED", }); } // Verify the email await user.verifyEmail(); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Email verified successfully", { userId: user.id, email: user.email, }); res.json({ message: "Email verified successfully", user: { id: user.id, email: user.email, isVerified: true, }, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Email verification error", { error: error.message, stack: error.stack, }); res.status(500).json({ error: "Email verification failed. Please try again.", }); } }); // Resend verification email endpoint router.post( "/resend-verification", loginLimiter, // Use login limiter for rate limiting (max 3 per hour) sanitizeInput, async (req, res) => { try { // Get user from cookies const { accessToken } = req.cookies; if (!accessToken) { return res.status(401).json({ error: "Authentication required", code: "NO_TOKEN", }); } const decoded = jwt.verify(accessToken, process.env.JWT_SECRET); const user = await User.findByPk(decoded.id); if (!user) { return res.status(404).json({ error: "User not found", code: "USER_NOT_FOUND", }); } // Check if already verified if (user.isVerified) { return res.status(400).json({ error: "Email already verified", code: "ALREADY_VERIFIED", }); } // Generate new verification token await user.generateVerificationToken(); // Send verification email try { await emailService.sendVerificationEmail(user, user.verificationToken); } catch (emailError) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to resend verification email", { error: emailError.message, userId: user.id, email: user.email, }); return res.status(500).json({ error: "Failed to send verification email. Please try again.", }); } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Verification email resent", { userId: user.id, email: user.email, }); res.json({ message: "Verification email sent successfully", }); } catch (error) { if (error.name === "TokenExpiredError") { return res.status(401).json({ error: "Session expired. Please log in again.", code: "TOKEN_EXPIRED", }); } const reqLogger = logger.withRequestId(req.id); reqLogger.error("Resend verification error", { error: error.message, stack: error.stack, }); res.status(500).json({ error: "Failed to resend verification email. Please try again.", }); } } ); // Refresh token endpoint router.post("/refresh", async (req, res) => { try { const { refreshToken } = req.cookies; if (!refreshToken) { return res.status(401).json({ error: "Refresh token required" }); } // Verify refresh token const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); if (!decoded.id || decoded.type !== "refresh") { return res.status(401).json({ error: "Invalid refresh token" }); } // Find user const user = await User.findByPk(decoded.id); if (!user) { return res.status(401).json({ error: "User not found" }); } // Generate new access token const newAccessToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: "15m", }); // Set new access token cookie res.cookie("accessToken", newAccessToken, { httpOnly: true, secure: process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "qa", sameSite: "strict", maxAge: 15 * 60 * 1000, }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Token refresh successful", { userId: user.id }); res.json({ user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName, isVerified: user.isVerified, }, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Token refresh error", { error: error.message, stack: error.stack, userId: req.user?.id }); res.status(401).json({ error: "Invalid or expired refresh token" }); } }); // Logout endpoint router.post("/logout", (req, res) => { const reqLogger = logger.withRequestId(req.id); reqLogger.info("User logout", { userId: req.user?.id || 'anonymous' }); // Clear cookies res.clearCookie("accessToken"); res.clearCookie("refreshToken"); res.json({ message: "Logged out successfully" }); }); // Auth status check endpoint - returns 200 regardless of auth state const { optionalAuth } = require("../middleware/auth"); router.get("/status", optionalAuth, async (req, res) => { if (req.user) { res.json({ authenticated: true, user: req.user }); } else { res.json({ authenticated: false }); } }); module.exports = router;