diff --git a/server/eslint.config.js b/server/eslint.config.js index 52e292ef..cbf803e0 100644 --- a/server/eslint.config.js +++ b/server/eslint.config.js @@ -5,7 +5,7 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( { - ignores: ['dist', 'node_modules'], + ignores: ['dist', 'node_modules', 'scripts', 'eslint.config.js'], }, eslint.configs.recommended, tseslint.configs.strictTypeChecked, @@ -22,7 +22,7 @@ export default tseslint.config( rules: { '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], }, } ); diff --git a/server/src/api-docs/swagger.ts b/server/src/api-docs/swagger.ts index 3dc496ec..90536302 100644 --- a/server/src/api-docs/swagger.ts +++ b/server/src/api-docs/swagger.ts @@ -1,4 +1,4 @@ -import swaggerUi from 'swagger-ui-express'; +import swaggerUi, { JsonObject } from 'swagger-ui-express'; import fs from 'fs'; import YAML from 'yaml'; import { RequestHandler } from 'express'; @@ -9,8 +9,8 @@ import { RequestHandler } from 'express'; * @param {string} documentPath The file path to the Swagger YAML document. * @returns {Array} An array for serving and setting up Swagger UI. * */ -export default (documentPath: string): Array => { +export default (documentPath: string): RequestHandler[] => { const file = fs.readFileSync(documentPath, 'utf8'); - const swaggerDocument = YAML.parse(file); + const swaggerDocument = YAML.parse(file) as JsonObject; return swaggerUi.serve.concat(swaggerUi.setup(swaggerDocument)); }; diff --git a/server/src/controllers/AuthenticationController.ts b/server/src/controllers/AuthenticationController.ts index 3e0f6456..acdedb2e 100644 --- a/server/src/controllers/AuthenticationController.ts +++ b/server/src/controllers/AuthenticationController.ts @@ -60,10 +60,10 @@ export class AuthenticationController implements AuthenticationControllerType { // 3. Revoke Google OAuth token - we don't use it anymore. // Do not wait for the response. Continue with login process. - this.googleOAuthService.revokeToken(googleAccessToken); + await this.googleOAuthService.revokeToken(googleAccessToken); googleAccessToken = ''; - if (!oauthUser || !oauthUser.emailVerified) { + if (!oauthUser.emailVerified) { throw new Error('Could not verify google auth code'); } } catch (error) { @@ -74,7 +74,7 @@ export class AuthenticationController implements AuthenticationControllerType { // Check if the user is allowed to access to the system const user = await this.userRepository.findUserByEmail(oauthUser.email); - if (!user || !user.isActive) { + if (!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'); @@ -96,11 +96,13 @@ export class AuthenticationController implements AuthenticationControllerType { res.status(200).json(user); } + // eslint-disable-next-line @typescript-eslint/require-await async getSession(req: Request, res: Response): Promise { const user = res.locals.user as AuthenticatedUser; res.status(200).json(user); } + // eslint-disable-next-line @typescript-eslint/require-await async logout(req: Request, res: Response): Promise { res.clearCookie(TOKEN_COOKIE_NAME); res.status(204).end(); @@ -112,7 +114,7 @@ export class AuthenticationController implements AuthenticationControllerType { try { const url = new URL(redirectURI); return allowedHosts.includes(url.hostname); - } catch (error) { + } catch { return false; } } diff --git a/server/src/controllers/DashboardController.ts b/server/src/controllers/DashboardController.ts index f5c9b0e5..8d464903 100644 --- a/server/src/controllers/DashboardController.ts +++ b/server/src/controllers/DashboardController.ts @@ -15,6 +15,7 @@ export class DashboardController implements DashboardControllerType { /** * Retrieve the dashboard data. */ + // eslint-disable-next-line @typescript-eslint/require-await async getDashboard(req: Request, res: Response) { const response = { demographics: { diff --git a/server/src/controllers/GeographyController.ts b/server/src/controllers/GeographyController.ts index 88877cf4..ec730333 100644 --- a/server/src/controllers/GeographyController.ts +++ b/server/src/controllers/GeographyController.ts @@ -12,29 +12,23 @@ export class GeographyController implements GeographyControllerType { this.geographyRepository = geographyRepository; } - async getCountries(req: Request, res: Response, next: NextFunction): Promise { + async getCountries(req: Request, res: Response): 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); - } + + const countries = await this.geographyRepository.searchCountry(query, limit); + const jsonResponse = countries.map(({ name, flag }) => { + return { name, flag }; + }); + res.json(jsonResponse); } - async getCities(req: Request, res: Response, next: NextFunction): Promise { + async getCities(req: Request, res: Response): 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); - } + + const cities = await this.geographyRepository.searchCity(query, limit); + const jsonResponse = cities.map((city) => city.name); + res.json(jsonResponse); } } diff --git a/server/src/controllers/SearchController.ts b/server/src/controllers/SearchController.ts index 8c50b7c5..1110f26c 100644 --- a/server/src/controllers/SearchController.ts +++ b/server/src/controllers/SearchController.ts @@ -31,19 +31,15 @@ export class SearchController implements SearchControllerType { this.traineesRepository = traineesRepository; } - async search(req: Request, res: Response, next: NextFunction) { + async search(req: Request, res: Response) { 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; - } + + results = await this.getSearchResults(searchQuery, limit); const response: SearchResponse = { hits: { @@ -75,7 +71,7 @@ export class SearchController implements SearchControllerType { .map((trainee) => { return { id: trainee.id, - name: `${trainee.displayName}`, + name: trainee.displayName, thumbnail: trainee.thumbnailURL ?? null, cohort: trainee.educationInfo.currentCohort ?? null, profilePath: trainee.profilePath, diff --git a/server/src/controllers/Trainee/EmploymentHistoryController.ts b/server/src/controllers/Trainee/EmploymentHistoryController.ts index 599e5d00..98b829ab 100644 --- a/server/src/controllers/Trainee/EmploymentHistoryController.ts +++ b/server/src/controllers/Trainee/EmploymentHistoryController.ts @@ -12,35 +12,27 @@ export interface EmploymentHistoryControllerType { export class EmploymentHistoryController implements EmploymentHistoryControllerType { constructor(private readonly traineesRepository: TraineesRepository) {} - async getEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { - try { - const traineeID = req.params.id; - const employmentHistory = await this.traineesRepository.getEmploymentHistory(traineeID); - res.json(employmentHistory); - } catch (error: any) { - next(error); - } + async getEmploymentHistory(req: Request, res: Response): Promise { + const traineeID = req.params.id; + const employmentHistory = await this.traineesRepository.getEmploymentHistory(traineeID); + res.json(employmentHistory); } - async addEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { + async addEmploymentHistory(req: Request, res: Response): Promise { const traineeID = req.params.id; const employmentHistoryData: EmploymentHistory = req.body; try { validateEmploymentHistory(employmentHistoryData); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) 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); - } + const newEmploymentHistory = await this.traineesRepository.addEmploymentHistory(traineeID, employmentHistoryData); + res.status(201).json(newEmploymentHistory); } - async updateEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { + async updateEmploymentHistory(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -69,41 +61,30 @@ export class EmploymentHistoryController implements EmploymentHistoryControllerT try { validateEmploymentHistory(historyToUpdate); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - try { - const updatedEmploymentHistory = await this.traineesRepository.updateEmploymentHistory( - trainee.id, - historyToUpdate - ); + const updatedEmploymentHistory = await this.traineesRepository.updateEmploymentHistory(trainee.id, historyToUpdate); - res.json(updatedEmploymentHistory); - } catch (error: any) { - next(error); - } + res.json(updatedEmploymentHistory); } - async deleteEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise { - try { - const trainee = await this.traineesRepository.getTrainee(req.params.id); - if (!trainee) { - res.status(404).send(new ResponseError('Trainee not found')); - return; - } - - const employmentHistoryID = 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(req: Request, res: Response): Promise { + const trainee = await this.traineesRepository.getTrainee(req.params.id); + if (!trainee) { + res.status(404).send(new ResponseError('Trainee not found')); + return; + } - await this.traineesRepository.deleteEmploymentHistory(trainee.id, employmentHistoryID); - res.status(204).send(); - } catch (error: any) { - next(error); + const employmentHistoryID = req.params.employmentHistoryID; + if (!trainee.employmentInfo.employmentHistory.find((history) => history.id === employmentHistoryID)) { + res.status(404).send(new ResponseError('Employment history not found')); + return; } + + await this.traineesRepository.deleteEmploymentHistory(trainee.id, employmentHistoryID); + res.status(204).send(); } } diff --git a/server/src/controllers/Trainee/InteractionController.ts b/server/src/controllers/Trainee/InteractionController.ts index c9243b2b..93afe4d5 100644 --- a/server/src/controllers/Trainee/InteractionController.ts +++ b/server/src/controllers/Trainee/InteractionController.ts @@ -17,22 +17,18 @@ export class InteractionController implements InteractionControllerType { private notificationService: NotificationService ) {} - async getInteractions(req: Request, res: Response, next: NextFunction): Promise { + async getInteractions(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); 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); - } + const interactions = await this.traineesRepository.getInteractions(trainee.id); + res.status(200).json(interactions); } - async addInteraction(req: Request, res: Response, next: NextFunction): Promise { + async addInteraction(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -51,21 +47,17 @@ export class InteractionController implements InteractionControllerType { if (!reporter) { throw new Error(`Invalid reporter ID ${reporterID}. User not found.`); } - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - try { - const interaction = await this.traineesRepository.addInteraction(req.params.id, newInteraction); - res.status(201).json(interaction); - this.notificationService.interactionCreated(trainee, interaction); - } catch (error: any) { - next(error); - } + const interaction = await this.traineesRepository.addInteraction(req.params.id, newInteraction); + res.status(201).json(interaction); + await this.notificationService.interactionCreated(trainee, interaction); } - async updateInteraction(req: Request, res: Response, next: NextFunction): Promise { + async updateInteraction(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -91,22 +83,16 @@ export class InteractionController implements InteractionControllerType { // Validate new interaction model after applying the changes try { validateInteraction(interactionToUpdate); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - try { - const updatedInteraction = await this.traineesRepository.updateInteraction(req.params.id, interactionToUpdate); - res.status(200).json(updatedInteraction); - } catch (error: any) { - console.error(error); - next(error); - return; - } + const updatedInteraction = await this.traineesRepository.updateInteraction(req.params.id, interactionToUpdate); + res.status(200).json(updatedInteraction); } - async deleteInteraction(req: Request, res: Response, next: NextFunction): Promise { + async deleteInteraction(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -118,12 +104,7 @@ export class InteractionController implements InteractionControllerType { return; } - try { - await this.traineesRepository.deleteInteraction(req.params.id, req.params.interactionID); - res.status(204).end(); - } catch (error: any) { - next(error); - return; - } + await this.traineesRepository.deleteInteraction(req.params.id, req.params.interactionID); + res.status(204).end(); } } diff --git a/server/src/controllers/Trainee/ProfilePictureController.ts b/server/src/controllers/Trainee/ProfilePictureController.ts index 05977c34..fe55705e 100644 --- a/server/src/controllers/Trainee/ProfilePictureController.ts +++ b/server/src/controllers/Trainee/ProfilePictureController.ts @@ -43,7 +43,7 @@ export class ProfilePictureController implements ProfilePictureControllerType { // Handle file upload. try { await this.uploadService.uploadImage(req, res, 'profilePicture'); - } catch (error: any) { + } catch (error) { if (error instanceof UploadServiceError) { res.status(400).send(new ResponseError(error.message)); } else { @@ -60,35 +60,26 @@ export class ProfilePictureController implements ProfilePictureControllerType { 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(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.imageService.resizeImage(originalFilePath, largeFilePath, 700, 700); + await this.imageService.resizeImage(largeFilePath, smallFilePath, 70, 70); + + // 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; + await this.traineesRepository.updateTrainee(trainee); + res.status(201).send({ imageURL, thumbnailURL }); // Cleanup fs.unlink(originalFilePath, (error) => { @@ -114,18 +105,14 @@ export class ProfilePictureController implements ProfilePictureControllerType { 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; - } + + 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; + await this.traineesRepository.updateTrainee(trainee); res.status(204).end(); } diff --git a/server/src/controllers/Trainee/StrikeController.ts b/server/src/controllers/Trainee/StrikeController.ts index 80c30e01..1a70a7e3 100644 --- a/server/src/controllers/Trainee/StrikeController.ts +++ b/server/src/controllers/Trainee/StrikeController.ts @@ -19,22 +19,18 @@ export class StrikeController implements StrikeControllerType { private readonly notificationService: NotificationService ) {} - async getStrikes(req: Request, res: Response, next: NextFunction) { + async getStrikes(req: Request, res: Response) { const trainee = await this.traineesRepository.getTrainee(req.params.id); 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); - } + const strikes = await this.traineesRepository.getStrikes(trainee.id); + res.status(200).json(strikes); } - async addStrike(req: Request, res: Response, next: NextFunction): Promise { + async addStrike(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -53,21 +49,17 @@ export class StrikeController implements StrikeControllerType { if (!reporter) { throw new Error(`Invalid reporter ID ${reporterID}. User not found.`); } - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - try { - const strike = await this.traineesRepository.addStrike(req.params.id, newStrike); - res.status(201).json(strike); - this.notificationService.strikeCreated(trainee, strike); - } catch (error: any) { - next(error); - } + const strike = await this.traineesRepository.addStrike(req.params.id, newStrike); + res.status(201).json(strike); + await this.notificationService.strikeCreated(trainee, strike); } - async updateStrike(req: Request, res: Response, next: NextFunction): Promise { + async updateStrike(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -92,32 +84,23 @@ export class StrikeController implements StrikeControllerType { // Validate new strike model after applying the changes try { validateStrike(strikeToUpdate); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - try { - const updatedStrike = await this.traineesRepository.updateStrike(req.params.id, strikeToUpdate); - res.status(200).json(updatedStrike); - } catch (error: any) { - next(error); - return; - } + const updatedStrike = await this.traineesRepository.updateStrike(req.params.id, strikeToUpdate); + res.status(200).json(updatedStrike); } - async deleteStrike(req: Request, res: Response, next: NextFunction): Promise { + async deleteStrike(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); return; } - try { - await this.traineesRepository.deleteStrike(req.params.id, req.params.strikeId); - res.status(204).end(); - } catch (error: any) { - next(error); - return; - } + + await this.traineesRepository.deleteStrike(req.params.id, req.params.strikeId); + res.status(204).end(); } } diff --git a/server/src/controllers/Trainee/TestController.ts b/server/src/controllers/Trainee/TestController.ts index 3a7fcfcd..5c01a4e6 100644 --- a/server/src/controllers/Trainee/TestController.ts +++ b/server/src/controllers/Trainee/TestController.ts @@ -16,22 +16,18 @@ export class TestController implements TestControllerType { private readonly notificationService: NotificationService ) {} - async getTests(req: Request, res: Response, next: NextFunction): Promise { + async getTests(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); 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); - } + const tests = await this.traineesRepository.getTests(trainee.id); + res.status(200).json(tests); } - async addTest(req: Request, res: Response, next: NextFunction): Promise { + async addTest(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -43,21 +39,17 @@ export class TestController implements TestControllerType { // Validate new test model before creation try { validateTest(newTest); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - 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 test = await this.traineesRepository.addTest(trainee.id, newTest); + res.status(201).json(test); + await this.notificationService.testCreated(trainee, test); } - async updateTest(req: Request, res: Response, next: NextFunction): Promise { + async updateTest(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -82,22 +74,16 @@ export class TestController implements TestControllerType { // Validate new test model after applying the changes try { validateTest(testToUpdate); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } - try { - const updatedTest = await this.traineesRepository.updateTest(trainee.id, testToUpdate); - res.status(200).json(updatedTest); - } catch (error: any) { - console.error(error); - next(error); - return; - } + const updatedTest = await this.traineesRepository.updateTest(trainee.id, testToUpdate); + res.status(200).json(updatedTest); } - async deleteTest(req: Request, res: Response, next: NextFunction): Promise { + async deleteTest(req: Request, res: Response): Promise { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -109,12 +95,7 @@ export class TestController implements TestControllerType { return; } - try { - await this.traineesRepository.deleteTest(req.params.id, req.params.testID); - res.status(204).end(); - } catch (error: any) { - next(error); - return; - } + await this.traineesRepository.deleteTest(req.params.id, req.params.testID); + res.status(204).end(); } } diff --git a/server/src/controllers/Trainee/TraineeController.ts b/server/src/controllers/Trainee/TraineeController.ts index 85f0f53c..31d2cb06 100644 --- a/server/src/controllers/Trainee/TraineeController.ts +++ b/server/src/controllers/Trainee/TraineeController.ts @@ -17,21 +17,17 @@ export class TraineeController implements TraineeControllerType { private readonly notificationService: NotificationService ) {} - async getTrainee(req: Request, res: Response, next: NextFunction) { + async getTrainee(req: Request, res: Response) { const traineeId = 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); + const trainee = await this.traineesRepository.getTrainee(traineeId); + if (!trainee) { + res.status(404).json({ error: 'Trainee was not found' }); + return; } + res.status(200).json(trainee); } - async createTrainee(req: Request, res: Response, next: NextFunction): Promise { + async createTrainee(req: Request, res: Response): Promise { const body = req.body; body.educationInfo = body.educationInfo ?? {}; body.employmentInfo = body.employmentInfo ?? {}; @@ -39,37 +35,29 @@ export class TraineeController implements TraineeControllerType { // Check if the request is valid try { validateTrainee(req.body); - } catch (error: any) { - const message: string = `Invalid trainee information. ` + error.message; + } catch (error) { + const message: string = + `Invalid trainee information. ` + (error instanceof Error ? error.message : String(error)); res.status(400).json(new ResponseError(message)); return; } // 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; - } + let emailExists = false; + emailExists = await this.traineesRepository.isEmailExists(email); if (emailExists) { - const message: string = `There is already another trainee in the system with the email ${email}`; + const message = `There is already another trainee in the system with the email ${email}`; res.status(400).json(new ResponseError(message)); return; } // 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)); - } + const newTrainee = await this.traineesRepository.createTrainee(req.body); + res.status(201).json(newTrainee); } - async updateTrainee(req: Request, res: Response, next: NextFunction) { + async updateTrainee(req: Request, res: Response) { const trainee = await this.traineesRepository.getTrainee(req.params.id); if (!trainee) { res.status(404).send(new ResponseError('Trainee not found')); @@ -82,22 +70,20 @@ export class TraineeController implements TraineeControllerType { // Validate new trainee model after applying the changes try { validateTrainee(trainee); - } catch (error: any) { - res.status(400).send(new ResponseError(error.message)); + } catch (error) { + if (error instanceof Error) res.status(400).send(new ResponseError(error.message)); return; } // 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); + res.status(200).json(trainee); + await this.notificationService.traineeUpdated(trainee, changes, res.locals.user as AuthenticatedUser); } - async deleteTrainee(req: Request, res: Response, next: NextFunction) { + // eslint-disable-next-line @typescript-eslint/require-await + async deleteTrainee(req: Request, res: Response) { res.status(500).send('Not implemented'); } @@ -106,7 +92,7 @@ export class TraineeController implements TraineeControllerType { private applyObjectUpdate( source: any, destination: any, - nestLevel: number = 0, + nestLevel = 0, changes: UpdateChange[] = [] ): UpdateChange[] { // safeguard against infinite recursion @@ -114,7 +100,7 @@ export class TraineeController implements TraineeControllerType { 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; } diff --git a/server/src/index.ts b/server/src/index.ts index 475012fe..a5434cd8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -5,7 +5,7 @@ import dotenv from 'dotenv'; import { initializeSentry, setupSentry } from './monitoring/Sentry'; initializeSentry(); -import express, { Request, Response, NextFunction } from 'express'; +import express, { Request, Response } from 'express'; import cors from 'cors'; import path from 'path'; import helmet from 'helmet'; @@ -187,7 +187,7 @@ class Main { setupSentry(this.app); // Global error handler - this.app.use((error: Error, req: Request, res: Response, next: NextFunction) => { + this.app.use((error: Error, req: Request, res: Response) => { if (this.isProduction) { res.status(500).json(new ResponseError('Something broke!')); } else { diff --git a/server/src/models/Interaction.ts b/server/src/models/Interaction.ts index 8a5186ab..6f4fc0d1 100644 --- a/server/src/models/Interaction.ts +++ b/server/src/models/Interaction.ts @@ -39,6 +39,8 @@ export const validateInteraction = (interaction: InteractionWithReporterID): voi throw new Error('Interaction details are required'); } if (!Object.values(InteractionType).includes(interaction.type)) { - throw new Error(`Unknown interaction type [${Object.values(InteractionType)}]`); + throw new Error( + `Unknown interaction type: ${interaction.type}. Allowed types: ${Object.values(InteractionType).join(', ')}` + ); } }; diff --git a/server/src/models/Trainee.ts b/server/src/models/Trainee.ts index da1a50b5..afc639d6 100644 --- a/server/src/models/Trainee.ts +++ b/server/src/models/Trainee.ts @@ -171,7 +171,7 @@ export interface TraineeEmploymentInfo { export const getDisplayName = (trainee: Trainee): string => { const { preferredName, firstName, lastName } = trainee.personalInfo; - const name: string = preferredName ? preferredName : firstName; + const name: string = preferredName ?? firstName; return `${name} ${lastName}`; }; diff --git a/server/src/repositories/TokenRepository.ts b/server/src/repositories/TokenRepository.ts index ad5a9d9f..4af1c3d7 100644 --- a/server/src/repositories/TokenRepository.ts +++ b/server/src/repositories/TokenRepository.ts @@ -15,7 +15,7 @@ export class MongooseTokenRepository implements TokenRepository { } async addToken(token: Token) { - this.TokenModel.create(token); + await this.TokenModel.create(token); } async findToken(payload: string): Promise { diff --git a/server/src/repositories/TraineesRepository.ts b/server/src/repositories/TraineesRepository.ts index ee2ee4a0..99435183 100644 --- a/server/src/repositories/TraineesRepository.ts +++ b/server/src/repositories/TraineesRepository.ts @@ -3,7 +3,7 @@ import { StrikeWithReporter, StrikeWithReporterID, Test, Trainee, EmploymentHist import { TraineeSchema } from '../schemas'; import { WithMongoID } from '../utils/database'; -import mongoose from 'mongoose'; +import mongoose, { RootFilterQuery } from 'mongoose'; export interface TraineesRepository { getAllTrainees(): Promise; @@ -53,12 +53,16 @@ export class MongooseTraineesRepository implements TraineesRepository { async getTraineesByCohort( fromCohort: number | undefined, toCohort: number | undefined, - includeNullCohort: boolean = false + includeNullCohort = false ): Promise { - let condition: any = { 'educationInfo.currentCohort': { $gte: fromCohort ?? 0, $lte: toCohort ?? 999 } }; + let condition: RootFilterQuery; + condition = { + 'educationInfo.currentCohort': { $gte: fromCohort ?? 0, $lte: toCohort ?? 999 }, + }; if (includeNullCohort) { condition = { $or: [condition, { 'educationInfo.currentCohort': null }] }; } + return await this.TraineeModel.find(condition) .select([ 'thumbnailURL', @@ -87,7 +91,7 @@ export class MongooseTraineesRepository implements TraineesRepository { return await this.TraineeModel.create(trainee); } - async deleteTrainee(id: string): Promise { + deleteTrainee(_id: string): Promise { throw new Error('Not implemented'); } @@ -112,7 +116,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return trainee.educationInfo.strikes || []; + return this.safeArray(trainee.educationInfo.strikes); } async addStrike(traineeID: string, strike: StrikeWithReporterID): Promise { @@ -126,7 +130,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return updatedTrainee.educationInfo.strikes.at(-1) as StrikeWithReporter; + return this.getLastItem(updatedTrainee.educationInfo.strikes); } async updateStrike(traineeID: string, strike: StrikeWithReporterID): Promise { @@ -141,7 +145,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return updatedTrainee.educationInfo.strikes.find((strike) => strike.id === dbStrike._id) as StrikeWithReporter; + return this.safeFindItem(updatedTrainee.educationInfo.strikes, (strike) => strike.id === dbStrike._id); } async deleteStrike(traineeID: string, strikeID: string): Promise { @@ -161,7 +165,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return trainee.interactions || []; + return this.safeArray(trainee.interactions); } async addInteraction(traineeID: string, interaction: InteractionWithReporterID): Promise { @@ -175,7 +179,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return updatedTrainee.interactions.at(-1) as InteractionWithReporter; + return this.getLastItem(updatedTrainee.interactions); } async updateInteraction(traineeID: string, interaction: InteractionWithReporterID): Promise { @@ -190,9 +194,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Interaction was not found'); } - return updatedTrainee.interactions.find( - (interaction) => interaction.id === dbInteraction._id - ) as InteractionWithReporter; + return this.safeFindItem(updatedTrainee.interactions, (interaction) => interaction.id === dbInteraction._id); } async deleteInteraction(traineeID: string, interactionID: string): Promise { @@ -206,7 +208,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return trainee.educationInfo.tests || []; + return this.safeArray(trainee.educationInfo.tests); } async addTest(traineeID: string, test: Test): Promise { @@ -220,7 +222,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return updatedTrainee.educationInfo.tests.at(-1) as Test; + return this.getLastItem(updatedTrainee.educationInfo.tests); } async updateTest(traineeID: string, test: Test): Promise { @@ -254,7 +256,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return trainee.employmentInfo.employmentHistory || []; + return this.safeArray(trainee.employmentInfo.employmentHistory); } async addEmploymentHistory(traineeID: string, employmentHistory: EmploymentHistory): Promise { @@ -268,7 +270,7 @@ export class MongooseTraineesRepository implements TraineesRepository { throw new Error('Trainee not found'); } - return updatedTrainee.employmentInfo.employmentHistory.at(-1) as EmploymentHistory; + return this.getLastItem(updatedTrainee.employmentInfo.employmentHistory); } async updateEmploymentHistory(traineeID: string, employmentHistory: EmploymentHistory): Promise { @@ -299,4 +301,25 @@ export class MongooseTraineesRepository implements TraineesRepository { { $pull: { 'employmentInfo.employmentHistory': { _id: employmentHistoryID } } } ); } + + // ? Reason: It wasn't clear whether the best is to follow linter's suggestions or to ignore them + // Therefore, these helper methods are added + // the methods currently solves the linter's errors while keeping the code readable and maybe unnecessarily verbose + // but can be modified easily to follow the previous code style if needed + + private getLastItem(arr: T[]): T { + const item = arr.at(-1); + if (!item) throw new Error('Array unexpectedly empty'); + return item; + } + + private safeFindItem(array: T[], predicate: (value: T, index: number, obj: T[]) => boolean): T { + const result = array.find(predicate); + if (!result) throw new Error('Item not found'); + return result; + } + + private safeArray(array: T[] | undefined | null): T[] { + return Array.isArray(array) ? array : []; + } } diff --git a/server/src/services/GoogleOAuthService.ts b/server/src/services/GoogleOAuthService.ts index 6b2a52b3..d28aa85b 100644 --- a/server/src/services/GoogleOAuthService.ts +++ b/server/src/services/GoogleOAuthService.ts @@ -1,9 +1,22 @@ -export interface GoogleOAuthUserInfo { - name: string; - email: string; - emailVerified: boolean; - pictureUrl?: string; -} +import z from 'zod'; + +const GoogleOAuthAccessTokenSchema = z.object({ + access_token: z.string(), +}); + +const GoogleOAuthUserInfoSchema = z + .object({ + email: z.email(), + email_verified: z.boolean(), + name: z.string(), + picture: z.url().optional(), + }) + .transform(({ email_verified, ...rest }) => ({ + ...rest, + emailVerified: email_verified, + })); + +export type GoogleOAuthUserInfo = z.infer; export interface GoogleOAuthServiceType { getUserInfo(accessToken: string): Promise; @@ -39,13 +52,10 @@ export class GoogleOAuthService { throw new Error(`Failed to fetch user info: ${response.statusText}`); } - const json: any = await response.json(); - return { - email: json.email, - name: json.name, - emailVerified: json.email_verified, - pictureUrl: json.picture, - }; + const parsed = GoogleOAuthUserInfoSchema.safeParse(await response.json()); + if (!parsed.success) throw new Error(`Error in parsing Google OAuth user info: ${z.prettifyError(parsed.error)}`); + + return parsed.data; } async exchangeAuthCodeForToken(code: string, redirectURI: string): Promise { @@ -68,8 +78,10 @@ export class GoogleOAuthService { throw new Error(`Failed to Google OAuth exchange token: ${response.statusText}`); } - const json: any = await response.json(); - return json.access_token; + const parsed = GoogleOAuthAccessTokenSchema.safeParse(await response.json()); + if (!parsed.success) throw new Error(z.prettifyError(parsed.error)); + + return parsed.data.access_token; } async revokeToken(accessToken: string): Promise { diff --git a/server/src/services/LetterGenerator.ts b/server/src/services/LetterGenerator.ts index c1030e4e..b4cb1d16 100644 --- a/server/src/services/LetterGenerator.ts +++ b/server/src/services/LetterGenerator.ts @@ -4,9 +4,7 @@ import PizZip from 'pizzip'; import Docxtemplater from 'docxtemplater'; import Stream from 'stream'; -export type LetterData = { - [key: string]: string; -}; +export type LetterData = Record; export enum LetterType { GITHUB_TRAINEE = 'github_trainee', @@ -36,7 +34,7 @@ export class LetterGenerator implements LetterGeneratorType { let outputDocument: Buffer; try { const templateDocument = Buffer.concat(await Array.fromAsync(templateDocumentStream)); - outputDocument = await this.populateTemplate(templateDocument, data); + outputDocument = this.populateTemplate(templateDocument, data); } catch (error) { throw new Error(`Letter generation error: failed to generate document from template: ${String(error)}`); } @@ -48,7 +46,7 @@ export class LetterGenerator implements LetterGeneratorType { // This function takes a docx template document and replaces the placeholders with actual data. // The process involves unzipping the docx file, parsing it with Docxtemplater, and then generating a new docx file. - private async populateTemplate(templateDocument: Buffer, letterData: LetterData): Promise { + private populateTemplate(templateDocument: Buffer, letterData: LetterData): Buffer { // Unzip the content of the docx file const zip = new PizZip(templateDocument); diff --git a/server/src/services/NotificationService.ts b/server/src/services/NotificationService.ts index b2aef94b..24591a4b 100644 --- a/server/src/services/NotificationService.ts +++ b/server/src/services/NotificationService.ts @@ -90,7 +90,7 @@ export class SlackNotificationService implements NotificationService { resultIcon = '✅'; } else if (test.result === TestResult.PassedWithWarning) { resultIcon = '🟡'; - } else if (test.result === TestResult.Disqualified) { + } else { resultIcon = '‼️'; } diff --git a/server/src/services/PDFService.ts b/server/src/services/PDFService.ts index a6e6a5b5..c7ef4acd 100644 --- a/server/src/services/PDFService.ts +++ b/server/src/services/PDFService.ts @@ -42,7 +42,7 @@ export class PDFService implements PDFServiceType { try { response = await fetch(url, { method: 'POST', body: formData }); } catch (error) { - const errorMessage = `Failed to connect to Gotenberg service at ${this.gotenbergUrl}: ${String(error)}`; + const errorMessage = `Failed to connect to Gotenberg service at ${this.gotenbergUrl.toString()}: ${String(error)}`; sentry.captureException(new Error(errorMessage)); throw new Error(errorMessage); } diff --git a/server/src/services/StorageService.ts b/server/src/services/StorageService.ts index 7da3a47b..8fd0e577 100644 --- a/server/src/services/StorageService.ts +++ b/server/src/services/StorageService.ts @@ -62,7 +62,7 @@ export class StorageService implements StorageServiceType { const uploadResponse = await upload.done(); if (!uploadResponse.Location) { - throw new Error(`Failed to upload file ${uploadResponse.$metadata}`); + throw new Error(`Failed to upload file ${JSON.stringify(uploadResponse.$metadata)}`); } return uploadResponse.Location; diff --git a/server/src/services/UploadService.ts b/server/src/services/UploadService.ts index 92d267c8..84209c2b 100644 --- a/server/src/services/UploadService.ts +++ b/server/src/services/UploadService.ts @@ -5,7 +5,7 @@ import * as size from '../utils/fileSize'; import * as Sentry from '@sentry/node'; const IMAGE_MAX_SIZE = size.MB(10); -type UploadFileFilter = (req: any, file: Express.Multer.File, callback: multer.FileFilterCallback) => void; +type UploadFileFilter = (req: unknown, file: Express.Multer.File, callback: multer.FileFilterCallback) => void; export class UploadServiceError extends Error { constructor(message: string) { @@ -33,23 +33,30 @@ export class UploadService implements UploadServiceType { }).single(fieldName); return new Promise((resolve, reject) => { - upload(req, res, (error: any) => { - if (error) { - if (error instanceof multer.MulterError) { - reject(new UploadServiceError(error.message)); - return; - } - reject(error); + void upload(req, res, (error) => { + if (!error) { + resolve(); return; } - resolve(); + + let rejectionReason: Error; + if (error instanceof multer.MulterError) { + rejectionReason = new UploadServiceError(error.message); + } else if (error instanceof Error) { + rejectionReason = error; + } else { + rejectionReason = new Error(String(error)); + } + + reject(rejectionReason); + return; }); }); } cleanupTempFiles(): void { console.log(`Cleaning up temp directory: ${this.tempDir}`); - fs.rm(this.tempDir, { recursive: true, force: true }, (error: any) => { + fs.rm(this.tempDir, { recursive: true, force: true }, (error) => { if (error) { Sentry.captureException(error); } @@ -57,7 +64,7 @@ export class UploadService implements UploadServiceType { } private get imageFileFilter(): UploadFileFilter { - return (_: any, file: Express.Multer.File, callback: multer.FileFilterCallback) => { + return (_: unknown, file: Express.Multer.File, callback: multer.FileFilterCallback) => { const isImage = file.mimetype.toLocaleLowerCase().startsWith('image/'); if (isImage) { callback(null, isImage); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index bda2cc0b..129642bf 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,12 +1,17 @@ // This type is used to add a _id field to a model when interacting with MongoDB. + +import { ToObjectOptions } from 'mongoose'; + // It is required when creating mongoose schemas and models. -export type WithMongoID = { _id: string }; +export interface WithMongoID { + _id: string; +} -export const jsonFormatting = { +export const jsonFormatting: ToObjectOptions = { virtuals: true, versionKey: false, minimize: false, - transform: (_: any, ret: any) => { + transform: (_, ret) => { delete ret._id; delete ret.reporterID; },