diff --git a/package-lock.json b/package-lock.json index abf4c01..99a2bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "crypto": "^1.0.1", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-rate-limit": "^7.5.0", "express-session": "^1.18.1", "google-auth-library": "^9.15.1", "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", "nodemailer": "^6.10.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -1541,6 +1543,24 @@ "node": ">=6.0.0" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -2581,6 +2601,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express-session": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", @@ -3971,6 +4006,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 4c50c42..d523742 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,12 @@ "crypto": "^1.0.1", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-rate-limit": "^7.5.0", "express-session": "^1.18.1", "google-auth-library": "^9.15.1", "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", "nodemailer": "^6.10.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index d1783ba..e582f2b 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -20,7 +20,7 @@ import { googleVerifyIdToken } from '../utils/googleVerifyToken.utils.js'; /* eslint no-undef:off */ // Authentication Controllers -export const signup = async (req, res) => { +export const signup = async (req, res, next) => { try { const { error } = signupValidation(req.body); if (error) { @@ -79,11 +79,11 @@ export const signup = async (req, res) => { user: user, }); } catch (error) { - return res.status(500).json({ message: `Signup failed: ${error.message}` }); + next(error); } }; -export const verifyEmail = async (req, res) => { +export const verifyEmail = async (req, res, next) => { try { const { error } = verifyEmailValidation(req.body); if (error) { @@ -120,11 +120,11 @@ export const verifyEmail = async (req, res) => { return res.status(200).json({ message: 'Email verified successfully' }); } catch (error) { - return res.status(500).json({ message: 'Verification failed', error }); + next(error); } }; -export const signin = async (req, res) => { +export const signin = async (req, res, next) => { try { const { email, password } = req.body; @@ -178,11 +178,11 @@ export const signin = async (req, res) => { }, }); } catch (error) { - return res.status(500).json({ message: 'Signin failed', error }); + next(error); } }; -export const forgotPassword = async (req, res) => { +export const forgotPassword = async (req, res, next) => { try { const { error } = forgotPasswordValidation(); if (error) { @@ -225,13 +225,11 @@ export const forgotPassword = async (req, res) => { message: 'Password reset OTP sent', }); } catch (error) { - return res - .status(500) - .json({ message: 'Password reset request failed', error }); + next(error); } }; -export const resetPassword = async (req, res) => { +export const resetPassword = async (req, res, next) => { try { const { error } = resetPasswordValidation(); if (error) { @@ -274,11 +272,11 @@ export const resetPassword = async (req, res) => { return res.status(200).json({ message: 'Password reset successfully' }); } catch (error) { - return res.status(500).json({ message: 'Password reset failed', error }); + next(error); } }; -export const refreshAccessToken = async (req, res) => { +export const refreshAccessToken = async (req, res, next) => { try { const { refreshToken } = req.body; @@ -308,7 +306,7 @@ export const refreshAccessToken = async (req, res) => { accessToken: newAccessToken, }); } catch (error) { - return res.status(401).json({ message: 'Token refresh failed', error }); + next(error); } }; diff --git a/src/index.js b/src/index.js index 9f65173..3bbe382 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,19 @@ import express from 'express'; import * as dotenv from 'dotenv'; dotenv.config(); +import morgan from 'morgan'; +import helmet from 'helmet'; +import cors from 'cors'; +import { apiLimiter } from './utils/apiLimiter.js'; import passport from 'passport'; import session from 'express-session'; import authRouter from './routes/auth.routes.js'; +import { errorHandler, notFound } from './middlewares/errorHandler.js'; import { configureGoogleStrategy } from './strategies/google-strategy.js'; /* eslint no-undef: off */ const PORT = process.env.PORT; + const app = express(); app.use( @@ -25,13 +31,29 @@ app.use( app.use(passport.initialize()); app.use(passport.session()); +//// Cors Policy +app.use(cors()); + +// Helmet helps you secure your Express apps by setting various HTTP headers. +app.use(helmet()); +app.use(helmet.contentSecurityPolicy()); + configureGoogleStrategy(); +// Rate limiter middleware +app.use(apiLimiter); + +//morgan is a HTTP request logger middleware for Node.js +app.use(morgan('dev')); app.use(express.json()); // for parsing application/json app.use(express.urlencoded({ extended: true })); app.use(authRouter); +// Error handling middleware +app.use(notFound); +app.use(errorHandler); + app.listen(PORT, () => { /* eslint no-console:off */ console.log( diff --git a/src/middlewares/errorHandler.middleware.js b/src/middlewares/errorHandler.middleware.js new file mode 100644 index 0000000..a54a41b --- /dev/null +++ b/src/middlewares/errorHandler.middleware.js @@ -0,0 +1,14 @@ +export const notFound = (req, res, next) => { + const error = new Error(`Resource not found - ${req.originalUrl}`); + res.status(404); + next(error); +}; +/* eslint-disable */ +export const errorHandler = (err, req, res, next) => { + const statusCode = res.statusCode === 200 ? 500 : res.statusCode; + res.status(statusCode).json({ + message: err.message, + stack: process.env.NODE_ENV === 'production' ? null : err.stack, // Hide stack trace in production + statusCode, + }); +}; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index bfc280c..e844e88 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -10,15 +10,16 @@ import { googleOAuthCallback, googleOAuthLogin, } from '../controllers/auth.controller.js'; +import { apiLimiter } from '../utils/apiLimiter.js'; const router = Router(); -router.post('/api/auth/signup', signup); -router.post('/api/auth/verifyEmail', verifyEmail); -router.post('/api/auth/signin', signin); -router.post('/api/auth/forgotPassword', forgotPassword); -router.post('/api/auth/resetPassword', resetPassword); -router.post('/api/auth/refreshAccessToken', refreshAccessToken); +router.post('/api/auth/signup', apiLimiter, signup); +router.post('/api/auth/verifyEmail', apiLimiter, verifyEmail); +router.post('/api/auth/signin', apiLimiter, signin); +router.post('/api/auth/forgotPassword', apiLimiter, forgotPassword); +router.post('/api/auth/resetPassword', apiLimiter, resetPassword); +router.post('/api/auth/refreshAccessToken', apiLimiter, refreshAccessToken); // Google OAuth Routes // Initiate Google OAuth authentication diff --git a/src/utils/apiLimiter.js b/src/utils/apiLimiter.js new file mode 100644 index 0000000..a0bd15d --- /dev/null +++ b/src/utils/apiLimiter.js @@ -0,0 +1,12 @@ +import rateLimit from 'express-rate-limit'; + +export const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + standardHeaders: true, + legacyHeaders: false, + message: { + status: 429, + message: 'Too many requests, please try again later.', + }, +});