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 { 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); // 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, }); 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, }, // 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, }, // 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 { idToken } = req.body; if (!idToken) { return res.status(400).json({ error: "ID token is required" }); } // Verify the Google ID token const ticket = await googleClient.verifyIdToken({ idToken, 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 user = await User.create({ email, firstName, lastName, authProvider: "google", providerId: googleId, profileImage: picture, username: email.split("@")[0] + "_" + googleId.slice(-6), // Generate unique username }); } // 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, }, // Don't send token in response body for security }); } catch (error) { if (error.message && error.message.includes("Token used too late")) { return res .status(401) .json({ error: "Google token has expired. Please try again." }); } if (error.message && error.message.includes("Invalid token")) { return res .status(401) .json({ error: "Invalid Google token. Please try again." }); } if (error.message && error.message.includes("Wrong number of segments")) { return res .status(400) .json({ error: "Malformed Google token. Please try again." }); } const reqLogger = logger.withRequestId(req.id); reqLogger.error("Google auth error", { error: error.message, stack: error.stack, tokenInfo: logger.sanitize({ idToken: req.body.idToken }) }); res .status(500) .json({ error: "Google authentication failed. 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, }, }); } 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" }); }); module.exports = router;