diff --git a/.gitignore b/.gitignore index 0565ff2..ba0fe42 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist tsconfig.tsbuildinfo updtlog.sh +start.sh public/upload/tccp-*.* public/avatar/tccp-*.* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e819a59 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,20 @@ +{ + "printWidth": 150, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "arrowParens": "always", + "requirePragma": false, + "insertPragma": false, + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "vueIndentScriptAndStyle": false, + "endOfLine": "auto", + "embeddedLanguageFormatting": "auto" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cea5716..4758dbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tccp-server", - "version": "1.0.0", + "version": "1.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "tccp-server", - "version": "1.0.0", + "version": "1.0.3", "license": "ISC", "dependencies": { "@types/bcryptjs": "^2.4.6", @@ -33,7 +33,7 @@ "express-fileupload": "^1.4.3", "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.1.5", - "good-logs": "^1.2.2", + "good-logs": "^1.2.3", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", @@ -1218,9 +1218,9 @@ } }, "node_modules/good-logs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-logs/-/good-logs-1.2.2.tgz", - "integrity": "sha512-mxzcjN1zETC4yvdJgfSZb6VplIJcrPcX84OGOLlHZSDQc+V/io4ZQK0nWeM2UH51hbC1Mbcv1NsdoGojGMMqqQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/good-logs/-/good-logs-1.2.3.tgz", + "integrity": "sha512-XKMesKsDVqJ0WypccQaTDGSxaRdR9vE7eQ6KzHSdRNqMePqlVXv0R8oqOV2K1RUFa7znTsxrmveWzBpoj505Ww==", "dependencies": { "@types/express": "^4.17.21", "colors": "^1.4.0", @@ -3842,9 +3842,9 @@ } }, "good-logs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-logs/-/good-logs-1.2.2.tgz", - "integrity": "sha512-mxzcjN1zETC4yvdJgfSZb6VplIJcrPcX84OGOLlHZSDQc+V/io4ZQK0nWeM2UH51hbC1Mbcv1NsdoGojGMMqqQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/good-logs/-/good-logs-1.2.3.tgz", + "integrity": "sha512-XKMesKsDVqJ0WypccQaTDGSxaRdR9vE7eQ6KzHSdRNqMePqlVXv0R8oqOV2K1RUFa7znTsxrmveWzBpoj505Ww==", "requires": { "@types/express": "^4.17.21", "colors": "^1.4.0", diff --git a/package.json b/package.json index fc4a460..a6fad1c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "express-fileupload": "^1.4.3", "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.1.5", - "good-logs": "^1.2.2", + "good-logs": "^1.2.3", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", @@ -66,4 +66,4 @@ "tsc-alias": "^1.8.8", "typedoc": "^0.25.8" } -} \ No newline at end of file +} diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..e3bcd7b --- /dev/null +++ b/sample.env @@ -0,0 +1,43 @@ +API_HOST=http://localhost:{PORT} +API_URL=http://localhost:{PORT}/api +CLIENT_URL=https://fakeprojct.com +CORS_ALLOWED_ORIGIN=https://fakeprojct.com,http://localhost:5173,http://localhost:5174 +API_VERSION=/v1.0.3 +PORT=3003 +DEV_CLIENT_PORT=5173 +NODE_ENV=production + +# Database +# db +DB_URI=mongodb+srv://fakeuser:fakepassword@fakecluster.mongodb.net/FakeDB?retryWrites=true&w=majority +DB_NAME=FakeDB +DB_PROTOCOL=mongodb+srv:// +DB_HOST=fakecluster.mongodb.net +DB_USER=fakeuser +DB_PW=fakepassword + +# jwt +JWT_SECRET=fakeServer +JWT_EXP=30d +JWT_COOKIE_EXPIRE= + +# mail - mailtrap +SMTP_HOST=fake.smtp.mailtrap.io +SMTP_PORT=2525 +SMTP_EMAIL=fakeemail +SMTP_PASSWORD=fakepassword +FROM_EMAIL=noreply@fakeprojct.io +FROM_NAME=FakeName + +# rate limit +RATE_LIMIT=100 + +# neocoder +GEOCODER_PROVIDER=fakeprovider +GEOCODER_API_KEY=fakeapikey + +# photo +FILE_UPLOAD_PATH=./public/upload +AVATAR_UPLOAD_PATH=./public/avatar +MAX_FILE_UPLOAD=1000000 +MAX_AVATAR_UPLOAD=500000 \ No newline at end of file diff --git a/src/config/global.ts b/src/config/global.ts index ed00fda..708d422 100644 --- a/src/config/global.ts +++ b/src/config/global.ts @@ -10,13 +10,10 @@ dotenv.config() const GLOBAL = { APP_NAME: 'The CodeCoach Projct', APP_SERVER_NAME: 'tccp-server', - API_HOST: conNex( - process.env.API_HOST?.replace('{PORT}', process.env.PORT ?? '') || '' - ), - API_URL: conNex( - process.env.API_URL?.replace('{PORT}', process.env.PORT ?? '') || '', - process.env.API_VERSION || '' - ), + API_HOST: conNex(process.env.API_HOST?.replace('{PORT}', process.env.PORT ?? '') || ''), + API_URL: conNex(process.env.API_URL?.replace('{PORT}', process.env.PORT ?? '') || '', process.env.API_VERSION || ''), + CLIENT_DEV_URL: conNex(process.env.API_HOST?.replace('{PORT}', process.env.DEV_CLIENT_PORT ?? '') || ''), + CORS_ALLOWED_ORIGIN: process.env.CORS_ALLOWED_ORIGIN?.split(',') || [], API_VERSION: process.env.API_VERSION || '', PORT: process.env.PORT || 3005, ENV: process.env.NODE_ENV || '', @@ -33,15 +30,11 @@ const GLOBAL = { MAX_FILE_UPLOAD: process.env.MAX_FILE_UPLOAD || NumKey.ONE_MB, PHOTO_UPLOAD_PATH: process.env.PHOTO_UPLOAD_PATH, - PHOTO_FILENAME: (bootcampId: ObjectId, name: string) => - `tccp-${bootcampId}${path.parse(name).ext}`, - AVATAR_FILENAME: (userId: ObjectId, name: string) => - `tccp-av-${userId}${path.parse(name).ext}`, + PHOTO_FILENAME: (bootcampId: ObjectId, name: string) => `tccp-${bootcampId}${path.parse(name).ext}`, + AVATAR_FILENAME: (userId: ObjectId, name: string) => `tccp-av-${userId}${path.parse(name).ext}`, - PHOTO_UPLOAD_MV: (photo: any, cb: any) => - photo.mv(`${process.env.FILE_UPLOAD_PATH}/${photo.name}`, cb), - AVATAR_UPLOAD_MV: (photo: any, cb: any) => - photo.mv(`${process.env.AVATAR_UPLOAD_PATH}/${photo.name}`, cb), + PHOTO_UPLOAD_MV: (photo: any, cb: any) => photo.mv(`${process.env.FILE_UPLOAD_PATH}/${photo.name}`, cb), + AVATAR_UPLOAD_MV: (photo: any, cb: any) => photo.mv(`${process.env.AVATAR_UPLOAD_PATH}/${photo.name}`, cb), // @mail - nodemailer - mailtrap MAIL_FROM: `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`, diff --git a/src/config/header.ts b/src/config/header.ts deleted file mode 100644 index 0743bc4..0000000 --- a/src/config/header.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IExpressController } from '@interface/middleware' - -const setHeader: IExpressController = (req, res, next) => { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE') - res.setHeader( - 'Access-Control-Allow-Headers', - 'Content-Type, Authorization, Origin, X-Requested-With, Accept' - ) - res.setHeader('Access-Control-Allow-Credentials', 'true') - res.setHeader('Access-Control-Allow-Origin', 'same-origin') - res.setHeader('Access-Control-Max-Age', 86400) - - next() -} - -export default setHeader diff --git a/src/config/index.ts b/src/config/index.ts index 4f6fa35..1eedc8d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,6 +1,5 @@ import path from 'path' export { default as App } from '@config/server' -export { default as setHeader } from '@config/header' export { default as connectDb } from '@config/db' export { default as GLOBAL } from '@config/global' diff --git a/src/config/server.ts b/src/config/server.ts index 7140b89..25ecb89 100644 --- a/src/config/server.ts +++ b/src/config/server.ts @@ -14,10 +14,10 @@ import mongoSanitize from 'express-mongo-sanitize' import helmet from 'helmet' import hpp from 'hpp' import rateLimit from 'express-rate-limit' -import { setHeader, connectDb } from '@config' +import { connectDb } from '@config' import { AppRouter } from '@app-router' import { mainRoute } from '@route' -import { xssHandler, errorHandler, notFound } from '@middleware' +import { xssHandler, errorHandler, notFound, corsConfig } from '@middleware' import { LogInitRequest, ServerStatus } from '@decorator' import { Key } from '@constant/enum' import options from '@util/geocoder' @@ -95,15 +95,15 @@ class App { this._app.use(express.static(Key.Public)) this._app.use(morgan(Key.MorganDev)) this._app.use(cookieParser()) - this._app.use(cors()) - this._app.use(setHeader) this._app.use(fileupload()) + this._app.use(cors(corsConfig)) this._app.use(mongoSanitize()) this._app.use(helmet()) this._app.use(xssHandler) this._app.use(rateLimit(GLOBAL.LIMITER)) this._app.use(hpp()) this.registerRoute() + this._app.use(errorHandler) this._app.use(notFound) } @@ -147,21 +147,11 @@ class App { try { this._app.listen(GLOBAL.PORT, () => { - goodlog.server( - GLOBAL.PORT as number, - GLOBAL.API_VERSION, - prod, - this.isConnected - ) + goodlog.server(GLOBAL.PORT as number, GLOBAL.API_VERSION, prod, this.isConnected) }) } catch (error: any) { process.on(Key.UnhandledRejection, (err) => { - goodlog.server( - GLOBAL.PORT as number, - GLOBAL.API_VERSION, - prod, - this.isConnected - ) + goodlog.server(GLOBAL.PORT as number, GLOBAL.API_VERSION, prod, this.isConnected) goodlog.error(error.message) this.isConnected = false }) diff --git a/src/constant/enum/schema.ts b/src/constant/enum/schema.ts index 42f098f..4f074f9 100644 --- a/src/constant/enum/schema.ts +++ b/src/constant/enum/schema.ts @@ -3,7 +3,7 @@ enum SCHEMA { NAME = 'Please add a bootcamp name/title', MAX_LENGTH_NAME = 'Name can not be more than 50 characters', DESCRIPTION = 'Please add a description', - MAX_LENGTH_DESCRIPTION = 'Description can not be more than 500 characters', + MAX_LENGTH_DESCRIPTION = 'Description can not be more than 250 characters', URL = 'Please use a valid URL with HTTP or HTTPS', MAX_LENGTH_PHONE = 'Phone number can not be longer than 20 characters', EMAIL = 'Please add a valid email', @@ -27,7 +27,7 @@ enum SCHEMA { // @user FIRST_NAME = 'Please provide your first name', LAST_NAME = 'Please provide your family/last name', - PASSWORD = 'Please add a password', + PASSWORD = 'Please add a password' } -export default SCHEMA +export default SCHEMA; diff --git a/src/constant/response.ts b/src/constant/response.ts index 0f6ce95..0b49eb5 100644 --- a/src/constant/response.ts +++ b/src/constant/response.ts @@ -26,8 +26,7 @@ const RESPONSE = { 204: 'NO CONTENT: The server successfully processed the request but there is no content to send in the response.', PHOTO_UPLOADED: 'OK: Photo Uploaded', AVATAR_UPLOADED: 'OK: Avatar Uploaded', - COURSES_DELETED: (data: string) => - `Courses being deleted from bootcamp ID: ${data}. Reload page to see the effect`, + COURSES_DELETED: (data: string) => `Courses being deleted from bootcamp ID: ${data}. Reload page to see the effect`, COLLECTION_SEED: ' MOCK MIGRATION SUCCESSFUL 🌱 ', COLLECTION_DESTROYED: ' COLLECTION/s DESTROYED 💥 ', LOGOUT: `User logged out`, @@ -49,28 +48,23 @@ const RESPONSE = { 500: 'INTERNAL SERVER ERROR: Server encountered an Unhandled Exception', 503: 'SERVICE UNAVAILABLE: The server is temporarily unable to handle the Request', 504: 'GATEWAY TIMEOUT: The server acting as a gateway did not receive a timely response from an upstream server', + CORS_NOT_ALLOWED: 'CORS ERROR: Not allowed by Access-Control-Allow-Origin', ENTITY_EXISTS: 'Entity already exists', ALREADY_EXISTS: (data: string) => `${data} already exists`, NOT_FOUND_COURSE: (data: string) => `Course not found with id of ${data}`, - NOT_FOUND_BOOTCAMP: (data: string) => - `Bootcamp not found with id of ${data}`, - NOT_FOUND_FEEDBACK: (data: string) => - `No feedback found with the id ${data}`, + NOT_FOUND_BOOTCAMP: (data: string) => `Bootcamp not found with id of ${data}`, + NOT_FOUND_FEEDBACK: (data: string) => `No feedback found with the id ${data}`, NOT_FOUND: (data: string) => `There is no user with id ${data}`, - BOOTCAMP_ALREADY_PUBLISHED: (data: string) => - `The user with ID ${data} has already published a bootcamp`, + BOOTCAMP_ALREADY_PUBLISHED: (data: string) => `The user with ID ${data} has already published a bootcamp`, FAILED_UPLOAD: ' Please upload a file ', FAILED_UPLOAD_AVATAR: ' Please upload an avatar ', - FAILED_FILESIZE: (fileSize: number) => - `File size cannot exceed ${fileSize}`, + FAILED_FILESIZE: (fileSize: number) => `File size cannot exceed ${fileSize}`, FAILED_SEED: ' FAILED TO SEED COLLECTION/s SEED ', FAILED_DESTROY: ' FAILED TO DESTROY COLLECTION/s ', INVALID_CREDENTIAL: 'Please provide a valid email and password', INVALID_TOKEN: 'Invalid token', - NOT_OWNER: (user: string, course: string) => - `User ${user} is unauthorized to update course ${course}`, - ROLE_NOT_ALLOWED: (data: string) => - `Current role ${data} is unauthorized to access this route`, + NOT_OWNER: (user: string, course: string) => `User ${user} is unauthorized to update course ${course}`, + ROLE_NOT_ALLOWED: (data: string) => `Current role ${data} is unauthorized to access this route`, parseErr: (err: any) => `Error parsing JSON: ${err}`, NotInstance: 'This class cannot be instantiated', STATIC_CLASS: 'This is a static class', diff --git a/src/controller/auth.ts b/src/controller/auth.ts index 2a684a4..00770f6 100644 --- a/src/controller/auth.ts +++ b/src/controller/auth.ts @@ -7,13 +7,7 @@ import { sendEmail } from '@util' import { User } from '@model' import { Key, Code } from '@constant/enum' import { use, LogRequest } from '@decorator' -import { - RESPONSE, - PathDir, - thirtyDaysFromNow, - fiveSecFromNow, - expire, -} from '@constant' +import { RESPONSE, PathDir, thirtyDaysFromNow, fiveSecFromNow, expire } from '@constant' /** * @path {baseUrl}/auth @@ -32,11 +26,7 @@ class AuthController { * @param res - Response * @returns void */ - private static _sendTokenResponse = ( - user: any, - statusCode: number, - res: any - ) => { + private static _sendTokenResponse = (user: any, statusCode: number, res: any) => { const token = user.getSignedJwtToken() const options = { expires: thirtyDaysFromNow, @@ -59,34 +49,19 @@ class AuthController { //@route POST /register //@access PUBLIC @use(LogRequest) - public static async register( - req: Request, - res: Response, - next: NextFunction - ) { + public static async register(req: Request, res: Response, next: NextFunction) { const { email, username } = req.body const emailExist = await User.findOne({ email }) const usernameExist = await User.findOne({ username }) if (emailExist) { - res - .status(Code.FORBIDDEN) - .json({ message: RESPONSE.error.ALREADY_EXISTS(email) }) - return next( - new ErrorResponse(RESPONSE.error.ALREADY_EXISTS(email), Code.FORBIDDEN) - ) + res.status(Code.FORBIDDEN).json({ message: RESPONSE.error.ALREADY_EXISTS(email) }) + return next(new ErrorResponse(RESPONSE.error.ALREADY_EXISTS(email), Code.FORBIDDEN)) } if (usernameExist) { - res - .status(Code.FORBIDDEN) - .json({ message: RESPONSE.error.ALREADY_EXISTS(email) }) - return next( - new ErrorResponse( - RESPONSE.error.ALREADY_EXISTS(username), - Code.FORBIDDEN - ) - ) + res.status(Code.FORBIDDEN).json({ message: RESPONSE.error.ALREADY_EXISTS(email) }) + return next(new ErrorResponse(RESPONSE.error.ALREADY_EXISTS(username), Code.FORBIDDEN)) } const user = await User.create(req.body) @@ -102,25 +77,19 @@ class AuthController { const { email, password } = req.body if (!email || !password) { - return next( - new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.BAD_REQUEST) - ) + return next(new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.BAD_REQUEST)) } const user = await User.findOne({ email }).select(Key.Password) if (!user) { - return next( - new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.UNAUTHORIZED) - ) + return next(new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.UNAUTHORIZED)) } const isMatch = await user.matchPassword(password) if (!isMatch) { - return next( - new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.UNAUTHORIZED) - ) + return next(new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.UNAUTHORIZED)) } AuthController._sendTokenResponse(user, Code.OK, res) @@ -130,11 +99,7 @@ class AuthController { //@route GET /auth/log-out //@access PRIVATE @use(LogRequest) - public static async logout( - _req: Request, - res: Response, - _next: NextFunction - ) { + public static async logout(_req: Request, res: Response, _next: NextFunction) { res.cookie(Key.Token, Key.None, { expires: fiveSecFromNow, httpOnly: true, @@ -165,11 +130,7 @@ class AuthController { //@route PUT /update //@access PRIVATE @use(LogRequest) - public static async updateAccount( - req: any, - res: Response, - _next: NextFunction - ) { + public static async updateAccount(req: any, res: Response, _next: NextFunction) { AuthController.setUserId(req) const fieldsToUpdate = { @@ -183,14 +144,10 @@ class AuthController { location: req.body.location, } - const user = await User.findByIdAndUpdate( - AuthController._userId, - fieldsToUpdate, - { - new: true, - runValidators: true, - } - ) + const user = await User.findByIdAndUpdate(AuthController._userId, fieldsToUpdate, { + new: true, + runValidators: true, + }) res.status(Code.OK).json({ success: true, @@ -203,21 +160,13 @@ class AuthController { //@route PUT /update-password //@access PRIVATE @use(LogRequest) - public static async updatePassword( - req: any, - res: Response, - next: NextFunction - ) { + public static async updatePassword(req: any, res: Response, next: NextFunction) { AuthController.setUserId(req) - const user = await User.findById(AuthController._userId).select( - Key.Password - ) + const user = await User.findById(AuthController._userId).select(Key.Password) if (!(await user?.matchPassword(req.body.currentPassword))) { - return next( - new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.UNAUTHORIZED) - ) + return next(new ErrorResponse(RESPONSE.error.INVALID_CREDENTIAL, Code.UNAUTHORIZED)) } if (user) { @@ -233,26 +182,18 @@ class AuthController { //@route POST /forgot-password //@access PUBLIC @use(LogRequest) - public static async forgotPassword( - req: Request, - res: Response, - next: NextFunction - ) { + public static async forgotPassword(req: Request, res: Response, next: NextFunction) { const userEmail = req.body.email const user = await User.findOne({ email: req.body.email }) if (!user) { - return next( - new ErrorResponse(RESPONSE.error.NOT_FOUND(userEmail), Code.NOT_FOUND) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND(userEmail), Code.NOT_FOUND)) } const resetToken = user.getResetPasswordToken() await user.save({ validateBeforeSave: false }) - const message = RESPONSE.error.RESET_MESSAGE( - PathDir.RESET_FULL_EMAIL(req, resetToken) - ) + const message = RESPONSE.error.RESET_MESSAGE(PathDir.RESET_FULL_EMAIL(req, resetToken)) try { await sendEmail({ email: user.email, @@ -269,12 +210,7 @@ class AuthController { validateBeforeSave: false, }) - return next( - new ErrorResponse( - RESPONSE.error.FAILED_EMAIL, - Code.INTERNAL_SERVER_ERROR - ) - ) + return next(new ErrorResponse(RESPONSE.error.FAILED_EMAIL, Code.INTERNAL_SERVER_ERROR)) } } @@ -289,15 +225,8 @@ class AuthController { //@route PUT /reset-password/:resetToken //@access PUBLIC @use(LogRequest) - public static async resetPassword( - req: Request, - res: Response, - next: NextFunction - ) { - let resetPasswordToken = crypto - .createHash(Key.CryptoHash) - .update(req.params.resetToken) - .digest(Key.Hex) + public static async resetPassword(req: Request, res: Response, next: NextFunction) { + let resetPasswordToken = crypto.createHash(Key.CryptoHash).update(req.params.resetToken).digest(Key.Hex) const user = await User.findOne({ resetPasswordToken, @@ -305,9 +234,7 @@ class AuthController { }) if (!user) { - return next( - new ErrorResponse(RESPONSE.error.INVALID_TOKEN, Code.ALREADY_REPORTED) - ) + return next(new ErrorResponse(RESPONSE.error.INVALID_TOKEN, Code.ALREADY_REPORTED)) } user.password = req.body.password diff --git a/src/controller/bootcamp.ts b/src/controller/bootcamp.ts index e3f7fe6..281c786 100644 --- a/src/controller/bootcamp.ts +++ b/src/controller/bootcamp.ts @@ -1,21 +1,21 @@ -import goodlog from 'good-logs' -import { App, GLOBAL } from '@config' -import { Bootcamp } from '@model' -import { Request, Response, NextFunction } from 'express' -import { use, LogRequest } from '@decorator' -import { IResponseExtended } from '@interface' -import { Key, NumKey, Code } from '@constant/enum' -import { RESPONSE } from '@constant' -import { ErrorResponse, DataResponse } from '@util' +import goodlog from 'good-logs'; +import { App, GLOBAL } from '@config'; +import { Bootcamp } from '@model'; +import { Request, Response, NextFunction } from 'express'; +import { use, LogRequest } from '@decorator'; +import { IResponseExtended } from '@interface'; +import { Key, NumKey, Code } from '@constant/enum'; +import { RESPONSE } from '@constant'; +import { ErrorResponse, DataResponse } from '@util'; /** * Bootcamp Controller * @path {baseUrl}/api/{apiVer}/bootcamp */ class BootcampController { - private static _bootcampId: string - private static _userId: string - private static _userRole: string + private static _bootcampId: string; + private static _userId: string; + private static _userRole: string; /** * setRequest - Set request parameters @@ -24,265 +24,185 @@ class BootcampController { * @returns void */ static setRequest(req: Request) { - this._bootcampId = req.params.id + this._bootcampId = req.params.id; } static setUserId(req: any) { - this._userId = req.user.id - this._userRole = req.user.role + this._userId = req.user.id; + this._userRole = req.user.role; } //@desc Get ALL bootcamps //@route GET /bootcamp //@access PUBLIC @use(LogRequest) - public static async getBootcamps( - _req: Request, - res: Response, - _next: NextFunction - ) { - res.status(Code.OK).json((res as IResponseExtended).advancedResult) + public static async getBootcamps(_req: Request, res: Response, _next: NextFunction) { + res.status(Code.OK).json((res as IResponseExtended).advancedResult); } //@desc Get single bootcamp //@route GET /bootcamp/:id //@access PUBLIC @use(LogRequest) - public static async getBootcamp( - req: Request, - res: Response, - next: NextFunction - ) { - BootcampController.setRequest(req) + public static async getBootcamp(req: Request, res: Response, next: NextFunction) { + BootcampController.setRequest(req); - const bootcamp = await Bootcamp.findById( - BootcampController._bootcampId - ).populate(Key.UserVirtual, Key.BootcampPopulate) + const bootcamp = await Bootcamp.findById(BootcampController._bootcampId).populate(Key.UserVirtual, Key.BootcampPopulate); if (!bootcamp) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), Code.NOT_FOUND)); } - res.status(Code.OK).json({ success: true, data: bootcamp }) + res.status(Code.OK).json({ success: true, data: bootcamp }); } //@desc Create NEW bootcamp //@route POST /bootcamp //@access PRIVATE @use(LogRequest) - public static async createBootcamp( - req: any, - res: Response, - next: NextFunction - ) { + public static async createBootcamp(req: any, res: Response, next: NextFunction) { // this._userId = req.user.id - BootcampController.setUserId(req) + BootcampController.setUserId(req); - req.body.user = BootcampController._userId + req.body.user = BootcampController._userId; const publishedBootcamp = await Bootcamp.findOne({ - user: BootcampController._userId, - }) + user: BootcampController._userId + }); if (publishedBootcamp && BootcampController._userRole !== Key.Admin) { - return next( - new ErrorResponse( - RESPONSE.error.BOOTCAMP_ALREADY_PUBLISHED(BootcampController._userId), - Code.BAD_REQUEST - ) - ) + return next(new ErrorResponse(RESPONSE.error.BOOTCAMP_ALREADY_PUBLISHED(BootcampController._userId), Code.BAD_REQUEST)); } - const bootcamp = await Bootcamp.create(req.body) + const bootcamp = await Bootcamp.create(req.body); res.status(Code.CREATED).json({ success: true, - data: bootcamp, - }) + data: bootcamp + }); } //@desc UPDATE bootcamp //@route PUT /api/{apiVer}/bootcamps/:id //@access PRIVATE @use(LogRequest) - public static async updateBootcamp( - req: any, - res: Response, - next: NextFunction - ) { - BootcampController.setRequest(req) - BootcampController.setUserId(req) + public static async updateBootcamp(req: any, res: Response, next: NextFunction) { + BootcampController.setRequest(req); + BootcampController.setUserId(req); - let bootcamp = await Bootcamp.findById(BootcampController._bootcampId) + let bootcamp = await Bootcamp.findById(BootcampController._bootcampId); if (!bootcamp) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), Code.NOT_FOUND)); } - if ( - bootcamp.user.toString() !== BootcampController._userRole && - BootcampController._userRole !== Key.Admin - ) { - return next(new ErrorResponse(RESPONSE.error[401], Code.UNAUTHORIZED)) + if (bootcamp.user.toString() !== BootcampController._userRole && BootcampController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error[401], Code.UNAUTHORIZED)); } bootcamp = await Bootcamp.findOneAndUpdate(req.params.id, req.body, { new: true, - runValidators: true, - }) + runValidators: true + }); res.status(Code.OK).json({ success: true, message: RESPONSE.success.UPDATED, - data: bootcamp, - }) + data: bootcamp + }); } //@desc DELETE bootcamp //@route DELETE /api/{apiVer}/bootcamps/:id //@access PRIVATE @use(LogRequest) - public static async deleteBootcamp( - req: any, - res: Response, - next: NextFunction - ) { - BootcampController.setRequest(req) - BootcampController.setUserId(req) + public static async deleteBootcamp(req: any, res: Response, next: NextFunction) { + BootcampController.setRequest(req); + BootcampController.setUserId(req); - const bootcamp = await Bootcamp.findById(BootcampController._bootcampId) + const bootcamp = await Bootcamp.findById(BootcampController._bootcampId); if (!bootcamp) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), Code.NOT_FOUND)); } - if ( - bootcamp.user.toString() !== BootcampController._userId && - BootcampController._userRole !== Key.Admin - ) { - return next(new ErrorResponse(RESPONSE.error[401], Code.UNAUTHORIZED)) + if (bootcamp.user.toString() !== BootcampController._userId && BootcampController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error[401], Code.UNAUTHORIZED)); } - await Bootcamp.deleteOne({ _id: BootcampController._bootcampId }) + await Bootcamp.deleteOne({ _id: BootcampController._bootcampId }); - res - .status(Code.OK) - .json({ success: true, message: RESPONSE.success.DELETED, data: {} }) + res.status(Code.OK).json({ success: true, message: RESPONSE.success.DELETED, data: {} }); } //@desc Get bootcamps within a radius //@route GET /api/v1/bootcamps/radius/:zipcode/:distance //@access PRIVATE @use(LogRequest) - public static async getBootcampsInRadius( - req: Request, - res: Response, - _next: NextFunction - ) { - const { zipcode, distance } = req.params + public static async getBootcampsInRadius(req: Request, res: Response, _next: NextFunction) { + const { zipcode, distance } = req.params; - const loc = await App.geocoder.geocode(zipcode) - const lat = loc[0].latitude - const lng = loc[0].longitude + const loc = await App.geocoder.geocode(zipcode); + const lat = loc[0].latitude; + const lng = loc[0].longitude; - const radius = Number(distance) / NumKey.EarthRadius + const radius = Number(distance) / NumKey.EarthRadius; const bootcamps = await Bootcamp.find({ - location: { $geoWithin: { $centerSphere: [[lng, lat], radius] } }, - }) + location: { $geoWithin: { $centerSphere: [[lng, lat], radius] } } + }); res.status(Code.ACCEPTED).json({ success: true, count: bootcamps.length, - data: bootcamps, - }) + data: bootcamps + }); } //@desc Upload photo for bootcamp //@route PUT /bootcamp/:id/photo //@access PRIVATE @use(LogRequest) - public static async uploadBootcampPhoto( - req: any, - res: Response, - next: NextFunction - ) { - BootcampController.setRequest(req) + public static async uploadBootcampPhoto(req: any, res: Response, next: NextFunction) { + BootcampController.setRequest(req); - const photo = req.files.photo + const photo = req.files.photo; - const bootcamp = await Bootcamp.findById(BootcampController._bootcampId) + const bootcamp = await Bootcamp.findById(BootcampController._bootcampId); if (!bootcamp) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_BOOTCAMP(BootcampController._bootcampId), Code.NOT_FOUND)); } if (!req.files) { - return next( - new ErrorResponse(RESPONSE.error.FAILED_UPLOAD, Code.BAD_REQUEST) - ) + return next(new ErrorResponse(RESPONSE.error.FAILED_UPLOAD, Code.BAD_REQUEST)); } if (!photo.mimetype.startsWith(Key.Image)) { - return next( - new ErrorResponse(RESPONSE.error.FAILED_UPLOAD, Code.BAD_REQUEST) - ) + return next(new ErrorResponse(RESPONSE.error.FAILED_UPLOAD, Code.BAD_REQUEST)); } if (photo.size > GLOBAL.MAX_FILE_UPLOAD) { - return next( - new ErrorResponse( - RESPONSE.error.FAILED_FILESIZE(NumKey.ONE_MB), - Code.BAD_REQUEST - ) - ) + return next(new ErrorResponse(RESPONSE.error.FAILED_FILESIZE(NumKey.ONE_MB), Code.BAD_REQUEST)); } - photo.name = GLOBAL.PHOTO_FILENAME(bootcamp._id, photo.name) + photo.name = GLOBAL.PHOTO_FILENAME(bootcamp._id, photo.name); GLOBAL.PHOTO_UPLOAD_MV(photo, async (error: any) => { - goodlog.error(error?.message) + goodlog.error(error?.message); if (error) { - return next( - new ErrorResponse( - RESPONSE.error.FAILED_UPLOAD, - Code.INTERNAL_SERVER_ERROR - ) - ) + return next(new ErrorResponse(RESPONSE.error.FAILED_UPLOAD, Code.INTERNAL_SERVER_ERROR)); } await Bootcamp.findByIdAndUpdate(BootcampController._bootcampId, { - photo: photo.name, - }) + photo: `/upload/${photo.name}` + }); - const response = DataResponse.success( - { photo: photo.name, bootcampName: bootcamp.name }, - BootcampController._bootcampId - ) + const response = DataResponse.success({ photo: photo.name, bootcampName: bootcamp.name }, BootcampController._bootcampId); res.status(Code.OK).json({ success: true, message: RESPONSE.success.PHOTO_UPLOADED, - response, - }) - }) + response + }); + }); } } -export default BootcampController +export default BootcampController; diff --git a/src/controller/course.ts b/src/controller/course.ts index 479377d..db0b453 100644 --- a/src/controller/course.ts +++ b/src/controller/course.ts @@ -37,11 +37,7 @@ class CourseController { //@route GET /bootcamp/:bootcampId/course //@access PUBLIC @use(LogRequest) - public static async getCourses( - req: Request, - res: Response, - _next: NextFunction - ) { + public static async getCourses(req: Request, res: Response, _next: NextFunction) { CourseController.setRequest(req) if (CourseController._bootcampId) { @@ -63,11 +59,7 @@ class CourseController { //@route GET /course/:id //@access PUBLIC @use(LogRequest) - public static async getCourse( - req: Request, - res: Response, - next: NextFunction - ) { + public static async getCourse(req: Request, res: Response, next: NextFunction) { CourseController.setRequest(req) const course = await Course.findById(CourseController._courseId).populate({ @@ -76,12 +68,7 @@ class CourseController { }) if (!course) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_COURSE(CourseController._courseId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_COURSE(CourseController._courseId), Code.NOT_FOUND)) } res.status(Code.OK).json({ success: true, @@ -104,24 +91,11 @@ class CourseController { const bootcamp = await Bootcamp.findById(CourseController._bootcampId) if (!bootcamp) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_BOOTCAMP(CourseController._bootcampId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_BOOTCAMP(CourseController._bootcampId), Code.NOT_FOUND)) } - if ( - bootcamp.user.toString() !== CourseController._userId && - CourseController._userRole !== Key.Admin - ) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_OWNER(req.user.id, CourseController._bootcampId), - 401 - ) - ) + if (bootcamp.user.toString() !== CourseController._userId && CourseController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error.NOT_OWNER(req.user.id, CourseController._bootcampId), 401)) } const course = await Course.create(req.body) @@ -136,51 +110,24 @@ class CourseController { //@route PUT /courses/:id //@access PRIVATE @use(LogRequest) - public static async updateCourse( - req: any, - res: Response, - next: NextFunction - ) { + public static async updateCourse(req: any, res: Response, next: NextFunction) { CourseController.setRequest(req) CourseController.setUserId(req) let course = await Course.findById(CourseController._courseId) if (!course) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_OWNER( - CourseController._userId, - CourseController._courseId - ), - Code.UNAUTHORIZED - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_OWNER(CourseController._userId, CourseController._courseId), Code.UNAUTHORIZED)) } - if ( - course.user.toString() !== CourseController._userId && - CourseController._userRole !== Key.Admin - ) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_OWNER( - CourseController._userId, - CourseController._courseId - ), - Code.UNAUTHORIZED - ) - ) + if (course.user.toString() !== CourseController._userId && CourseController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error.NOT_OWNER(CourseController._userId, CourseController._courseId), Code.UNAUTHORIZED)) } - course = await Course.findByIdAndUpdate( - CourseController._courseId, - req.body, - { - new: true, - runValidators: true, - } - ) + course = await Course.findByIdAndUpdate(CourseController._courseId, req.body, { + new: true, + runValidators: true, + }) res.status(Code.OK).json({ success: true, @@ -192,38 +139,18 @@ class CourseController { //@route DELETE /course/:id //@access PRIVATE @use(LogRequest) - public static async deleteCourse( - req: any, - res: Response, - next: NextFunction - ) { + public static async deleteCourse(req: any, res: Response, next: NextFunction) { CourseController.setRequest(req) CourseController.setUserId(req) const course = await Course.findById(CourseController._courseId) if (!course) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_COURSE(CourseController._courseId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_COURSE(CourseController._courseId), Code.NOT_FOUND)) } - if ( - course.user.toString() !== CourseController._userId && - CourseController._userRole !== Key.Admin - ) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_OWNER( - CourseController._userId, - CourseController._courseId - ), - Code.UNAUTHORIZED - ) - ) + if (course.user.toString() !== CourseController._userId && CourseController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error.NOT_OWNER(CourseController._userId, CourseController._courseId), Code.UNAUTHORIZED)) } await Course.deleteOne({ _id: CourseController._courseId }) diff --git a/src/controller/feedback.ts b/src/controller/feedback.ts index 5a0e97b..9df3cef 100644 --- a/src/controller/feedback.ts +++ b/src/controller/feedback.ts @@ -38,11 +38,7 @@ class FeedbackController { //@route GET /feedback //@route GET /bootcamp/:bootcampId/feedback //@access PUBLIC - public static async getFeedbacks( - req: Request, - res: Response, - next: NextFunction - ) { + public static async getFeedbacks(req: Request, res: Response, next: NextFunction) { if (req.params.bootcampId) { const feedbacks = await Feedback.find({ bootcamp: req.params.bootcampId }) @@ -61,27 +57,16 @@ class FeedbackController { //@route GET /feedback/:id //@access PUBLIC @use(LogRequest) - public static async getFeedback( - req: Request, - res: Response, - next: NextFunction - ) { + public static async getFeedback(req: Request, res: Response, next: NextFunction) { FeedbackController.setRequest(req) - const feedback = await Feedback.findById( - FeedbackController._feedbackId - ).populate({ + const feedback = await Feedback.findById(FeedbackController._feedbackId).populate({ path: Key.BootcampVirtual, select: Key.DefaultSelect, }) if (!feedback) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_FEEDBACK(FeedbackController._feedbackId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_FEEDBACK(FeedbackController._feedbackId), Code.NOT_FOUND)) } res.status(Code.OK).json({ @@ -105,12 +90,7 @@ class FeedbackController { const bootcamp = await Bootcamp.findById(FeedbackController._bootcampId) if (!bootcamp) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_BOOTCAMP(FeedbackController._bootcampId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_BOOTCAMP(FeedbackController._bootcampId), Code.NOT_FOUND)) } const feedback = await Feedback.create(req.body) @@ -126,48 +106,24 @@ class FeedbackController { //@route PUT /feedback/:id //@access PUBLIC @use(LogRequest) - public static async updateFeedback( - req: any, - res: Response, - next: NextFunction - ) { + public static async updateFeedback(req: any, res: Response, next: NextFunction) { FeedbackController.setRequest(req) FeedbackController.setUserId(req) let feedback = await Feedback.findById(FeedbackController._feedbackId) if (!feedback) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_FEEDBACK(FeedbackController._feedbackId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_FEEDBACK(FeedbackController._feedbackId), Code.NOT_FOUND)) } - if ( - feedback.user.toString() !== FeedbackController._userId && - FeedbackController._userRole !== Key.Admin - ) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_OWNER( - FeedbackController._userId, - FeedbackController._feedbackId - ), - Code.UNAUTHORIZED - ) - ) + if (feedback.user.toString() !== FeedbackController._userId && FeedbackController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error.NOT_OWNER(FeedbackController._userId, FeedbackController._feedbackId), Code.UNAUTHORIZED)) } - feedback = await Feedback.findByIdAndUpdate( - FeedbackController._feedbackId, - req.body, - { - new: true, - runValidators: true, - } - ) + feedback = await Feedback.findByIdAndUpdate(FeedbackController._feedbackId, req.body, { + new: true, + runValidators: true, + }) res.status(Code.OK).json({ success: true, @@ -180,38 +136,18 @@ class FeedbackController { //@route DELETE /feedback/:id //@access PUBLIC @use(LogRequest) - public static async deleteFeedback( - req: any, - res: Response, - next: NextFunction - ) { + public static async deleteFeedback(req: any, res: Response, next: NextFunction) { FeedbackController.setRequest(req) FeedbackController.setUserId(req) const feedback = await Feedback.findById(FeedbackController._feedbackId) if (!feedback) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_FOUND_FEEDBACK(FeedbackController._feedbackId), - Code.NOT_FOUND - ) - ) + return next(new ErrorResponse(RESPONSE.error.NOT_FOUND_FEEDBACK(FeedbackController._feedbackId), Code.NOT_FOUND)) } - if ( - feedback.user.toString() !== FeedbackController._userId && - FeedbackController._userRole !== Key.Admin - ) { - return next( - new ErrorResponse( - RESPONSE.error.NOT_OWNER( - FeedbackController._userId, - FeedbackController._feedbackId - ), - Code.UNAUTHORIZED - ) - ) + if (feedback.user.toString() !== FeedbackController._userId && FeedbackController._userRole !== Key.Admin) { + return next(new ErrorResponse(RESPONSE.error.NOT_OWNER(FeedbackController._userId, FeedbackController._feedbackId), Code.UNAUTHORIZED)) } await Feedback.deleteOne({ _id: FeedbackController._feedbackId }) diff --git a/src/decorator/debugger.ts b/src/decorator/debugger.ts index 86e8b8b..1153bb1 100644 --- a/src/decorator/debugger.ts +++ b/src/decorator/debugger.ts @@ -1,5 +1,4 @@ -import { logger } from '@middleware' - +import goodlog from 'good-logs' /** * A decorator that helps for debugging * - Logs the method name and the time it was called @@ -12,13 +11,9 @@ function debug(message?: string) { const originalMethod = desc.value desc.value = function (...args: any[]) { - logger.log( - `[${new Date().toISOString()}] Debugging ${key}${ - message ? `: ${message}` : '' - }` - ) + goodlog.log(`[${new Date().toISOString()}] Debugging ${key}${message ? `: ${message}` : ''}`) const result = originalMethod.apply(this, args) - logger.log(`[${new Date().toISOString()}] ${key} execution completed`) + goodlog.log(`[${new Date().toISOString()}] ${key} execution completed`) return result } } diff --git a/src/decorator/log-request.ts b/src/decorator/log-request.ts index 8ef85bf..a7ed201 100644 --- a/src/decorator/log-request.ts +++ b/src/decorator/log-request.ts @@ -3,11 +3,7 @@ import goodlog from 'good-logs' import { Request, Response, NextFunction } from 'express' import { IExpressController } from '@interface/middleware' -const LogRequest: IExpressController = ( - req: Request, - res: Response, - next: NextFunction -) => { +const LogRequest: IExpressController = (req: Request, res: Response, next: NextFunction) => { goodlog.req(req, res, next) next() } diff --git a/src/decorator/server-status.ts b/src/decorator/server-status.ts index 177f51b..48ba89c 100644 --- a/src/decorator/server-status.ts +++ b/src/decorator/server-status.ts @@ -1,10 +1,6 @@ import { IIsConnected } from '@interface/utility' -function ServerStatus( - target: IIsConnected, - propertyKey: string, - descriptor: PropertyDescriptor -): PropertyDescriptor { +function ServerStatus(target: IIsConnected, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { const originalMethod = descriptor.value descriptor.value = function (...args: any[]) { diff --git a/src/middleware/advanced-result.ts b/src/middleware/advanced-result.ts index dfe3634..8567d73 100644 --- a/src/middleware/advanced-result.ts +++ b/src/middleware/advanced-result.ts @@ -5,75 +5,68 @@ import { IUser } from '@interface/model' import { RESPONSE, REGEX, REMOVE_FIELDS } from '@constant' import { Key, TYPE } from '@constant/enum' -const advancedResult = - (model: any, populate?: any) => - async ( - req: Request, - res: Response | IResponseExtended, - next: NextFunction - ) => { - let query - let queryStr = JSON.stringify(req.query) +const advancedResult = (model: any, populate?: any) => async (req: Request, res: Response | IResponseExtended, next: NextFunction) => { + let query + let queryStr = JSON.stringify(req.query) - const reqQuery = { ...req.query } + const reqQuery = { ...req.query } - REMOVE_FIELDS.forEach((param) => delete reqQuery[param]) + REMOVE_FIELDS.forEach((param) => delete reqQuery[param]) - queryStr = queryStr.replace(REGEX.MAP_LOC, (match) => `$${match}`) + queryStr = queryStr.replace(REGEX.MAP_LOC, (match) => `$${match}`) - query = model.find(JSON.parse(queryStr)) + query = model.find(JSON.parse(queryStr)) - if (req.query.select && typeof req.query.select === 'string') { - const fields = req.query.select.split(',').join(' ') as keyof IUser - query = query.select(fields) - } + if (req.query.select && typeof req.query.select === 'string') { + const fields = req.query.select.split(',').join(' ') as keyof IUser + query = query.select(fields) + } - if (req.query.sort && typeof req.query.sort === 'string') { - const sortBy = req.query.sort.split(',').join(' ') - query = query.sort(sortBy) - } else { - query = query.sort(Key.Name) - } + if (req.query.sort && typeof req.query.sort === 'string') { + const sortBy = req.query.sort.split(',').join(' ') + query = query.sort(sortBy) + } else { + query = query.sort(Key.Name) + } - //sort - const page = parseInt((req.query.page as any) || '', 10) || 1 - const limit = parseInt((req.query.limit as any) || '', 10) || 25 - const startIndex = (page - 1) * limit - const endIndex = page * limit - const total = await model.countDocuments() + //sort + const page = parseInt((req.query.page as any) || '', 10) || 1 + const limit = parseInt((req.query.limit as any) || '', 10) || 25 + const startIndex = (page - 1) * limit + const endIndex = page * limit + const total = await model.countDocuments() - query = query.skip(startIndex).limit(limit) + query = query.skip(startIndex).limit(limit) - if (populate) { - const populateString = - typeof populate === TYPE.Function ? populate() : populate - query = query.populate(populateString) - } - const results = await query - const pagination: IPagination = {} + if (populate) { + const populateString = typeof populate === TYPE.Function ? populate() : populate + query = query.populate(populateString) + } + const results = await query + const pagination: IPagination = {} - if (endIndex < total) { - pagination.next = { - page: page + 1, - limit, - } + if (endIndex < total) { + pagination.next = { + page: page + 1, + limit, } + } - if (startIndex > 0) { - pagination.prev = { - page: page - 1, - limit, - } + if (startIndex > 0) { + pagination.prev = { + page: page - 1, + limit, } + } - ;(res as IResponseExtended).advancedResult = { - success: true, - message: RESPONSE.success[200], - count: results.length, - pagination, - data: results, - } - next() + ;(res as IResponseExtended).advancedResult = { + success: true, + message: RESPONSE.success[200], + count: results.length, + pagination, + data: results, } + next() +} export default advancedResult diff --git a/src/middleware/auth-protect.ts b/src/middleware/auth-protect.ts index b5dccf6..411aed3 100644 --- a/src/middleware/auth-protect.ts +++ b/src/middleware/auth-protect.ts @@ -19,10 +19,7 @@ const protect = asyncHandler(async (req: any, res, next) => { if (req.cookies.token) { token = req.cookies.token - } else if ( - req.headers.authorization && - req.headers.authorization.startsWith(Key.Bearer) - ) { + } else if (req.headers.authorization && req.headers.authorization.startsWith(Key.Bearer)) { token = req.headers.authorization.split(' ')[1] } diff --git a/src/middleware/cors-config.ts b/src/middleware/cors-config.ts new file mode 100644 index 0000000..8dfe927 --- /dev/null +++ b/src/middleware/cors-config.ts @@ -0,0 +1,17 @@ +import { CorsOptions } from 'cors' +import { GLOBAL } from '@config' +import { RESPONSE } from '@constant' + +const allowedOrigins = GLOBAL.CORS_ALLOWED_ORIGIN + +const corsConfig = { + origin: (origin: string, callback: (err: Error | null, allow?: boolean) => void) => { + if (allowedOrigins.indexOf(origin || '') !== -1 || !origin) { + callback(null, true) + } else { + callback(new Error(RESPONSE.error.CORS_NOT_ALLOWED)) + } + }, +} as CorsOptions + +export default corsConfig diff --git a/src/middleware/error-handler.ts b/src/middleware/error-handler.ts index 5debdee..02f544f 100644 --- a/src/middleware/error-handler.ts +++ b/src/middleware/error-handler.ts @@ -17,12 +17,7 @@ class ErrorCallback extends Error { } } -const errorHandler = ( - err: ErrorCallback, - req: Request, - res: Response, - next: NextFunction -) => { +const errorHandler = (err: ErrorCallback, req: Request, res: Response, next: NextFunction) => { let statusCode = res.statusCode === 0 ? 500 : res.statusCode let message = err.message let errors = err.errors diff --git a/src/middleware/header.ts b/src/middleware/header.ts new file mode 100644 index 0000000..524456e --- /dev/null +++ b/src/middleware/header.ts @@ -0,0 +1,11 @@ +import { IExpressController } from '@interface/middleware' + +const setHeader: IExpressController = (req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, PUT') + + next() +} + +export default setHeader diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 8e01649..5489ca6 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,7 +1,8 @@ -export { default as logger } from '@middleware/logger' export { default as xssHandler } from '@middleware/xss-handler' export { default as errorHandler } from '@middleware/error-handler' export { default as asyncHandler } from '@middleware/async-handler' export { default as notFound } from '@middleware/not-found' export { default as advancedResult } from '@middleware/advanced-result' +export { default as corsConfig } from '@middleware/cors-config' +export { default as setHeader } from '@middleware/header' export { protect, authorize } from '@middleware/auth-protect' diff --git a/src/middleware/logger.ts b/src/middleware/logger.ts deleted file mode 100644 index cf2b6a1..0000000 --- a/src/middleware/logger.ts +++ /dev/null @@ -1,84 +0,0 @@ -import 'colors' -import ILogger from '@interface/logger' -import { Key } from '@constant/enum' - -declare module 'colors' { - interface String { - yellow: string - bgCyan: string - bgRed: string - red: string - bgYellow: string - } -} - -const logger: ILogger = { - // @type - custom - custom: (color: any, ...message: string[]) => - console.log(message.join('')[color]), - - // @type - log :Default - log: (...message: string[]) => console.log(message.join(' ').yellow), - - // @type - info - info: (...message: string[]) => console.log(message.join(' ').bgCyan), - - // @type - warn - warn: (...message: string[]) => console.warn(message.join(' ').bgYellow), - - // @type - table -for array and obj - tbl: (...message: any[]) => console.table(message), - - // @type - error - error: (...message: string[]) => console.log(message.join('').red.bold), - - // @type - debug - debug: (...message: string[]) => console.debug(message.join(' ').bgRed), - - req: (req, res) => { - console.log('') - console.log(Key.ReqMethod.dim, req.method.yellow.bold) - console.log(Key.ReqURL.dim, req.url.yellow.bold) - console.log(Key.ReqTime.dim, new Date().toString().yellow.bold) - console.log('') - }, - - /** - * - * @param port - server port - * @param apiRoot - api version - * @param isProd - send the status of the server environment - * @param isConnected - send the status of the server connection - * @return void - */ - server: (port: any, apiRoot: any, isProd: boolean, isConnected: boolean) => { - console.log(Key.ServerPort.dim, port.bgYellow) - console.log(Key.ServerAPIVersion.dim, apiRoot.bgYellow) - - if (isProd) { - console.log(Key.Environment.dim, Key.Production.bgBlue.bold) - } else { - console.log(Key.Environment.dim, Key.Development.bgMagenta.bold) - } - - if (isConnected) { - console.log(Key.ServerStatus.dim, Key.Connected.green.bold) - } else { - console.log(Key.ServerStatus.dim, Key.NotConnected.red.bold) - } - }, - - // @preset - database - db: (host: any, dbName: any, isConnected: boolean) => { - const DB_LOG = [ - { - HOST: host, - DATABASE: dbName, - STATUS: isConnected ? Key.Connected : Key.NotConnected, - }, - ] - console.table(DB_LOG) - }, -} - -export default logger diff --git a/src/model/Bootcamp.ts b/src/model/Bootcamp.ts index 09f6b88..802d4dd 100644 --- a/src/model/Bootcamp.ts +++ b/src/model/Bootcamp.ts @@ -1,12 +1,12 @@ -import { App } from '@config' -import slugify from 'slugify' -import goodlog from 'good-logs' -import mongoose, { Schema, model } from 'mongoose' -import { IBootcamp } from '@interface/model' -import { REGEX, RESPONSE, DATABASE_INDEX } from '@constant' -import { Key, COLOR, SCHEMA, LOCALE, CareerOptions } from '@constant/enum' +import { App } from '@config'; +import slugify from 'slugify'; +import goodlog from 'good-logs'; +import mongoose, { Schema, model } from 'mongoose'; +import { IBootcamp } from '@interface/model'; +import { REGEX, RESPONSE, DATABASE_INDEX } from '@constant'; +import { Key, COLOR, SCHEMA, LOCALE, CareerOptions } from '@constant/enum'; -const TAG = Key.Bootcamp +const TAG = Key.Bootcamp; const BootcampSchema = new Schema( { @@ -15,104 +15,104 @@ const BootcampSchema = new Schema( required: [true, SCHEMA.NAME], unique: true, trim: true, - maxlength: [50, SCHEMA.MAX_LENGTH_NAME], + maxlength: [50, SCHEMA.MAX_LENGTH_NAME] }, slug: String, description: { type: String, required: [true, SCHEMA.DESCRIPTION], - maxlength: [500, SCHEMA.MAX_LENGTH_DESCRIPTION], + maxlength: [250, SCHEMA.MAX_LENGTH_DESCRIPTION] }, website: { type: String, - match: [REGEX.URL, SCHEMA.URL], + match: [REGEX.URL, SCHEMA.URL] }, phone: { type: String, - maxlength: [20, SCHEMA.MAX_LENGTH_PHONE], + maxlength: [20, SCHEMA.MAX_LENGTH_PHONE] }, email: { type: String, - match: [REGEX.EMAIL, SCHEMA.EMAIL], + match: [REGEX.EMAIL, SCHEMA.EMAIL] }, address: { type: String, - required: [true, SCHEMA.ADDRESS], + required: [true, SCHEMA.ADDRESS] }, location: { // GeoJSON Point type: { type: String, - enum: [SCHEMA.LOCATION_TYPE as string], + enum: [SCHEMA.LOCATION_TYPE as string] }, coordinates: { type: [Number], - index: SCHEMA.LOCATION_COORDINATES_INDEX as string, + index: SCHEMA.LOCATION_COORDINATES_INDEX as string }, formattedAddress: String, street: String, city: String, state: String, zipcode: String, - country: String, + country: String }, duration: { type: String, - required: true, + required: true }, careers: { type: [String], required: true, - enum: Object.values(CareerOptions), + enum: Object.values(CareerOptions) }, averageRating: { type: Number, min: [1, SCHEMA.AVERAGE_RATING_MIN], - max: [10, SCHEMA.AVERAGE_RATING_MAX], + max: [10, SCHEMA.AVERAGE_RATING_MAX] }, averageCost: Number, photo: { type: String, - default: SCHEMA.DEFAULT_PHOTO, + default: SCHEMA.DEFAULT_PHOTO }, housing: { type: Boolean, - default: false, + default: false }, jobAssistance: { type: Boolean, - default: false, + default: false }, jobGuarantee: { type: Boolean, - default: false, + default: false }, acceptGi: { type: Boolean, - default: false, + default: false }, user: { type: Schema.Types.ObjectId, ref: Key.User, - required: true, - }, + required: true + } }, { timestamps: true, collation: { locale: LOCALE.EN, strength: 2 }, collection: TAG, toJSON: { virtuals: true }, - toObject: { virtuals: true }, + toObject: { virtuals: true } } -) +); BootcampSchema.pre(Key.Save, function (next) { - this.slug = slugify(this.name, { lower: true }) - next() -}) + this.slug = slugify(this.name, { lower: true }); + next(); +}); BootcampSchema.pre(Key.Save, async function (next) { - const loc = await App.geocoder.geocode(this.address) + const loc = await App.geocoder.geocode(this.address); this.location = { type: Key.GeocoderType, coordinates: [loc[0].longitude, loc[0].latitude], @@ -121,34 +121,26 @@ BootcampSchema.pre(Key.Save, async function (next) { city: loc[0].city || '', state: loc[0].stateCode || '', zipcode: loc[0].zipcode || '', - country: loc[0].countryCode || '', - } + country: loc[0].countryCode || '' + }; - this.address = '' -}) + this.address = ''; +}); -BootcampSchema.pre( - new RegExp(Key.Remove), - async function (this: IBootcamp, next) { - goodlog.custom( - COLOR.INVERSE, - RESPONSE.success.COURSES_DELETED(this.name as string) - ) - await mongoose - .model(Key.Course) - .deleteMany({ bootcamp: this?._id as Schema.Types.ObjectId }) - next() - } -) +BootcampSchema.pre(new RegExp(Key.Remove), async function (this: IBootcamp, next) { + goodlog.custom(COLOR.INVERSE, RESPONSE.success.COURSES_DELETED(this.name as string)); + await mongoose.model(Key.Course).deleteMany({ bootcamp: this?._id as Schema.Types.ObjectId }); + next(); +}); BootcampSchema.virtual(Key.CourseVirtual, { ref: Key.Course, localField: Key.id, foreignField: Key.BootcampVirtual, - justOne: false, -}) + justOne: false +}); -BootcampSchema.index(DATABASE_INDEX.BOOTCAMP) +BootcampSchema.index(DATABASE_INDEX.BOOTCAMP); -const Bootcamp = model(TAG, BootcampSchema) -export default Bootcamp +const Bootcamp = model(TAG, BootcampSchema); +export default Bootcamp; diff --git a/src/model/Course.ts b/src/model/Course.ts index 7f13bd5..81589a9 100644 --- a/src/model/Course.ts +++ b/src/model/Course.ts @@ -52,9 +52,7 @@ const CourseSchema = new Schema( } ) -CourseSchema.statics.getAverageCost = async function ( - bootcampId: Schema.Types.ObjectId -): Promise { +CourseSchema.statics.getAverageCost = async function (bootcampId: Schema.Types.ObjectId): Promise { const obj = await this.aggregate([ { $match: { bootcamp: bootcampId }, diff --git a/src/route/bootcamp/bootcamp.ts b/src/route/bootcamp/bootcamp.ts index dcd5c01..242fb06 100644 --- a/src/route/bootcamp/bootcamp.ts +++ b/src/route/bootcamp/bootcamp.ts @@ -16,39 +16,15 @@ router.use(PathParam.REDIR_COURSE, courseRoute) router.use(PathParam.REDIR_FEEDBACK, feedbackRoute) router.route(PathParam.DISTANCE).get(bootcampController.getBootcampsInRadius) - -router - .route(PathParam.F_SLASH) - .get(advancedResult(Bootcamp), bootcampController.getBootcamps) - .post(bootcampController.createBootcamp) - -router - .route(PathParam.CREATE) - .post( - protect, - authorize(Key.Trainer, Key.Admin), - bootcampController.createBootcamp - ) - +router.route(PathParam.F_SLASH).get(advancedResult(Bootcamp), bootcampController.getBootcamps).post(bootcampController.createBootcamp) +router.route(PathParam.CREATE).post(protect, authorize(Key.Trainer, Key.Admin), bootcampController.createBootcamp) router .route(PathParam.ID) .get(bootcampController.getBootcamp) - .put( - protect, - authorize(Key.Trainer, Key.Admin), - bootcampController.updateBootcamp - ) - .delete( - protect, - authorize(Key.Trainer, Key.Admin), - bootcampController.deleteBootcamp - ) + .put(protect, authorize(Key.Trainer, Key.Admin), bootcampController.updateBootcamp) + .delete(protect, authorize(Key.Trainer, Key.Admin), bootcampController.deleteBootcamp) -router.put( - PathParam.UPLOAD_PHOTO, - protect, - bootcampController.uploadBootcampPhoto -) +router.put(PathParam.UPLOAD_PHOTO, protect, bootcampController.uploadBootcampPhoto) const bootcampRoute = router export default bootcampRoute diff --git a/src/util/send-email.ts b/src/util/send-email.ts index 8e69ec4..908e522 100644 --- a/src/util/send-email.ts +++ b/src/util/send-email.ts @@ -10,9 +10,7 @@ const sendEmail = async (options: IEmailOptions) => { const info = await App.transporter.sendMail(App.message(options)) try { - const info: SentMessageInfo = await App.transporter.sendMail( - App.message(options) - ) + const info: SentMessageInfo = await App.transporter.sendMail(App.message(options)) goodlog.log(Key.MessageSent, info.messageId) } catch (error) { if (error instanceof Error) {