From 34932ce4d49a88adb73f94c267270c3dd8659f5b Mon Sep 17 00:00:00 2001 From: Eric Zheng Date: Thu, 7 May 2026 14:56:24 +1200 Subject: [PATCH] feat: cleaned up back-end and added security (helmet, CORS, etc.) --- pnpm-lock.yaml | 29 +++++++++++++++++++++++ server/package.json | 2 ++ server/server.ts | 11 +++------ server/src/app.ts | 33 +++++++++++++++++++++++++++ server/src/middleware/errorHandler.ts | 10 ++++++++ server/src/middleware/notFound.ts | 6 +++++ server/src/middleware/rateLimit.ts | 8 +++++++ server/src/routes/developers.ts | 19 +++++++++++---- server/src/routes/health.ts | 12 ++++++++++ server/src/utils/apiError.ts | 28 +++++++++++++++++++++++ 10 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 server/src/app.ts create mode 100644 server/src/middleware/errorHandler.ts create mode 100644 server/src/middleware/notFound.ts create mode 100644 server/src/middleware/rateLimit.ts create mode 100644 server/src/routes/health.ts create mode 100644 server/src/utils/apiError.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5c161f..e582f7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,12 @@ importers: express: specifier: ^5.2.1 version: 5.2.1 + express-rate-limit: + specifier: ^8.4.1 + version: 8.4.1(express@5.2.1) + helmet: + specifier: ^8.1.0 + version: 8.1.0 mongodb: specifier: ^7.1.0 version: 7.1.1 @@ -1202,6 +1208,12 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + express-rate-limit@8.4.1: + resolution: {integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -1350,6 +1362,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -1392,6 +1408,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3459,6 +3479,11 @@ snapshots: etag@1.8.1: {} + express-rate-limit@8.4.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@5.2.1: dependencies: accepts: 2.0.0 @@ -3628,6 +3653,8 @@ snapshots: dependencies: function-bind: 1.1.2 + helmet@8.1.0: {} + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -3667,6 +3694,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: diff --git a/server/package.json b/server/package.json index ca31bae..89cace7 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,8 @@ "dependencies": { "cors": "^2.8.6", "express": "^5.2.1", + "express-rate-limit": "^8.4.1", + "helmet": "^8.1.0", "mongodb": "^7.1.0", "mongoose": "^9.3.2" }, diff --git a/server/server.ts b/server/server.ts index 74a7d46..e7371e5 100644 --- a/server/server.ts +++ b/server/server.ts @@ -2,20 +2,15 @@ import dotenv from 'dotenv'; dotenv.config({ path: './.env' }); -import express from 'express'; -import cors from 'cors'; import connectDB from './db/connection.js'; -import developerRoutes from './src/routes/developers.js'; +import app from './src/app.js'; + +// entry point for starting the back-end server -const app = express(); const PORT = process.env.PORT || 5050; connectDB(); -app.use(cors()); -app.use(express.json()); -app.use('/api/developers', developerRoutes); - app.listen(PORT, () => { console.log(`server is running on port ${PORT}`); }); \ No newline at end of file diff --git a/server/src/app.ts b/server/src/app.ts new file mode 100644 index 0000000..d3bd145 --- /dev/null +++ b/server/src/app.ts @@ -0,0 +1,33 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import developerRoutes from './routes/developers.js'; +import healthRoutes from './routes/health.js'; +import { apiRateLimit } from './middleware/rateLimit.js'; +import { notFound } from './middleware/notFound.js'; +import { errorHandler } from './middleware/errorHandler.js'; + +// splitting + +const app = express(); + +// security: +app.use(helmet()); +app.use( + cors({ + origin: process.env.CLIENT_ORIGIN || 'http://localhost:5173', + credentials: true, + }) +); +app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || '1mb' })); +app.use('/api', apiRateLimit); +app.use('/api/health', healthRoutes); + +// endpoints -- add new endpoints below +app.use('/api/developers', developerRoutes); + +// fallbacks +app.use(notFound); +app.use(errorHandler); + +export default app; diff --git a/server/src/middleware/errorHandler.ts b/server/src/middleware/errorHandler.ts new file mode 100644 index 0000000..c458a13 --- /dev/null +++ b/server/src/middleware/errorHandler.ts @@ -0,0 +1,10 @@ +import { ErrorRequestHandler } from 'express'; +import { sendError } from '../utils/apiError.js'; + +export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => { + if (process.env.NODE_ENV !== 'test') { + console.error(err); + } + + sendError(res, 500, 'INTERNAL_ERROR', 'Something went wrong'); +}; diff --git a/server/src/middleware/notFound.ts b/server/src/middleware/notFound.ts new file mode 100644 index 0000000..e44654a --- /dev/null +++ b/server/src/middleware/notFound.ts @@ -0,0 +1,6 @@ +import { RequestHandler } from 'express'; +import { sendError } from '../utils/apiError.js'; + +export const notFound: RequestHandler = (_req, res) => { + sendError(res, 404, 'NOT_FOUND', 'Route not found'); +}; diff --git a/server/src/middleware/rateLimit.ts b/server/src/middleware/rateLimit.ts new file mode 100644 index 0000000..df63ab1 --- /dev/null +++ b/server/src/middleware/rateLimit.ts @@ -0,0 +1,8 @@ +import rateLimit from 'express-rate-limit'; + +export const apiRateLimit = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 300, + standardHeaders: true, + legacyHeaders: false, +}); diff --git a/server/src/routes/developers.ts b/server/src/routes/developers.ts index 4ca6f48..ce59374 100644 --- a/server/src/routes/developers.ts +++ b/server/src/routes/developers.ts @@ -1,5 +1,7 @@ import { Router, Request, Response } from 'express'; +import mongoose from 'mongoose'; import Developer, { IDeveloperDocument } from '../models/Developer.js'; +import { sendError } from '../utils/apiError.js'; const router = Router(); @@ -8,17 +10,24 @@ router.get('/', async (_req: Request, res: Response) => { const developers = await Developer.find(); res.json(developers); } catch { - res.status(500).json({ message: 'oh hell nah' }); + sendError(res, 500, 'INTERNAL_ERROR', 'Unable to fetch developers'); } }); router.get('/:id', async (req: Request, res: Response) => { try { + if (!mongoose.isValidObjectId(req.params.id)) { + return sendError(res, 400, 'BAD_REQUEST', 'Invalid id'); + } + const developer = await Developer.findById(req.params.id); - if (!developer) return res.status(404).json({ message: 'oh hell nah' }); + if (!developer) { + return sendError(res, 404, 'NOT_FOUND', 'Developer not found'); + } + res.json(developer); } catch { - res.status(500).json({ message: 'oh hell nah' }); + sendError(res, 500, 'INTERNAL_ERROR', 'Unable to fetch developer'); } }); @@ -27,8 +36,8 @@ router.post('/', async (req: Request, res: R const developer = new Developer(req.body); await developer.save(); res.status(201).json(developer); - } catch{ - res.status(400).json({ message: 'oh hell nah' }); + } catch { + sendError(res, 400, 'VALIDATION_ERROR', 'Invalid developer payload'); } }); diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts new file mode 100644 index 0000000..2a85941 --- /dev/null +++ b/server/src/routes/health.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; + +const router = Router(); + +router.get('/', (_req, res) => { + res.json({ + status: 'ok', + service: 'umsa-api', + }); +}); + +export default router; diff --git a/server/src/utils/apiError.ts b/server/src/utils/apiError.ts new file mode 100644 index 0000000..311e110 --- /dev/null +++ b/server/src/utils/apiError.ts @@ -0,0 +1,28 @@ +import { Response } from 'express'; + +export type ApiErrorCode = + | 'BAD_REQUEST' + | 'INTERNAL_ERROR' + | 'NOT_FOUND' + | 'VALIDATION_ERROR'; + +export type ApiErrorResponse = { + error: { + code: ApiErrorCode; + message: string; + }; +}; + +export const sendError = ( + res: Response, + status: number, + code: ApiErrorCode, + message: string +): Response => { + return res.status(status).json({ + error: { + code, + message, + }, + }); +};