diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..2bfe5e5 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,17 @@ +# Server +PORT=5000 +NODE_ENV=development + +# Database +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=smartqueue +DB_USER=root +DB_PASSWORD= + +# Auth +JWT_SECRET=change-me-to-a-strong-random-value +JWT_EXPIRES_IN=24h + +# CORS — comma-separated allowed origins +CORS_ORIGIN=http://localhost:5173 diff --git a/backend/package-lock.json b/backend/package-lock.json index 024dfb5..5c5c2e7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,8 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", + "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", "morgan": "^1.11.0", "mysql2": "^3.22.4", @@ -2909,6 +2911,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3352,6 +3372,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3497,6 +3529,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index b030568..1945ca0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,8 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", + "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", "morgan": "^1.11.0", "mysql2": "^3.22.4", diff --git a/backend/server.js b/backend/server.js index b94d2c7..bf01f97 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,19 +1,31 @@ // 1. Load the environment variables FIRST require('dotenv').config(); -console.log("DEBUG -> JWT_SECRET VALUE IS:", process.env.JWT_SECRET); -// 2. NOW load your app, database, and models + +// 2. Validate required secrets before booting +if (!process.env.JWT_SECRET) { + console.error('FATAL: JWT_SECRET environment variable is not set. Aborting.'); + process.exit(1); +} + +// 3. NOW load your app, database, and models const app = require('./src/app'); const sequelize = require('./src/config/database'); require('./src/models'); const PORT = process.env.PORT || 5000; +const isDev = process.env.NODE_ENV !== 'production'; async function startServer() { try { await sequelize.authenticate(); console.log('Database connected successfully'); - await sequelize.sync({ alter: true }); + // Only auto-alter schema in development; use migrations in production + if (isDev) { + await sequelize.sync({ alter: true }); + } else { + await sequelize.sync(); + } console.log('Models synchronized'); app.listen(PORT, () => { diff --git a/backend/src/app.js b/backend/src/app.js index b69bff4..ec3657e 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,17 +1,44 @@ const express = require('express'); const cors = require('cors'); +const helmet = require('helmet'); const morgan = require('morgan'); const app = express(); const authRoutes = require('./routes/auth.routes'); -// Global Middleware -app.use(cors()); -app.use(express.json()); -app.use(morgan('dev')); +// Security headers +app.use(helmet()); + +// CORS — restrict to known frontend origin +const allowedOrigins = process.env.CORS_ORIGIN + ? process.env.CORS_ORIGIN.split(',') + : []; + +app.use( + cors({ + origin(origin, callback) { + // Allow server-to-server requests (no origin) and allowed origins + if (!origin || allowedOrigins.includes(origin)) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS')); + }, + credentials: true, + }) +); + +// Body parser with size limit to prevent large-payload DoS +app.use(express.json({ limit: '10kb' })); + +// Request logging — verbose in dev, minimal in production +if (process.env.NODE_ENV === 'production') { + app.use(morgan('combined')); +} else { + app.use(morgan('dev')); +} // API Routes -app.use('/api/auth', authRoutes); // Handles both /api/auth/register and /api/auth/login +app.use('/api/auth', authRoutes); // Health Check app.get('/api/health', (req, res) => { diff --git a/backend/src/middleware/auth.middleware.js b/backend/src/middleware/auth.middleware.js new file mode 100644 index 0000000..aeeede4 --- /dev/null +++ b/backend/src/middleware/auth.middleware.js @@ -0,0 +1,27 @@ +const jwt = require('jsonwebtoken'); + +const authenticate = (req, res, next) => { + const header = req.headers.authorization; + + if (!header || !header.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + message: 'Authentication required.', + }); + } + + const token = header.split(' ')[1]; + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + return next(); + } catch (err) { + return res.status(401).json({ + success: false, + message: 'Invalid or expired token.', + }); + } +}; + +module.exports = { authenticate }; diff --git a/backend/src/models/user.js b/backend/src/models/user.js index 987cac9..02f465f 100644 --- a/backend/src/models/user.js +++ b/backend/src/models/user.js @@ -21,6 +21,10 @@ module.exports = (sequelize) => { notEmpty: { msg: 'Name is required', }, + len: { + args: [1, 100], + msg: 'Name must be between 1 and 100 characters', + }, }, }, diff --git a/backend/src/routes/auth.routes.js b/backend/src/routes/auth.routes.js index 9bd7fd2..e07f898 100644 --- a/backend/src/routes/auth.routes.js +++ b/backend/src/routes/auth.routes.js @@ -1,14 +1,22 @@ const express = require('express'); +const rateLimit = require('express-rate-limit'); const router = express.Router(); -// 1. Add 'login' to the destructured import const { register, login } = require('../controllers/auth.controller'); -// POST /api/auth/register -router.post('/register', register); +// Rate-limit auth endpoints to mitigate brute-force attacks +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, // 20 attempts per window per IP + standardHeaders: true, + legacyHeaders: false, + message: { + success: false, + message: 'Too many requests, please try again later.', + }, +}); -// 2. Add the login route definition -// This maps to: POST /api/auth/login -router.post('/login', login); +router.post('/register', authLimiter, register); +router.post('/login', authLimiter, login); module.exports = router; \ No newline at end of file