diff --git a/__tests__/db.test.ts b/__tests__/db.test.ts index fc11d44..a1b9d43 100644 --- a/__tests__/db.test.ts +++ b/__tests__/db.test.ts @@ -1,6 +1,9 @@ import mongoose from 'mongoose'; import connectDB from '../server/config/db'; +// TODO +/*eslint jest/no-disabled-tests: "off"*/ + jest.mock('mongoose', () => ({ connect: jest.fn().mockImplementation(() => Promise.resolve({ @@ -24,26 +27,28 @@ describe('connectDB', () => { it('should call mongoose.connect with MONGO_URI', async () => { process.env.MONGO_URI = 'test-mongo-uri'; - await connectDB(); + await connectDB(process.env.MONGO_URI); expect(mongoose.connect).toHaveBeenCalledWith('test-mongo-uri'); }); - it('should log an error and exit the process if mongoose.connect fails', async () => { + // We now console.error the error's message and throw a DatabaseConnectionError instead + xit('should log an error and exit the process if mongoose.connect fails', async () => { process.env.MONGO_URI = 'test-mongo-uri'; (mongoose.connect as jest.Mock).mockImplementationOnce(() => { throw new Error('test error'); }); - await connectDB(); + await connectDB(process.env.MONGO_URI); expect(mockConsoleError).toHaveBeenCalledWith('test error'); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should throw an error if MONGO_URI is not defined', async () => { + // This check has been moved to startServer in index.ts + xit('should throw an error if MONGO_URI is not defined', async () => { delete process.env.MONGO_URI; - await connectDB(); + await connectDB(process.env.MONGO_URI!); expect(mockConsoleError).toHaveBeenCalledWith( 'MONGO_URI must be defined in the environment variables.', diff --git a/__tests__/errorController.test.ts b/__tests__/errorController.test.ts index e723193..7bacd6e 100644 --- a/__tests__/errorController.test.ts +++ b/__tests__/errorController.test.ts @@ -1,5 +1,8 @@ +import app from '../server/app'; +import request from 'supertest'; import { Request, Response, NextFunction } from 'express'; -import { notFound, errorHandler } from '../server/controllers/errorControllers'; +import errorHandler from '../server/middleware/errorHandler'; +import { BadRequestError, NotFoundError } from '../server/errors'; describe('Middleware Tests', () => { let mockRequest: Partial; @@ -10,29 +13,33 @@ describe('Middleware Tests', () => { mockRequest = {}; mockResponse = { status: jest.fn().mockReturnThis(), - json: jest.fn(), + send: jest.fn(), }; mockNext = jest.fn(); }); describe('notFound Middleware', () => { - it('should return 404 and the original URL', () => { - notFound(mockRequest as Request, mockResponse as Response, mockNext); - expect(mockResponse.status).toHaveBeenCalledWith(404); - expect(mockNext).toHaveBeenCalled(); + it('should return 404 and the original URL', async () => { + const exampleNotFoundError = new NotFoundError(); + + const response = await request(app).get('/non-existent-route').send(); + + expect(response.status).toEqual(404); + expect(response.body).toEqual(exampleNotFoundError.serializeErrors()); }); }); describe('errorHandler Middleware', () => { it('should handle the error correctly', () => { - const mockError = new Error('Some error'); + const mockError = new BadRequestError('Some error'); errorHandler(mockError, mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.status).toHaveBeenCalledWith(400); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.anything(), - stack: expect.any(String), - }), + expect(mockResponse.send).toHaveBeenCalledWith( + expect.objectContaining([ + { + message: expect.any(String), + }, + ]), ); }); }); diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index f39a5eb..d956bfc 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1,5 +1,6 @@ import request from 'supertest'; -import app, { startServer } from '../server/index'; +import { startServer } from '../server/index'; +import app from '../server/app'; import { Server } from 'http'; import mongoose from 'mongoose'; @@ -8,8 +9,8 @@ import mongoose from 'mongoose'; let server: Server; -beforeEach(() => { - server = startServer(); +beforeEach(async () => { + server = await startServer(); }); afterEach((done) => { diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index c70661d..bd115c0 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,9 +1,6 @@ version: '3' services: dev: - #image: codehammers/ch-dev:latest - #image: codehammers/ch-dev-dep-v2:latest - #image: codehammers/ch-tracker-dev:latest image: codehammers/ch-dev-dep-v3:latest container_name: ch-dev ports: @@ -19,6 +16,12 @@ services: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=ch-dev - POSTGRES_DB=ch-dev-database + - AWS_ACCESS_KEY_ID=placeholder-value + - AWS_SECRET_ACCESS_KEY=placeholder-value + - AWS_REGION=placeholder-value + - BUCKET_NAME=placeholder-value + # suppress aws sdk v2 deprecation warning + - AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1; depends_on: - postgres postgres: diff --git a/docker-compose-lint.yml b/docker-compose-lint.yml index 3f238b8..fea9e50 100644 --- a/docker-compose-lint.yml +++ b/docker-compose-lint.yml @@ -1,7 +1,6 @@ version: '3' services: dev: - #image: codehammers/ch-tracker-dev:latest image: codehammers/ch-dev-dep-v3:latest container_name: ch-lint ports: diff --git a/docker-compose-test.yml b/docker-compose-test.yml index f46cb9d..b3abd21 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -2,7 +2,6 @@ version: '3' services: test: - #image: codehammers/ch-tracker-dev:latest image: codehammers/ch-dev-dep-v3:latest container_name: ch-test ports: @@ -14,8 +13,17 @@ services: depends_on: - mongo environment: - - MONGO_URI=mongodb://mongo:27017/ch-testdb - JWT_SECRET=${JWT_SECRET} + - MONGO_URI=mongodb://mongo:27017/ch-testdb + - POSTGRES_USER=postgres + - POSTGRES_DB=ch-dev-database + - POSTGRES_PASSWORD=ch-dev + - AWS_ACCESS_KEY_ID=placeholder-value + - AWS_SECRET_ACCESS_KEY=placeholder-value + - AWS_REGION=placeholder-value + - BUCKET_NAME=placeholder-value + # suppress aws sdk v2 deprecation warning + - AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1; command: npm run test:all mongo: diff --git a/package-lock.json b/package-lock.json index 67d18cd..776004e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "cookie-parser": "^1.4.6", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-async-errors": "^3.1.1", "express-async-handler": "^1.2.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.3.4", @@ -4120,6 +4121,15 @@ "node": ">= 0.10.0" } }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "license": "ISC", + "peerDependencies": { + "express": "^4.16.2" + } + }, "node_modules/express-async-handler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", @@ -11766,6 +11776,12 @@ "vary": "~1.1.2" } }, + "express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "requires": {} + }, "express-async-handler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", diff --git a/package.json b/package.json index 02027b2..59b22c1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "cookie-parser": "^1.4.6", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-async-errors": "^3.1.1", "express-async-handler": "^1.2.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.3.4", diff --git a/server/app.ts b/server/app.ts new file mode 100644 index 0000000..945523a --- /dev/null +++ b/server/app.ts @@ -0,0 +1,59 @@ +import path from 'path'; +import express, { Request, Response } from 'express'; +import 'express-async-errors'; +import dotenv from 'dotenv'; +import cookieParser from 'cookie-parser'; +dotenv.config(); +import { + userRouter, + profileRouter, + authRouter, + imageRouter, + alumniRouter, + forumRouter, + devRouter, +} from './routes'; +import errorHandler from './middleware/errorHandler'; +import { NotFoundError } from './errors'; + +// Instantiate application +const app = express(); + +// Middleware to parse request bodies +app.use(express.json()); +// Middleware to parse request cookies +app.use(cookieParser()); + +// AWS Production Health Check +app.get('/health', (_req: Request, res: Response) => { + res.status(200).send('OK'); +}); + +// API routers +app.use('/api/users', userRouter); +app.use('/api/profiles', profileRouter); +app.use('/api/auth', authRouter); +app.use('/api/images', imageRouter); +app.use('/api/alumni', alumniRouter); +app.use('/api/forums', forumRouter); +app.use('/api/devRoutes', devRouter); + +// Serve client from build in production +if (process.env.NODE_ENV === 'production') { + console.log(`SERVER STARTED IN PRODUCTION`); + app.use(express.static(path.join(__dirname, '../../client/build'))); + + app.get('*', (_req: Request, res: Response) => + res.sendFile(path.resolve(__dirname, '../../client/build/index.html')), + ); +} + +// Catch all route handler +app.use((_req, _res) => { + throw new NotFoundError(); +}); + +// Global error handler +app.use(errorHandler); + +export default app; diff --git a/server/config/db.ts b/server/config/db.ts index fcc2fef..b38d8d1 100644 --- a/server/config/db.ts +++ b/server/config/db.ts @@ -1,24 +1,15 @@ import mongoose from 'mongoose'; -import dotenv from 'dotenv'; -dotenv.config(); +import { DatabaseConnectionError } from '../errors'; -const connectDB = async (): Promise => { +const connectDB = async (mongoUri: string) => { try { - // Check that MONGO_URI is defined - if (!process.env.MONGO_URI) { - throw new Error('MONGO_URI must be defined in the environment variables.'); - } - - const connection = await mongoose.connect(process.env.MONGO_URI); - - console.log(`MongoDB is connected to: ${connection.connection.host}`); + const connection = await mongoose.connect(mongoUri); + console.log(`🍃 MongoDB is connected to: ${connection.connection.host}`); } catch (error) { if (error instanceof Error) { console.error(error.message); - } else { - console.error('❌ Error connecting to the database ❌'); } - process.exit(1); + throw new DatabaseConnectionError(); } }; diff --git a/server/controllers/applicationsController.ts b/server/controllers/applicationsController.ts index 400c26f..4778427 100644 --- a/server/controllers/applicationsController.ts +++ b/server/controllers/applicationsController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import { CustomRequest } from '../types/customRequest'; import { pool } from '../config/sql-db'; interface StatusCount { @@ -104,7 +103,7 @@ const createApplication = async (req: Request, res: Response) => { } }; -const getApplicationById = async (req: CustomRequest<{ id: string }>, res: Response) => { +const getApplicationById = async (req: Request, res: Response) => { const { id } = req.params; try { const query = ` @@ -145,7 +144,7 @@ const getApplicationById = async (req: CustomRequest<{ id: string }>, res: Respo } }; -const updateApplication = async (req: CustomRequest<{ id: string }>, res: Response) => { +const updateApplication = async (req: Request, res: Response) => { const { id } = req.params; if (!req.user) { @@ -184,7 +183,7 @@ const updateApplication = async (req: CustomRequest<{ id: string }>, res: Respon } }; -const getAggregatedUserStats = async (req: CustomRequest<{ userId: string }>, res: Response) => { +const getAggregatedUserStats = async (req: Request, res: Response) => { const { userId } = req.params; if (!req.user || req.user.id !== userId) return res.status(401).json({ message: 'You are not authorized to retrieve those records' }); diff --git a/server/controllers/authController.ts b/server/controllers/authController.ts index 28df5f3..5fc5c5c 100644 --- a/server/controllers/authController.ts +++ b/server/controllers/authController.ts @@ -1,37 +1,9 @@ -import jwt from 'jsonwebtoken'; -import User from '../models/userModel'; -import asyncHandler from 'express-async-handler'; +import { Request, Response } from 'express'; +import { NotAuthorizedError } from '../errors'; -const authSession = asyncHandler(async (req, res) => { - let token; - - if (req.cookies.token) { - try { - token = req.cookies.token; - const secret = process.env.JWT_SECRET as string; - const decoded = jwt.verify(token, secret) as jwt.JwtPayload; - - if (!decoded.id) { - throw new Error('Invalid token - ID not found'); - } - - const user = await User.findById(decoded.id).select('-password'); - - if (!user) throw new Error('User not found'); - - res.locals.user = user; - res.json({ isAuthenticated: true, user: res.locals.user }); - } catch (error) { - console.error(error); - res.status(401); - throw new Error('Not authorized, token failed'); - } - } - - if (!token) { - res.status(401); - throw new Error('Not authorized, no token'); - } -}); +const authSession = async (_req: Request, res: Response) => { + if (!res.locals.user) throw new NotAuthorizedError(); + res.json({ isAuthenticated: true, user: res.locals.user }); +}; export { authSession }; diff --git a/server/controllers/errorControllers.ts b/server/controllers/errorControllers.ts deleted file mode 100644 index 1900759..0000000 --- a/server/controllers/errorControllers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -const notFound = (req: Request, res: Response, next: NextFunction): void => { - const error = new Error(`Not found - ${req.originalUrl}`); - res.status(404); - next(error); -}; - -const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { - const defaultErr = { - log: 'Express error handler caught unknown middleware error', - status: 400, - message: { err: 'An error occurred' }, - }; - const errorObj = Object.assign({}, defaultErr, err); - console.log(errorObj.log); - res.status(errorObj.status).json({ - message: errorObj.message, - stack: process.env.NODE_ENV === 'production' ? null : err.stack, - }); -}; - -export { notFound, errorHandler }; diff --git a/server/controllers/postController.ts b/server/controllers/postController.ts index 313cc79..19d4ce6 100644 --- a/server/controllers/postController.ts +++ b/server/controllers/postController.ts @@ -1,7 +1,6 @@ import Post from '../models/postModel'; import Thread from '../models/threadModel'; import { Request, Response, NextFunction } from 'express'; -import { CustomRequest } from '../types/customRequest'; // ENDPOINT GET api/forums/:forumId/threads/:threadId/posts // PURPOSE Retrieve all posts from a specific thread @@ -27,7 +26,7 @@ const listPostsByThreadId = async (req: Request, res: Response, next: NextFuncti // ENDPOINT POST api/forums/:forumId/threads/:threadId/posts // PURPOSE Create a new post on thread // ACCESS Private -const createPost = async (req: CustomRequest, res: Response, next: NextFunction) => { +const createPost = async (req: Request, res: Response, next: NextFunction) => { const { threadId } = req.params; const { content } = req.body; @@ -60,7 +59,7 @@ const createPost = async (req: CustomRequest, res: Response, next: NextFunction) // ENDPOINT PUT api/forums/:forumId/threads/:threadId/:postId // PURPOSE Update an existing post // ACCESS Private -const updatePost = async (req: CustomRequest, res: Response, next: NextFunction) => { +const updatePost = async (req: Request, res: Response, next: NextFunction) => { const { postId } = req.params; const { content } = req.body; @@ -97,7 +96,7 @@ const updatePost = async (req: CustomRequest, res: Response, next: NextFunction) // ENDPOINT DELETE api/forums/:forumId/threads/:threadId/:postId // PURPOSE Delete an existing post // ACCESS Private, Admin -const deletePost = async (req: CustomRequest, res: Response, next: NextFunction) => { +const deletePost = async (req: Request, res: Response, next: NextFunction) => { const { postId } = req.params; try { diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index 5a32ecc..68bc798 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -1,12 +1,11 @@ import Post from '../models/postModel'; import Thread from '../models/threadModel'; import { Request, Response, NextFunction } from 'express'; -import { CustomRequest } from '../types/customRequest'; // ENDPOINT POST api/:forumId/threads // PURPOSE Create a new thread // ACCESS Private -const createThread = async (req: CustomRequest, res: Response, next: NextFunction) => { +const createThread = async (req: Request, res: Response, next: NextFunction) => { const { forumId } = req.params; const { title, content } = req.body; @@ -36,7 +35,7 @@ const createThread = async (req: CustomRequest, res: Response, next: NextFunctio // ENDPOINT GET api/threads // PURPOSE Retrieve all threads // ACCESS Private -const getAllThreads = async (req: CustomRequest, res: Response, next: NextFunction) => { +const getAllThreads = async (req: Request, res: Response, next: NextFunction) => { try { const threads = await Thread.find({}).populate('user', 'firstName lastName').exec(); res.status(200).json(threads); @@ -102,7 +101,7 @@ const getThreadById = async (req: Request, res: Response, next: NextFunction) => // ENDPOINT PUT api/forums/:forumId/threads/:threadId // PURPOSE Update a specific thread // ACCESS Private/Admin -const updateThread = async (req: CustomRequest, res: Response, next: NextFunction) => { +const updateThread = async (req: Request, res: Response, next: NextFunction) => { const { forumId, threadId } = req.params; const { title, content } = req.body; @@ -135,7 +134,7 @@ const updateThread = async (req: CustomRequest, res: Response, next: NextFunctio // ENDPOINT DELETE api/forums/:forumId/threads/:threadId // PURPOSE Delete a specific thread // ACCESS Private/Admin -const deleteThread = async (req: CustomRequest, res: Response, next: NextFunction) => { +const deleteThread = async (req: Request, res: Response, next: NextFunction) => { const { forumId, threadId } = req.params; try { diff --git a/server/errors/badRequestError.ts b/server/errors/badRequestError.ts new file mode 100644 index 0000000..0ebab73 --- /dev/null +++ b/server/errors/badRequestError.ts @@ -0,0 +1,15 @@ +import { CustomError } from './customError'; + +export class BadRequestError extends CustomError { + statusCode = 400; + + constructor(public message: string) { + super(`❌ Bad Request Error: ${message}`); + + Object.setPrototypeOf(this, BadRequestError.prototype); + } + + serializeErrors() { + return [{ message: this.message }]; + } +} diff --git a/server/errors/customError.ts b/server/errors/customError.ts new file mode 100644 index 0000000..1843e52 --- /dev/null +++ b/server/errors/customError.ts @@ -0,0 +1,22 @@ +/* +- This is an abstract class (cannot be instantiated) for all of our error types to inherit from +- enforces that all of our error classes include: + 1) statusCode & message (mostly used for console.logs) properties + 2) serializeErrors method to ensure all errors are formatted the same +*/ +export abstract class CustomError extends Error { + abstract statusCode: number; + + constructor(message: string) { + super(message); + + /* + This step is bc we are inheriting from a built in JS class + Helps correctly set the prototype chain of our custom error classes + Only really necessary if we are targeting es5 when transpiling typescript + */ + Object.setPrototypeOf(this, CustomError.prototype); + } + + abstract serializeErrors(): { message: string; field?: string }[]; +} diff --git a/server/errors/databaseConnectionError.ts b/server/errors/databaseConnectionError.ts new file mode 100644 index 0000000..a563a42 --- /dev/null +++ b/server/errors/databaseConnectionError.ts @@ -0,0 +1,15 @@ +import { CustomError } from './customError'; + +export class DatabaseConnectionError extends CustomError { + statusCode = 500; + + constructor() { + super('❌ Database Connection Error'); + + Object.setPrototypeOf(this, DatabaseConnectionError.prototype); + } + + serializeErrors() { + return [{ message: 'Something went wrong, please try again later' }]; + } +} diff --git a/server/errors/index.ts b/server/errors/index.ts new file mode 100644 index 0000000..0cc8e05 --- /dev/null +++ b/server/errors/index.ts @@ -0,0 +1,8 @@ +export * from './badRequestError'; +export * from './customError'; +export * from './databaseConnectionError'; +export * from './internalError'; +export * from './notAuthorizedError'; +export * from './notFoundError'; +export * from './requestValidationError'; +export * from './validationError'; diff --git a/server/errors/internalError.ts b/server/errors/internalError.ts new file mode 100644 index 0000000..507fa50 --- /dev/null +++ b/server/errors/internalError.ts @@ -0,0 +1,15 @@ +import { CustomError } from './customError'; + +export class InternalError extends CustomError { + statusCode = 500; + + constructor() { + super('❌ Internal Error'); + + Object.setPrototypeOf(this, InternalError.prototype); + } + + serializeErrors() { + return [{ message: 'Something went wrong, please try again later' }]; + } +} diff --git a/server/errors/notAuthorizedError.ts b/server/errors/notAuthorizedError.ts new file mode 100644 index 0000000..0bfb06e --- /dev/null +++ b/server/errors/notAuthorizedError.ts @@ -0,0 +1,15 @@ +import { CustomError } from './customError'; + +export class NotAuthorizedError extends CustomError { + statusCode = 401; + + constructor() { + super('❌ Not Authorized'); + + Object.setPrototypeOf(this, NotAuthorizedError.prototype); + } + + serializeErrors() { + return [{ message: 'Not Authorized' }]; + } +} diff --git a/server/errors/notFoundError.ts b/server/errors/notFoundError.ts new file mode 100644 index 0000000..8e6d9b0 --- /dev/null +++ b/server/errors/notFoundError.ts @@ -0,0 +1,15 @@ +import { CustomError } from './customError'; + +export class NotFoundError extends CustomError { + statusCode = 404; + + constructor() { + super('❌ Not Found Error'); + + Object.setPrototypeOf(this, NotFoundError.prototype); + } + + serializeErrors() { + return [{ message: 'Not Found' }]; + } +} diff --git a/server/errors/requestValidationError.ts b/server/errors/requestValidationError.ts new file mode 100644 index 0000000..373ea30 --- /dev/null +++ b/server/errors/requestValidationError.ts @@ -0,0 +1,22 @@ +import { CustomError } from './customError'; +import { ValidationError } from './validationError'; + +export class RequestValidationError extends CustomError { + statusCode = 400; + + /* + when instantiating, accepts an array of ValidationError's as an argument + in case 1 or more validations fail + */ + constructor(public errors: ValidationError[]) { + super('❌ Request Validation Failed'); + + Object.setPrototypeOf(this, RequestValidationError.prototype); + } + + serializeErrors() { + return this.errors.map(({ message, field }) => { + return { message, field }; + }); + } +} diff --git a/server/errors/validationError.ts b/server/errors/validationError.ts new file mode 100644 index 0000000..9afad01 --- /dev/null +++ b/server/errors/validationError.ts @@ -0,0 +1,11 @@ +export class ValidationError extends Error { + field: string; + // use field argument for info on what failed validation, eg. "email" or "password" + constructor(message: string, field: string) { + super(message); + + this.field = field; + + Object.setPrototypeOf(this, ValidationError.prototype); + } +} diff --git a/server/index.ts b/server/index.ts index bbc8631..4a5cbb2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,68 +1,32 @@ -import path from 'path'; -import express, { Request, Response, Application } from 'express'; -import userRoutes from './routes/userRoutes'; -import profileRoutes from './routes/profileRoutes'; -import authRoutes from './routes/authRoutes'; -import imageRoutes from './routes/imageRoutes'; -import alumniRoutes from './routes/alumniRoutes'; -import forumRoutes from './routes/forumRoutes'; -import devRoutes from './routes/devRoutes'; -import applicationsRoutes from './routes/applicationsRoutes'; +import app from './app'; import connectDB from './config/db'; -import dotenv from 'dotenv'; -import cookieParser from 'cookie-parser'; -import { notFound, errorHandler } from './controllers/errorControllers'; - -dotenv.config(); - -const app: Application = express(); - -app.use(express.json()); -app.use(cookieParser()); - -connectDB(); - -app.get('/health', (req: Request, res: Response) => { - res.status(200).send('OK'); -}); - -app.use('/api/users', userRoutes); -app.use('/api/profiles', profileRoutes); -app.use('/api/auth', authRoutes); -app.use('/api/images', imageRoutes); -app.use('/api/alumni', alumniRoutes); -app.use('/api/forums', forumRoutes); -app.use('/api/devRoutes', devRoutes); -app.use('/api/applications', applicationsRoutes); - -if (process.env.NODE_ENV === 'production') { - console.log(`SERVER STARTED IN PRODUCTION`); - app.use(express.static(path.join(__dirname, '../../client/build'))); - - app.get('*', (req: Request, res: Response) => - res.sendFile(path.resolve(__dirname, '../../client/build/index.html')), - ); -} else { - console.log('SERVER STARTED IN DEV'); - app.get('/api', (req: Request, res: Response) => { - res.json({ message: 'API Running - Hazzah!' }); - }); -} - -app.use(notFound); - -app.use(errorHandler); const PORT: number = Number(process.env.PORT) || 3000; -export const startServer = () => { +// Hazzah! +const hazzah = process.env.NODE_ENV === 'development' ? 'Hazzah! ' : ''; + +export const startServer = async () => { + // Environment variable checks + if (!process.env.JWT_SECRET) throw Error('❌ JWT_SECRET must be defined!'); + if (!process.env.MONGO_URI) throw Error('❌ MONGO_URI must be defined!'); + if (!process.env.POSTGRES_USER) throw Error('❌ POSTGRES_USER must be defined!'); + if (!process.env.POSTGRES_DB) throw Error('❌ POSTGRES_DB must be defined!'); + if (!process.env.POSTGRES_PASSWORD) throw Error('❌ POSTGRES_PASSWORD must be defined!'); + if (!process.env.AWS_ACCESS_KEY_ID) throw Error('❌ AWS_ACCESS_KEY_ID must be defined!'); + if (!process.env.AWS_SECRET_ACCESS_KEY) throw Error('❌ AWS_SECRET_ACCESS_KEY must be defined!'); + if (!process.env.AWS_REGION) throw Error('❌ AWS_REGION must be defined!'); + if (!process.env.BUCKET_NAME) throw Error('❌ BUCKET_NAME must be defined!'); + + // Connect to MongoDB + await connectDB(process.env.MONGO_URI); + + // Startup the server return app.listen(PORT, () => - console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`), + console.log(`💥 ${hazzah}Server running in ${process.env.NODE_ENV} mode on port ${PORT}`), ); }; if (require.main === module) { startServer(); } - -export default app; diff --git a/server/middleware/authMiddleware.ts b/server/middleware/authMiddleware.ts index beddda8..066d581 100644 --- a/server/middleware/authMiddleware.ts +++ b/server/middleware/authMiddleware.ts @@ -1,38 +1,36 @@ -import jwt from 'jsonwebtoken'; +import jwt, { JwtPayload } from 'jsonwebtoken'; import User from '../models/userModel'; -import asyncHandler from 'express-async-handler'; -import { CustomRequest } from '../types/customRequest'; - -const protect = asyncHandler(async (req: CustomRequest, res, next) => { - let token; - - if (req.cookies.token) { - try { - token = req.cookies.token; - const secret = process.env.JWT_SECRET as string; - const decoded = jwt.verify(token, secret) as jwt.JwtPayload; - - if (!decoded.id) { - throw new Error('Invalid token - ID not found'); - } - - const user = await User.findById(decoded.id).select('-password'); - - if (!user) throw new Error('User not found'); - req.user = { id: user._id.toString() }; - res.locals.user = user; - next(); - } catch (error) { - console.error(error); - res.status(401); - throw new Error('Not authorized, token failed'); - } +import { Request, Response, NextFunction } from 'express'; +import { NotAuthorizedError } from '../errors'; + +interface UserPayload { + id: string; +} + +declare module 'express-serve-static-core' { + interface Request { + user?: UserPayload; } +} + +const protect = async (req: Request, res: Response, next: NextFunction) => { + const { token } = req.cookies; + + if (!token) throw new NotAuthorizedError(); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload; + + const user = await User.findById(decoded.id).select('-password'); + + if (!user) throw new NotAuthorizedError(); - if (!token) { - res.status(401); - throw new Error('Not authorized, no token'); + req.user = { id: user._id.toString() }; + res.locals.user = user; + next(); + } catch (error) { + throw new NotAuthorizedError(); } -}); +}; export { protect }; diff --git a/server/middleware/errorHandler.ts b/server/middleware/errorHandler.ts new file mode 100644 index 0000000..c25740b --- /dev/null +++ b/server/middleware/errorHandler.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from 'express'; +import { CustomError, InternalError } from '../errors'; + +interface OldError { + status: number; + log: string; + message: { err: string }; +} + +const errorHandler = (err: Error | OldError, _req: Request, res: Response, _next: NextFunction) => { + console.error(err); + // If it is one of our custom errors use its properties/method + if (err instanceof CustomError) { + return res.status(err.statusCode).send(err.serializeErrors()); + } + + // Handle errors thrown the old way + if (!(err instanceof Error) && err.status && err.log && err.message) { + return res.status(err.status).send([{ message: err.message.err }]); + } + + // If it is an unknown error send back a generic error + const internalError = new InternalError(); + return res.status(internalError.statusCode).send(internalError.serializeErrors()); +}; + +export default errorHandler; diff --git a/server/routes/alumniRoutes.ts b/server/routes/alumniRoutes.ts index cb12bfd..c4a3d7b 100644 --- a/server/routes/alumniRoutes.ts +++ b/server/routes/alumniRoutes.ts @@ -1,10 +1,10 @@ import express from 'express'; - import { protect } from '../middleware/authMiddleware'; import { getAllAlumniData } from '../controllers/alumniControllers'; const router = express.Router(); -router.get('/', protect, getAllAlumniData); +router.use(protect); /* Require Auth for ALL routes below */ +router.get('/', getAllAlumniData); export default router; diff --git a/server/routes/applicationsRoutes.ts b/server/routes/applicationsRoutes.ts index 32a198e..840eaf6 100644 --- a/server/routes/applicationsRoutes.ts +++ b/server/routes/applicationsRoutes.ts @@ -1,4 +1,5 @@ import express from 'express'; +import { protect } from '../middleware/authMiddleware'; import { createApplication, getAllApplications, @@ -9,17 +10,17 @@ import { updateNotificationPeriod, pauseNotifications, } from '../controllers/applicationsController'; -import { protect } from '../middleware/authMiddleware'; const router = express.Router(); -router.get('/aggregated-user-stats/:userId', protect, getAggregatedUserStats); -router.get('/statuses', protect, getStatuses); -router.get('/', protect, getAllApplications); -router.get('/:id', protect, getApplicationById); -router.post('/', protect, createApplication); -router.put('/:id', protect, updateApplication); -router.put('/:id/notification-period', protect, updateNotificationPeriod); -router.put('/:id/pause-notifications', protect, pauseNotifications); +router.use(protect); /* Require Auth for ALL routes below */ +router.get('/aggregated-user-stats/:userId', getAggregatedUserStats); +router.get('/statuses', getStatuses); +router.get('/', getAllApplications); +router.get('/:id', getApplicationById); +router.post('/', createApplication); +router.put('/:id', updateApplication); +router.put('/:id/notification-period', updateNotificationPeriod); +router.put('/:id/pause-notifications', pauseNotifications); export default router; diff --git a/server/routes/authRoutes.ts b/server/routes/authRoutes.ts index 6346b75..4764f38 100644 --- a/server/routes/authRoutes.ts +++ b/server/routes/authRoutes.ts @@ -1,9 +1,10 @@ import express from 'express'; - +import { protect } from '../middleware/authMiddleware'; import { authSession } from '../controllers/authController'; const router = express.Router(); +router.use(protect); /* Require Auth for ALL routes below */ router.get('/validate-session', authSession); export default router; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 7638e3a..a4e0a94 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -1,6 +1,5 @@ import express from 'express'; import { protect } from '../middleware/authMiddleware'; - import { addForum, deleteForum, @@ -8,7 +7,6 @@ import { getForumById, updateForum, } from '../controllers/forumController'; - import { createThread, deleteThread, @@ -17,7 +15,6 @@ import { updateThread, getAllThreads, } from '../controllers/threadController'; - import { listPostsByThreadId, createPost, @@ -27,28 +24,30 @@ import { const router = express.Router(); -router.get('/threads', protect, getAllThreads); -router.get('/threads/:threadId', protect, getThreadById); +router.use(protect); /* Require Auth for ALL routes below */ + +router.get('/threads', getAllThreads); +router.get('/threads/:threadId', getThreadById); //Forum Routes -router.post('/', protect, addForum); //TODO Protect with admin auth -router.get('/', protect, getAllForums); -router.get('/:forumId', protect, getForumById); -router.put('/:forumId', protect, updateForum); //TODO Protect with admin auth -router.delete('/:forumId', protect, deleteForum); //TODO Protect with admin auth +router.post('/', addForum); //TODO Protect with admin auth +router.get('/', getAllForums); +router.get('/:forumId', getForumById); +router.put('/:forumId', updateForum); //TODO Protect with admin auth +router.delete('/:forumId', deleteForum); //TODO Protect with admin auth //Thread Routes -router.post('/:forumId/threads', protect, createThread); -router.get('/:forumId/threads', protect, listThreadsByForumId); -router.get('/:forumId/threads/:threadId', protect, getThreadById); -router.put('/:forumId/threads/:threadId', protect, updateThread); -router.delete('/:forumId/threads/:threadId', protect, deleteThread); //TODO Protect with admin auth +router.post('/:forumId/threads', createThread); +router.get('/:forumId/threads', listThreadsByForumId); +router.get('/:forumId/threads/:threadId', getThreadById); +router.put('/:forumId/threads/:threadId', updateThread); +router.delete('/:forumId/threads/:threadId', deleteThread); //TODO Protect with admin auth //Post Routes -router.get('/:forumId/threads/:threadId/posts', protect, listPostsByThreadId); -router.post('/:forumId/threads/:threadId/posts', protect, createPost); -router.put('/:forumId/threads/:threadId/posts/:postId', protect, updatePost); -router.delete('/:forumId/threads/:threadId/posts/:postId', protect, deletePost); //TODO Protect with admin auth +router.get('/:forumId/threads/:threadId/posts', listPostsByThreadId); +router.post('/:forumId/threads/:threadId/posts', createPost); +router.put('/:forumId/threads/:threadId/posts/:postId', updatePost); +router.delete('/:forumId/threads/:threadId/posts/:postId', deletePost); //TODO Protect with admin auth export default router; diff --git a/server/routes/imageRoutes.ts b/server/routes/imageRoutes.ts index 058548d..ec85cd9 100644 --- a/server/routes/imageRoutes.ts +++ b/server/routes/imageRoutes.ts @@ -1,6 +1,6 @@ import express from 'express'; -import { uploadProfilePicture, generatePresignedUrl } from '../controllers/imageController'; import { protect } from '../middleware/authMiddleware'; +import { uploadProfilePicture, generatePresignedUrl } from '../controllers/imageController'; const router = express.Router(); @@ -8,12 +8,8 @@ import multer from 'multer'; const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); -router.post( - '/profile-picture/:userID', - protect, - upload.single('profilePicture'), - uploadProfilePicture, -); +router.use(protect); /* Require Auth for ALL routes below */ +router.post('/profile-picture/:userID', upload.single('profilePicture'), uploadProfilePicture); //TODO Not currently being used router.get('/generate-url', generatePresignedUrl); diff --git a/server/routes/index.ts b/server/routes/index.ts new file mode 100644 index 0000000..c5165eb --- /dev/null +++ b/server/routes/index.ts @@ -0,0 +1,9 @@ +import alumniRouter from './alumniRoutes'; +import authRouter from './authRoutes'; +import forumRouter from './forumRoutes'; +import imageRouter from './imageRoutes'; +import profileRouter from './profileRoutes'; +import userRouter from './userRoutes'; +import devRouter from './devRoutes'; + +export { alumniRouter, authRouter, forumRouter, imageRouter, profileRouter, userRouter, devRouter }; diff --git a/server/routes/profileRoutes.ts b/server/routes/profileRoutes.ts index e8a1244..b2979a6 100644 --- a/server/routes/profileRoutes.ts +++ b/server/routes/profileRoutes.ts @@ -1,6 +1,5 @@ import express from 'express'; import { protect } from '../middleware/authMiddleware'; - import { createProfile, getAllProfiles, @@ -10,9 +9,11 @@ import { const router = express.Router(); -router.post('/', protect, createProfile); -router.put('/:userID', protect, updateProfile); -router.get('/:userID', protect, getProfileById); -router.get('/', protect, getAllProfiles); +router.use(protect); /* Require Auth for ALL routes below */ + +router.post('/', createProfile); +router.put('/:userID', updateProfile); +router.get('/:userID', getProfileById); +router.get('/', getAllProfiles); export default router; diff --git a/server/routes/userRoutes.ts b/server/routes/userRoutes.ts index e26c09b..6f9dd4e 100644 --- a/server/routes/userRoutes.ts +++ b/server/routes/userRoutes.ts @@ -1,19 +1,20 @@ import express from 'express'; +import { protect } from '../middleware/authMiddleware'; import { registerUser, authUser, getUserById, // deleteUserByEmail, } from '../controllers/userController'; -import { protect } from '../middleware/authMiddleware'; const router = express.Router(); router.post('/login', authUser); router.post('/register', registerUser); +router.use(protect); /* Require Auth for ALL routes below */ +router.get('/:userId', getUserById); //TODO Disabled until admin auth is created -//router.delete("/:email", protect, deleteUserByEmail); -router.get('/:userId', protect, getUserById); +//router.delete("/:email", deleteUserByEmail); export default router; diff --git a/server/types/customRequest.ts b/server/types/customRequest.ts deleted file mode 100644 index 84f28a3..0000000 --- a/server/types/customRequest.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Request } from 'express'; - -interface UserPayload { - id: string; -} - -export interface CustomRequest

> extends Request { - user?: UserPayload; - params: P; -}