diff --git a/backend-app/.env.example b/backend-app/.env.example index deeaf3d..bd7fdd7 100644 --- a/backend-app/.env.example +++ b/backend-app/.env.example @@ -1,7 +1,7 @@ NODE_ENV = "development" API_VERSION = "1.0.0" -MONGO_URI = "mongodb://localhost:27017/S-W-O" -MONGO_URI_TEST = "mongodb://localhost:27017/S-W-O-TEST" +MONGO_URI = "mongodb://mongodb:27017/S-W-O" +MONGO_URI_TEST = "mongodb://mongodb:27017/S-W-O-TEST" PORT = 5000 ADMIN_EMAIL = "admin@swf.com" ADMIN_PASSWORD = "password123418746" diff --git a/backend-app/.gitignore b/backend-app/.gitignore index 40b0322..184fbc3 100644 --- a/backend-app/.gitignore +++ b/backend-app/.gitignore @@ -27,6 +27,9 @@ coverage # Compiled binary addons (http://nodejs.org/api/addons.html) build +# TSOA generated routes +routes.ts + # Dependency directories node_modules/ jspm_packages/ diff --git a/backend-app/app.ts b/backend-app/app.ts index 5ea094c..b22e4a1 100644 --- a/backend-app/app.ts +++ b/backend-app/app.ts @@ -14,9 +14,9 @@ import swaggerDocs from './utils/swagger/index'; import handleAPIVersion from './middlewares/api_version_controll'; import { COOKIE_SECRET, CURRENT_ENV } from './config/app_config'; import cookieParser from 'cookie-parser'; -import routesVersioning from 'express-routes-versioning'; -import indexRouter from './routes/index'; -import { RegisterRoutes } from './routes/routes'; +// import routesVersioning from 'express-routes-versioning'; +// import indexRouter from './routes/index'; +import { RegisterRoutes } from './routes'; const app = express(); @@ -78,12 +78,12 @@ app.get('/', (_req: Request, res: Response) => { }); // routes -app.use( - `/api`, - routesVersioning()({ - '1.0.0': indexRouter, - }) -); +// app.use( +// `/api`, +// routesVersioning()({ +// '1.0.0': indexRouter, +// }) +// ); // register routes RegisterRoutes(app); diff --git a/backend-app/config/logger_config.ts b/backend-app/config/logger_config.ts index 4d03ddc..f7fd426 100644 --- a/backend-app/config/logger_config.ts +++ b/backend-app/config/logger_config.ts @@ -28,7 +28,7 @@ const formatLogMessage = format.printf( * when the log level is debug, debug and all the levels above it will be logged. * when the log level is warn, warn and all the levels above it will be logged. */ -const logLevel = CURRENT_ENV === 'development' ? 'debug' : 'warn'; +const logLevel = CURRENT_ENV === 'development' ? 'debug' : 'info'; /** * @description - This is the configuration for the logger diff --git a/backend-app/config/nodemon.json b/backend-app/config/nodemon.json new file mode 100644 index 0000000..a14823c --- /dev/null +++ b/backend-app/config/nodemon.json @@ -0,0 +1,10 @@ +{ + "watch": ["."], + "ignore": [ + "docs/api_docs/swagger.json", + "routes/routes.ts", + "controllers/" + ], + "ext": "js ts json", + "exec": "ts-node server.ts" +} diff --git a/backend-app/config/nodemon.tsoa.json b/backend-app/config/nodemon.tsoa.json new file mode 100644 index 0000000..eae1abe --- /dev/null +++ b/backend-app/config/nodemon.tsoa.json @@ -0,0 +1,9 @@ +{ + "watch": ["controllers/"], + "ignore": ["docs/api_docs/swagger.json", "routes/routes.ts"], + "ext": "js ts json", + "exec": "npm run generate ", + "events": { + "restart": "echo Controller changed, regenerating swagger docs..." + } +} diff --git a/backend-app/controllers/auth_controllers/auth_controller.ts b/backend-app/controllers/auth_controllers/auth_controller.ts index 394300c..99576d2 100644 --- a/backend-app/controllers/auth_controllers/auth_controller.ts +++ b/backend-app/controllers/auth_controllers/auth_controller.ts @@ -1,5 +1,4 @@ import mongoose from 'mongoose'; -import { IReq, IRes, INext } from '@interfaces/vendors'; import { promisify } from 'util'; import AppError from '@utils/app_error'; import Role from '@utils/authorization/roles/role'; @@ -12,21 +11,43 @@ import { import AuthUtils from '@utils/authorization/auth_utils'; import searchCookies from '@utils/searchCookie'; import User from '@models/user/user_model'; -import { Request, Response, NextFunction } from 'express'; -import { IUser } from '@root/interfaces/models/i_user'; - +import { + Request, + Res, + TsoaResponse, + Controller, + Get, + Post, + Tags, + Query, + Body, + Route, +} from 'tsoa'; +import { IReq } from '@root/interfaces/vendors'; +import { Response, SuccessResponse } from '@tsoa/runtime'; const generateActivationKey = async () => { const randomBytesPromiseified = promisify(require('crypto').randomBytes); const activationKey = (await randomBytesPromiseified(32)).toString('hex'); return activationKey; }; -export const githubHandler = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { +@Route('api/auth') +@Tags('Authentication') +export class AuthController extends Controller { + @Get('github/callback') + @Response(400, 'Invalid access token or code') + @Response(500, 'User role does not exist. Please contact the admin.') + @SuccessResponse( + 204, + ` + - User logged in successfully + \n- User created successfully` + ) + public async githubHandler( + @Request() _req: Express.Request, + @Res() res: TsoaResponse<204, { message: string }>, + @Query() code?: string + ): Promise { const Roles = await Role.getRoles(); // check if user role exists if (!Roles.USER) @@ -34,15 +55,12 @@ export const githubHandler = async ( 500, 'User role does not exist. Please contact the admin.' ); - const { code } = req.query as { - code: string; - }; - if (!code) throw new AppError(400, 'Please provide code'); const { access_token } = await getGithubOAuthToken(code); if (!access_token) throw new AppError(400, 'Invalid code'); const githubUser = await getGithubOAuthUser(access_token); const primaryEmail = await getGithubOAuthUserPrimaryEmail(access_token); + // check if user exists const exists = await User.findOne({ email: primaryEmail }); if (exists) { const accessToken = AuthUtils.generateAccessToken( @@ -51,11 +69,14 @@ export const githubHandler = async ( const refreshToken = AuthUtils.generateRefreshToken( exists._id.toString() ); - AuthUtils.setAccessTokenCookie(res, accessToken); - AuthUtils.setRefreshTokenCookie(res, refreshToken); - return res.sendStatus(204); + AuthUtils.setAccessTokenCookie(this, accessToken); + AuthUtils.setRefreshTokenCookie(this, refreshToken); + res(204, { message: 'User logged in successfully' }); + return; } + // check if user is a new github user if (!githubUser) throw new AppError(400, 'Invalid access token'); + // create new user const createdUser = await User.create({ name: githubUser.name, email: primaryEmail, @@ -67,49 +88,63 @@ export const githubHandler = async ( githubOauthAccessToken: access_token, active: true, }); - + // set cookies const accessToken = AuthUtils.generateAccessToken( createdUser._id.toString() ); const refreshToken = AuthUtils.generateRefreshToken( createdUser._id.toString() ); - AuthUtils.setAccessTokenCookie(res, accessToken); - AuthUtils.setRefreshTokenCookie(res, refreshToken); - res.status(201).json(createdUser); - } catch (err) { - next(err); + AuthUtils.setAccessTokenCookie(this, accessToken); + AuthUtils.setRefreshTokenCookie(this, refreshToken); + + res(204, { message: 'User created successfully' }); } -}; -export const login = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const { email, password } = req.body as { - email: string; - password: string; - }; + @Post('login') + @Response( + 400, + `- Please provide email and password + \n- Invalid email or password + \n- You haven't set a password yet. Please login with GitHub and set a password from your profile page.` + ) + @Response(401, 'Invalid email or password') + @Response( + 403, + 'Your account has been banned. Please contact the admin for more information.' + ) + @SuccessResponse(200, 'OK') + public async login( + @Request() _req: Express.Request, + @Res() res: TsoaResponse<200, { accessToken: string; user: any }>, + @Body() body?: { email?: string; password?: string } + ): Promise { + const { email, password } = body; // 1) check if password exist - if (!password) { - throw new AppError(400, 'Please provide a password'); - } - // to type safty on password - if (typeof password !== 'string') { - throw new AppError(400, 'Invalid password format'); + if (!password || !email) { + throw new AppError(400, 'Please provide email and password'); } - // 2) check if user exist and password is correct const user = await User.findOne({ email, }).select('+password'); - // check if password exist and it is a string - if (!user?.password || typeof user.password !== 'string') + if (!user) { throw new AppError(400, 'Invalid email or password'); + } + + // check if password exist and it is a string + // TODO: add test for this + if (!user?.password) + throw new AppError( + 400, + "You haven't set a password yet. Please login with github and set a password from your profile page." + ); + + if (!(await user.correctPassword(password, user.password))) { + throw new AppError(401, 'Invalid email or password'); + } // Check if the account is banned if (user && user?.accessRestricted) @@ -118,39 +153,39 @@ export const login = async ( 'Your account has been banned. Please contact the admin for more information.' ); - if (!user || !(await user.correctPassword(password, user.password))) { - throw new AppError(401, 'Email or Password is wrong'); - } - // 3) All correct, send accessToken & refreshToken to client via cookie const accessToken = AuthUtils.generateAccessToken(user._id.toString()); const refreshToken = AuthUtils.generateRefreshToken( user._id.toString() ); - AuthUtils.setAccessTokenCookie(res, accessToken); - AuthUtils.setRefreshTokenCookie(res, refreshToken); + AuthUtils.setAccessTokenCookie(this, accessToken); + AuthUtils.setRefreshTokenCookie(this, refreshToken); // Remove the password from the output user.password = undefined; - res.status(200).json({ + return res(200, { accessToken, user, }); - } catch (err) { - next(err); } -}; - -export const signup = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { + @Post('signup') + @Response( + 400, + `- Please provide a password + \n- Please provide an email + \n- Please provide a name + ` + ) + @Response(500, 'User role does not exist. Please contact the admin.') + @SuccessResponse(201, 'Created') + public async signup( + @Request() _req: Express.Request, + @Res() res: TsoaResponse<201, { accessToken: string; user: any }>, + @Body() body?: { name?: string; email?: string; password?: string } + ) { const activationKey = await generateActivationKey(); const Roles = await Role.getRoles(); - // check if user role exists if (!Roles.USER) throw new AppError( @@ -159,13 +194,13 @@ export const signup = async ( ); // check if password is provided - if (!req.body.password) + if (!body.password) throw new AppError(400, 'Please provide a password'); const userpayload = { - name: req.body.name, - email: req.body.email, - password: req.body.password, + name: body.name, + email: body.email, + password: body.password, roles: [Roles.USER.name], authorities: Roles.USER.authorities, active: !REQUIRE_ACTIVATION, @@ -177,27 +212,25 @@ export const signup = async ( const refreshToken = AuthUtils.generateRefreshToken( user._id.toString() ); - AuthUtils.setAccessTokenCookie(res, accessToken); - AuthUtils.setRefreshTokenCookie(res, refreshToken); + AuthUtils.setAccessTokenCookie(this, accessToken); + AuthUtils.setRefreshTokenCookie(this, refreshToken); // Remove the password and activation key from the output user.password = undefined; user.activationKey = undefined; - res.status(201).json({ + return res(201, { accessToken, user, }); - } catch (err) { - next(err); } -}; - -export const tokenRefresh = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { + @Get('refreshToken') + @Response(400, 'You have to login to continue.') + @Response(400, 'Invalid refresh token') + @SuccessResponse(204, 'Token refreshed successfully') + public async tokenRefres( + @Request() req: IReq, + @Res() res: TsoaResponse<204, { message: string }> + ): Promise { // get the refresh token from httpOnly cookie const refreshToken = searchCookies(req, 'refresh_token'); if (!refreshToken) @@ -210,44 +243,41 @@ export const tokenRefresh = async ( if (!user) throw new AppError(400, 'Invalid refresh token'); const accessToken = AuthUtils.generateAccessToken(user._id.toString()); //set or override accessToken cookie. - AuthUtils.setAccessTokenCookie(res, accessToken); - res.sendStatus(204); - } catch (err) { - next(err); + AuthUtils.setAccessTokenCookie(this, accessToken); + res(204, { message: 'Token refreshed successfully' }); } -}; -export const logout = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { + @Get('logout') + @Response(400, 'Please provide access token') + @SuccessResponse(204, 'Logged out successfully') + public logout( + @Request() req: IReq, + @Res() res: TsoaResponse<204, { message: string }> + ): void { const accessToken = searchCookies(req, 'access_token'); if (!accessToken) throw new AppError(400, 'Please provide access token'); - const accessTokenPayload = - await AuthUtils.verifyAccessToken(accessToken); - if (!accessTokenPayload || !accessTokenPayload._id) - throw new AppError(400, 'Invalid access token'); - res.sendStatus(204); - } catch (err) { - next(err); + // delete the access token cookie + this.setHeader( + 'Set-Cookie', + `access_token=; HttpOnly; Path=/; Expires=${new Date(0)}` + ); + res(204, { message: 'Logged out successfully' }); } -}; - -interface ActivationParams { - id: string; - activationKey: string; -} - -export const activateAccount = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const { id, activationKey } = req.query as unknown as ActivationParams; - + @Get('activate') + @Response( + 400, + `- Please provide activation key + \n- Please provide user id + \n- Please provide a valid user id` + ) + @Response(404, 'User does not exist') + @Response(409, 'User is already active') + public async activateAccount( + @Request() _req: IReq, + @Res() res: TsoaResponse<200, { user: any }>, + @Query() id?: string, + @Query() activationKey?: string + ): Promise { if (!activationKey) { throw new AppError(400, 'Please provide activation key'); } @@ -282,82 +312,8 @@ export const activateAccount = async ( // Remove the password from the output user.password = undefined; - res.status(200).json({ + return res(200, { user, }); - } catch (err) { - next(err); } -}; - -export const protect = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const accessToken = searchCookies(req, 'access_token'); - - if (!accessToken) throw new AppError(401, 'Please login to continue'); - - const accessTokenPayload = - await AuthUtils.verifyAccessToken(accessToken); - - if (!accessTokenPayload || !accessTokenPayload._id) - throw new AppError(401, 'Invalid access token'); - // 3) check if the user is exist (not deleted) - const user: IUser = await User.findById(accessTokenPayload._id).select( - 'accessRestricted active roles authorities restrictions name email' - ); - if (!user) { - throw new AppError(401, 'This user is no longer exist'); - } - - // Check if the account is banned - if (user?.accessRestricted) - throw new AppError( - 403, - 'Your account has been banned. Please contact the admin for more information.' - ); - - // check if account is active - if (!user.active) - throw new AppError( - 403, - 'Your account is not active. Please activate your account to continue.' - ); - - // Create a new request object with the user property set to the user object - - req.user = user; - next(); - } catch (err) { - // check if the token is expired - if (err.name === 'TokenExpiredError') { - return next(new AppError(401, 'Your token is expired')); - } - if (err.name === 'JsonWebTokenError') { - return next(new AppError(401, err.message)); - } - next(err); - } -}; - -// Authorization check if the user have rights to do this action -export const restrictTo = - (...roles: string[]) => - (req: IReq, res: IRes, next: INext) => { - try { - const roleExist = roles.some((role) => - req.user.roles.includes(role) - ); - if (!roleExist) - throw new AppError( - 403, - 'You are not allowed to do this action' - ); - next(); - } catch (err) { - next(err); - } - }; +} diff --git a/backend-app/controllers/auth_controllers/github_controller.ts b/backend-app/controllers/auth_controllers/github_controller.ts index d594aae..94c6a41 100644 --- a/backend-app/controllers/auth_controllers/github_controller.ts +++ b/backend-app/controllers/auth_controllers/github_controller.ts @@ -1,10 +1,43 @@ import axios from 'axios'; -import Repository from '@interfaces/github_repo'; import AppError from '@utils/app_error'; -import { INext, IReq, IRes } from '@interfaces/vendors'; +import { IReq } from '@interfaces/vendors'; +import { + Controller, + Get, + Request, + Res, + Route, + Security, + Tags, + TsoaResponse, +} from 'tsoa'; -export const getRecentRepo = async (req: IReq, res: IRes, next: INext) => { - try { +interface Repository { + id: number; + name: string; + full_name: string; + description: string; + isFork: boolean; + language: string; + license: string | null; + openedIssuesCount: number; + repoCreatedAt: string; + url: string; +} +import { Response, SuccessResponse } from '@tsoa/runtime'; + +@Security('jwt') +@Route('api/github') +@Tags('GitHub') +export class GitHub extends Controller { + @Get('recent-repo') + @Response(401, 'You are not logged in') + @Response(400, 'No repositories found') + @SuccessResponse(200, 'OK') + public async getRecentRepo( + @Request() req: IReq, + @Res() res: TsoaResponse<200, { recentRepository: Repository }> + ) { if (!req.user) { throw new AppError(401, 'You are not logged in'); } @@ -44,10 +77,6 @@ export const getRecentRepo = async (req: IReq, res: IRes, next: INext) => { ); const recentRepository = sortedRepository[0]; - res.status(200).json({ - recentRepository, - }); - } catch (err) { - next(err); + res(200, { recentRepository }); } -}; +} diff --git a/backend-app/controllers/auth_controllers/password_management.ts b/backend-app/controllers/auth_controllers/password_management.ts index aacbc98..7d9634c 100644 --- a/backend-app/controllers/auth_controllers/password_management.ts +++ b/backend-app/controllers/auth_controllers/password_management.ts @@ -3,15 +3,43 @@ import logger from '@utils/logger'; import AppError from '@utils/app_error'; import generateTokens from '@utils/authorization/generate_tokens'; import validator from 'validator'; -import { NextFunction, Request, Response } from 'express'; - -export const updatePassword = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const { email, resetKey, password } = req.body; +import { Body, Controller, Post, Res, Route, Tags, TsoaResponse } from 'tsoa'; +import { Response, SuccessResponse } from '@tsoa/runtime'; + +interface UpdatePasswordRequestBody { + email: string; + resetKey: string; + password: string; +} + +interface ForgotPasswordRequestBody { + email: string; +} + +@Route('api/password-management') +@Tags('Password Management') +export class PasswordManagementController extends Controller { + @Post('update-password') + @Response( + 400, + `- Invalid email format + \n- Please provide reset key + \n- Invalid reset key` + ) + @Response(404, 'User with this email does not exist') + @SuccessResponse(200, 'OK') + public async updatePassword( + @Body() requestBody: UpdatePasswordRequestBody, + @Res() + res: TsoaResponse< + 200, + { + token: { accessToken: string; refreshToken: string }; + user: any; + } + > + ) { + const { email, resetKey, password } = requestBody; if (!validator.isEmail(email)) throw new AppError(400, 'Invalid email format'); @@ -35,22 +63,22 @@ export const updatePassword = async ( const token = generateTokens(user.id); user.password = undefined; - res.status(200).json({ - token, - user, - }); - } catch (err) { - next(err); + res(200, { token, user }); } -}; -export const forgotPassword = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const { email } = req.body; + @Post('forgot-password') + @Response( + 400, + `- Please provide email. + \n- Invalid email format.` + ) + @Response(404, 'User with this email does not exist') + @SuccessResponse(200, 'Email with reset key sent successfully') + public async forgotPassword( + @Body() requestBody: ForgotPasswordRequestBody, + @Res() res: TsoaResponse<200, { message: string }> + ) { + const { email } = requestBody; if (!email) throw new AppError(400, 'Please provide email'); @@ -73,10 +101,8 @@ export const forgotPassword = async ( // eslint-disable-next-line no-warning-comments // TODO: send email with reset key - res.status(200).json({ + res(200, { message: 'Email with reset key sent successfully', }); - } catch (err) { - next(err); } -}; +} diff --git a/backend-app/controllers/base_controller.ts b/backend-app/controllers/base_controller.ts index 1eeebe5..57a703f 100644 --- a/backend-app/controllers/base_controller.ts +++ b/backend-app/controllers/base_controller.ts @@ -1,137 +1,106 @@ -import { NextFunction, RequestHandler } from 'express'; +import { IUser } from '@root/interfaces/models/i_user'; import { Model } from 'mongoose'; import AppError from '@utils/app_error'; import APIFeatures from '@utils/api_features'; -import { IReq, IRes } from '@interfaces/vendors'; + +/** /** * Delete a document by ID (soft delete) * @param {Model} Model - The mongoose model * @returns {Function} - Express middleware function */ -export const deleteOne = - (Model: Model): RequestHandler => - async (req: IReq, res: IRes, next: NextFunction): Promise => { - try { - const doc = await Model.findByIdAndUpdate( - req.params.id, - { - deleted: true, - ...(req.user && { deletedBy: req.user?._id }), - deletedAt: Date.now(), - }, - { new: true } - ); - - if (!doc) throw new AppError(404, 'No document found with that id'); - - res.status(204).json({ - data: null, - }); - } catch (error) { - next(error); - } - }; +export const deleteOne = async ( + Model: Model, + userId: string, + id: string +): Promise => { + const doc = await Model.findByIdAndUpdate( + id, + { + deleted: true, + deletedBy: userId, + deletedAt: Date.now(), + }, + { new: true } + ); + if (!doc) throw new AppError(404, 'No document found with that id'); +}; /** * Update a document by ID * @param {Model} Model - The mongoose model * @returns {Function} - Express middleware function */ -export const updateOne = - (Model: Model): RequestHandler => - async (req: IReq, res: IRes, next: NextFunction) => { - try { - // get the user who is updating the document - const userid = req.user?._id; - req.body.updatedBy = userid; - const payload = new Model(req.body); - const doc = await Model.findByIdAndUpdate(req.params.id, payload, { - new: true, - runValidators: true, - }); +export const updateOne = async ( + Model: Model, + userId: string, + id: string, + body: any +): Promise => { + body.updatedBy = userId; + const payload = new Model(body); + const doc = await Model.findByIdAndUpdate(id, payload, { + new: true, + runValidators: true, + }); - if (!doc) throw new AppError(404, 'No document found with that id'); - - res.status(200).json({ - doc, - }); - } catch (error) { - next(error); - } - }; + if (!doc) throw new AppError(404, 'No document found with that id'); +}; /** * Create a new document * @param {Model} Model - The mongoose model * @returns {Function} - Express middleware function */ -export const createOne = - (Model: Model): RequestHandler => - async (req: IReq, res: IRes, next: NextFunction) => { - try { - // get the user who is creating the document - if (req.user === undefined) - throw new AppError( - 401, - 'You are not authorized to perform this action' - ); - const userid = req.user._id; - req.body.createdBy = userid; +export const createOne = async ( + Model: Model, + body: any, + user: IUser +): Promise => { + // get the user who is creating the document + if (user === undefined) + throw new AppError( + 401, + 'You are not authorized to perform this action' + ); + const userid = user._id; + body.createdBy = userid; - const doc = await Model.create(req.body); + const doc = await Model.create(body); + + return doc; +}; - res.status(201).json({ - doc, - }); - } catch (error) { - next(error); - } - }; /** * Get a document by ID * @param {Model} Model - The mongoose model * @returns {Function} - Express middleware function */ -export const getOne = - (Model: Model): RequestHandler => - async (req: IReq, res: IRes, next: NextFunction) => { - try { - const doc = await Model.findById(req.params.id); +export const getOne = async (Model: Model, id: string): Promise => { + const doc = await Model.findById(id); - if (!doc) throw new AppError(404, 'No document found with that id'); + if (!doc) throw new AppError(404, 'No document found with that id'); - res.status(200).json({ - doc, - }); - } catch (error) { - next(error); - } - }; + return doc; +}; /** * Get all documents * @param {Model} Model - The mongoose model + * @param {Object} query - The query object * @returns {Function} - Express middleware function */ -export const getAll = - (Model: Model): RequestHandler => - async (req: IReq, res: IRes, next: NextFunction) => { - try { - const features = new APIFeatures( - Model.find(), - req.query as Record - ) - .sort() - .paginate(); +export const getAll = async ( + Model: Model, + query: Record +): Promise => { + const features = new APIFeatures(Model.find(), query).sort().paginate(); - const doc = await features.query; + const doc = await features.query; - res.status(200).json({ - results: doc.length, - data: doc, - }); - } catch (error) { - next(error); - } + return { + results: doc.length, + data: doc, }; +}; diff --git a/backend-app/controllers/calendar_controllers/calendar_base_controller.ts b/backend-app/controllers/calendar_controllers/calendar_base_controller.ts index 39af3cc..fdce186 100644 --- a/backend-app/controllers/calendar_controllers/calendar_base_controller.ts +++ b/backend-app/controllers/calendar_controllers/calendar_base_controller.ts @@ -1,50 +1,73 @@ -import { IReq, IRes, INext } from '@interfaces/vendors'; +import { + Controller, + Post, + Route, + Tags, + Request, + Res, + Body, + TsoaResponse, + Delete, +} from 'tsoa'; +import { IReq } from '@interfaces/vendors'; import AppError from '@utils/app_error'; import * as calendar_validators from './calendar_validators'; - -export const updateCalendar = async ( - req: IReq, - _res: IRes, - next: INext -): Promise => { - try { - const calendar = await calendar_validators.validateCalendar(req); - // check if user is not admin nor the owner of the calendar - if ( - calendar.createdBy !== req.user._id.toString() || - !req.user.roles.includes('ADMIN') || - !req.user.roles.includes('SUPER_ADMIN') - ) { - throw new AppError( - 403, - 'You are not allowed to update this calendar' - ); +import { Response, SuccessResponse } from 'tsoa'; +@Route('api/calendar') +@Tags('Calendar') +export class CalendarController extends Controller { + @Post('update') + @Response(403, 'You are not allowed to update this calendar') + @SuccessResponse(204, 'No Content') + public async updateCalendar( + @Request() req: IReq, + @Res() _res: TsoaResponse<204, void>, + @Body() body?: any + ): Promise { + try { + const calendar = await calendar_validators.validateCalendar(req); + // check if user is not admin nor the owner of the calendar + if ( + calendar.createdBy !== req.user._id.toString() || + !req.user.roles.includes('ADMIN') || + !req.user.roles.includes('SUPER_ADMIN') + ) { + throw new AppError( + 403, + 'You are not allowed to update this calendar' + ); + } + // TODO: update calendar + } catch (err) { + this.setStatus(err.statusCode || 500); + throw new Error(err.message); } - // TODO: update calendar - } catch (err) { - next(err); } -}; -export const deleteCalendar = async ( - req: IReq, - _res: IRes, - next: INext -): Promise => { - try { - const calendar = await calendar_validators.validateCalendar(req); - // check if user is not admin nor the owner of the calendar - if ( - calendar.createdBy !== req.user._id.toString() || - !req.user.roles.includes('SUPER_ADMIN') - ) { - throw new AppError( - 403, - 'You are not allowed to delete this calendar' - ); + @Delete('delete') + @Response(403, 'You are not allowed to delete this calendar') + @SuccessResponse(204, 'No Content') + public async deleteCalendar( + @Request() req: IReq, + @Res() _res: TsoaResponse<204, void>, + @Body() body?: any + ): Promise { + try { + const calendar = await calendar_validators.validateCalendar(req); + // check if user is not admin nor the owner of the calendar + if ( + calendar.createdBy !== req.user._id.toString() || + !req.user.roles.includes('SUPER_ADMIN') + ) { + throw new AppError( + 403, + 'You are not allowed to delete this calendar' + ); + } + // TODO: delete calendar + } catch (err) { + this.setStatus(err.statusCode || 500); + throw new Error(err.message); } - // TODO: delete calendar - } catch (err) { - next(err); } -}; +} diff --git a/backend-app/controllers/calendar_controllers/participents_controller.ts b/backend-app/controllers/calendar_controllers/participents_controller.ts index 86b9f4f..42df1e4 100644 --- a/backend-app/controllers/calendar_controllers/participents_controller.ts +++ b/backend-app/controllers/calendar_controllers/participents_controller.ts @@ -1,11 +1,48 @@ -import { INext, IReq, IRes } from '@interfaces/vendors'; +import { IReq } from '@interfaces/vendors'; import AppError from '@utils/app_error'; import validator from 'validator'; import * as calendar_validators from './calendar_validators'; import { ObjectId } from 'mongoose'; +import { + Request, + Res, + Body, + Controller, + Delete, + Post, + Route, + Security, + Tags, + TsoaResponse, + SuccessResponse, + Response, +} from 'tsoa'; -export const inviteUsersByEmail = async (req: IReq, res: IRes, next: INext) => { - try { +interface InviteUsersByEmailRequestBody { + emails: string[]; +} + +@Security('jwt') +@Route('calendar/participants') +@Tags('Calendar') +export class CalendarParticipantsController extends Controller { + @Post('invite') + @Response(400, 'Emails are required') + @Response( + 403, + `- You do not have permission to invite users to this calendar + \n- This calendar is not shareable` + ) + @SuccessResponse(200, 'OK') + public async inviteUsersByEmail( + @Request() req: IReq, + @Body() body: InviteUsersByEmailRequestBody, + @Res() + res: TsoaResponse< + 200, + { validEmails: string[]; invalidEmails: string[] } + > + ) { // check if calendar exists const calendar = await calendar_validators.validateCalendar(req); // check if user is the calendar owner @@ -20,7 +57,7 @@ export const inviteUsersByEmail = async (req: IReq, res: IRes, next: INext) => { throw new AppError(403, 'This calendar is not shareable'); } // get emails from request body - const emails = req.body.emails; + const emails = body.emails; if (!emails) { throw new AppError(400, 'Emails are required'); } @@ -35,38 +72,36 @@ export const inviteUsersByEmail = async (req: IReq, res: IRes, next: INext) => { } }); // TODO: send emails to valid emails - res.status(200).json({ + res(200, { validEmails, invalidEmails, }); - } catch (err) { - next(err); } -}; -export const removeCalendarParticipants = async ( - req: IReq, - res: IRes, - next: INext -) => { - try { + @Delete('remove') + @Response(403, 'You are not the owner of this calendar') + @SuccessResponse(200, 'OK') + public async removeCalendarParticipants( + @Request() req: IReq, + @Body() body: any, + @Res() res: TsoaResponse<200, { calendar: any }> + ) { const calendar = await calendar_validators.validateCalendar(req); // check if user is the calendar owner if (calendar.createdBy.toString() !== req.user._id.toString()) { throw new AppError(403, 'You are not the owner of this calendar'); } // get list of participants to remove from calendar - const listOfParticipants: ObjectId[] = req.body.participants; + const listOfParticipants: ObjectId[] = body.participants; // remove users from list of participants in calendar calendar.participants = calendar.participants.filter( (participant: ObjectId) => !listOfParticipants.includes(participant) ); // save calendar await calendar.save(); - res.status(200).json({ + // return calendar + res(200, { calendar, }); - } catch (err) { - next(err); } -}; +} diff --git a/backend-app/controllers/tryingtsoa.ts b/backend-app/controllers/tryingtsoa.ts deleted file mode 100644 index c48fd5f..0000000 --- a/backend-app/controllers/tryingtsoa.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Controller, Route } from 'tsoa'; -import { Body, Post, Query, Response, Path, Middlewares } from '@tsoa/runtime'; -import { Request, Response as i_res } from 'express'; - -// interface IUser { -// /** -// * @isString Please enter a valid name as a string -// * @minLength 1 Name must have at least 1 character -// * @maxLength 255 Name should not exceed 255 characters -// */ -// name: string; - -// /** -// * @isString Please enter a valid email address -// * @isEmail Please provide a valid email format -// * @minLength 5 Email must have at least 5 characters -// * @maxLength 255 Email should not exceed 255 characters -// */ -// email: string; - -// /** -// * @isString Please enter a valid address as a string -// * @maxLength 255 Address should not exceed 255 characters -// */ -// address?: string; - -// /** -// * @isString Please enter a valid password as a string -// * @minLength 8 Password must have at least 8 characters -// * @maxLength 255 Password should not exceed 255 characters -// */ -// password?: string; - -// /** -// * @isArray Please provide an array of authorities -// * @minItems 1 At least one authority is required -// */ -// authorities: string[]; - -// /** -// * @isArray Please provide an array of restrictions -// * @uniqueItems Restrictions should be unique -// */ -// restrictions: string[]; - -// /** -// * @isArray Please provide an array of roles -// * @uniqueItems Roles should be unique -// */ -// roles: string[]; - -// /** -// * @isBool Please provide a valid boolean value for active -// */ -// active: boolean; - -// /** -// * @isString Please provide a valid activation key as a string -// * @maxLength 255 Activation key should not exceed 255 characters -// */ -// activationKey?: string; - -// /** -// * @isBool Please provide a valid boolean value for accessRestricted -// */ -// accessRestricted: boolean; - -// /** -// * @isString Please provide a valid GitHub OAuth access token as a string -// * @maxLength 255 GitHub OAuth access token should not exceed 255 characters -// */ -// githubOauthAccessToken?: string; - -// /** -// * @isString Please provide a valid reset key as a string -// * @maxLength 255 Reset key should not exceed 255 characters -// */ -// resetKey?: string; - -// /** -// * @isDateTime Please provide a valid date and time for createdAt -// */ -// createdAt: Date; - -// /** -// * @isDateTime Please provide a valid date and time for updatedAt -// */ -// updatedAt: Date; - -// /** -// * @isBool Please provide a valid boolean value for deleted -// */ -// deleted: boolean; - -// /** -// * @isString Please enter a valid deleted by as a string -// * @maxLength 255 Deleted by should not exceed 255 characters -// */ -// deletedBy?: string; - -// /** -// * @isDateTime Please provide a valid date and time for deletedAt -// */ -// deletedAt?: Date; - -// /** -// * @isString Please enter a valid created by as a string -// * @maxLength 255 Created by should not exceed 255 characters -// */ -// createdBy?: string; - -// /** -// * @isString Please enter a valid updated by as a string -// * @maxLength 255 Updated by should not exceed 255 characters -// */ -// updatedBy?: string; -// } - -interface ValidateErrorJSON { - message: 'Validation failed'; - details: { [name: string]: unknown }; -} - -function customMiddleware(req: Request, res: i_res, next: any) { - // Perform any necessary operations or modifications - next(); -} - -@Route('users') -export class UsersController extends Controller { - /** - * Retrieves the details of an existing user. - * Supply the unique user ID from either and receive corresponding user details. - */ - @Response(404, 'Not Found') - @Post('{userId}') - @Middlewares(customMiddleware) - public getUser( - @Path() userId: number, - @Query() name?: string, - @Body() body?: { name: string } - ): {} { - // return new UsersService().get(userId, name); - return { userId, name }; - } -} diff --git a/backend-app/controllers/users_controllers/admin_controller.ts b/backend-app/controllers/users_controllers/admin_controller.ts index 5595c0b..f8f5b4e 100644 --- a/backend-app/controllers/users_controllers/admin_controller.ts +++ b/backend-app/controllers/users_controllers/admin_controller.ts @@ -1,130 +1,63 @@ -import { IReq, IRes, INext } from '@interfaces/vendors'; +import { IReq } from '@interfaces/vendors'; import USER from '@models/user/user_model'; import Role from '@utils/authorization/roles/role'; import AppError from '@utils/app_error'; import validateActions from '@utils/authorization/validate_actions'; +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Res, + Route, + Security, + Tags, + TsoaResponse, +} from 'tsoa'; +import { + Response, + SuccessResponse, + Put, + Example, + Request, +} from '@tsoa/runtime'; +import Actions from '@constants/actions'; +import { InspectAuthority } from '@root/decorators/inspect_authority'; -export const addAdmin = async (req: IReq, res: IRes, next: INext) => { - try { - const Roles = await Role.getRoles(); - const { userId } = req.params; - const user = await USER.findById(userId); - if (!user) throw new AppError(404, 'No user found with this id'); - if (!Roles.ADMIN) - throw new AppError( - 500, - 'Error in base roles, please contact an admin' - ); - if (user.roles?.includes(Roles.ADMIN.name)) - throw new AppError(400, 'User is already an admin'); - user.roles?.push(Roles.ADMIN.name); - const existingAuthorities = user.authorities; - const existingRestrictions = user.restrictions; - user.authorities = Array.from( - new Set([...Roles.ADMIN.authorities, ...existingAuthorities]) - ); - user.restrictions = Array.from( - new Set([...Roles.ADMIN.restrictions, ...existingRestrictions]) - ); - await user.save(); - res.status(200).json({ - message: 'User is now an admin', - }); - } catch (err) { - next(err); - } -}; - -export const removeAdmin = async (req: IReq, res: IRes, next: INext) => { - try { - const Roles = await Role.getRoles(); - const { userId } = req.params; - const user = await USER.findById(userId); - if (!user) throw new AppError(404, 'No user found with this id'); - if (!Roles.ADMIN || !Roles.USER) - throw new AppError( - 500, - 'Error in base roles, please contact an admin' - ); - if (req.user._id?.toString() === userId?.toString()) - throw new AppError(400, 'You cannot remove yourself as an admin'); - if (!user.roles?.includes(Roles.ADMIN.name)) - throw new AppError(400, 'User is not an admin'); - user.roles = user.roles.filter((role) => role !== Roles.ADMIN.name); - user.authorities = Roles.USER.authorities; - user.restrictions = Roles.USER.restrictions; - await user.save(); - res.status(200).json({ - message: 'User is no longer an admin', - }); - } catch (err) { - next(err); - } -}; - -export const addSuperAdmin = async (req: IReq, res: IRes, next: INext) => { - try { - const Roles = await Role.getRoles(); - const { userId } = req.params; - const user = await USER.findById(userId); - if (!user) throw new AppError(404, 'No user found with this id'); - if (req.user._id?.toString() === userId?.toString()) - throw new AppError(400, 'You cannot make yourself a super admin'); - if (user.roles?.includes(Roles.SUPER_ADMIN.name)) - throw new AppError(400, 'User is already a super admin'); - user.roles?.push(Roles.SUPER_ADMIN.name); - const existingRestrictions = user.restrictions; - user.authorities = Roles.SUPER_ADMIN.authorities; - user.restrictions = Array.from( - new Set([ - ...Roles.SUPER_ADMIN.restrictions, - ...existingRestrictions, - ]) - ); - await user.save(); - res.status(200).json({ - message: 'User is now a super admin', - }); - } catch (err) { - next(err); - } -}; - -export const removeSuperAdmin = async (req: IReq, res: IRes, next: INext) => { - const { userId } = req.params; - try { - const Roles = await Role.getRoles(); - const user = await USER.findById(userId); - if (!user) throw new AppError(404, 'No user found with this id'); - if (req.user._id?.toString() === userId?.toString()) - throw new AppError( - 400, - 'You cannot remove yourself as a super admin' - ); - if (!user.roles?.includes(Roles.SUPER_ADMIN.name)) - throw new AppError(400, 'User is not a super admin'); - user.roles = user.roles.filter( - (role) => role !== Roles.SUPER_ADMIN.name - ); - user.authorities = Roles.ADMIN.authorities; - user.restrictions = Roles.ADMIN.restrictions; - await user.save(); - res.status(200).json({ - message: 'User is no longer a super admin', - }); - } catch (err) { - next(err); - } -}; - -export const authorizeOrRestrict = async ( - req: IReq, - res: IRes, - next: INext -) => { - try { - const { authorities, restrictions } = req.body; - const { userId } = req.params; +interface RoleType { + name: string; + authorities: string[]; + restrictions: string[]; +} +@Security('jwt') +@Route('admin') +@Tags('Admin') +export class AdminController extends Controller { + @Example({ + message: 'User is now an admin', + }) + @Response( + 400, + `- One or many actions are invalid in the authorities array. + \n- One or many actions are invalid in the restrictions array. + \n- You cannot change your own authorities or restrictions. + \n- No user found with this id. + \n- User is a super admin. + ` + ) + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER) + @Put('authorize-or-restrict/{userId}') + async authorizeOrRestrict( + @Path() userId: string, + @Request() req: IReq, + @Res() res: TsoaResponse<200, any>, + @Body() + body: Omit + ) { + const { authorities, restrictions } = body; if (!validateActions(authorities)) throw new AppError( 400, @@ -154,17 +87,31 @@ export const authorizeOrRestrict = async ( new Set([...restrictions, ...existingRestrictions]) ); await user.save(); - res.status(200).json({ + return res(200, { message: 'User authorities and restrictions updated', }); - } catch (err) { - next(err); } -}; -export const banUser = async (req: IReq, res: IRes, next: INext) => { - const { userId } = req.params; - try { + @Example({ + message: 'User is now banned', + }) + @Response( + 400, + ` + - You cannot ban yourself. + \n- User is already banned. + \n- You cannot ban a super admin. + \n- You cannot ban an admin` + ) + @Response(404, ' No user found with this id') + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER, Actions.BAN_USER) + @Put('ban-user/{userId}') + async banUser( + @Request() req: IReq, + @Res() res: TsoaResponse<200, any>, + @Path() userId: string + ) { const Roles = await Role.getRoles(); const user = await USER.findById(userId); if (!user) throw new AppError(404, 'No user found with this id'); @@ -178,17 +125,27 @@ export const banUser = async (req: IReq, res: IRes, next: INext) => { throw new AppError(400, 'You cannot ban an admin'); user.accessRestricted = true; await user.save(); - res.status(200).json({ + return res(200, { message: 'User is now banned', }); - } catch (err) { - next(err); } -}; - -export const unbanUser = async (req: IReq, res: IRes, next: INext) => { - const { userId } = req.params; - try { + @Example({ + message: 'User is now unbanned', + }) + @Response( + 400, + `- You cannot unban yourself. + \n- User is not banned.` + ) + @Response(404, 'No user found with this id') + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER, Actions.BAN_USER) + @Put('unban-user/{userId}') + async unbanUser( + @Request() req: IReq, + @Res() res: TsoaResponse<200, any>, + @Path() userId: string + ) { const user = await USER.findById(userId); if (!user) throw new AppError(404, 'No user found with this id'); if (req.user._id?.toString() === userId?.toString()) @@ -197,17 +154,22 @@ export const unbanUser = async (req: IReq, res: IRes, next: INext) => { throw new AppError(400, 'User is not banned'); user.accessRestricted = false; await user.save(); - res.status(200).json({ + return res(200, { message: 'User is now unbanned', }); - } catch (err) { - next(err); } -}; -export const createRole = async (req: IReq, res: IRes, next: INext) => { - const { name, authorities, restrictions } = req.body; - try { + @Response(400, 'Role already exists') + @SuccessResponse('201', 'CREATED') + @InspectAuthority(Actions.MANAGE_ROLES) + @Post('role') + async createRole( + @Res() res: TsoaResponse<201, any>, + + @Body() + body: RoleType + ): Promise<{ message: string; data: RoleType }> { + const { name, authorities, restrictions } = body; if (await Role.getRoleByName(name)) throw new AppError(400, 'Role already exists'); const createdRole = await Role.createRole( @@ -215,74 +177,98 @@ export const createRole = async (req: IReq, res: IRes, next: INext) => { authorities, restrictions ); - res.status(201).json({ + return res(201, { message: 'Role created', data: createdRole, }); - } catch (err) { - next(err); } -}; - -export const getRoles = async (_req: IReq, res: IRes, next: INext) => { - try { + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.MANAGE_ROLES) + @Get('role') + async getRoles(@Res() res: TsoaResponse<200, any>): Promise<{ + message: string; + data: { + [key: string]: RoleType; + }; + }> { const roles = await Role.getRoles(); - res.status(200).json({ + return res(200, { message: 'Roles retrieved', data: roles, }); - } catch (err) { - next(err); } -}; - -export const getRole = async (req: IReq, res: IRes, next: INext) => { - const { name } = req.params; - try { - const singleRole = await Role.getRoleByName(name as string); - res.status(200).json({ + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.MANAGE_ROLES) + @Get('role/{name}') + async getRole( + @Res() res: TsoaResponse<200, any>, + @Path() name: string + ): Promise<{ + message: string; + data: RoleType; + }> { + const singleRole = await Role.getRoleByName(name); + return res(200, { message: 'Role retrieved', data: singleRole, }); - } catch (err) { - next(err); } -}; - -export const deleteRole = async (req: IReq, res: IRes, next: INext) => { - const { name } = req.params; - try { - const deletedRole = await Role.deleteRoleByName(name as string); - res.status(200).json({ + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.MANAGE_ROLES) + @Delete('role/{name}') + async deleteRole( + @Res() res: TsoaResponse<200, any>, + @Path() name: string + ): Promise<{ + message: string; + data: RoleType; + }> { + const deletedRole = await Role.deleteRoleByName(name); + return res(200, { message: 'Role deleted', data: deletedRole, }); - } catch (err) { - next(err); } -}; -export const updateRole = async (req: IReq, res: IRes, next: INext) => { - const { name } = req.params; - const { authorities, restrictions } = req.body; - try { + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.MANAGE_ROLES) + @Put('role/{name}') + async updateRole( + @Path() name: string, + @Res() res: TsoaResponse<200, any>, + @Body() body: Omit + ): Promise<{ + message: string; + data: RoleType; + }> { + const { authorities, restrictions } = body; const updatedRole = await Role.updateRoleByName( - name as string, + name, authorities, restrictions ); - res.status(200).json({ + return res(200, { message: 'Role updated', data: updatedRole, }); - } catch (err) { - next(err); } -}; - -export const assignRoleToUser = async (req: IReq, res: IRes, next: INext) => { - const { userId, name } = req.params; - try { + @Example({ + message: 'Role assigned to user', + }) + @Response(400, 'User already has this role') + @Response( + 404, + `- No user found with this id. + \n- No role found with this name.` + ) + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.MANAGE_ROLES) + @Put('assign-role/{name}/{userId}') + async assignRoleToUser( + @Res() res: TsoaResponse<200, any>, + @Path() name: string, + @Path() userId: string + ) { const user = await USER.findById(userId); const role = await Role.getRoleByName(name as string); if (!user) throw new AppError(404, 'No user found with this id'); @@ -297,17 +283,27 @@ export const assignRoleToUser = async (req: IReq, res: IRes, next: INext) => { new Set([...role.restrictions, ...user.restrictions]) ); await user.save(); - res.status(200).json({ + return res(200, { message: 'Role assigned to user', }); - } catch (err) { - next(err); } -}; - -export const removeRoleFromUser = async (req: IReq, res: IRes, next: INext) => { - const { userId, name } = req.params; - try { + @Example({ + message: 'Role removed to user', + }) + @Response(400, 'User does not have this role') + @Response( + 404, + `- No role found with this name. + \n- No user found with this id.` + ) + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.MANAGE_ROLES) + @Put('remove-role/{name}/{userId}') + async removeRoleFromUser( + @Res() res: TsoaResponse<200, any>, + @Path() name: string, + @Path() userId: string + ) { const role = await Role.getRoleByName(name as string); if (!role) throw new AppError(404, 'No role found with this name'); const user = await USER.findById(userId); @@ -322,10 +318,8 @@ export const removeRoleFromUser = async (req: IReq, res: IRes, next: INext) => { (restriction) => !role.restrictions.includes(restriction) ); await user.save(); - res.status(200).json({ + return res(200, { message: 'Role removed from user', }); - } catch (err) { - next(err); } -}; +} diff --git a/backend-app/controllers/users_controllers/super_admin_controller.ts b/backend-app/controllers/users_controllers/super_admin_controller.ts new file mode 100644 index 0000000..fb3a66f --- /dev/null +++ b/backend-app/controllers/users_controllers/super_admin_controller.ts @@ -0,0 +1,169 @@ +import { IReq } from '@interfaces/vendors'; +import USER from '@models/user/user_model'; +import Role from '@utils/authorization/roles/role'; +import AppError from '@utils/app_error'; +import { Controller, Res, Route, Security, Tags, TsoaResponse } from 'tsoa'; +import { + Response, + Path, + SuccessResponse, + Put, + Example, + Request, +} from '@tsoa/runtime'; +import { InspectAuthority } from '@root/decorators/inspect_authority'; +import Actions from '@constants/actions'; + +@Security('jwt') +@Route('super-admin') +@Tags('Super Admin') +export class SuperAdminController extends Controller { + @Example({ + message: 'User is now an admin', + }) + @Response(404, 'No user found with this id') + @Response(500, 'Error in base roles, please contact an admin') + @Response(400, 'User is already an admin') + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER) + @Put('add-admin/{userId}') + async addAdmin( + @Res() res: TsoaResponse<200, any>, + @Path() userId: string + ): Promise<{ message: string }> { + const Roles = await Role.getRoles(); + const user = await USER.findById(userId); + if (!user) throw new AppError(404, 'No user found with this id'); + if (!Roles.ADMIN) + throw new AppError( + 500, + 'Error in base roles, please contact an admin' + ); + if (user.roles?.includes(Roles.ADMIN.name)) + throw new AppError(400, 'User is already an admin'); + user.roles?.push(Roles.ADMIN.name); + const existingAuthorities = user.authorities; + const existingRestrictions = user.restrictions; + user.authorities = Array.from( + new Set([...Roles.ADMIN.authorities, ...existingAuthorities]) + ); + user.restrictions = Array.from( + new Set([...Roles.ADMIN.restrictions, ...existingRestrictions]) + ); + await user.save(); + return res(200, { + message: 'User is now an admin', + }); + } + + @Example({ message: 'User is no longer an admin' }) + @Response( + 400, + `- You cannot remove yourself as an admin. + \n- User is not an admin. + ` + ) + @Response(500, 'Error in base roles, please contact an admin') + @Response(404, 'No user found with this id') + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER) + @Put('remove-admin/{userId}') + async removeAdmin( + @Res() res: TsoaResponse<200, any>, + @Path() userId: string, + @Request() req: IReq + ): Promise<{ message: string }> { + const Roles = await Role.getRoles(); + const user = await USER.findById(userId); + if (!user) throw new AppError(404, 'No user found with this id'); + if (!Roles.ADMIN || !Roles.USER) + throw new AppError( + 500, + 'Error in base roles, please contact an admin' + ); + if (req.user._id?.toString() === userId?.toString()) + throw new AppError(400, 'You cannot remove yourself as an admin'); + if (!user.roles?.includes(Roles.ADMIN.name)) + throw new AppError(400, 'User is not an admin'); + user.roles = user.roles.filter((role) => role !== Roles.ADMIN.name); + user.authorities = Roles.USER.authorities; + user.restrictions = Roles.USER.restrictions; + await user.save(); + return res(200, { + message: 'User is no longer an admin', + }); + } + + @Example({ message: 'User is now a super admin' }) + @Response( + 400, + `- You cannot make yourself a super admin. + \n- User is already a super admin` + ) + @Response(404, 'No user found with this id') + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER) + @Put('add-super-admin/{userId}') + async addSuperAdmin( + @Res() res: TsoaResponse<200, any>, + @Path() userId: string, + @Request() req: IReq + ) { + const Roles = await Role.getRoles(); + const user = await USER.findById(userId); + if (!user) throw new AppError(404, 'No user found with this id'); + if (req.user._id?.toString() === userId?.toString()) + throw new AppError(400, 'You cannot make yourself a super admin'); + if (user.roles?.includes(Roles.SUPER_ADMIN.name)) + throw new AppError(400, 'User is already a super admin'); + user.roles?.push(Roles.SUPER_ADMIN.name); + const existingRestrictions = user.restrictions; + user.authorities = Roles.SUPER_ADMIN.authorities; + user.restrictions = Array.from( + new Set([ + ...Roles.SUPER_ADMIN.restrictions, + ...existingRestrictions, + ]) + ); + await user.save(); + return res(200, { + message: 'User is now a super admin', + }); + } + + @Example({ message: 'User is no longer a super admin' }) + @Response( + 400, + `- You cannot remove yourself as a super admin. + \n- User is not a super admin.` + ) + @Response(404, 'No user found with this id') + @SuccessResponse('200', 'OK') + @InspectAuthority(Actions.UPDATE_USER) + @Put('remove-super-admin/{userId}') + async removeSuperAdmin( + @Res() res: TsoaResponse<200, any>, + @Path() userId: string, + @Request() req: IReq + ) { + const Roles = await Role.getRoles(); + const user = await USER.findById(userId); + if (!user) throw new AppError(404, 'No user found with this id'); + if (req.user._id?.toString() === userId?.toString()) + throw new AppError( + 400, + 'You cannot remove yourself as a super admin' + ); + if (!user.roles?.includes(Roles.SUPER_ADMIN.name)) + throw new AppError(400, 'User is not a super admin'); + user.roles = user.roles.filter( + (role) => role !== Roles.SUPER_ADMIN.name + ); + user.authorities = Roles.ADMIN.authorities; + user.restrictions = Roles.ADMIN.restrictions; + await user.save(); + return res(200, { + message: 'User is no longer a super admin', + }); + } +} diff --git a/backend-app/controllers/users_controllers/user_controller.ts b/backend-app/controllers/users_controllers/user_controller.ts index 04e5dce..1efc208 100644 --- a/backend-app/controllers/users_controllers/user_controller.ts +++ b/backend-app/controllers/users_controllers/user_controller.ts @@ -1,45 +1,74 @@ -import User from '@models/user/user_model'; -import * as base from '@controllers/base_controller'; +import { Controller, Get, Delete, Patch } from '@tsoa/runtime'; +// import * as base from '@controllers/base_controller'; import AppError from '@utils/app_error'; -import { INext, IReq, IRes } from '@interfaces/vendors'; +import { IReq } from '@interfaces/vendors'; +import { + Request, + Res, + Route, + Security, + TsoaResponse, + Response, + SuccessResponse, + Tags, +} from 'tsoa'; +import User from '@root/models/user/user_model'; -export const getMe = (req: IReq, res: IRes) => { - // return data of the current user - res.status(200).json({ - user: req.user, - }); -}; +@Route('api/users') +@Security('jwt') +@Tags('User') +export class UserController extends Controller { + @Response(401, 'Unauthorized') + @SuccessResponse(200, 'OK') + @Get('me') + public getMe(@Request() req: IReq, @Res() res: TsoaResponse<200, any>) { + if (!req.user) { + throw new AppError(401, 'Please log in again!'); + } + // return data of the current user + res(200, req.user); + } -export const deleteMe = async (req: IReq, res: IRes, next: INext) => { - try { + @Response(401, 'Unauthorized') + @SuccessResponse(204, 'No Content') + @Delete('me') + public async deleteMe( + @Request() req: IReq, + @Res() res: TsoaResponse<204, { message: string }> + ) { await User.findByIdAndUpdate(req.user._id, { deleted: true, deletedAt: Date.now(), deletedBy: req.user._id, }); - - res.status(204).json({ - data: null, - }); - } catch (error) { - next(error); + return res(204, { message: 'User deleted successfully' }); } -}; -export const updateMe = async (req: IReq, res: IRes, next: INext) => { - try { + @Response(401, 'Unauthorized') + @Response( + 400, + `- This route is not for role updates. Please use /updateRole + \n- This route is not for password updates. Please use auth/updateMyPassword` + ) + @Response(404, 'No document found with that ID') + @SuccessResponse(200, 'OK') + @Patch('me') + public async updateMe( + @Request() req: IReq, + @Res() res: TsoaResponse<200, any> + ) { // 1) Create error if user POSTs password data if (req.body.password || req.body.passwordConfirm) { throw new AppError( 400, - 'This route is not for password updates. Please use /updateMyPassword' + 'This route is not for password updates. Please use api/password-management/update-password' ); } // create error if user tries to update role if (req.body.roles) { throw new AppError( 400, - 'This route is not for role updates. Please use /updateRole' + 'This route is not for role updates. Please use /update-role' ); } // 2) Filtered out unwanted fields names that are not allowed to be updated @@ -47,26 +76,36 @@ export const updateMe = async (req: IReq, res: IRes, next: INext) => { name: req.body.name, email: req.body.email, }; + // TODO: the email should have a unique way to update // 3) Update user document const doc = await User.findByIdAndUpdate(req.user._id, payload, { new: true, runValidators: true, }); if (!doc) { - return next(new AppError(404, 'No document found with that id')); + throw new AppError(404, 'No document found with that ID'); } - res.status(200).json({ - doc, - }); - } catch (error) { - next(error); + res(200, doc); } -}; -export const getAllUsers = base.getAll(User); -export const getUser = base.getOne(User); + // @Get() + // public async getAllUsers(): Promise { + // return Promise.resolve(base.getAll(User)); + // } + + // @Get('{id}') + // public async getUser(@Path() id: string): Promise { + // return base.getOne(User, id); + // } + + // @Put('{id}') + // public async updateUser(id: string): Promise { + // base.updateOne(User,id); + // } -// Don't update password on this -export const updateUser = base.updateOne(User); -export const deleteUser = base.deleteOne(User); + // @Delete('{id}') + // public async deleteUser(id: string): Promise { + // base.deleteOne(User,id); + // } +} diff --git a/backend-app/decorators/inspect_authority.ts b/backend-app/decorators/inspect_authority.ts new file mode 100644 index 0000000..f15b95f --- /dev/null +++ b/backend-app/decorators/inspect_authority.ts @@ -0,0 +1,44 @@ +import { Request } from 'express'; +import User from '@models/user/user_model'; +import AppError from '@root/utils/app_error'; + +export function InspectAuthority(...actions: string[]): MethodDecorator { + return function ( + target: Object, + propertyKey: string | symbol, + descriptor: PropertyDescriptor + ) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + const req: Request = args[0]; + const next: Function = args[2]; + + User.findById(req.user._id) + .then((user) => { + if (!user) + throw new AppError( + 401, + 'The user belonging to this token does no longer exist' + ); + if (user.isAuthorizedTo(actions)) { + if (!user.isRestrictedFrom(actions)) { + originalMethod.apply(this, args); + } else { + throw new AppError( + 403, + 'You are restricted from performing this action, contact the admin for more information' + ); + } + } else { + throw new AppError( + 403, + 'You do not have permission to perform this action ; required permissions: ' + + actions + ); + } + }) + .catch((error) => next(error)); + }; + return descriptor; + }; +} diff --git a/backend-app/interfaces/vendors.ts b/backend-app/interfaces/vendors.ts index 889afdf..68704a3 100644 --- a/backend-app/interfaces/vendors.ts +++ b/backend-app/interfaces/vendors.ts @@ -1,4 +1,4 @@ -import { Request } from 'express'; +import { Request, Response } from 'express'; export interface IReq extends Request { user: { @@ -13,8 +13,6 @@ export interface IReq extends Request { }; } -import { Response } from 'express'; - export interface IRes extends Response {} import { NextFunction } from 'express'; diff --git a/backend-app/middlewares/api_version_controll.ts b/backend-app/middlewares/api_version_controll.ts index 4ca2cec..fa667b0 100644 --- a/backend-app/middlewares/api_version_controll.ts +++ b/backend-app/middlewares/api_version_controll.ts @@ -9,8 +9,8 @@ import { API_VERSION } from '@config/app_config'; * @param {NextFunction} next - Express next function to pass control to the next middleware or route handler. */ const handleAPIVersion = (req: Request, _res: Response, next: NextFunction) => { - if (!req.headers['accept-version']) { - req.headers['accept-version'] = API_VERSION; + if (!req.headers['api-version']) { + req.headers['api-version'] = API_VERSION; } next(); }; diff --git a/backend-app/middlewares/authentications.ts b/backend-app/middlewares/authentications.ts new file mode 100644 index 0000000..a0ae218 --- /dev/null +++ b/backend-app/middlewares/authentications.ts @@ -0,0 +1,97 @@ +import { IUser } from '@root/interfaces/models/i_user'; +import User from '@root/models/user/user_model'; +import AppError from '@root/utils/app_error'; +import AuthUtils from '@root/utils/authorization/auth_utils'; +import * as express from 'express'; + +export function expressAuthentication( + req: express.Request, + securityName: string, + scopes?: string[] +): Promise { + return new Promise((resolve, reject) => { + switch (securityName) { + case 'jwt': + validateAccessToken(req) + .then((user) => { + req.user = user; + if (!scopes || scopes.length === 0) + return resolve(user); + // validate the role + const roleExist = scopes.some((role) => + req.user.roles.includes(role) + ); + if (!roleExist) + reject( + new AppError( + 403, + 'You are not allowed to do this action' + ) + ); + return resolve(user); + }) + .catch((err) => { + // check if the token is expired + if (err.name === 'TokenExpiredError') { + return reject( + new AppError(401, 'Your token is expired') + ); + } + if (err.name === 'JsonWebTokenError') { + return reject(new AppError(401, err.message)); + } + return reject(err); + }); + break; + default: + return reject( + new AppError( + 401, + 'Unknown security type, Please contact the admin' + ) + ); + } + }); +} + +async function validateAccessToken(req: express.Request): Promise { + // bearer token cookie and header + const accessToken = + req.header('access_token') || + req.cookies?.access_token || + req.header('authorization')?.replace('Bearer ', ''); + if (!accessToken) + throw new AppError( + 401, + 'Access Token is required, Please login to continue' + ); + + const accessTokenPayload = await AuthUtils.verifyAccessToken(accessToken); + + if (!accessTokenPayload || !accessTokenPayload._id) + throw new AppError(401, 'Invalid access token'); + + // 3) check if the user is exist (not deleted) + const user: IUser = await User.findById(accessTokenPayload._id).select( + 'accessRestricted active roles authorities restrictions name email' + ); + if (!user) { + throw new AppError(401, 'This user is no longer exist'); + } + + // Check if the account is banned + if (user?.accessRestricted) + throw new AppError( + 403, + 'Your account has been banned. Please contact the admin for more information.' + ); + + // check if account is active + if (!user.active) + throw new AppError( + 403, + 'Your account is not active. Please activate your account to continue.' + ); + + return user; +} diff --git a/backend-app/middlewares/authorization.ts b/backend-app/middlewares/authorization.ts deleted file mode 100644 index 1fbb403..0000000 --- a/backend-app/middlewares/authorization.ts +++ /dev/null @@ -1,40 +0,0 @@ -import AppError from '@utils/app_error'; -import { INext, IReq, IRes } from '@interfaces/vendors'; -import User from '@models/user/user_model'; - -const restrictTo = - (...actions: string[]) => - async (req: IReq, res: IRes, next: INext): Promise => { - try { - // get the user by id - const user = await User.findById(req.user._id); - if (!user) - throw new AppError( - 401, - 'The user belonging to this token does no longer exist' - ); - if (user.isAuthorizedTo(actions)) { - if (!user.isRestrictedFrom(actions)) { - next(); - } else { - next( - new AppError( - 403, - 'You are restricted from performing this action, contact the admin for more information' - ) - ); - } - } else { - next( - new AppError( - 403, - 'You do not have permission to perform this action ; required permissions: ' + - actions - ) - ); - } - } catch (error) { - next(error); - } - }; -export default restrictTo; diff --git a/backend-app/middlewares/global_error_handler.ts b/backend-app/middlewares/global_error_handler.ts index 1321df7..a964f5b 100644 --- a/backend-app/middlewares/global_error_handler.ts +++ b/backend-app/middlewares/global_error_handler.ts @@ -47,8 +47,7 @@ const errorHandler = ( message = (err as any).message; } else { message = - "We're sorry, something went wrong. Please try again later." + - err; + "It's not you. It's us. We are having some problems. Please try again later."; } } else { message = (err as any).message; @@ -78,8 +77,6 @@ const errorHandler = ( }, message, }; - // logger.debug((err as any).stack); - // Send the response res.status((err as any).statusCode).json(response); }; diff --git a/backend-app/models/notifications/notification_model.ts b/backend-app/models/notifications/notification_model.ts new file mode 100644 index 0000000..22e1bca --- /dev/null +++ b/backend-app/models/notifications/notification_model.ts @@ -0,0 +1,118 @@ +import mongoose, { Document, Model, Schema } from 'mongoose'; +import validator from 'validator'; +import metaData from '@constants/meta_data'; + +enum SendThrough { + SMS = "sms", + Gmail = "gmail", + Notification = "notification" +} +enum CategoryType{ + Message = "message", + Alert = "alert", + Reminder = "reminder", + Other = "other" +} + +interface INotification extends Document { + id: string; + sentDate: Date; + receivedDate: Date; + seen: boolean; + content: string; + category: CategoryType; + sender: string | mongoose.Types.ObjectId; + receiver: mongoose.Types.ObjectId; + sendThrough: SendThrough; +} + +const notificationSchema: Schema = new mongoose.Schema( + { + sentDate: { + type: Date, + default: Date.now(), + }, + receivedDate: { + type: Date, + }, + seen: { + type: Boolean, + default: false, + }, + content: { + type: String, + required: [true, 'Notification must have a content'], + }, + category: { + type: String, + enum: [ + CategoryType.Message, + CategoryType.Reminder, + CategoryType.Alert, + CategoryType.Other, + ], + default: CategoryType.Alert, + validate: { + validator: function (value: string) { + return validator.isIn(value, [ + CategoryType.Message, + CategoryType.Reminder, + CategoryType.Alert, + CategoryType.Other, + ]); + }, + message: 'Invalid Category value', + }, + }, + sender: { + type: String, + required: [ + true, + 'The sender is required & please contact the admin to fix this issue', + ], + ref: 'User', + }, + receiver: { + type: Schema.Types.ObjectId, + required: [true, 'The receiver cannot be empty'], + ref: 'User', + }, + sendThrough: { + type: String, + enum: [ + SendThrough.SMS, + SendThrough.Gmail, + SendThrough.Notification, + ], + default: SendThrough.Notification, + validate: { + validator: function (value: string) { + return validator.isIn(value, [ + SendThrough.SMS, + SendThrough.Gmail, + SendThrough.Notification, + ]); + }, + message: 'Invalid sendThrough value', + }, + }, + }, + { + timestamps: true, + } +); + +// add meta data to the schema +metaData.apply(notificationSchema) + +notificationSchema.pre('find', function () { + this.where({ deleted: false }); +}); + +notificationSchema.pre('findOne', function () { + this.where({ deleted: false }); +}); + +const Notification: Model = mongoose.model("Notification", notificationSchema); +export default Notification; + diff --git a/backend-app/nodemon.json b/backend-app/nodemon.json deleted file mode 100644 index 3fa2346..0000000 --- a/backend-app/nodemon.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "watch": ["."], - "ignore": ["swagger.json", "routes/routes.ts"], - "ext": "js ts json", - "exec": "npm run generate && ts-node server.ts" -} diff --git a/backend-app/package-lock.json b/backend-app/package-lock.json index 5348c68..b31e0a3 100644 --- a/backend-app/package-lock.json +++ b/backend-app/package-lock.json @@ -34,6 +34,7 @@ "mocha": "10.2.0", "mongoose": "7.5.0", "morgan": "1.10.0", + "nodemon": "3.0.2", "prettier": "3.0.3", "qs": "6.11.2", "supertest": "6.3.3", @@ -62,10 +63,12 @@ "@types/validator": "13.11.1", "@typescript-eslint/parser": "6.7.2", "chai": "4.3.10", + "concurrently": "8.2.2", "eslint": "8.48.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-prettier": "5.0.0", "lint-staged": "14.0.1", + "osascript": "1.2.0", "prettier": "3.0.3", "ts-mocha": "10.0.0", "ts-node": "^10.4.0", @@ -83,6 +86,18 @@ "node": ">=0.10.0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2072,6 +2087,91 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -2173,6 +2273,22 @@ "node": ">= 8" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2516,6 +2632,12 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/duplex-child-process": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/duplex-child-process/-/duplex-child-process-0.0.5.tgz", + "integrity": "sha512-3WVvFnyEYmFYXi2VB9z9XG8y4MbCMEPYrSGYROY3Pp7TT5qsyrdv+rZS6ydjQvTegHMc00pbrl4V/OOwrzo1KQ==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3475,9 +3597,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -3908,6 +4033,11 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5641,6 +5771,73 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/nodemon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", + "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -5849,6 +6046,15 @@ "node": ">=0.10.0" } }, + "node_modules/osascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/osascript/-/osascript-1.2.0.tgz", + "integrity": "sha512-PSSeitQlvnz8DslZ/sQCn35GHHhdbSaTWtFINdKeiFbso0xtHeEOtA6X60JrsRj1JQ6iG63ghMv/jWH4moTcUw==", + "dev": true, + "dependencies": { + "duplex-child-process": "0.0.5" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6048,6 +6254,11 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -6155,6 +6366,12 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -6273,6 +6490,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", @@ -6446,6 +6672,15 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -6477,6 +6712,17 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6576,6 +6822,12 @@ "memory-pager": "^1.0.2" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6996,6 +7248,31 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/touch/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -7007,6 +7284,15 @@ "node": ">=12" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -7366,6 +7652,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/backend-app/package.json b/backend-app/package.json index cdfb417..83b2a1b 100644 --- a/backend-app/package.json +++ b/backend-app/package.json @@ -5,9 +5,9 @@ "main": "app.ts", "scripts": { "debug": "ndb server.ts", - "start": "nodemon", - "start:build": "node build/server.js", - "test": "ts-mocha --bail", + "dev": "concurrently -p name -n SWO,TSOA \"nodemon --config config/nodemon.json\" \" nodemon --config config/nodemon.tsoa.json\"", + "start": "node build/server.js", + "test": "tsoa spec-and-routes && tsoa swagger && ts-mocha --bail", "test:build": "cd ./build && mocha --bail", "clean": "tsc --build --clean", "build": "tsc", @@ -52,6 +52,7 @@ "xss-clean": "^0.1.4" }, "devDependencies": { + "nodemon": "3.0.2", "@inquirer/prompts": "3.1.1", "@types/bcrypt": "5.0.0", "@types/chai": "4.3.6", @@ -70,10 +71,12 @@ "@types/validator": "13.11.1", "@typescript-eslint/parser": "6.7.2", "chai": "4.3.10", + "concurrently": "8.2.2", "eslint": "8.48.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-prettier": "5.0.0", "lint-staged": "14.0.1", + "osascript": "1.2.0", "prettier": "3.0.3", "ts-mocha": "10.0.0", "ts-node": "^10.4.0", diff --git a/backend-app/routes/auth_routes.ts b/backend-app/routes/auth_routes.ts deleted file mode 100644 index 3807dac..0000000 --- a/backend-app/routes/auth_routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as authController from '@controllers/auth_controllers/auth_controller'; -import * as password_management from '@controllers/auth_controllers/password_management'; -import express, { Router } from 'express'; - -const router = express.Router(); - -router.post('/signup', authController.signup); -router.post('/login', authController.login); -router.delete('/logout', authController.logout); -router.get('/refreshToken', authController.tokenRefresh); -router.get('/activate', authController.activateAccount); -router.patch('/forgotPassword', password_management.forgotPassword); -router.patch('/updateMyPassword', password_management.updatePassword); -router.get('/github/callback', authController.githubHandler); - -const authRoutes = (mainrouter: Router) => { - mainrouter.use('/auth', router); -}; - -export default authRoutes; diff --git a/backend-app/routes/calendar_routes.ts b/backend-app/routes/calendar_routes.ts deleted file mode 100644 index cb6dc99..0000000 --- a/backend-app/routes/calendar_routes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from 'express'; -import * as base from '@controllers/base_controller'; -import { restrictTo } from '@controllers/auth_controllers/auth_controller'; -import Calendar from '@models/calendar/calendar_model'; -const router = Router(); - -router.post('/', base.createOne(Calendar)); -router.get('/:id', base.getOne(Calendar)); - -router.patch('/:id', base.updateOne(Calendar)); -router.delete('/:id', base.deleteOne(Calendar)); - -router.use(restrictTo('ADMIN', 'SUPER_ADMIN')); - -router.get('/', base.getAll(Calendar)); - -export default router; diff --git a/backend-app/routes/github_routes.ts b/backend-app/routes/github_routes.ts deleted file mode 100644 index 2235a2a..0000000 --- a/backend-app/routes/github_routes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from 'express'; -const router = Router(); -import * as githubController from '@controllers/auth_controllers/github_controller'; - -router.get('/recent-repo', githubController.getRecentRepo); - -/** - * Registers the GitHub routes with the main router and adds them to the Swagger documentation. - * @function - * @name githubRoutes - * @param {Object} mainrouter - Express router object. - */ -const githubRoutes = (mainrouter: Router) => { - mainrouter.use('/github', router); -}; - -export default githubRoutes; diff --git a/backend-app/routes/index.ts b/backend-app/routes/index.ts deleted file mode 100644 index 327bd29..0000000 --- a/backend-app/routes/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Router } from 'express'; -import userRoutes from './users/user_route'; -import adminRoutes from './users/admin_route'; -import superAdminRoutes from './users/super_admin_route'; -import authRoutes from './auth_routes'; -import githubRoutes from './github_routes'; -import { protect } from '@controllers/auth_controllers/auth_controller'; - -const router = Router(); - -// public routes -authRoutes(router); - -router.use(protect); - -// protected routes -userRoutes(router); -adminRoutes(router); -superAdminRoutes(router); -githubRoutes(router); - -export default router; diff --git a/backend-app/routes/routes.ts b/backend-app/routes/routes.ts deleted file mode 100644 index 5ac0183..0000000 --- a/backend-app/routes/routes.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { - Controller, - ValidationService, - FieldErrors, - ValidateError, - TsoaRoute, - HttpStatusCodeLiteral, - TsoaResponse, - fetchMiddlewares, -} from '@tsoa/runtime'; -// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { UsersController } from './../controllers/tryingtsoa'; -import type { RequestHandler, Router } from 'express'; - -// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - -const models: TsoaRoute.Models = { - ValidateErrorJSON: { - dataType: 'refObject', - properties: { - message: { - dataType: 'enum', - enums: ['Validation failed'], - required: true, - }, - details: { - dataType: 'nestedObjectLiteral', - nestedProperties: {}, - additionalProperties: { dataType: 'any' }, - required: true, - }, - }, - additionalProperties: false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -}; -const validationService = new ValidationService(models); - -// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - -export function RegisterRoutes(app: Router) { - // ########################################################################################################### - // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look - // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa - // ########################################################################################################### - app.post( - '/users/:userId', - ...fetchMiddlewares(UsersController), - ...fetchMiddlewares(UsersController.prototype.getUser), - - function UsersController_getUser( - request: any, - response: any, - next: any - ) { - const args = { - userId: { - in: 'path', - name: 'userId', - required: true, - dataType: 'double', - }, - name: { in: 'query', name: 'name', dataType: 'string' }, - body: { - in: 'body', - name: 'body', - dataType: 'nestedObjectLiteral', - nestedProperties: { - name: { dataType: 'string', required: true }, - }, - }, - }; - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = getValidatedArgs(args, request, response); - - const controller = new UsersController(); - - const promise = controller.getUser.apply( - controller, - validatedArgs as any - ); - promiseHandler(controller, promise, response, undefined, next); - } catch (err) { - return next(err); - } - } - ); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - function isController(object: any): object is Controller { - return ( - 'getHeaders' in object && - 'getStatus' in object && - 'setStatus' in object - ); - } - - function promiseHandler( - controllerObj: any, - promise: any, - response: any, - successStatus: any, - next: any - ) { - return Promise.resolve(promise) - .then((data: any) => { - let statusCode = successStatus; - let headers; - if (isController(controllerObj)) { - headers = controllerObj.getHeaders(); - statusCode = controllerObj.getStatus() || statusCode; - } - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - returnHandler(response, statusCode, data, headers); - }) - .catch((error: any) => next(error)); - } - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - function returnHandler( - response: any, - statusCode?: number, - data?: any, - headers: any = {} - ) { - if (response.headersSent) { - return; - } - Object.keys(headers).forEach((name: string) => { - response.set(name, headers[name]); - }); - if ( - data && - typeof data.pipe === 'function' && - data.readable && - typeof data._read === 'function' - ) { - response.status(statusCode || 200); - data.pipe(response); - } else if (data !== null && data !== undefined) { - response.status(statusCode || 200).json(data); - } else { - response.status(statusCode || 204).end(); - } - } - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - function responder( - response: any - ): TsoaResponse { - return function (status, data, headers) { - returnHandler(response, status, data, headers); - }; - } - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - function getValidatedArgs(args: any, request: any, response: any): any[] { - const fieldErrors: FieldErrors = {}; - const values = Object.keys(args).map((key) => { - const name = args[key].name; - switch (args[key].in) { - case 'request': - return request; - case 'query': - return validationService.ValidateParam( - args[key], - request.query[name], - name, - fieldErrors, - undefined, - { noImplicitAdditionalProperties: 'throw-on-extras' } - ); - case 'queries': - return validationService.ValidateParam( - args[key], - request.query, - name, - fieldErrors, - undefined, - { noImplicitAdditionalProperties: 'throw-on-extras' } - ); - case 'path': - return validationService.ValidateParam( - args[key], - request.params[name], - name, - fieldErrors, - undefined, - { noImplicitAdditionalProperties: 'throw-on-extras' } - ); - case 'header': - return validationService.ValidateParam( - args[key], - request.header(name), - name, - fieldErrors, - undefined, - { noImplicitAdditionalProperties: 'throw-on-extras' } - ); - case 'body': - return validationService.ValidateParam( - args[key], - request.body, - name, - fieldErrors, - undefined, - { noImplicitAdditionalProperties: 'throw-on-extras' } - ); - case 'body-prop': - return validationService.ValidateParam( - args[key], - request.body[name], - name, - fieldErrors, - 'body.', - { noImplicitAdditionalProperties: 'throw-on-extras' } - ); - case 'formData': - if (args[key].dataType === 'file') { - return validationService.ValidateParam( - args[key], - request.file, - name, - fieldErrors, - undefined, - { - noImplicitAdditionalProperties: - 'throw-on-extras', - } - ); - } else if ( - args[key].dataType === 'array' && - args[key].array.dataType === 'file' - ) { - return validationService.ValidateParam( - args[key], - request.files, - name, - fieldErrors, - undefined, - { - noImplicitAdditionalProperties: - 'throw-on-extras', - } - ); - } else { - return validationService.ValidateParam( - args[key], - request.body[name], - name, - fieldErrors, - undefined, - { - noImplicitAdditionalProperties: - 'throw-on-extras', - } - ); - } - case 'res': - return responder(response); - } - }); - - if (Object.keys(fieldErrors).length > 0) { - throw new ValidateError(fieldErrors, ''); - } - return values; - } - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -} - -// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/backend-app/routes/users/admin_route.ts b/backend-app/routes/users/admin_route.ts index 93ad441..5f3896f 100644 --- a/backend-app/routes/users/admin_route.ts +++ b/backend-app/routes/users/admin_route.ts @@ -1,16 +1,15 @@ import { Router } from 'express'; -import { - authorizeOrRestrict, - banUser, - unbanUser, - createRole, - updateRole, - getRole, - getRoles, - deleteRole, - assignRoleToUser, - removeRoleFromUser, -} from '@controllers/users_controllers/admin_controller'; +import // authorizeOrRestrict, +// banUser, +// unbanUser, +// createRole, +// updateRole, +// getRole, +// getRoles, +// deleteRole, +// assignRoleToUser, +// removeRoleFromUser, +'@controllers/users_controllers/admin_controller'; import * as authController from '@controllers/auth_controllers/auth_controller'; import restrictTo from '@middlewares/authorization'; import Actions from '@constants/actions'; @@ -42,8 +41,8 @@ router.get('/users', userController.getAllUsers); */ router.put( '/authorize-or-restrict/:userId', - restrictTo(Actions.UPDATE_USER), - authorizeOrRestrict + restrictTo(Actions.UPDATE_USER) + // authorizeOrRestrict ); /** @@ -55,8 +54,8 @@ router.put( **/ router.put( '/ban-user/:userId', - restrictTo(Actions.UPDATE_USER, Actions.BAN_USER), - banUser + restrictTo(Actions.UPDATE_USER, Actions.BAN_USER) + // banUser ); /** @@ -68,8 +67,8 @@ router.put( **/ router.put( '/unban-user/:userId', - restrictTo(Actions.UPDATE_USER, Actions.BAN_USER), - unbanUser + restrictTo(Actions.UPDATE_USER, Actions.BAN_USER) + // unbanUser ); /** @@ -78,7 +77,11 @@ router.put( * @description Get all roles * @access Super Admin **/ -router.put('/role', restrictTo(Actions.MANAGE_ROLES), getRoles); +router.put( + '/role', + restrictTo(Actions.MANAGE_ROLES) + // getRoles +); /** * @protected @@ -87,7 +90,11 @@ router.put('/role', restrictTo(Actions.MANAGE_ROLES), getRoles); * @access Super Admin * @param {string} name - Name of the role to find **/ -router.put('/role/:name', restrictTo(Actions.MANAGE_ROLES), getRole); +router.put( + '/role/:name', + restrictTo(Actions.MANAGE_ROLES) + // getRole +); /** * @protected @@ -95,7 +102,11 @@ router.put('/role/:name', restrictTo(Actions.MANAGE_ROLES), getRole); * @description Create a role * @access Super Admin **/ -router.post('/role', restrictTo(Actions.MANAGE_ROLES), createRole); +router.post( + '/role', + restrictTo(Actions.MANAGE_ROLES) + // createRole +); /** * @protected @@ -104,7 +115,11 @@ router.post('/role', restrictTo(Actions.MANAGE_ROLES), createRole); * @access Super Admin * @param {string} name - Name of the role to update **/ -router.put('/role/:name', restrictTo(Actions.MANAGE_ROLES), updateRole); +router.put( + '/role/:name', + restrictTo(Actions.MANAGE_ROLES) + // updateRole +); /** * @protected @@ -113,7 +128,11 @@ router.put('/role/:name', restrictTo(Actions.MANAGE_ROLES), updateRole); * @access Super Admin * @param {string} name - Name of the role to delete **/ -router.delete('/role/:name', restrictTo(Actions.MANAGE_ROLES), deleteRole); +router.delete( + '/role/:name', + restrictTo(Actions.MANAGE_ROLES) + // deleteRole +); /** * @protected @@ -125,8 +144,8 @@ router.delete('/role/:name', restrictTo(Actions.MANAGE_ROLES), deleteRole); * */ router.put( '/assign-role/:name/:userId', - restrictTo(Actions.MANAGE_ROLES), - assignRoleToUser + restrictTo(Actions.MANAGE_ROLES) + // assignRoleToUser ); /** * @protected @@ -138,8 +157,8 @@ router.put( * */ router.put( '/remove-role/:name/:userId', - restrictTo(Actions.MANAGE_ROLES), - removeRoleFromUser + restrictTo(Actions.MANAGE_ROLES) + // removeRoleFromUser ); const adminRoutes = (mainrouter: Router) => { diff --git a/backend-app/routes/users/super_admin_route.ts b/backend-app/routes/users/super_admin_route.ts index 3edcca7..805e4a1 100644 --- a/backend-app/routes/users/super_admin_route.ts +++ b/backend-app/routes/users/super_admin_route.ts @@ -3,12 +3,11 @@ import { Router } from 'express'; import * as authController from '@controllers/auth_controllers/auth_controller'; import restrictTo from '@middlewares/authorization'; import Actions from '@constants/actions'; -import { - addSuperAdmin, - removeSuperAdmin, - addAdmin, - removeAdmin, -} from '@controllers/users_controllers/admin_controller'; +import // addSuperAdmin, +// removeSuperAdmin, +// addAdmin, +// removeAdmin, +'@controllers/users_controllers/admin_controller'; const router = Router(); @@ -23,8 +22,8 @@ router.use(authController.restrictTo('SUPER_ADMIN')); */ router.put( '/add-super-admin/:userId', - restrictTo(Actions.UPDATE_USER), - addSuperAdmin + restrictTo(Actions.UPDATE_USER) + // addSuperAdmin ); /* @@ -36,8 +35,8 @@ router.put( **/ router.put( '/remove-super-admin/:userId', - restrictTo(Actions.UPDATE_USER, Actions.REMOVE_SUPER_ADMIN), - removeSuperAdmin + restrictTo(Actions.UPDATE_USER, Actions.REMOVE_SUPER_ADMIN) + // removeSuperAdmin ); /** @@ -47,7 +46,7 @@ router.put( * @access Super Admin * @param {string} userId - Id of the user to add admin role to */ -router.put('/add-admin/:userId', restrictTo(Actions.UPDATE_USER), addAdmin); +// router.put('/add-admin/:userId', restrictTo(Actions.UPDATE_USER), addAdmin); /** * @protected @@ -58,8 +57,8 @@ router.put('/add-admin/:userId', restrictTo(Actions.UPDATE_USER), addAdmin); */ router.put( '/remove-admin/:userId', - restrictTo(Actions.UPDATE_USER), - removeAdmin + restrictTo(Actions.UPDATE_USER) + // removeAdmin ); const superAdminRoutes = (mainrouter: express.Router) => { diff --git a/backend-app/routes/users/user_route.ts b/backend-app/routes/users/user_route.ts deleted file mode 100644 index a056b42..0000000 --- a/backend-app/routes/users/user_route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Router } from 'express'; -import * as userController from '@controllers/users_controllers/user_controller'; - -const router = Router(); -router - .route('/me') - /** - * @swagger - * /api/useron: - * get: - * summary: Get a list of users - * description: Retrieve a list of users from the database. - * requestBody: - * description: Optional description in *Markdown* - * required: false - * responses: - * 200: - * description: A list of users. - * content: - * application/json: - * example: - * - id: 1 - * name: John Doe - * - id: 2 - * name: Jane Smith - * x-swagger-jsdoc: - * tryItOutEnabled: true // Enable "Try it out" by default - */ - .get(userController.getMe) - .delete(userController.deleteMe) - .patch(userController.updateMe); - -const userRoutes = (mainrouter: Router) => { - mainrouter.use('/users', router); -}; - -export default userRoutes; diff --git a/backend-app/server.ts b/backend-app/server.ts index c88549e..e262711 100644 --- a/backend-app/server.ts +++ b/backend-app/server.ts @@ -4,6 +4,7 @@ import logger from '@utils/logger'; import fs from 'fs'; import { DATABASE, PORT } from './config/app_config'; import createRoles from './utils/authorization/roles/create_roles'; +import createDefaultUser from './utils/create_default_user'; process.on('uncaughtException', (err) => { logger.error('UNCAUGHT EXCEPTION!!! shutting down ...'); @@ -18,6 +19,7 @@ mongoose.set('strictQuery', true); let expServer: Promise; // Connect the database +logger.info('Connecting to DB ...'); mongoose .connect( DATABASE as string, @@ -26,11 +28,14 @@ mongoose .then(() => { logger.info('DB Connected Successfully!'); expServer = startServer(); - logger.info(`Swagger Will Be Available at /docs /docs-json`); + logger.info(`API Docs Avaiable at /docs , /docs-json`); }) .catch((err: Error) => { logger.error( - 'DB Connection Failed! \n\tException : ' + err + '\n' + err.stack + 'DB Connection Failed! \n\tException : ' + + err.name + + ' : ' + + err.message ); }); @@ -43,14 +48,15 @@ mongoose.connection.on('disconnected', () => { const startServer = async (): Promise => { if (!fs.existsSync('.env')) logger.warn('.env file not found, using .env.example file'); - logger.info(`App running on http://localhost:${PORT}`); + logger.info(`App running on :`); + logger.info(` ----------------------------`); + logger.info(`| http://localhost:${PORT}/docs |`); + logger.info(` ----------------------------`); await createRoles(); + createDefaultUser(); return app.listen(PORT); }; -import createDefaultUser from './utils/create_default_user'; -createDefaultUser(); - process.on('unhandledRejection', (err: Error) => { logger.error('UNHANDLED REJECTION!!! shutting down ...'); logger.error(`${err.name}, ${err.message}, ${err.stack}`); @@ -68,6 +74,7 @@ process.on('SIGTERM', () => { logger.info('💥 Process terminated!'); process.exit(0); }); + process.exit(1); }); process.on('SIGINT', () => { @@ -76,4 +83,5 @@ process.on('SIGINT', () => { logger.info('💥 Process terminated!'); process.exit(0); }); + process.exit(1); }); diff --git a/backend-app/tests/e2e/auth/auth.spec.ts b/backend-app/tests/e2e/auth/auth.spec.ts index 4795396..07dcff8 100644 --- a/backend-app/tests/e2e/auth/auth.spec.ts +++ b/backend-app/tests/e2e/auth/auth.spec.ts @@ -109,7 +109,7 @@ describe('Auth API', () => { expect(res.status).to.equal(400); expect(res.body).to.have.property( 'message', - 'Invalid email or password' + 'Please provide email and password' ); }); @@ -134,7 +134,7 @@ describe('Auth API', () => { expect(res.status).to.equal(400); expect(res.body).to.have.property( 'message', - 'Please provide a password' + 'Please provide email and password' ); }); @@ -147,7 +147,7 @@ describe('Auth API', () => { expect(res.status).to.equal(401); expect(res.body).to.have.property( 'message', - 'Email or Password is wrong' + 'Invalid email or password' ); }); }); @@ -298,7 +298,6 @@ describe('Auth API', () => { .set('Cookie', `refresh_token=${refreshToken}; HttpOnly`); expect(res.status).to.equal(204); - expect(res.header['set-cookie']).to.not.include('access_token'); }); }); @@ -308,7 +307,7 @@ describe('Auth API', () => { const accessToken = generateAccessToken(user._id.toString()); res = await agent - .delete('/api/auth/logout') + .get('/api/auth/logout') .set('Cookie', `access_token=${accessToken}`); expect(res.status).to.equal(204); @@ -323,7 +322,7 @@ describe('Auth API', () => { }); it('should return an error if access token is not provided', async () => { - res = await agent.delete('/api/auth/logout'); + res = await agent.get('/api/auth/logout'); expect(res.status).to.equal(400); expect(res.body).to.have.property( @@ -366,13 +365,12 @@ describe('User API', () => { .set('Cookie', `access_token=${accessToken}`); expect(res.status).to.equal(200); - expect(res.body).to.have.property('user'); - expect(res.body.user).to.have.property( + expect(res.body).to.have.property( '_id', user._id.toString().toString() ); - expect(res.body.user).to.have.property('name', user.name); - expect(res.body.user).to.have.property('email', user.email); + expect(res.body).to.have.property('name', user.name); + expect(res.body).to.have.property('email', user.email); }); }); @@ -389,7 +387,7 @@ describe('User API', () => { expect(res.status).to.equal(400); expect(res.body).to.have.property( 'message', - 'This route is not for password updates. Please use /updateMyPassword' + 'This route is not for password updates. Please use api/password-management/update-password' ); }); @@ -404,7 +402,7 @@ describe('User API', () => { expect(res.status).to.equal(400); expect(res.body).to.have.property( 'message', - 'This route is not for role updates. Please use /updateRole' + 'This route is not for role updates. Please use /update-role' ); }); @@ -432,12 +430,11 @@ describe('User API', () => { }); expect(res.status).to.equal(200); - expect(res.body).to.have.property('doc'); - expect(res.body.doc).to.have.property( + expect(res.body).to.have.property( '_id', user._id.toString().toString() ); - expect(res.body.doc).to.have.property('name', 'Updated Name'); + expect(res.body).to.have.property('name', 'Updated Name'); // Check if the user was updated in the database const updatedUser = await User.findById(user._id.toString()); diff --git a/backend-app/tsoa.json b/backend-app/tsoa.json index 74d7542..c86e112 100644 --- a/backend-app/tsoa.json +++ b/backend-app/tsoa.json @@ -4,10 +4,28 @@ "controllerPathGlobs": ["./controllers/**/*.ts"], "spec": { "outputDirectory": "./docs/api_docs/", - "specVersion": 2, - "specValidation": false + "specVersion": 3, + "specValidation": false, + "securityDefinitions": { + "jwt": { + "type": "apiKey", + "name": "access_token", + "in": "header" + }, + "apiKey": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + } + }, + "tags": [ + { + "name": "GitHub" + } + ] }, "routes": { - "routesDir": "routes" + "routesDir": ".", + "authenticationModule": "./middlewares/authentications.ts" } } diff --git a/backend-app/utils/authorization/auth_utils.ts b/backend-app/utils/authorization/auth_utils.ts index 16f71e3..0fc2ac4 100644 --- a/backend-app/utils/authorization/auth_utils.ts +++ b/backend-app/utils/authorization/auth_utils.ts @@ -22,20 +22,17 @@ class AuthUtils { }); } static setAccessTokenCookie(res: any, accessToken: string): AuthUtils { - res.cookie('access_token', accessToken, { - secure: true, - sameSite: 'strict', - maxAge: ACCESS_TOKEN_COOKIE_EXPIRY_TIME, - }); + res.setHeader( + 'Set-Cookie', + `access_token=${accessToken}; HttpOnly; Secure; SameSite=Strict; Max-Age=${ACCESS_TOKEN_COOKIE_EXPIRY_TIME}` + ); return this; } static setRefreshTokenCookie(res: any, refreshToken: string): AuthUtils { - res.cookie('refresh_token', refreshToken, { - httpOnly: true, - secure: true, - sameSite: 'strict', - maxAge: REFRESH_TOKEN_COOKIE_EXPIRY_TIME, - }); + res.setHeader( + 'Set-Cookie', + `refresh_token=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Max-Age=${REFRESH_TOKEN_COOKIE_EXPIRY_TIME}` + ); return this; } static async verifyAccessToken(token: string): Promise { diff --git a/backend-app/utils/swagger/index.ts b/backend-app/utils/swagger/index.ts index 56c71c4..8bfa2a7 100644 --- a/backend-app/utils/swagger/index.ts +++ b/backend-app/utils/swagger/index.ts @@ -1,7 +1,7 @@ import { CURRENT_ENV } from '@config/app_config'; import { IRes } from '@interfaces/vendors'; +import { NextFunction, Request, Response } from 'express'; import swaggerUi from 'swagger-ui-express'; -import * as swaggerjson from '@root/docs/api_docs/swagger.json'; /** * This function configures the swagger documentation @@ -10,11 +10,25 @@ import * as swaggerjson from '@root/docs/api_docs/swagger.json'; */ const swaggerDocs = (app: any): void => { if (CURRENT_ENV === 'production') return; - app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerjson)); + let swaggerJson: any; + // Import swaggerjson inside setup function + app.use( + '/docs', + swaggerUi.serve, + (req: Request, res: Response, next: NextFunction) => { + delete require.cache[ + require.resolve('@root/docs/api_docs/swagger.json') + ]; + swaggerJson = require('@root/docs/api_docs/swagger.json'); + swaggerUi.setup(swaggerJson); + next(); + }, + swaggerUi.setup(swaggerJson) + ); // Get docs in JSON format app.get('/docs-json', (_: any, res: IRes) => { res.setHeader('Content-Type', 'application/json'); - res.send(swaggerjson); + res.send(require('@root/docs/api_docs/swagger.json')); }); }; diff --git a/docker-compose.yml b/docker-compose.yml index 776947b..8697c22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,16 @@ services: build: ./backend-app ports: - "8000:8000" + depends_on: + - mongodb frontend: build: ./frontend-app ports: - "3000:3000" depends_on: - - backend-api \ No newline at end of file + - backend-api + mongodb: + image: mongo + container_name: mongodb + ports: + - "27017:27017"