From b394a84dac0a1d7317bbe56ebe3558f0664f7783 Mon Sep 17 00:00:00 2001 From: stasel <2033301+stasel@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:52:02 +0200 Subject: [PATCH] WIP: Introduced handlers for backend architecture --- .../controllers/AuthenticationController.ts | 85 ++++------ server/src/controllers/CohortsController.ts | 20 ++- server/src/controllers/DashboardController.ts | 36 +++-- server/src/controllers/GeographyController.ts | 43 +++--- server/src/controllers/SearchController.ts | 78 +++------- .../Trainee/EmploymentHistoryController.ts | 109 ++++--------- .../Trainee/InteractionController.ts | 124 +++++---------- .../controllers/Trainee/LetterController.ts | 55 +++---- .../Trainee/ProfilePictureController.ts | 145 ++++++------------ .../controllers/Trainee/StrikeController.ts | 111 ++++---------- .../src/controllers/Trainee/TestController.ts | 110 ++++--------- .../controllers/Trainee/TraineeController.ts | 96 ++++-------- server/src/controllers/UserController.ts | 109 +++---------- server/src/errors/HttpError.ts | 40 +++++ server/src/errors/index.ts | 1 + server/src/handlers/AuthenticationHandler.ts | 52 +++++++ server/src/handlers/CohortsHandler.ts | 25 +++ server/src/handlers/DashboardHandler.ts | 24 +++ .../src/handlers/EmploymentHistoryHandler.ts | 64 ++++++++ server/src/handlers/GeographyHandler.ts | 38 +++++ server/src/handlers/Handler.ts | 18 +++ server/src/handlers/InteractionHandler.ts | 71 +++++++++ server/src/handlers/LetterHandler.ts | 30 ++++ server/src/handlers/ProfilePictureHandler.ts | 56 +++++++ server/src/handlers/SearchHandler.ts | 39 +++++ server/src/handlers/StrikeHandler.ts | 69 +++++++++ server/src/handlers/TestHandler.ts | 61 ++++++++ server/src/handlers/TraineeHandler.ts | 52 +++++++ server/src/handlers/UserHandler.ts | 66 ++++++++ server/src/handlers/index.ts | 14 ++ server/src/index.ts | 85 ++++++---- server/src/middlewares/AuthMiddleware.ts | 2 +- server/src/routes/AuthenticationRouter.ts | 18 +-- server/src/routes/CohortsRouter.ts | 10 +- server/src/routes/DashboardRouter.ts | 10 +- server/src/routes/GeographyRouter.ts | 12 +- server/src/routes/SearchRouter.ts | 10 +- server/src/routes/TraineesRouter.ts | 77 +++++----- server/src/routes/UserRouter.ts | 19 +-- 39 files changed, 1209 insertions(+), 875 deletions(-) create mode 100644 server/src/errors/HttpError.ts create mode 100644 server/src/errors/index.ts create mode 100644 server/src/handlers/AuthenticationHandler.ts create mode 100644 server/src/handlers/CohortsHandler.ts create mode 100644 server/src/handlers/DashboardHandler.ts create mode 100644 server/src/handlers/EmploymentHistoryHandler.ts create mode 100644 server/src/handlers/GeographyHandler.ts create mode 100644 server/src/handlers/Handler.ts create mode 100644 server/src/handlers/InteractionHandler.ts create mode 100644 server/src/handlers/LetterHandler.ts create mode 100644 server/src/handlers/ProfilePictureHandler.ts create mode 100644 server/src/handlers/SearchHandler.ts create mode 100644 server/src/handlers/StrikeHandler.ts create mode 100644 server/src/handlers/TestHandler.ts create mode 100644 server/src/handlers/TraineeHandler.ts create mode 100644 server/src/handlers/UserHandler.ts create mode 100644 server/src/handlers/index.ts diff --git a/server/src/controllers/AuthenticationController.ts b/server/src/controllers/AuthenticationController.ts index 3e0f6456..1abc1781 100644 --- a/server/src/controllers/AuthenticationController.ts +++ b/server/src/controllers/AuthenticationController.ts @@ -1,64 +1,57 @@ -import { Request, Response } from 'express'; import { GoogleOAuthServiceType, GoogleOAuthUserInfo, TokenServiceType } from '../services'; import { UserRepository } from '../repositories'; -import { ResponseError, AuthenticatedUser } from '../models'; +import { User } from '../models'; +import { BadRequestError, ForbiddenError, UnauthorizedError } from '../errors'; import * as Sentry from '@sentry/node'; -export interface AuthenticationControllerType { - login(req: Request, res: Response): Promise; - logout(req: Request, res: Response): Promise; - getSession(req: Request, res: Response): Promise; +export interface LoginParams { + authCode: string; + redirectURI: string; } -export const TOKEN_COOKIE_NAME = 'dojo_token'; +export interface LoginResult { + user: User; + token: string; +} + +export interface AuthenticationControllerType { + login(params: LoginParams): Promise; +} /** - * A class provides methods for handling user authentication, - * including login, logout, and session retrieval. - * - * @class - * */ + * Handles user authentication: verifies Google OAuth credentials, ensures the + * user is authorized to access the system, and issues a JWT for the session. + */ export class AuthenticationController implements AuthenticationControllerType { private readonly userRepository: UserRepository; private readonly googleOAuthService: GoogleOAuthServiceType; private readonly tokenService: TokenServiceType; - private readonly tokenExpirationInDays: number; constructor( userRepository: UserRepository, googleOAuthService: GoogleOAuthServiceType, - tokenService: TokenServiceType, - tokenExpirationInDays: number + tokenService: TokenServiceType ) { this.userRepository = userRepository; this.googleOAuthService = googleOAuthService; this.tokenService = tokenService; - this.tokenExpirationInDays = tokenExpirationInDays; } - async login(req: Request, res: Response) { - const authCode = req.body.authCode?.trim(); - const redirectURI = req.body.redirectURI?.trim(); + async login({ authCode, redirectURI }: LoginParams): Promise { if (!authCode || !redirectURI) { - res.status(400).json(new ResponseError('Code and redirectURI are required')); - return; + throw new BadRequestError('Code and redirectURI are required'); } if (!this.isValidRedirectURI(redirectURI)) { - res.status(400).json(new ResponseError('Invalid redirectURI')); - return; + throw new BadRequestError('Invalid redirectURI'); } - // Verify user identity against Google OAuth service let oauthUser: GoogleOAuthUserInfo; try { - // 1. Exchange authentication code for access token let googleAccessToken = await this.googleOAuthService.exchangeAuthCodeForToken(authCode, redirectURI); - - // 2. Get user info from Google oauthUser = await this.googleOAuthService.getUserInfo(googleAccessToken); - // 3. Revoke Google OAuth token - we don't use it anymore. + // Revoke Google OAuth token - we don't use it anymore. // Do not wait for the response. Continue with login process. this.googleOAuthService.revokeToken(googleAccessToken); googleAccessToken = ''; @@ -67,52 +60,28 @@ export class AuthenticationController implements AuthenticationControllerType { throw new Error('Could not verify google auth code'); } } catch (error) { - res.status(401).json(new ResponseError('Could not verify user')); Sentry.captureException(error); - return; + throw new UnauthorizedError('Could not verify user'); } - // Check if the user is allowed to access to the system const user = await this.userRepository.findUserByEmail(oauthUser.email); if (!user || !user.isActive) { - res.status(403).json(new ResponseError('Not allowed to login')); Sentry.setUser({ email: oauthUser.email }); Sentry.captureMessage(`Attempt to login with unauthorized user`, 'warning'); - return; + throw new ForbiddenError('Not allowed to login'); } - // Login Successful! Generate and send JWT token - const jwtToken = this.tokenService.generateAccessToken(user); - - // Set the token in a cookie - const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; - res.cookie(TOKEN_COOKIE_NAME, jwtToken, { - httpOnly: true, - secure: isSecure, - sameSite: 'strict', - maxAge: 1000 * 60 * 60 * 24 * this.tokenExpirationInDays, - }); - - res.status(200).json(user); - } - - async getSession(req: Request, res: Response): Promise { - const user = res.locals.user as AuthenticatedUser; - res.status(200).json(user); + const token = this.tokenService.generateAccessToken(user); + return { user, token }; } - async logout(req: Request, res: Response): Promise { - res.clearCookie(TOKEN_COOKIE_NAME); - res.status(204).end(); - } - - private isValidRedirectURI(redirectURI: string) { + private isValidRedirectURI(redirectURI: string): boolean { const allowedHosts = ['localhost', 'dojo-test.hackyourfuture.net', 'dojo.hackyourfuture.net']; try { const url = new URL(redirectURI); return allowedHosts.includes(url.hostname); - } catch (error) { + } catch { return false; } } diff --git a/server/src/controllers/CohortsController.ts b/server/src/controllers/CohortsController.ts index 583add10..e91e60db 100644 --- a/server/src/controllers/CohortsController.ts +++ b/server/src/controllers/CohortsController.ts @@ -1,8 +1,7 @@ -import { Request, Response } from 'express'; import { TraineesRepository } from '../repositories'; import { LearningStatus, Track, calculateAverageTestScore, Trainee } from '../models'; -interface Cohort { +export interface Cohort { cohort: number | null; trainees: TraineeSummary[]; } @@ -24,8 +23,13 @@ interface TraineeSummary { averageTestScore: number | null; } +export interface GetCohortsParams { + start: number; + end: number; +} + export interface CohortsControllerType { - getCohorts(req: Request, res: Response): Promise; + getCohorts(params: GetCohortsParams): Promise; } export class CohortsController implements CohortsControllerType { @@ -34,13 +38,7 @@ export class CohortsController implements CohortsControllerType { this.traineesRepository = traineesRepository; } - async getCohorts(req: Request, res: Response) { - const MAX_POSSIBLE_COHORT = 999; - let start = Number.parseInt(req.query.start as string) || 0; - let end = Number.parseInt(req.query.end as string) || MAX_POSSIBLE_COHORT; - start = Math.max(start, 0); - end = Math.min(end, 999); - + async getCohorts({ start, end }: GetCohortsParams): Promise { const trainees = await this.traineesRepository.getTraineesByCohort(start, end, true); // Group trainees by cohort @@ -58,7 +56,7 @@ export class CohortsController implements CohortsControllerType { trainees: (trainees ?? []).map(this.getTraineeSummary.bind(this)), }; }); - res.status(200).json(result); + return result; } private getTraineeSummary(trainee: Trainee): TraineeSummary { diff --git a/server/src/controllers/DashboardController.ts b/server/src/controllers/DashboardController.ts index f5c9b0e5..835958fd 100644 --- a/server/src/controllers/DashboardController.ts +++ b/server/src/controllers/DashboardController.ts @@ -1,22 +1,30 @@ -import { Request, Response } from 'express'; +export interface Distribution { + label: string; + value: number; + percent: number; +} + +export interface DashboardData { + demographics: { + genderDistribution: Distribution[]; + countryOfOrigin: Distribution[]; + }; + program: { + graduations: Distribution[]; + employment: Distribution[]; + }; +} export interface DashboardControllerType { - getDashboard(req: Request, res: Response): Promise; + getDashboard(): Promise; } /** - * A class provides methods for handling dashboard operations. - * - * @class - * */ + * Provides aggregated statistics for the dashboard view. + */ export class DashboardController implements DashboardControllerType { - constructor() {} - - /** - * Retrieve the dashboard data. - */ - async getDashboard(req: Request, res: Response) { - const response = { + async getDashboard(): Promise { + return { demographics: { genderDistribution: [ { label: 'Man', value: 214, percent: 57.1 }, @@ -43,7 +51,5 @@ export class DashboardController implements DashboardControllerType { ], }, }; - - res.status(200).json(response); } } diff --git a/server/src/controllers/GeographyController.ts b/server/src/controllers/GeographyController.ts index 88877cf4..7846fb94 100644 --- a/server/src/controllers/GeographyController.ts +++ b/server/src/controllers/GeographyController.ts @@ -1,9 +1,18 @@ -import { Request, Response, NextFunction } from 'express'; import { GeographyRepository } from '../repositories'; +export interface CountryResult { + name: string; + flag: string; +} + +export interface SearchGeographyParams { + query: string; + limit: number | null; +} + export interface GeographyControllerType { - getCountries(req: Request, res: Response, next: NextFunction): Promise; - getCities(req: Request, res: Response, next: NextFunction): Promise; + getCountries(params: SearchGeographyParams): Promise; + getCities(params: SearchGeographyParams): Promise; } export class GeographyController implements GeographyControllerType { @@ -12,29 +21,13 @@ export class GeographyController implements GeographyControllerType { this.geographyRepository = geographyRepository; } - async getCountries(req: Request, res: Response, next: NextFunction): Promise { - const query = (req.query.q as string) ?? ''; - const limit = Number.parseInt(req.query.limit as string) || null; - try { - const countries = await this.geographyRepository.searchCountry(query, limit); - const jsonResponse = countries.map(({ name, flag }) => { - return { name, flag }; - }); - res.json(jsonResponse); - } catch (error) { - next(error); - } + async getCountries({ query, limit }: SearchGeographyParams): Promise { + const countries = await this.geographyRepository.searchCountry(query, limit); + return countries.map(({ name, flag }) => ({ name, flag })); } - async getCities(req: Request, res: Response, next: NextFunction): Promise { - const query = (req.query.q as string) ?? ''; - const limit = Number.parseInt(req.query.limit as string) || null; - try { - const cities = await this.geographyRepository.searchCity(query, limit); - const jsonResponse = cities.map((city) => city.name); - res.json(jsonResponse); - } catch (error) { - next(error); - } + async getCities({ query, limit }: SearchGeographyParams): Promise { + const cities = await this.geographyRepository.searchCity(query, limit); + return cities.map((city) => city.name); } } diff --git a/server/src/controllers/SearchController.ts b/server/src/controllers/SearchController.ts index 8c50b7c5..6c4cd8b5 100644 --- a/server/src/controllers/SearchController.ts +++ b/server/src/controllers/SearchController.ts @@ -1,18 +1,8 @@ -import { NextFunction, Request, Response } from 'express'; import { TraineesRepository } from '../repositories'; import { Trainee } from '../models'; import removeAccents from 'remove-accents'; -interface SearchResponse { - hits: SearchHits; -} - -interface SearchHits { - data: SearchResult[]; - size: number; -} - -interface SearchResult { +export interface SearchResult { id: string; name: string; thumbnail: string | null; @@ -21,8 +11,13 @@ interface SearchResult { searchScore: number; } +export interface SearchParams { + query: string; + limit: number; +} + export interface SearchControllerType { - search(req: Request, res: Response, next: NextFunction): Promise; + search(params: SearchParams): Promise; } export class SearchController implements SearchControllerType { @@ -31,68 +26,39 @@ export class SearchController implements SearchControllerType { this.traineesRepository = traineesRepository; } - async search(req: Request, res: Response, next: NextFunction) { - const maxAllowedLimit = 50; - const inputLimit = Number(req.query.limit) || 20; - const limit = Math.min(inputLimit, maxAllowedLimit); - const searchQuery: string = (req.query.q as string) ?? ''; - - let results: SearchResult[] = []; - try { - results = await this.getSearchResults(searchQuery, limit); - } catch (error) { - next(error); - return; - } - - const response: SearchResponse = { - hits: { - data: results, - size: results.length, - }, - }; - res.status(200).json(response); - } - - private async getSearchResults(searchQuery: string, limit: number): Promise { - // If the search query is too short, return an empty array - if (searchQuery.length < 2) { + async search({ query, limit }: SearchParams): Promise { + if (query.length < 2) { return []; } - const keywords = searchQuery + const keywords = query .split(' ') - .map((query) => query.toLocaleLowerCase().trim()) + .map((q) => q.toLocaleLowerCase().trim()) .map(removeAccents) - .filter((query) => query.length > 0); + .filter((q) => q.length > 0); // Note: This is not efficient to fetch all trainees from the DB and then filter on the server side. // In an ideal scenario we should have a complex query to filter on the DB side. // However, because our dataset is not large, we can get away with this for now. const trainees = await this.traineesRepository.getAllTrainees(); - // Process all trainees, calculate the search score and return the top N results return trainees - .map((trainee) => { - return { - id: trainee.id, - name: `${trainee.displayName}`, - thumbnail: trainee.thumbnailURL ?? null, - cohort: trainee.educationInfo.currentCohort ?? null, - profilePath: trainee.profilePath, - searchScore: this.calculateScore(trainee, keywords), - }; - }) + .map((trainee) => ({ + id: trainee.id, + name: `${trainee.displayName}`, + thumbnail: trainee.thumbnailURL ?? null, + cohort: trainee.educationInfo.currentCohort ?? null, + profilePath: trainee.profilePath, + searchScore: this.calculateScore(trainee, keywords), + })) .filter((trainee) => trainee.searchScore > 0) .sort(this.compareSearchResults) .slice(0, limit); } private calculateScore(trainee: Trainee, keywords: string[]): number { - // Exact match email if (keywords.length === 1 && keywords[0] === trainee.contactInfo.email.toLocaleLowerCase()) { return 1000; } - // Exact match github handle if (keywords.length === 1 && keywords[0] === trainee.contactInfo.githubHandle?.toLocaleLowerCase()) { return 1000; } @@ -101,7 +67,6 @@ export class SearchController implements SearchControllerType { const lastName = removeAccents(trainee.personalInfo.lastName.toLocaleLowerCase()); const preferredName = removeAccents(trainee.personalInfo.preferredName?.toLocaleLowerCase() ?? ''); - // calculate score per keyword const scores = keywords.map((keyword) => { const preferredNameMatchScore = this.getMatchScore(keyword, preferredName) * 3; const firstNameMatchScore = this.getMatchScore(keyword, firstName) * 2; @@ -109,12 +74,10 @@ export class SearchController implements SearchControllerType { return preferredNameMatchScore + firstNameMatchScore + lastNameMatchScore; }); - // All keywords must have a score. If one of the keywords does not match, return 0 if (scores.some((score) => score === 0)) { return 0; } - // Otherwise, return the sum of all scores per keyword return scores.reduce((acc, score) => acc + score); } @@ -132,7 +95,6 @@ export class SearchController implements SearchControllerType { } private compareSearchResults(r1: SearchResult, r2: SearchResult): number { - // Primary sort by search score descending, secondary sort by cohort descending if (r1.searchScore !== r2.searchScore) { return r2.searchScore - r1.searchScore; } diff --git a/server/src/controllers/Trainee/EmploymentHistoryController.ts b/server/src/controllers/Trainee/EmploymentHistoryController.ts index 39f3c227..0dfff92d 100644 --- a/server/src/controllers/Trainee/EmploymentHistoryController.ts +++ b/server/src/controllers/Trainee/EmploymentHistoryController.ts @@ -1,109 +1,60 @@ -import { Request, Response, NextFunction } from 'express'; -import { ResponseError, EmploymentHistory, validateEmploymentHistory } from '../../models'; +import { EmploymentHistory, validateEmploymentHistory } from '../../models'; import { TraineesRepository } from '../../repositories'; +import { BadRequestError, NotFoundError } from '../../errors'; export interface EmploymentHistoryControllerType { - getEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; - addEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; - updateEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; - deleteEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; + getEmploymentHistory(traineeId: string): Promise; + addEmploymentHistory(traineeId: string, history: EmploymentHistory): Promise; + updateEmploymentHistory(traineeId: string, history: EmploymentHistory): Promise; + deleteEmploymentHistory(traineeId: string, historyId: string): Promise; } export class EmploymentHistoryController implements EmploymentHistoryControllerType { constructor(private readonly traineesRepository: TraineesRepository) {} - async getEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { - try { - const traineeID = String(req.params.id); - const employmentHistory = await this.traineesRepository.getEmploymentHistory(traineeID); - res.json(employmentHistory); - } catch (error: any) { - next(error); - } + async getEmploymentHistory(traineeId: string): Promise { + return this.traineesRepository.getEmploymentHistory(traineeId); } - async addEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { - const traineeID = String(req.params.id); - const employmentHistoryData: EmploymentHistory = req.body; + async addEmploymentHistory(traineeId: string, history: EmploymentHistory): Promise { try { - validateEmploymentHistory(employmentHistoryData); + validateEmploymentHistory(history); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; - } - - try { - const newEmploymentHistory = await this.traineesRepository.addEmploymentHistory(traineeID, employmentHistoryData); - res.status(201).json(newEmploymentHistory); - } catch (error: any) { - next(error); + throw new BadRequestError(error.message); } + return this.traineesRepository.addEmploymentHistory(traineeId, history); } - async updateEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async updateEmploymentHistory(traineeId: string, history: EmploymentHistory): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const employmentHistoryData = trainee.employmentInfo.employmentHistory.find( - (history) => history.id === String(req.params.employmentHistoryID) - ); - if (!employmentHistoryData) { - res.status(404).send(new ResponseError('Employment history was not found')); - return; + const existing = trainee.employmentInfo.employmentHistory.find((h) => h.id === history.id); + if (!existing) { + throw new NotFoundError('Employment history was not found'); } - const historyToUpdate: EmploymentHistory = { - id: employmentHistoryData.id, - type: req.body.type, - companyName: req.body.companyName, - role: req.body.role, - startDate: new Date(req.body.startDate), - endDate: req.body.endDate ? new Date(req.body.endDate) : undefined, - feeCollected: req.body.feeCollected, - feeAmount: req.body.feeAmount, - comments: req.body.comments, - }; - try { - validateEmploymentHistory(historyToUpdate); + validateEmploymentHistory(history); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const updatedEmploymentHistory = await this.traineesRepository.updateEmploymentHistory( - trainee.id, - historyToUpdate - ); - - res.json(updatedEmploymentHistory); - } catch (error: any) { - next(error); - } + return this.traineesRepository.updateEmploymentHistory(trainee.id, history); } - async deleteEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { - try { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); - if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - - const employmentHistoryID = String(req.params.employmentHistoryID); - if (!trainee.employmentInfo.employmentHistory.find((history) => history.id === employmentHistoryID)) { - res.status(404).send(new ResponseError('Employment history not found')); - return; - } + async deleteEmploymentHistory(traineeId: string, historyId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); + if (!trainee) { + throw new NotFoundError('Trainee not found'); + } - await this.traineesRepository.deleteEmploymentHistory(trainee.id, employmentHistoryID); - res.status(204).send(); - } catch (error: any) { - next(error); + if (!trainee.employmentInfo.employmentHistory.find((h) => h.id === historyId)) { + throw new NotFoundError('Employment history not found'); } + + await this.traineesRepository.deleteEmploymentHistory(trainee.id, historyId); } } diff --git a/server/src/controllers/Trainee/InteractionController.ts b/server/src/controllers/Trainee/InteractionController.ts index 33cae380..73167147 100644 --- a/server/src/controllers/Trainee/InteractionController.ts +++ b/server/src/controllers/Trainee/InteractionController.ts @@ -1,132 +1,82 @@ -import { Request, Response, NextFunction } from 'express'; import { TraineesRepository, UserRepository } from '../../repositories'; -import { AuthenticatedUser, InteractionWithReporterID, ResponseError } from '../../models'; +import { Interaction, InteractionWithReporterID } from '../../models'; import { NotificationService } from '../../services'; import { validateInteraction } from '../../models/Interaction'; +import { BadRequestError, NotFoundError } from '../../errors'; + export interface InteractionControllerType { - getInteractions(req: Request, res: Response, next: NextFunction): Promise; - addInteraction(req: Request, res: Response, next: NextFunction): Promise; - updateInteraction(req: Request, res: Response, next: NextFunction): Promise; - deleteInteraction(req: Request, res: Response, next: NextFunction): Promise; + getInteractions(traineeId: string): Promise; + addInteraction(traineeId: string, interaction: InteractionWithReporterID): Promise; + updateInteraction(traineeId: string, interaction: InteractionWithReporterID): Promise; + deleteInteraction(traineeId: string, interactionId: string): Promise; } export class InteractionController implements InteractionControllerType { constructor( private readonly traineesRepository: TraineesRepository, private readonly userRepository: UserRepository, - private notificationService: NotificationService + private readonly notificationService: NotificationService ) {} - async getInteractions(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async getInteractions(traineeId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - - try { - const interactions = await this.traineesRepository.getInteractions(trainee.id); - res.status(200).json(interactions); - } catch (error: any) { - next(error); + throw new NotFoundError('Trainee not found'); } + return this.traineesRepository.getInteractions(trainee.id); } - async addInteraction(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async addInteraction(traineeId: string, interaction: InteractionWithReporterID): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const { date, type, title, details } = req.body; - const user = res.locals.user as AuthenticatedUser; - const reporterID = req.body.reporterID || user.id; - const newInteraction = { date, type, title, details, reporterID } as InteractionWithReporterID; - - // Validate new interaction model before creation try { - validateInteraction(newInteraction); - const reporter = await this.userRepository.findUserByID(reporterID); + validateInteraction(interaction); + const reporter = await this.userRepository.findUserByID(interaction.reporterID); if (!reporter) { - throw new Error(`Invalid reporter ID ${reporterID}. User not found.`); + throw new Error(`Invalid reporter ID ${interaction.reporterID}. User not found.`); } } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const interaction = await this.traineesRepository.addInteraction(String(req.params.id), newInteraction); - res.status(201).json(interaction); - this.notificationService.interactionCreated(trainee, interaction); - } catch (error: any) { - next(error); - } + const created = await this.traineesRepository.addInteraction(traineeId, interaction); + this.notificationService.interactionCreated(trainee, created); + return created; } - async updateInteraction(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async updateInteraction(traineeId: string, interaction: InteractionWithReporterID): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const interaction = trainee.interactions.find((interaction) => interaction.id === String(req.params.interactionID)); - if (!interaction) { - res.status(404).send(new ResponseError('Interaction not found')); - return; + const existing = trainee.interactions.find((i) => i.id === interaction.id); + if (!existing) { + throw new NotFoundError('Interaction not found'); } - const user = res.locals.user as AuthenticatedUser; - const interactionToUpdate: InteractionWithReporterID = { - id: String(req.params.interactionID), - date: req.body.date, - type: req.body.type, - title: req.body.title, - details: req.body.details, - reporterID: req.body.reporterID || user.id, - }; - - // Validate new interaction model after applying the changes try { - validateInteraction(interactionToUpdate); + validateInteraction(interaction); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const updatedInteraction = await this.traineesRepository.updateInteraction( - String(req.params.id), - interactionToUpdate - ); - res.status(200).json(updatedInteraction); - } catch (error: any) { - console.error(error); - next(error); - return; - } + return this.traineesRepository.updateInteraction(traineeId, interaction); } - async deleteInteraction(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async deleteInteraction(traineeId: string, interactionId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - if (!trainee.interactions.find((interaction) => interaction.id === String(req.params.interactionID))) { - res.status(404).send(new ResponseError('Interaction not found')); - return; + if (!trainee.interactions.find((i) => i.id === interactionId)) { + throw new NotFoundError('Interaction not found'); } - try { - await this.traineesRepository.deleteInteraction(String(req.params.id), String(req.params.interactionID)); - res.status(204).end(); - } catch (error: any) { - next(error); - return; - } + await this.traineesRepository.deleteInteraction(traineeId, interactionId); } } diff --git a/server/src/controllers/Trainee/LetterController.ts b/server/src/controllers/Trainee/LetterController.ts index 151212c0..3b0c7368 100644 --- a/server/src/controllers/Trainee/LetterController.ts +++ b/server/src/controllers/Trainee/LetterController.ts @@ -1,11 +1,22 @@ -import { Request, Response, NextFunction } from 'express'; -import { ResponseError, Trainee } from '../../models'; +import Stream from 'stream'; +import { Trainee } from '../../models'; import { LetterData, LetterGeneratorType, LetterType } from '../../services'; import { TraineesRepository } from '../../repositories'; -import sentry from '@sentry/node'; +import { BadRequestError, NotFoundError } from '../../errors'; + +export interface GenerateLetterParams { + traineeId: string; + type: LetterType; +} + +export interface GeneratedLetter { + stream: Stream.Readable; + fileName: string; + contentType: string; +} export interface LetterControllerType { - generateLetter(req: Request, res: Response, next: NextFunction): Promise; + generateLetter(params: GenerateLetterParams): Promise; } export class LetterController implements LetterControllerType { @@ -14,35 +25,25 @@ export class LetterController implements LetterControllerType { private readonly traineeRepository: TraineesRepository ) {} - async generateLetter(req: Request, res: Response, next: NextFunction): Promise { - const { type } = req.body as { type: LetterType }; - const traineeID = String(req.params.id); - - const trainee = await this.traineeRepository.getTrainee(traineeID); + async generateLetter({ traineeId, type }: GenerateLetterParams): Promise { + const trainee = await this.traineeRepository.getTrainee(traineeId); if (!trainee) { - const error = new ResponseError(`Trainee with ID ${traineeID} not found`); - res.status(404).json(error); - return; + throw new NotFoundError(`Trainee with ID ${traineeId} not found`); } if (!type || !Object.values(LetterType).includes(type)) { - const error = new ResponseError(`Invalid letter type provided. Available types: [${Object.values(LetterType)}]`); - res.status(400).json(error); - return; + throw new BadRequestError( + `Invalid letter type provided. Available types: [${Object.values(LetterType)}]` + ); } - try { - const data = this.getLetterData(trainee, type); - const pdfStream = await this.letterGenerator.generateLetter(type, data); - res.status(200).header({ - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment; filename="${this.getLetterFileName(trainee, type)}"`, - }); - pdfStream.pipe(res); - } catch (error) { - sentry.captureException(error); - next(error); - } + const data = this.getLetterData(trainee, type); + const stream = await this.letterGenerator.generateLetter(type, data); + return { + stream, + fileName: this.getLetterFileName(trainee, type), + contentType: 'application/pdf', + }; } private getLetterData(trainee: Trainee, type: LetterType): LetterData { diff --git a/server/src/controllers/Trainee/ProfilePictureController.ts b/server/src/controllers/Trainee/ProfilePictureController.ts index c1d32c29..2a3eaef0 100644 --- a/server/src/controllers/Trainee/ProfilePictureController.ts +++ b/server/src/controllers/Trainee/ProfilePictureController.ts @@ -1,133 +1,84 @@ -import { Request, Response, NextFunction } from 'express'; import { TraineesRepository } from '../../repositories'; -import { - StorageServiceType, - UploadServiceType, - UploadServiceError, - ImageServiceType, - AccessControl, -} from '../../services'; -import { ResponseError } from '../../models'; +import { StorageServiceType, ImageServiceType, AccessControl } from '../../services'; +import { NotFoundError } from '../../errors'; import * as Sentry from '@sentry/node'; import fs from 'fs'; +export interface ProfilePictureURLs { + imageURL: string; + thumbnailURL: string; +} + +export interface SetProfilePictureParams { + traineeId: string; + uploadedFilePath: string; + baseURL: string; +} + export interface ProfilePictureControllerType { - setProfilePicture(req: Request, res: Response, next: NextFunction): Promise; - deleteProfilePicture(req: Request, res: Response, next: NextFunction): Promise; + setProfilePicture(params: SetProfilePictureParams): Promise; + deleteProfilePicture(traineeId: string): Promise; } export class ProfilePictureController implements ProfilePictureControllerType { - private readonly traineesRepository: TraineesRepository; - private readonly storageService: StorageServiceType; - private readonly uploadService: UploadServiceType; - private readonly imageService: ImageServiceType; constructor( - traineesRepository: TraineesRepository, - storageService: StorageServiceType, - uploadService: UploadServiceType, - imageService: ImageServiceType - ) { - this.traineesRepository = traineesRepository; - this.storageService = storageService; - this.uploadService = uploadService; - this.imageService = imageService; - } + private readonly traineesRepository: TraineesRepository, + private readonly storageService: StorageServiceType, + private readonly imageService: ImageServiceType + ) {} - async setProfilePicture(req: Request, res: Response, next: NextFunction) { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async setProfilePicture({ traineeId, uploadedFilePath, baseURL }: SetProfilePictureParams): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - // Handle file upload. - try { - await this.uploadService.uploadImage(req, res, 'profilePicture'); - } catch (error: any) { - if (error instanceof UploadServiceError) { - res.status(400).send(new ResponseError(error.message)); - } else { - next(error); - } - return; - } - if (!req.file?.path) { - res.status(400).send(new ResponseError('No file was uploaded')); - return; - } + const largeFilePath = uploadedFilePath + '_large'; + const smallFilePath = uploadedFilePath + '_small'; - // Resize image to reduce file size and create a smaller version - const originalFilePath = req.file.path; - const largeFilePath = originalFilePath + '_large'; - const smallFilePath = originalFilePath + '_small'; try { - await this.imageService.resizeImage(originalFilePath, largeFilePath, 700, 700); + await this.imageService.resizeImage(uploadedFilePath, largeFilePath, 700, 700); await this.imageService.resizeImage(largeFilePath, smallFilePath, 70, 70); - } catch (error: any) { - next(error); - return; - } - try { - // Upload image to storage - const baseURL = process.env.STORAGE_BASE_URL ?? ''; const imageURL = new URL(this.getImageKey(trainee.id), baseURL).href; const thumbnailURL = new URL(this.getThumbnailKey(trainee.id), baseURL).href; - // Upload images to storage const largeFileStream = fs.createReadStream(largeFilePath); const smallFileStream = fs.createReadStream(smallFilePath); await this.storageService.upload(this.getImageKey(trainee.id), largeFileStream, AccessControl.Public); await this.storageService.upload(this.getThumbnailKey(trainee.id), smallFileStream, AccessControl.Public); - // update the trainee object with the new image URL trainee.imageURL = imageURL; trainee.thumbnailURL = thumbnailURL; - this.traineesRepository.updateTrainee(trainee); - res.status(201).send({ imageURL, thumbnailURL }); - } catch (error: any) { - next(error); - return; + await this.traineesRepository.updateTrainee(trainee); + return { imageURL, thumbnailURL }; + } finally { + this.cleanupFile(uploadedFilePath); + this.cleanupFile(largeFilePath); + this.cleanupFile(smallFilePath); } - - // Cleanup - fs.unlink(originalFilePath, (error) => { - if (error) { - Sentry.captureException(error); - } - }); - fs.unlink(largeFilePath, (error) => { - if (error) { - Sentry.captureException(error); - } - }); - fs.unlink(smallFilePath, (error) => { - if (error) { - Sentry.captureException(error); - } - }); } - async deleteProfilePicture(req: Request, res: Response, next: NextFunction) { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async deleteProfilePicture(traineeId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - try { - await this.storageService.delete(this.getImageKey(trainee.id)); - await this.storageService.delete(this.getThumbnailKey(trainee.id)); - - // update the trainee object with the new image URL - trainee.imageURL = undefined; - trainee.thumbnailURL = undefined; - this.traineesRepository.updateTrainee(trainee); - } catch (error: any) { - next(error); - return; + throw new NotFoundError('Trainee not found'); } - res.status(204).end(); + await this.storageService.delete(this.getImageKey(trainee.id)); + await this.storageService.delete(this.getThumbnailKey(trainee.id)); + + trainee.imageURL = undefined; + trainee.thumbnailURL = undefined; + await this.traineesRepository.updateTrainee(trainee); + } + + private cleanupFile(filePath: string) { + fs.unlink(filePath, (error) => { + if (error && (error as NodeJS.ErrnoException).code !== 'ENOENT') { + Sentry.captureException(error); + } + }); } private getImageKey(traineeId: string) { diff --git a/server/src/controllers/Trainee/StrikeController.ts b/server/src/controllers/Trainee/StrikeController.ts index d34f4b10..9f2b5fa2 100644 --- a/server/src/controllers/Trainee/StrikeController.ts +++ b/server/src/controllers/Trainee/StrikeController.ts @@ -1,15 +1,15 @@ -import { Request, Response, NextFunction } from 'express'; import { TraineesRepository } from '../../repositories'; -import { AuthenticatedUser, ResponseError, StrikeWithReporterID } from '../../models'; +import { StrikeWithReporter, StrikeWithReporterID } from '../../models'; import { NotificationService } from '../../services'; import { validateStrike } from '../../models/Strike'; import { UserRepository } from '../../repositories/UserRepository'; +import { BadRequestError, NotFoundError } from '../../errors'; export interface StrikeControllerType { - getStrikes(req: Request, res: Response, next: NextFunction): Promise; - addStrike(req: Request, res: Response, next: NextFunction): Promise; - updateStrike(req: Request, res: Response, next: NextFunction): Promise; - deleteStrike(req: Request, res: Response, next: NextFunction): Promise; + getStrikes(traineeId: string): Promise; + addStrike(traineeId: string, strike: StrikeWithReporterID): Promise; + updateStrike(traineeId: string, strike: StrikeWithReporterID): Promise; + deleteStrike(traineeId: string, strikeId: string): Promise; } export class StrikeController implements StrikeControllerType { @@ -19,105 +19,60 @@ export class StrikeController implements StrikeControllerType { private readonly notificationService: NotificationService ) {} - async getStrikes(req: Request, res: Response, next: NextFunction) { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async getStrikes(traineeId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - - try { - const strikes = await this.traineesRepository.getStrikes(trainee.id); - res.status(200).json(strikes); - } catch (error: any) { - next(error); + throw new NotFoundError('Trainee not found'); } + return this.traineesRepository.getStrikes(trainee.id); } - async addStrike(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async addStrike(traineeId: string, strike: StrikeWithReporterID): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const { reason, date, comments } = req.body; - const user = res.locals.user as AuthenticatedUser; - const reporterID = req.body.reporterID || user.id; - const newStrike = { reason, date, comments, reporterID } as StrikeWithReporterID; - - // Validate new strike model before creation try { - validateStrike(newStrike); - const reporter = await this.userRepository.findUserByID(reporterID); + validateStrike(strike); + const reporter = await this.userRepository.findUserByID(strike.reporterID); if (!reporter) { - throw new Error(`Invalid reporter ID ${reporterID}. User not found.`); + throw new Error(`Invalid reporter ID ${strike.reporterID}. User not found.`); } } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const strike = await this.traineesRepository.addStrike(String(req.params.id), newStrike); - res.status(201).json(strike); - this.notificationService.strikeCreated(trainee, strike); - } catch (error: any) { - next(error); - } + const created = await this.traineesRepository.addStrike(traineeId, strike); + this.notificationService.strikeCreated(trainee, created); + return created; } - async updateStrike(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async updateStrike(traineeId: string, strike: StrikeWithReporterID): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const strike = trainee.educationInfo.strikes.find((strike) => strike.id === String(req.params.strikeId)); - if (!strike) { - res.status(404).send(new ResponseError('Strike not found')); - return; + const existing = trainee.educationInfo.strikes.find((s) => s.id === strike.id); + if (!existing) { + throw new NotFoundError('Strike not found'); } - const user = res.locals.user as AuthenticatedUser; - const strikeToUpdate: StrikeWithReporterID = { - id: String(req.params.strikeId), - reason: req.body.reason, - date: req.body.date, - comments: req.body.comments, - reporterID: req.body.reporterID || user.id, - }; - - // Validate new strike model after applying the changes try { - validateStrike(strikeToUpdate); + validateStrike(strike); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const updatedStrike = await this.traineesRepository.updateStrike(String(req.params.id), strikeToUpdate); - res.status(200).json(updatedStrike); - } catch (error: any) { - next(error); - return; - } + return this.traineesRepository.updateStrike(traineeId, strike); } - async deleteStrike(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async deleteStrike(traineeId: string, strikeId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - try { - await this.traineesRepository.deleteStrike(String(req.params.id), String(req.params.strikeId)); - res.status(204).end(); - } catch (error: any) { - next(error); - return; + throw new NotFoundError('Trainee not found'); } + await this.traineesRepository.deleteStrike(traineeId, strikeId); } } diff --git a/server/src/controllers/Trainee/TestController.ts b/server/src/controllers/Trainee/TestController.ts index 137f5acf..aee7a7d4 100644 --- a/server/src/controllers/Trainee/TestController.ts +++ b/server/src/controllers/Trainee/TestController.ts @@ -1,13 +1,13 @@ -import { Request, Response, NextFunction } from 'express'; -import { ResponseError, Test, validateTest } from '../../models'; +import { Test, validateTest } from '../../models'; import { TraineesRepository } from '../../repositories'; import { NotificationService } from '../../services'; +import { BadRequestError, NotFoundError } from '../../errors'; export interface TestControllerType { - getTests(req: Request, res: Response, next: NextFunction): Promise; - addTest(req: Request, res: Response, next: NextFunction): Promise; - updateTest(req: Request, res: Response, next: NextFunction): Promise; - deleteTest(req: Request, res: Response, next: NextFunction): Promise; + getTests(traineeId: string): Promise; + addTest(traineeId: string, test: Test): Promise; + updateTest(traineeId: string, test: Test): Promise; + deleteTest(traineeId: string, testId: string): Promise; } export class TestController implements TestControllerType { @@ -16,105 +16,61 @@ export class TestController implements TestControllerType { private readonly notificationService: NotificationService ) {} - async getTests(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async getTests(traineeId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - - try { - const tests = await this.traineesRepository.getTests(trainee.id); - res.status(200).json(tests); - } catch (error: any) { - next(error); + throw new NotFoundError('Trainee not found'); } + return this.traineesRepository.getTests(trainee.id); } - async addTest(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async addTest(traineeId: string, test: Test): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const newTest = req.body as Test; - - // Validate new test model before creation try { - validateTest(newTest); + validateTest(test); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const test = await this.traineesRepository.addTest(trainee.id, newTest); - res.status(201).json(test); - this.notificationService.testCreated(trainee, test); - } catch (error: any) { - next(error); - } + const created = await this.traineesRepository.addTest(trainee.id, test); + this.notificationService.testCreated(trainee, created); + return created; } - async updateTest(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async updateTest(traineeId: string, test: Test): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - const test = trainee.educationInfo.tests.find((test) => test.id === String(req.params.testID)); - if (!test) { - res.status(404).send(new ResponseError('Test not found')); - return; + const existing = trainee.educationInfo.tests.find((t) => t.id === test.id); + if (!existing) { + throw new NotFoundError('Test not found'); } - const testToUpdate: Test = { - id: String(req.params.testID), - date: req.body.date, - type: req.body.type, - result: req.body.result, - score: Number.parseFloat(req.body.score), - comments: req.body.comments, - }; - - // Validate new test model after applying the changes try { - validateTest(testToUpdate); + validateTest(test); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - try { - const updatedTest = await this.traineesRepository.updateTest(trainee.id, testToUpdate); - res.status(200).json(updatedTest); - } catch (error: any) { - console.error(error); - next(error); - return; - } + return this.traineesRepository.updateTest(trainee.id, test); } - async deleteTest(req: Request, res: Response, next: NextFunction): Promise { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async deleteTest(traineeId: string, testId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - if (!trainee.educationInfo.tests.find((test) => test.id === String(req.params.testID))) { - res.status(404).send(new ResponseError('Test not found')); - return; + if (!trainee.educationInfo.tests.find((t) => t.id === testId)) { + throw new NotFoundError('Test not found'); } - try { - await this.traineesRepository.deleteTest(String(req.params.id), String(req.params.testID)); - res.status(204).end(); - } catch (error: any) { - next(error); - return; - } + await this.traineesRepository.deleteTest(traineeId, testId); } } diff --git a/server/src/controllers/Trainee/TraineeController.ts b/server/src/controllers/Trainee/TraineeController.ts index 2bfa54b7..6fa6bca6 100644 --- a/server/src/controllers/Trainee/TraineeController.ts +++ b/server/src/controllers/Trainee/TraineeController.ts @@ -1,14 +1,14 @@ -import { Request, Response, NextFunction } from 'express'; import { TraineesRepository } from '../../repositories'; -import { AuthenticatedUser, ResponseError } from '../../models'; +import { AuthenticatedUser, Trainee } from '../../models'; import { NotificationService, UpdateChange } from '../../services'; import { validateTrainee } from '../../models/Trainee'; +import { BadRequestError, NotFoundError } from '../../errors'; export interface TraineeControllerType { - getTrainee(req: Request, res: Response, next: NextFunction): Promise; - createTrainee(req: Request, res: Response, next: NextFunction): Promise; - updateTrainee(req: Request, res: Response, next: NextFunction): Promise; - deleteTrainee(req: Request, res: Response, next: NextFunction): Promise; + getTrainee(traineeId: string): Promise; + createTrainee(input: any): Promise; + updateTrainee(traineeId: string, input: any, actor: AuthenticatedUser): Promise; + deleteTrainee(traineeId: string): Promise; } export class TraineeController implements TraineeControllerType { @@ -17,88 +17,55 @@ export class TraineeController implements TraineeControllerType { private readonly notificationService: NotificationService ) {} - async getTrainee(req: Request, res: Response, next: NextFunction) { - const traineeId = String(req.params.id); - try { - const trainee = await this.traineesRepository.getTrainee(traineeId); - if (!trainee) { - res.status(404).json({ error: 'Trainee was not found' }); - return; - } - res.status(200).json(trainee); - } catch (error: any) { - next(error); + async getTrainee(traineeId: string): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); + if (!trainee) { + throw new NotFoundError('Trainee was not found'); } + return trainee; } - async createTrainee(req: Request, res: Response, next: NextFunction): Promise { - const body = req.body; + async createTrainee(input: any): Promise { + const body = input ?? {}; body.educationInfo = body.educationInfo ?? {}; body.employmentInfo = body.employmentInfo ?? {}; - // Check if the request is valid try { - validateTrainee(req.body); + validateTrainee(body); } catch (error: any) { - const message: string = `Invalid trainee information. ` + error.message; - res.status(400).json(new ResponseError(message)); - return; + throw new BadRequestError(`Invalid trainee information. ${error.message}`); } - // Check if there is another trainee with the same email - const email = req.body.contactInfo.email; - let emailExists: boolean = false; - try { - emailExists = await this.traineesRepository.isEmailExists(email); - } catch (error: any) { - next(error); - return; - } + const email = body.contactInfo.email; + const emailExists = await this.traineesRepository.isEmailExists(email); if (emailExists) { - const message: string = `There is already another trainee in the system with the email ${email}`; - res.status(400).json(new ResponseError(message)); - return; + throw new BadRequestError(`There is already another trainee in the system with the email ${email}`); } - // Create new trainee and return it - try { - const newTrainee = await this.traineesRepository.createTrainee(req.body); - res.status(201).json(newTrainee); - } catch (error: any) { - res.status(500).send(new ResponseError(error.message)); - } + return this.traineesRepository.createTrainee(body); } - async updateTrainee(req: Request, res: Response, next: NextFunction) { - const trainee = await this.traineesRepository.getTrainee(String(req.params.id)); + async updateTrainee(traineeId: string, input: any, actor: AuthenticatedUser): Promise { + const trainee = await this.traineesRepository.getTrainee(traineeId); if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; + throw new NotFoundError('Trainee not found'); } - // Apply all changes from the request body to the trainee object - const changes = this.applyObjectUpdate(req.body, trainee); + const changes = this.applyObjectUpdate(input, trainee); - // Validate new trainee model after applying the changes try { validateTrainee(trainee); } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); - return; + throw new BadRequestError(error.message); } - // Save the updated trainee - try { - await this.traineesRepository.updateTrainee(trainee); - res.status(200).json(trainee); - this.notificationService.traineeUpdated(trainee, changes, res.locals.user as AuthenticatedUser); - } catch (error: any) { - res.status(500).send(new ResponseError(error.message)); - } + await this.traineesRepository.updateTrainee(trainee); + this.notificationService.traineeUpdated(trainee, changes, actor); + return trainee; } - async deleteTrainee(req: Request, res: Response, next: NextFunction) { - res.status(500).send('Not implemented'); + async deleteTrainee(_traineeId: string): Promise { + throw new Error('Not implemented'); } // This function updates the destination object with the source object. @@ -109,12 +76,11 @@ export class TraineeController implements TraineeControllerType { nestLevel: number = 0, changes: UpdateChange[] = [] ): UpdateChange[] { - // safeguard against infinite recursion if (nestLevel > 5) { return changes; } - for (let key of Object.keys(source)) { + for (const key of Object.keys(source)) { if (Array.isArray(source[key]) || !(key in destination)) { continue; } @@ -126,7 +92,6 @@ export class TraineeController implements TraineeControllerType { continue; } - // If the value has changed, record the change if (destination[key] !== source[key]) { changes.push({ fieldName: key, @@ -134,7 +99,6 @@ export class TraineeController implements TraineeControllerType { newValue: source[key], }); } - // Update the destination object with the new value destination[key] = source[key]; } diff --git a/server/src/controllers/UserController.ts b/server/src/controllers/UserController.ts index acf0a328..c70d7a38 100644 --- a/server/src/controllers/UserController.ts +++ b/server/src/controllers/UserController.ts @@ -1,107 +1,48 @@ -import z from 'zod'; import { UserRepository } from '../repositories'; -import { ResponseError } from '../models'; -import { CreateUserSchema, UpdateUserSchema } from '../models'; - -import type { RequestHandler, Request, Response } from 'express'; - -export interface UserController { - getAllUsers: RequestHandler; - createUser: RequestHandler; - updateUser: RequestHandler; - deleteUser: RequestHandler; +import { CreateUser, UpdateUser, User } from '../models'; +import { ConflictError, NotFoundError } from '../errors'; + +export interface UserControllerType { + getAllUsers(): Promise; + createUser(data: CreateUser): Promise; + updateUser(userId: string, data: UpdateUser): Promise; + deleteUser(userId: string): Promise; } -export class DefaultUserController implements UserController { - constructor(private userRepository: UserRepository) { - this.getAllUsers = this.getAllUsers.bind(this); - this.createUser = this.createUser.bind(this); - this.updateUser = this.updateUser.bind(this); - this.deleteUser = this.deleteUser.bind(this); - } +export class UserController implements UserControllerType { + constructor(private readonly userRepository: UserRepository) {} - /** - * GET /users - * get all users - * 200 | 500 - */ - async getAllUsers(req: Request, res: Response) { - const users = await this.userRepository.getAllUsers(); - res.status(200).json(users); + async getAllUsers(): Promise { + return this.userRepository.getAllUsers(); } - /** - * POST /users - * create a new user - * 201 | 400 | 409 | 500 - */ - async createUser(req: Request, res: Response) { - //validate - const user = CreateUserSchema.safeParse(req.body); - if (!user.success) { - res.status(400).json(z.flattenError(user.error)); - return; - } - - //check if email already exists - const existingUser = await this.userRepository.findUserByEmail(user.data.email); + async createUser(data: CreateUser): Promise { + const existingUser = await this.userRepository.findUserByEmail(data.email); if (existingUser) { - res.status(409).json(new ResponseError('Email already in use')); - return; + throw new ConflictError('Email already in use'); } - - //create user - const createdUser = await this.userRepository.createUser(user.data); - res.status(201).json(createdUser); + return this.userRepository.createUser(data); } - /** - * PUT /users/{id} - * update an existing user - * 200 | 400 | 404 | 500 - */ - async updateUser(req: Request, res: Response) { - const userId = String(req.params.id); - - //validate - const userUpdates = UpdateUserSchema.safeParse(req.body); - if (!userUpdates.success) { - res.status(400).json(z.flattenError(userUpdates.error)); - return; - } - - //check if email already exists if provided - if (userUpdates.data.email) { - const existingUser = await this.userRepository.findUserByEmail(userUpdates.data.email); + async updateUser(userId: string, data: UpdateUser): Promise { + if (data.email) { + const existingUser = await this.userRepository.findUserByEmail(data.email); if (existingUser && existingUser.id !== userId) { - res.status(409).json(new ResponseError('Email already in use')); - return; + throw new ConflictError('Email already in use'); } } - //update user - const updatedUser = await this.userRepository.updateUser(userId, userUpdates.data); + const updatedUser = await this.userRepository.updateUser(userId, data); if (!updatedUser) { - res.status(404).json(new ResponseError('User not found')); - } else { - res.status(200).json(updatedUser); + throw new NotFoundError('User not found'); } + return updatedUser; } - /** - * DELETE /users/{id} - * delete an existing user - * 204 | 404 | 500 - */ - async deleteUser(req: Request, res: Response) { - const userId = String(req.params.id); - + async deleteUser(userId: string): Promise { const deletedUser = await this.userRepository.deleteUser(userId); if (!deletedUser) { - res.status(404).json(new ResponseError('User not found')); - return; + throw new NotFoundError('User not found'); } - - res.status(204).send(); } } diff --git a/server/src/errors/HttpError.ts b/server/src/errors/HttpError.ts new file mode 100644 index 00000000..f9f46713 --- /dev/null +++ b/server/src/errors/HttpError.ts @@ -0,0 +1,40 @@ +export class HttpError extends Error { + readonly statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.statusCode = statusCode; + this.name = new.target.name; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class BadRequestError extends HttpError { + constructor(message: string = 'Bad request') { + super(400, message); + } +} + +export class UnauthorizedError extends HttpError { + constructor(message: string = 'Unauthorized') { + super(401, message); + } +} + +export class ForbiddenError extends HttpError { + constructor(message: string = 'Forbidden') { + super(403, message); + } +} + +export class NotFoundError extends HttpError { + constructor(message: string = 'Not found') { + super(404, message); + } +} + +export class ConflictError extends HttpError { + constructor(message: string = 'Conflict') { + super(409, message); + } +} diff --git a/server/src/errors/index.ts b/server/src/errors/index.ts new file mode 100644 index 00000000..497ace75 --- /dev/null +++ b/server/src/errors/index.ts @@ -0,0 +1 @@ +export * from './HttpError'; diff --git a/server/src/handlers/AuthenticationHandler.ts b/server/src/handlers/AuthenticationHandler.ts new file mode 100644 index 00000000..b8f10076 --- /dev/null +++ b/server/src/handlers/AuthenticationHandler.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { AuthenticationControllerType } from '../controllers/AuthenticationController'; +import { AuthenticatedUser } from '../models'; +import { sendError } from './Handler'; + +export const TOKEN_COOKIE_NAME = 'dojo_token'; + +export interface AuthenticationHandlerType { + login(req: Request, res: Response, next: NextFunction): Promise; + logout(req: Request, res: Response): Promise; + getSession(req: Request, res: Response): Promise; +} + +export class AuthenticationHandler implements AuthenticationHandlerType { + private readonly controller: AuthenticationControllerType; + private readonly tokenExpirationInDays: number; + + constructor(controller: AuthenticationControllerType, tokenExpirationInDays: number) { + this.controller = controller; + this.tokenExpirationInDays = tokenExpirationInDays; + } + + async login(req: Request, res: Response, next: NextFunction): Promise { + const authCode = req.body?.authCode?.trim() ?? ''; + const redirectURI = req.body?.redirectURI?.trim() ?? ''; + + try { + const { user, token } = await this.controller.login({ authCode, redirectURI }); + + const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; + res.cookie(TOKEN_COOKIE_NAME, token, { + httpOnly: true, + secure: isSecure, + sameSite: 'strict', + maxAge: 1000 * 60 * 60 * 24 * this.tokenExpirationInDays, + }); + res.status(200).json(user); + } catch (error) { + sendError(error, res, next); + } + } + + async getSession(_req: Request, res: Response): Promise { + const user = res.locals.user as AuthenticatedUser; + res.status(200).json(user); + } + + async logout(_req: Request, res: Response): Promise { + res.clearCookie(TOKEN_COOKIE_NAME); + res.status(204).end(); + } +} diff --git a/server/src/handlers/CohortsHandler.ts b/server/src/handlers/CohortsHandler.ts new file mode 100644 index 00000000..d5bd4f72 --- /dev/null +++ b/server/src/handlers/CohortsHandler.ts @@ -0,0 +1,25 @@ +import { Request, Response } from 'express'; +import { CohortsControllerType } from '../controllers/CohortsController'; + +export interface CohortsHandlerType { + getCohorts(req: Request, res: Response): Promise; +} + +export class CohortsHandler implements CohortsHandlerType { + private controller: CohortsControllerType; + + constructor(controller: CohortsControllerType) { + this.controller = controller; + } + + async getCohorts(req: Request, res: Response): Promise { + const MAX_POSSIBLE_COHORT = 999; + let start = Number.parseInt(req.query.start as string) || 0; + let end = Number.parseInt(req.query.end as string) || MAX_POSSIBLE_COHORT; + start = Math.max(start, 0); + end = Math.min(end, MAX_POSSIBLE_COHORT); + + const result = await this.controller.getCohorts({ start, end }); + res.status(200).json(result); + } +} diff --git a/server/src/handlers/DashboardHandler.ts b/server/src/handlers/DashboardHandler.ts new file mode 100644 index 00000000..38d2b0ed --- /dev/null +++ b/server/src/handlers/DashboardHandler.ts @@ -0,0 +1,24 @@ +import { Request, Response, NextFunction } from 'express'; +import { DashboardControllerType } from '../controllers/DashboardController'; +import { sendError } from './Handler'; + +export interface DashboardHandlerType { + getDashboard(req: Request, res: Response, next: NextFunction): Promise; +} + +export class DashboardHandler implements DashboardHandlerType { + private readonly controller: DashboardControllerType; + + constructor(controller: DashboardControllerType) { + this.controller = controller; + } + + async getDashboard(_req: Request, res: Response, next: NextFunction): Promise { + try { + const data = await this.controller.getDashboard(); + res.status(200).json(data); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/EmploymentHistoryHandler.ts b/server/src/handlers/EmploymentHistoryHandler.ts new file mode 100644 index 00000000..6c8666f5 --- /dev/null +++ b/server/src/handlers/EmploymentHistoryHandler.ts @@ -0,0 +1,64 @@ +import { Request, Response, NextFunction } from 'express'; +import { EmploymentHistoryControllerType } from '../controllers/Trainee/EmploymentHistoryController'; +import { EmploymentHistory } from '../models'; +import { sendError } from './Handler'; + +export interface EmploymentHistoryHandlerType { + getEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; + addEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; + updateEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; + deleteEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise; +} + +export class EmploymentHistoryHandler implements EmploymentHistoryHandlerType { + constructor(private readonly controller: EmploymentHistoryControllerType) {} + + async getEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { + try { + const history = await this.controller.getEmploymentHistory(String(req.params.id)); + res.status(200).json(history); + } catch (error) { + sendError(error, res, next); + } + } + + async addEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { + const history = req.body as EmploymentHistory; + try { + const created = await this.controller.addEmploymentHistory(String(req.params.id), history); + res.status(201).json(created); + } catch (error) { + sendError(error, res, next); + } + } + + async updateEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { + const history: EmploymentHistory = { + id: String(req.params.employmentHistoryID), + type: req.body?.type, + companyName: req.body?.companyName, + role: req.body?.role, + startDate: req.body?.startDate ? new Date(req.body.startDate) : (undefined as unknown as Date), + endDate: req.body?.endDate ? new Date(req.body.endDate) : undefined, + feeCollected: req.body?.feeCollected, + feeAmount: req.body?.feeAmount, + comments: req.body?.comments, + }; + + try { + const updated = await this.controller.updateEmploymentHistory(String(req.params.id), history); + res.status(200).json(updated); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.controller.deleteEmploymentHistory(String(req.params.id), String(req.params.employmentHistoryID)); + res.status(204).send(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/GeographyHandler.ts b/server/src/handlers/GeographyHandler.ts new file mode 100644 index 00000000..9e946dd7 --- /dev/null +++ b/server/src/handlers/GeographyHandler.ts @@ -0,0 +1,38 @@ +import { Request, Response, NextFunction } from 'express'; +import { GeographyControllerType } from '../controllers/GeographyController'; +import { sendError } from './Handler'; + +export interface GeographyHandlerType { + getCountries(req: Request, res: Response, next: NextFunction): Promise; + getCities(req: Request, res: Response, next: NextFunction): Promise; +} + +export class GeographyHandler implements GeographyHandlerType { + private readonly controller: GeographyControllerType; + + constructor(controller: GeographyControllerType) { + this.controller = controller; + } + + async getCountries(req: Request, res: Response, next: NextFunction): Promise { + const query = (req.query.q as string) ?? ''; + const limit = Number.parseInt(req.query.limit as string) || null; + try { + const countries = await this.controller.getCountries({ query, limit }); + res.status(200).json(countries); + } catch (error) { + sendError(error, res, next); + } + } + + async getCities(req: Request, res: Response, next: NextFunction): Promise { + const query = (req.query.q as string) ?? ''; + const limit = Number.parseInt(req.query.limit as string) || null; + try { + const cities = await this.controller.getCities({ query, limit }); + res.status(200).json(cities); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/Handler.ts b/server/src/handlers/Handler.ts new file mode 100644 index 00000000..5089df50 --- /dev/null +++ b/server/src/handlers/Handler.ts @@ -0,0 +1,18 @@ +import { Response, NextFunction } from 'express'; +import { HttpError } from '../errors'; +import { ResponseError } from '../models'; + +/** + * Translates a controller error to an HTTP response. + * + * Domain errors (HttpError subclasses) become structured responses with the + * appropriate status code. Anything else is forwarded to express's global + * error handler via `next`. + */ +export function sendError(error: unknown, res: Response, next: NextFunction): void { + if (error instanceof HttpError) { + res.status(error.statusCode).json(new ResponseError(error.message)); + return; + } + next(error); +} diff --git a/server/src/handlers/InteractionHandler.ts b/server/src/handlers/InteractionHandler.ts new file mode 100644 index 00000000..1fed6c16 --- /dev/null +++ b/server/src/handlers/InteractionHandler.ts @@ -0,0 +1,71 @@ +import { Request, Response, NextFunction } from 'express'; +import { InteractionControllerType } from '../controllers/Trainee/InteractionController'; +import { AuthenticatedUser, InteractionWithReporterID } from '../models'; +import { sendError } from './Handler'; + +export interface InteractionHandlerType { + getInteractions(req: Request, res: Response, next: NextFunction): Promise; + addInteraction(req: Request, res: Response, next: NextFunction): Promise; + updateInteraction(req: Request, res: Response, next: NextFunction): Promise; + deleteInteraction(req: Request, res: Response, next: NextFunction): Promise; +} + +export class InteractionHandler implements InteractionHandlerType { + constructor(private readonly controller: InteractionControllerType) {} + + async getInteractions(req: Request, res: Response, next: NextFunction): Promise { + try { + const interactions = await this.controller.getInteractions(String(req.params.id)); + res.status(200).json(interactions); + } catch (error) { + sendError(error, res, next); + } + } + + async addInteraction(req: Request, res: Response, next: NextFunction): Promise { + const user = res.locals.user as AuthenticatedUser; + const { date, type, title, details } = req.body ?? {}; + const interaction: InteractionWithReporterID = { + date, + type, + title, + details, + reporterID: req.body?.reporterID || user.id, + } as InteractionWithReporterID; + + try { + const created = await this.controller.addInteraction(String(req.params.id), interaction); + res.status(201).json(created); + } catch (error) { + sendError(error, res, next); + } + } + + async updateInteraction(req: Request, res: Response, next: NextFunction): Promise { + const user = res.locals.user as AuthenticatedUser; + const interaction: InteractionWithReporterID = { + id: String(req.params.interactionID), + date: req.body?.date, + type: req.body?.type, + title: req.body?.title, + details: req.body?.details, + reporterID: req.body?.reporterID || user.id, + }; + + try { + const updated = await this.controller.updateInteraction(String(req.params.id), interaction); + res.status(200).json(updated); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteInteraction(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.controller.deleteInteraction(String(req.params.id), String(req.params.interactionID)); + res.status(204).end(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/LetterHandler.ts b/server/src/handlers/LetterHandler.ts new file mode 100644 index 00000000..f3e6f1ad --- /dev/null +++ b/server/src/handlers/LetterHandler.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express'; +import sentry from '@sentry/node'; +import { LetterControllerType } from '../controllers/Trainee/LetterController'; +import { LetterType } from '../services'; +import { sendError } from './Handler'; + +export interface LetterHandlerType { + generateLetter(req: Request, res: Response, next: NextFunction): Promise; +} + +export class LetterHandler implements LetterHandlerType { + constructor(private readonly controller: LetterControllerType) {} + + async generateLetter(req: Request, res: Response, next: NextFunction): Promise { + const traineeId = String(req.params.id); + const type = req.body?.type as LetterType; + + try { + const { stream, fileName, contentType } = await this.controller.generateLetter({ traineeId, type }); + res.status(200).header({ + 'Content-Type': contentType, + 'Content-Disposition': `attachment; filename="${fileName}"`, + }); + stream.pipe(res); + } catch (error) { + sentry.captureException(error); + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/ProfilePictureHandler.ts b/server/src/handlers/ProfilePictureHandler.ts new file mode 100644 index 00000000..1e5c70f9 --- /dev/null +++ b/server/src/handlers/ProfilePictureHandler.ts @@ -0,0 +1,56 @@ +import { Request, Response, NextFunction } from 'express'; +import { ProfilePictureControllerType } from '../controllers/Trainee/ProfilePictureController'; +import { UploadServiceType, UploadServiceError } from '../services'; +import { ResponseError } from '../models'; +import { sendError } from './Handler'; + +export interface ProfilePictureHandlerType { + setProfilePicture(req: Request, res: Response, next: NextFunction): Promise; + deleteProfilePicture(req: Request, res: Response, next: NextFunction): Promise; +} + +export class ProfilePictureHandler implements ProfilePictureHandlerType { + constructor( + private readonly controller: ProfilePictureControllerType, + private readonly uploadService: UploadServiceType, + private readonly storageBaseURL: string + ) {} + + async setProfilePicture(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.uploadService.uploadImage(req, res, 'profilePicture'); + } catch (error: any) { + if (error instanceof UploadServiceError) { + res.status(400).json(new ResponseError(error.message)); + return; + } + next(error); + return; + } + + if (!req.file?.path) { + res.status(400).json(new ResponseError('No file was uploaded')); + return; + } + + try { + const urls = await this.controller.setProfilePicture({ + traineeId: String(req.params.id), + uploadedFilePath: req.file.path, + baseURL: this.storageBaseURL, + }); + res.status(201).json(urls); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteProfilePicture(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.controller.deleteProfilePicture(String(req.params.id)); + res.status(204).end(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/SearchHandler.ts b/server/src/handlers/SearchHandler.ts new file mode 100644 index 00000000..96d5b757 --- /dev/null +++ b/server/src/handlers/SearchHandler.ts @@ -0,0 +1,39 @@ +import { Request, Response, NextFunction } from 'express'; +import { SearchControllerType, SearchResult } from '../controllers/SearchController'; +import { sendError } from './Handler'; + +interface SearchResponse { + hits: { + data: SearchResult[]; + size: number; + }; +} + +export interface SearchHandlerType { + search(req: Request, res: Response, next: NextFunction): Promise; +} + +export class SearchHandler implements SearchHandlerType { + private readonly controller: SearchControllerType; + + constructor(controller: SearchControllerType) { + this.controller = controller; + } + + async search(req: Request, res: Response, next: NextFunction): Promise { + const maxAllowedLimit = 50; + const inputLimit = Number(req.query.limit) || 20; + const limit = Math.min(inputLimit, maxAllowedLimit); + const query = (req.query.q as string) ?? ''; + + try { + const results = await this.controller.search({ query, limit }); + const response: SearchResponse = { + hits: { data: results, size: results.length }, + }; + res.status(200).json(response); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/StrikeHandler.ts b/server/src/handlers/StrikeHandler.ts new file mode 100644 index 00000000..c53da859 --- /dev/null +++ b/server/src/handlers/StrikeHandler.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction } from 'express'; +import { StrikeControllerType } from '../controllers/Trainee/StrikeController'; +import { AuthenticatedUser, StrikeWithReporterID } from '../models'; +import { sendError } from './Handler'; + +export interface StrikeHandlerType { + getStrikes(req: Request, res: Response, next: NextFunction): Promise; + addStrike(req: Request, res: Response, next: NextFunction): Promise; + updateStrike(req: Request, res: Response, next: NextFunction): Promise; + deleteStrike(req: Request, res: Response, next: NextFunction): Promise; +} + +export class StrikeHandler implements StrikeHandlerType { + constructor(private readonly controller: StrikeControllerType) {} + + async getStrikes(req: Request, res: Response, next: NextFunction): Promise { + try { + const strikes = await this.controller.getStrikes(String(req.params.id)); + res.status(200).json(strikes); + } catch (error) { + sendError(error, res, next); + } + } + + async addStrike(req: Request, res: Response, next: NextFunction): Promise { + const user = res.locals.user as AuthenticatedUser; + const { reason, date, comments } = req.body ?? {}; + const strike: StrikeWithReporterID = { + reason, + date, + comments, + reporterID: req.body?.reporterID || user.id, + } as StrikeWithReporterID; + + try { + const created = await this.controller.addStrike(String(req.params.id), strike); + res.status(201).json(created); + } catch (error) { + sendError(error, res, next); + } + } + + async updateStrike(req: Request, res: Response, next: NextFunction): Promise { + const user = res.locals.user as AuthenticatedUser; + const strike: StrikeWithReporterID = { + id: String(req.params.strikeId), + reason: req.body?.reason, + date: req.body?.date, + comments: req.body?.comments, + reporterID: req.body?.reporterID || user.id, + }; + + try { + const updated = await this.controller.updateStrike(String(req.params.id), strike); + res.status(200).json(updated); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteStrike(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.controller.deleteStrike(String(req.params.id), String(req.params.strikeId)); + res.status(204).end(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/TestHandler.ts b/server/src/handlers/TestHandler.ts new file mode 100644 index 00000000..de30796e --- /dev/null +++ b/server/src/handlers/TestHandler.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from 'express'; +import { TestControllerType } from '../controllers/Trainee/TestController'; +import { Test } from '../models'; +import { sendError } from './Handler'; + +export interface TestHandlerType { + getTests(req: Request, res: Response, next: NextFunction): Promise; + addTest(req: Request, res: Response, next: NextFunction): Promise; + updateTest(req: Request, res: Response, next: NextFunction): Promise; + deleteTest(req: Request, res: Response, next: NextFunction): Promise; +} + +export class TestHandler implements TestHandlerType { + constructor(private readonly controller: TestControllerType) {} + + async getTests(req: Request, res: Response, next: NextFunction): Promise { + try { + const tests = await this.controller.getTests(String(req.params.id)); + res.status(200).json(tests); + } catch (error) { + sendError(error, res, next); + } + } + + async addTest(req: Request, res: Response, next: NextFunction): Promise { + const test = req.body as Test; + try { + const created = await this.controller.addTest(String(req.params.id), test); + res.status(201).json(created); + } catch (error) { + sendError(error, res, next); + } + } + + async updateTest(req: Request, res: Response, next: NextFunction): Promise { + const test: Test = { + id: String(req.params.testID), + date: req.body?.date, + type: req.body?.type, + result: req.body?.result, + score: Number.parseFloat(req.body?.score), + comments: req.body?.comments, + }; + + try { + const updated = await this.controller.updateTest(String(req.params.id), test); + res.status(200).json(updated); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteTest(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.controller.deleteTest(String(req.params.id), String(req.params.testID)); + res.status(204).end(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/TraineeHandler.ts b/server/src/handlers/TraineeHandler.ts new file mode 100644 index 00000000..72d0f8a8 --- /dev/null +++ b/server/src/handlers/TraineeHandler.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { TraineeControllerType } from '../controllers/Trainee/TraineeController'; +import { AuthenticatedUser } from '../models'; +import { sendError } from './Handler'; + +export interface TraineeHandlerType { + getTrainee(req: Request, res: Response, next: NextFunction): Promise; + createTrainee(req: Request, res: Response, next: NextFunction): Promise; + updateTrainee(req: Request, res: Response, next: NextFunction): Promise; + deleteTrainee(req: Request, res: Response, next: NextFunction): Promise; +} + +export class TraineeHandler implements TraineeHandlerType { + constructor(private readonly controller: TraineeControllerType) {} + + async getTrainee(req: Request, res: Response, next: NextFunction): Promise { + try { + const trainee = await this.controller.getTrainee(String(req.params.id)); + res.status(200).json(trainee); + } catch (error) { + sendError(error, res, next); + } + } + + async createTrainee(req: Request, res: Response, next: NextFunction): Promise { + try { + const trainee = await this.controller.createTrainee(req.body); + res.status(201).json(trainee); + } catch (error) { + sendError(error, res, next); + } + } + + async updateTrainee(req: Request, res: Response, next: NextFunction): Promise { + const actor = res.locals.user as AuthenticatedUser; + try { + const trainee = await this.controller.updateTrainee(String(req.params.id), req.body, actor); + res.status(200).json(trainee); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteTrainee(req: Request, res: Response, next: NextFunction): Promise { + try { + await this.controller.deleteTrainee(String(req.params.id)); + res.status(204).end(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/UserHandler.ts b/server/src/handlers/UserHandler.ts new file mode 100644 index 00000000..d242a505 --- /dev/null +++ b/server/src/handlers/UserHandler.ts @@ -0,0 +1,66 @@ +import { Request, Response, NextFunction } from 'express'; +import z from 'zod'; +import { UserControllerType } from '../controllers/UserController'; +import { CreateUserSchema, UpdateUserSchema } from '../models'; +import { sendError } from './Handler'; + +export interface UserHandlerType { + getAllUsers(req: Request, res: Response, next: NextFunction): Promise; + createUser(req: Request, res: Response, next: NextFunction): Promise; + updateUser(req: Request, res: Response, next: NextFunction): Promise; + deleteUser(req: Request, res: Response, next: NextFunction): Promise; +} + +export class UserHandler implements UserHandlerType { + constructor(private readonly controller: UserControllerType) {} + + async getAllUsers(_req: Request, res: Response, next: NextFunction): Promise { + try { + const users = await this.controller.getAllUsers(); + res.status(200).json(users); + } catch (error) { + sendError(error, res, next); + } + } + + async createUser(req: Request, res: Response, next: NextFunction): Promise { + const parsed = CreateUserSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json(z.flattenError(parsed.error)); + return; + } + + try { + const createdUser = await this.controller.createUser(parsed.data); + res.status(201).json(createdUser); + } catch (error) { + sendError(error, res, next); + } + } + + async updateUser(req: Request, res: Response, next: NextFunction): Promise { + const userId = String(req.params.id); + const parsed = UpdateUserSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json(z.flattenError(parsed.error)); + return; + } + + try { + const updatedUser = await this.controller.updateUser(userId, parsed.data); + res.status(200).json(updatedUser); + } catch (error) { + sendError(error, res, next); + } + } + + async deleteUser(req: Request, res: Response, next: NextFunction): Promise { + const userId = String(req.params.id); + try { + await this.controller.deleteUser(userId); + res.status(204).send(); + } catch (error) { + sendError(error, res, next); + } + } +} diff --git a/server/src/handlers/index.ts b/server/src/handlers/index.ts new file mode 100644 index 00000000..d29a3ab7 --- /dev/null +++ b/server/src/handlers/index.ts @@ -0,0 +1,14 @@ +export * from './Handler'; +export * from './AuthenticationHandler'; +export * from './SearchHandler'; +export * from './GeographyHandler'; +export * from './DashboardHandler'; +export * from './UserHandler'; +export * from './CohortsHandler'; +export * from './TraineeHandler'; +export * from './InteractionHandler'; +export * from './TestHandler'; +export * from './StrikeHandler'; +export * from './EmploymentHistoryHandler'; +export * from './LetterHandler'; +export * from './ProfilePictureHandler'; diff --git a/server/src/index.ts b/server/src/index.ts index 6d9f7112..d1a5afb1 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -32,9 +32,24 @@ import { ProfilePictureController, EmploymentHistoryController, StrikeController, - DefaultUserController, + UserController, LetterController, } from './controllers'; +import { + AuthenticationHandler, + CohortsHandler, + DashboardHandler, + EmploymentHistoryHandler, + GeographyHandler, + InteractionHandler, + LetterHandler, + ProfilePictureHandler, + SearchHandler, + StrikeHandler, + TestHandler, + TraineeHandler, + UserHandler, +} from './handlers'; import { MongooseTraineesRepository, MongooseUserRepository, MongooseGeographyRepository } from './repositories'; import { GoogleOAuthService, @@ -86,8 +101,9 @@ class Main { } const tokenExpirationInDays = Number.parseInt(process.env.JWT_EXPIRATION_DAYS ?? '7'); + const storageBaseURL = process.env.STORAGE_BASE_URL ?? ''; - // Dependencies + // Services const googleOAuthService = new GoogleOAuthService( process.env.GOOGLE_OAUTH_CLIENTID ?? '', process.env.GOOGLE_OAUTH_CLIENTSECRET ?? '' @@ -114,24 +130,16 @@ class Main { const pdfService = new PDFService(pdfServiceURL); console.log(`✅ PDF converter URL: ${pdfServiceURL}`); const letterGenerator = new LetterGenerator(pdfService, storageService); + + // Repositories const userRepository = new MongooseUserRepository(this.db); const traineesRepository = new MongooseTraineesRepository(this.db); const geographyRepository = new MongooseGeographyRepository(this.db); - // setup controllers - const authenticationController = new AuthenticationController( - userRepository, - googleOAuthService, - tokenService, - tokenExpirationInDays - ); + // Controllers (pure) + const authenticationController = new AuthenticationController(userRepository, googleOAuthService, tokenService); const traineeController = new TraineeController(traineesRepository, notificationService); - const profilePictureController = new ProfilePictureController( - traineesRepository, - storageService, - uploadService, - imageService - ); + const profilePictureController = new ProfilePictureController(traineesRepository, storageService, imageService); const interactionController = new InteractionController(traineesRepository, userRepository, notificationService); const testController = new TestController(traineesRepository, notificationService); const employmentHistoryController = new EmploymentHistoryController(traineesRepository); @@ -140,29 +148,44 @@ class Main { const geographyController = new GeographyController(geographyRepository); const dashboardController = new DashboardController(); const cohortsController = new CohortsController(traineesRepository); - const userController = new DefaultUserController(userRepository); + const userController = new UserController(userRepository); const letterController = new LetterController(letterGenerator, traineesRepository); - // Setup custom middlewares + // Custom middlewares const authMiddleware = new AuthMiddleware(tokenService); - // Setup routers - const authenticationRouter = new AuthenticationRouter(authenticationController, authMiddleware); + // Handlers (HTTP layer) + const authenticationHandler = new AuthenticationHandler(authenticationController, tokenExpirationInDays); + const traineeHandler = new TraineeHandler(traineeController); + const interactionHandler = new InteractionHandler(interactionController); + const testHandler = new TestHandler(testController); + const strikeHandler = new StrikeHandler(strikeController); + const employmentHistoryHandler = new EmploymentHistoryHandler(employmentHistoryController); + const profilePictureHandler = new ProfilePictureHandler(profilePictureController, uploadService, storageBaseURL); + const letterHandler = new LetterHandler(letterController); + const searchHandler = new SearchHandler(searchController); + const geographyHandler = new GeographyHandler(geographyController); + const dashboardHandler = new DashboardHandler(dashboardController); + const cohortsHandler = new CohortsHandler(cohortsController); + const userHandler = new UserHandler(userController); + + // Routers + const authenticationRouter = new AuthenticationRouter(authenticationHandler, authMiddleware); const traineeRouter = new TraineesRouter( - traineeController, - interactionController, - testController, - profilePictureController, - strikeController, - employmentHistoryController, - letterController, + traineeHandler, + interactionHandler, + testHandler, + profilePictureHandler, + strikeHandler, + employmentHistoryHandler, + letterHandler, [authMiddleware] ); - const searchRouter = new SearchRouter(searchController, [authMiddleware]); - const geographyRouter = new GeographyRouter(geographyController, [authMiddleware]); - const dashboardRouter = new DashboardRouter(dashboardController, [authMiddleware]); - const cohortsRouter = new CohortsRouter(cohortsController, [authMiddleware]); - const userRouter = new UserRouter(userController, [authMiddleware]); + const searchRouter = new SearchRouter(searchHandler, [authMiddleware]); + const geographyRouter = new GeographyRouter(geographyHandler, [authMiddleware]); + const dashboardRouter = new DashboardRouter(dashboardHandler, [authMiddleware]); + const cohortsRouter = new CohortsRouter(cohortsHandler, [authMiddleware]); + const userRouter = new UserRouter(userHandler, [authMiddleware]); // Define routes this.app.use('/api/auth', authenticationRouter.build()); diff --git a/server/src/middlewares/AuthMiddleware.ts b/server/src/middlewares/AuthMiddleware.ts index 4f314d94..256e57df 100644 --- a/server/src/middlewares/AuthMiddleware.ts +++ b/server/src/middlewares/AuthMiddleware.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { ResponseError } from '../models'; -import { TOKEN_COOKIE_NAME } from '../controllers/AuthenticationController'; +import { TOKEN_COOKIE_NAME } from '../handlers/AuthenticationHandler'; import { TokenService } from '../services/TokenService'; import Middleware from './Middleware'; import * as Sentry from '@sentry/node'; diff --git a/server/src/routes/AuthenticationRouter.ts b/server/src/routes/AuthenticationRouter.ts index f71608ba..363e895b 100644 --- a/server/src/routes/AuthenticationRouter.ts +++ b/server/src/routes/AuthenticationRouter.ts @@ -1,19 +1,15 @@ import { Router } from 'express'; import RouterType from './Router'; -import { AuthenticationControllerType } from '../controllers'; +import { AuthenticationHandlerType } from '../handlers/AuthenticationHandler'; import Middleware from '../middlewares/Middleware'; export class AuthenticationRouter implements RouterType { - private readonly authenticationController: AuthenticationControllerType; + private readonly handler: AuthenticationHandlerType; private readonly authMiddleware: Middleware; private readonly middlewares: Middleware[]; - constructor( - authenticationController: AuthenticationControllerType, - authMiddleware: Middleware, - middlewares: Middleware[] = [] - ) { - this.authenticationController = authenticationController; + constructor(handler: AuthenticationHandlerType, authMiddleware: Middleware, middlewares: Middleware[] = []) { + this.handler = handler; this.middlewares = middlewares; this.authMiddleware = authMiddleware; } @@ -21,16 +17,16 @@ export class AuthenticationRouter implements RouterType { build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.post('/login', this.authenticationController.login.bind(this.authenticationController)); + router.post('/login', this.handler.login.bind(this.handler)); router.post( '/logout', this.authMiddleware.handle.bind(this.authMiddleware), - this.authenticationController.logout.bind(this.authenticationController) + this.handler.logout.bind(this.handler) ); router.get( '/session', this.authMiddleware.handle.bind(this.authMiddleware), - this.authenticationController.getSession.bind(this.authenticationController) + this.handler.getSession.bind(this.handler) ); return router; } diff --git a/server/src/routes/CohortsRouter.ts b/server/src/routes/CohortsRouter.ts index c8a96940..26ff77b8 100644 --- a/server/src/routes/CohortsRouter.ts +++ b/server/src/routes/CohortsRouter.ts @@ -1,21 +1,21 @@ import { Router } from 'express'; import RouterType from './Router'; -import { CohortsControllerType } from '../controllers/CohortsController'; +import { CohortsHandlerType } from '../handlers/CohortsHandler'; import Middleware from '../middlewares/Middleware'; export class CohortsRouter implements RouterType { - private readonly cohortsController: CohortsControllerType; + private readonly cohortsHandler: CohortsHandlerType; private readonly middlewares: Middleware[]; - constructor(cohortsController: CohortsControllerType, middlewares: Middleware[] = []) { - this.cohortsController = cohortsController; + constructor(cohortsHandler: CohortsHandlerType, middlewares: Middleware[] = []) { + this.cohortsHandler = cohortsHandler; this.middlewares = middlewares; } build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.get('/', this.cohortsController.getCohorts.bind(this.cohortsController)); + router.get('/', this.cohortsHandler.getCohorts.bind(this.cohortsHandler)); return router; } } diff --git a/server/src/routes/DashboardRouter.ts b/server/src/routes/DashboardRouter.ts index 22607843..2f2e804e 100644 --- a/server/src/routes/DashboardRouter.ts +++ b/server/src/routes/DashboardRouter.ts @@ -1,21 +1,21 @@ import { Router } from 'express'; import RouterType from './Router'; -import { DashboardControllerType } from '../controllers/DashboardController'; +import { DashboardHandlerType } from '../handlers/DashboardHandler'; import Middleware from '../middlewares/Middleware'; export class DashboardRouter implements RouterType { - private readonly dashboardController: DashboardControllerType; + private readonly handler: DashboardHandlerType; private readonly middlewares: Middleware[]; - constructor(dashboardController: DashboardControllerType, middlewares: Middleware[] = []) { - this.dashboardController = dashboardController; + constructor(handler: DashboardHandlerType, middlewares: Middleware[] = []) { + this.handler = handler; this.middlewares = middlewares; } build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.get('/', this.dashboardController.getDashboard.bind(this.dashboardController)); + router.get('/', this.handler.getDashboard.bind(this.handler)); return router; } } diff --git a/server/src/routes/GeographyRouter.ts b/server/src/routes/GeographyRouter.ts index 22f10464..30b151cf 100644 --- a/server/src/routes/GeographyRouter.ts +++ b/server/src/routes/GeographyRouter.ts @@ -1,22 +1,22 @@ import { Router } from 'express'; import RouterType from './Router'; -import { GeographyControllerType } from '../controllers'; +import { GeographyHandlerType } from '../handlers/GeographyHandler'; import Middleware from '../middlewares/Middleware'; export class GeographyRouter implements RouterType { - private readonly geographyController: GeographyControllerType; + private readonly handler: GeographyHandlerType; private readonly middlewares: Middleware[]; - constructor(geographyController: GeographyControllerType, middlewares: Middleware[] = []) { - this.geographyController = geographyController; + constructor(handler: GeographyHandlerType, middlewares: Middleware[] = []) { + this.handler = handler; this.middlewares = middlewares; } build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.get('/cities', this.geographyController.getCities.bind(this.geographyController)); - router.get('/countries', this.geographyController.getCountries.bind(this.geographyController)); + router.get('/cities', this.handler.getCities.bind(this.handler)); + router.get('/countries', this.handler.getCountries.bind(this.handler)); return router; } } diff --git a/server/src/routes/SearchRouter.ts b/server/src/routes/SearchRouter.ts index d54e47c4..b00d28ff 100644 --- a/server/src/routes/SearchRouter.ts +++ b/server/src/routes/SearchRouter.ts @@ -1,21 +1,21 @@ import { Router } from 'express'; import RouterType from './Router'; -import { SearchControllerType } from '../controllers/SearchController'; +import { SearchHandlerType } from '../handlers/SearchHandler'; import Middleware from '../middlewares/Middleware'; export class SearchRouter implements RouterType { - private readonly searchController: SearchControllerType; + private readonly handler: SearchHandlerType; private readonly middlewares: Middleware[]; - constructor(searchController: SearchControllerType, middlewares: Middleware[] = []) { - this.searchController = searchController; + constructor(handler: SearchHandlerType, middlewares: Middleware[] = []) { + this.handler = handler; this.middlewares = middlewares; } build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.get('/', this.searchController.search.bind(this.searchController)); + router.get('/', this.handler.search.bind(this.handler)); return router; } } diff --git a/server/src/routes/TraineesRouter.ts b/server/src/routes/TraineesRouter.ts index 3f31c9f5..bc5f0c23 100644 --- a/server/src/routes/TraineesRouter.ts +++ b/server/src/routes/TraineesRouter.ts @@ -1,84 +1,85 @@ import { Router } from 'express'; import RouterType from './Router'; import { - TraineeControllerType, - InteractionControllerType, - TestControllerType, - ProfilePictureControllerType, - StrikeControllerType, - LetterControllerType, -} from '../controllers'; + TraineeHandlerType, + InteractionHandlerType, + TestHandlerType, + ProfilePictureHandlerType, + StrikeHandlerType, + EmploymentHistoryHandlerType, + LetterHandlerType, +} from '../handlers'; import Middleware from '../middlewares/Middleware'; -import { EmploymentHistoryControllerType } from '../controllers/Trainee/EmploymentHistoryController'; export class TraineesRouter implements RouterType { constructor( - private readonly traineeController: TraineeControllerType, - private readonly interactionController: InteractionControllerType, - private readonly testController: TestControllerType, - private readonly profilePictureController: ProfilePictureControllerType, - private readonly strikeController: StrikeControllerType, - private readonly employmentHistoryController: EmploymentHistoryControllerType, - private readonly letterController: LetterControllerType, + private readonly traineeHandler: TraineeHandlerType, + private readonly interactionHandler: InteractionHandlerType, + private readonly testHandler: TestHandlerType, + private readonly profilePictureHandler: ProfilePictureHandlerType, + private readonly strikeHandler: StrikeHandlerType, + private readonly employmentHistoryHandler: EmploymentHistoryHandlerType, + private readonly letterHandler: LetterHandlerType, private readonly middlewares: Middleware[] = [] ) {} build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.post('/', this.traineeController.createTrainee.bind(this.traineeController)); - router.get('/:id', this.traineeController.getTrainee.bind(this.traineeController)); - router.patch('/:id', this.traineeController.updateTrainee.bind(this.traineeController)); - router.delete('/:id', this.traineeController.deleteTrainee.bind(this.traineeController)); + + router.post('/', this.traineeHandler.createTrainee.bind(this.traineeHandler)); + router.get('/:id', this.traineeHandler.getTrainee.bind(this.traineeHandler)); + router.patch('/:id', this.traineeHandler.updateTrainee.bind(this.traineeHandler)); + router.delete('/:id', this.traineeHandler.deleteTrainee.bind(this.traineeHandler)); router.put( '/:id/profile-picture', - this.profilePictureController.setProfilePicture.bind(this.profilePictureController) + this.profilePictureHandler.setProfilePicture.bind(this.profilePictureHandler) ); router.delete( '/:id/profile-picture', - this.profilePictureController.deleteProfilePicture.bind(this.profilePictureController) + this.profilePictureHandler.deleteProfilePicture.bind(this.profilePictureHandler) ); - router.get('/:id/strikes', this.strikeController.getStrikes.bind(this.strikeController)); - router.post('/:id/strikes', this.strikeController.addStrike.bind(this.strikeController)); - router.put('/:id/strikes/:strikeId', this.strikeController.updateStrike.bind(this.strikeController)); - router.delete('/:id/strikes/:strikeId', this.strikeController.deleteStrike.bind(this.strikeController)); + router.get('/:id/strikes', this.strikeHandler.getStrikes.bind(this.strikeHandler)); + router.post('/:id/strikes', this.strikeHandler.addStrike.bind(this.strikeHandler)); + router.put('/:id/strikes/:strikeId', this.strikeHandler.updateStrike.bind(this.strikeHandler)); + router.delete('/:id/strikes/:strikeId', this.strikeHandler.deleteStrike.bind(this.strikeHandler)); - router.get('/:id/interactions', this.interactionController.getInteractions.bind(this.interactionController)); - router.post('/:id/interactions', this.interactionController.addInteraction.bind(this.interactionController)); + router.get('/:id/interactions', this.interactionHandler.getInteractions.bind(this.interactionHandler)); + router.post('/:id/interactions', this.interactionHandler.addInteraction.bind(this.interactionHandler)); router.put( '/:id/interactions/:interactionID', - this.interactionController.updateInteraction.bind(this.interactionController) + this.interactionHandler.updateInteraction.bind(this.interactionHandler) ); router.delete( '/:id/interactions/:interactionID', - this.interactionController.deleteInteraction.bind(this.interactionController) + this.interactionHandler.deleteInteraction.bind(this.interactionHandler) ); - router.get('/:id/tests', this.testController.getTests.bind(this.testController)); - router.post('/:id/tests', this.testController.addTest.bind(this.testController)); - router.put('/:id/tests/:testID', this.testController.updateTest.bind(this.testController)); - router.delete('/:id/tests/:testID', this.testController.deleteTest.bind(this.testController)); + router.get('/:id/tests', this.testHandler.getTests.bind(this.testHandler)); + router.post('/:id/tests', this.testHandler.addTest.bind(this.testHandler)); + router.put('/:id/tests/:testID', this.testHandler.updateTest.bind(this.testHandler)); + router.delete('/:id/tests/:testID', this.testHandler.deleteTest.bind(this.testHandler)); router.get( '/:id/employment-history', - this.employmentHistoryController.getEmploymentHistory.bind(this.employmentHistoryController) + this.employmentHistoryHandler.getEmploymentHistory.bind(this.employmentHistoryHandler) ); router.post( '/:id/employment-history', - this.employmentHistoryController.addEmploymentHistory.bind(this.employmentHistoryController) + this.employmentHistoryHandler.addEmploymentHistory.bind(this.employmentHistoryHandler) ); router.put( '/:id/employment-history/:employmentHistoryID', - this.employmentHistoryController.updateEmploymentHistory.bind(this.employmentHistoryController) + this.employmentHistoryHandler.updateEmploymentHistory.bind(this.employmentHistoryHandler) ); router.delete( '/:id/employment-history/:employmentHistoryID', - this.employmentHistoryController.deleteEmploymentHistory.bind(this.employmentHistoryController) + this.employmentHistoryHandler.deleteEmploymentHistory.bind(this.employmentHistoryHandler) ); - router.post('/:id/letters', this.letterController.generateLetter.bind(this.letterController)); + router.post('/:id/letters', this.letterHandler.generateLetter.bind(this.letterHandler)); return router; } } diff --git a/server/src/routes/UserRouter.ts b/server/src/routes/UserRouter.ts index beb10213..1495e076 100644 --- a/server/src/routes/UserRouter.ts +++ b/server/src/routes/UserRouter.ts @@ -1,25 +1,22 @@ import { Router } from 'express'; import Middleware from '../middlewares/Middleware'; - -import type { UserController } from '../controllers'; +import type { UserHandlerType } from '../handlers/UserHandler'; import type RouterType from './Router'; export class UserRouter implements RouterType { constructor( - private readonly userController: UserController, + private readonly handler: UserHandlerType, private readonly middlewares: Middleware[] - ) { - this.build = this.build.bind(this); - } + ) {} - build() { + build(): Router { const router = Router(); this.middlewares.forEach((middleware) => router.use(middleware.handle.bind(middleware))); - router.get('/', this.userController.getAllUsers); - router.post('/', this.userController.createUser); - router.put('/:id', this.userController.updateUser); - router.delete('/:id', this.userController.deleteUser); + router.get('/', this.handler.getAllUsers.bind(this.handler)); + router.post('/', this.handler.createUser.bind(this.handler)); + router.put('/:id', this.handler.updateUser.bind(this.handler)); + router.delete('/:id', this.handler.deleteUser.bind(this.handler)); return router; }