diff --git a/backend/db/seeders/20221121101837-PermissionSeeder.ts b/backend/db/seeders/20221121101837-PermissionSeeder.ts index 3b7f1d1..136542c 100644 --- a/backend/db/seeders/20221121101837-PermissionSeeder.ts +++ b/backend/db/seeders/20221121101837-PermissionSeeder.ts @@ -5,6 +5,11 @@ const allPerms = [ "mentor.acc.manage.own", "mentor.view", + "users.list", + "users.view", + + "notes.view", + "notes.create", "lm.view", "lm.action_requirements.view", @@ -19,13 +24,16 @@ const allPerms = [ "lm.endorsement_groups.create", "lm.training_types.view", + "lm.training_types.create", + "lm.training_types.edit", "atd.view", + "atd.override", // Overrides some permissions and allows user with this perm to see everything, irrespective of mentor group (for example) "atd.solo.delete", "atd.examiner.view", "atd.fast_track.view", "atd.atsim.view", - "atd.training_stations.view", + "atd.training_stations.sync", "atd.log_template.view", "atd.log_template.edit", diff --git a/backend/src/Router.ts b/backend/src/Router.ts index f9a61ad..60e0129 100644 --- a/backend/src/Router.ts +++ b/backend/src/Router.ts @@ -99,7 +99,6 @@ router.use( r.use( "/course", routerGroup((r: Router) => { - r.get("/my", UserCourseController.getMyCourses); r.get("/active", UserCourseController.getActiveCourses); r.get("/available", UserCourseController.getAvailableCourses); r.get("/completed", UserCourseController.getCompletedCourses); @@ -181,7 +180,6 @@ router.use( routerGroup((r: Router) => { r.get("/data", UserInformationAdminController.getUserDataByID); r.get("/data/basic", UserInformationAdminController.getBasicUserDataByID); - r.get("/data/sensitive", UserInformationAdminController.getSensitiveUserDataByID); r.post("/note", UserNoteAdminController.createUserNote); r.get("/notes", UserNoteAdminController.getGeneralUserNotes); @@ -189,7 +187,6 @@ router.use( r.get("/", UserAdminController.getAll); r.get("/min", UserAdminController.getAllUsersMinimalData); - r.get("/sensitive", UserAdminController.getAllSensitive); r.post("/enrol", UserCourseAdminController.enrolUser); @@ -366,8 +363,6 @@ router.use( "/training-station", routerGroup((r: Router) => { r.get("/", TrainingStationAdminController.getAll); - r.get("/:id", TrainingStationAdminController.getByID); - r.post("/sync", TrainingStationAdminController.syncStations); }) ); diff --git a/backend/src/controllers/permission/PermissionAdminController.ts b/backend/src/controllers/permission/PermissionAdminController.ts index 3b7cb25..3a606cf 100644 --- a/backend/src/controllers/permission/PermissionAdminController.ts +++ b/backend/src/controllers/permission/PermissionAdminController.ts @@ -37,7 +37,7 @@ async function create(request: Request, response: Response, next: NextFunction) const body = request.body as { name: string }; Validator.validate(body, { - name: [ValidationTypeEnum.NON_NULL] + name: [ValidationTypeEnum.NON_NULL], }); const [perm, created] = await Permission.findOrCreate({ @@ -68,12 +68,12 @@ async function destroy(request: Request, response: Response, next: NextFunction) const user: User = response.locals.user; PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit"); - const body = request.body as {perm_id: string}; + const body = request.body as { perm_id: string }; Validator.validate(body, { - perm_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + perm_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); - const res = await Permission.destroy({ + await Permission.destroy({ where: { id: body.perm_id, }, diff --git a/backend/src/controllers/permission/RoleAdminController.ts b/backend/src/controllers/permission/RoleAdminController.ts index af46a3f..c32fb07 100644 --- a/backend/src/controllers/permission/RoleAdminController.ts +++ b/backend/src/controllers/permission/RoleAdminController.ts @@ -20,7 +20,7 @@ async function getAll(_request: Request, response: Response, next: NextFunction) const roles = await Role.findAll(); response.send(roles); - } catch(e) { + } catch (e) { next(e); } } @@ -207,12 +207,12 @@ async function addPermission(request: Request, response: Response, next: NextFun try { const user: User = response.locals.user; const params = request.params; - const body = request.body as {permission_id?: string}; + const body = request.body as { permission_id?: string }; PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit", true); Validator.validate(body, { - permission_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + permission_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); const res = await RoleHasPermissions.create({ diff --git a/backend/src/controllers/solo/SoloAdminController.ts b/backend/src/controllers/solo/SoloAdminController.ts index a033041..0480cf9 100644 --- a/backend/src/controllers/solo/SoloAdminController.ts +++ b/backend/src/controllers/solo/SoloAdminController.ts @@ -6,10 +6,7 @@ import { User } from "../../models/User"; import { EndorsementGroupsBelongsToUsers } from "../../models/through/EndorsementGroupsBelongsToUsers"; import { TrainingSession } from "../../models/TrainingSession"; import PermissionHelper from "../../utility/helper/PermissionHelper"; -import { - createSolo as vateudCreateSolo, - removeSolo as vateudRemoveSolo -} from "../../libraries/vateud/VateudCoreLibrary"; +import { createSolo as vateudCreateSolo, removeSolo as vateudRemoveSolo } from "../../libraries/vateud/VateudCoreLibrary"; import { EndorsementGroup } from "../../models/EndorsementGroup"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; import { sequelize } from "../../core/Sequelize"; @@ -43,31 +40,37 @@ async function createSolo(request: Request, response: Response, next: NextFuncti solo_duration: [ValidationTypeEnum.NON_NULL], solo_start: [ValidationTypeEnum.NON_NULL], trainee_id: [ValidationTypeEnum.NON_NULL], - endorsement_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + endorsement_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); const startDate = dayjs.utc(body.solo_start); const endDate = startDate.add(Number(body.solo_duration), "days"); - const solo = await UserSolo.create({ - user_id: body.trainee_id, - created_by: user.id, - solo_used: Number(body.solo_duration), - extension_count: 0, - current_solo_start: startDate.toDate(), - current_solo_end: endDate.toDate(), - }, { - transaction: transaction - }); + const solo = await UserSolo.create( + { + user_id: body.trainee_id, + created_by: user.id, + solo_used: Number(body.solo_duration), + extension_count: 0, + current_solo_start: startDate.toDate(), + current_solo_end: endDate.toDate(), + }, + { + transaction: transaction, + } + ); - await EndorsementGroupsBelongsToUsers.create({ - user_id: body.trainee_id, - created_by: user.id, - endorsement_group_id: Number(body.endorsement_group_id), - solo_id: solo.id, - }, { - transaction: transaction - }); + await EndorsementGroupsBelongsToUsers.create( + { + user_id: body.trainee_id, + created_by: user.id, + endorsement_group_id: Number(body.endorsement_group_id), + solo_id: solo.id, + }, + { + transaction: transaction, + } + ); const endorsementGroup = await EndorsementGroup.findOne({ where: { @@ -109,7 +112,7 @@ async function updateSolo(request: Request, response: Response, next: NextFuncti try { const body = request.body as UpdateSoloRequestBody & { endorsement_group_id?: string }; Validator.validate(body, { - endorsement_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + endorsement_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); const currentSolo = await UserSolo.findOne({ @@ -129,40 +132,49 @@ async function updateSolo(request: Request, response: Response, next: NextFuncti user_id: body.trainee_id, solo_id: currentSolo.id, }, - transaction: transaction + transaction: transaction, }); - await EndorsementGroupsBelongsToUsers.create({ - user_id: body.trainee_id, - endorsement_group_id: Number(body.endorsement_group_id), - solo_id: currentSolo.id, - created_by: response.locals.user.id, - }, { - transaction: transaction - }); + await EndorsementGroupsBelongsToUsers.create( + { + user_id: body.trainee_id, + endorsement_group_id: Number(body.endorsement_group_id), + solo_id: currentSolo.id, + created_by: response.locals.user.id, + }, + { + transaction: transaction, + } + ); const newDuration = currentSolo.solo_used + Number(body.solo_duration); // If solo_start == NULL, then the solo is still active if (body.solo_start == null) { - await currentSolo.update({ - created_by: response.locals.user.id, - solo_used: newDuration, - current_solo_end: dayjs.utc(currentSolo.current_solo_start).add(newDuration, "days").toDate(), - }, { - transaction: transaction - }); + await currentSolo.update( + { + created_by: response.locals.user.id, + solo_used: newDuration, + current_solo_end: dayjs.utc(currentSolo.current_solo_start).add(newDuration, "days").toDate(), + }, + { + transaction: transaction, + } + ); } else { // If solo_start != NULL, then the solo is inactive and the new days have to be calculated (newDuration, for example, isn't correct! It's start_date + Number(body.solo_duration) // Else we'll add the entire solo duration to the length again :). - await currentSolo.update({ - created_by: response.locals.user.id, - solo_used: newDuration, - current_solo_start: dayjs.utc(body.solo_start).toDate(), - current_solo_end: dayjs.utc(body.solo_start).add(Number(body.solo_duration), "days").toDate(), - }, { - transaction: transaction - }); + await currentSolo.update( + { + created_by: response.locals.user.id, + solo_used: newDuration, + current_solo_start: dayjs.utc(body.solo_start).toDate(), + current_solo_end: dayjs.utc(body.solo_start).add(Number(body.solo_duration), "days").toDate(), + }, + { + transaction: transaction, + } + ); } const returnUser = await User.findOne({ @@ -196,7 +208,7 @@ async function extendSolo(request: Request, response: Response, next: NextFuncti try { const body = request.body as { trainee_id: string }; Validator.validate(body, { - trainee_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + trainee_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); // Check the user has had a training in the last 20 days. @@ -214,7 +226,7 @@ async function extendSolo(request: Request, response: Response, next: NextFuncti let cpt_planned = false; let training_last_20_days = false; - for (const trainingSession of (user?.training_sessions ?? [])) { + for (const trainingSession of user?.training_sessions ?? []) { if ( trainingSession.date != null && trainingSession.date > dayjs.utc().subtract(20, "days").startOf("day").toDate() && @@ -268,14 +280,14 @@ async function deleteSolo(request: Request, response: Response, next: NextFuncti const body = request.body as { trainee_id: string; solo_id: string }; Validator.validate(body, { trainee_id: [ValidationTypeEnum.NON_NULL], - solo_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + solo_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); const solo = await UserSolo.findOne({ where: { id: body.solo_id, }, - transaction: transaction + transaction: transaction, }); // 1. Delete all endorsements that are linked to the solo. @@ -283,7 +295,7 @@ async function deleteSolo(request: Request, response: Response, next: NextFuncti where: { solo_id: body.solo_id, }, - transaction: transaction + transaction: transaction, }); // 2. Delete the VATEUD Core Solo @@ -293,7 +305,7 @@ async function deleteSolo(request: Request, response: Response, next: NextFuncti where: { id: body.solo_id, }, - transaction: transaction + transaction: transaction, }); const returnUser = await User.findOne({ diff --git a/backend/src/controllers/training-log/TrainingLogController.ts b/backend/src/controllers/training-log/TrainingLogController.ts index 323bb6e..92050cb 100644 --- a/backend/src/controllers/training-log/TrainingLogController.ts +++ b/backend/src/controllers/training-log/TrainingLogController.ts @@ -28,7 +28,7 @@ async function getByUUID(request: Request, response: Response, next: NextFunctio return; } - if (!await trainingLog.userCanRead(user)) { + if (!(await trainingLog.userCanRead(user))) { throw new ForbiddenException("You are not permitted to view this training log."); } diff --git a/backend/src/controllers/training-request/TrainingRequestController.ts b/backend/src/controllers/training-request/TrainingRequestController.ts index 23f17a1..4c2e6d3 100644 --- a/backend/src/controllers/training-request/TrainingRequestController.ts +++ b/backend/src/controllers/training-request/TrainingRequestController.ts @@ -72,7 +72,7 @@ async function destroy(request: Request, response: Response, next: NextFunction) const body = request.body as { uuid: string }; Validator.validate(body, { - uuid: [ValidationTypeEnum.NON_NULL] + uuid: [ValidationTypeEnum.NON_NULL], }); const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ @@ -187,7 +187,7 @@ async function getByUUID(request: Request, response: Response, next: NextFunctio ], }); - if (!await trainingRequest?.canUserView(user)) { + if (!(await trainingRequest?.canUserView(user))) { throw new ForbiddenException("You are not allowed to view this training request"); } @@ -220,7 +220,7 @@ async function confirmInterest(request: Request, response: Response, next: NextF const trainingRequest = await TrainingRequest.findOne({ where: { uuid: trainingRequestUUID, - user_id: user.id + user_id: user.id, }, }); diff --git a/backend/src/controllers/training-session/TrainingSessionAdminController.ts b/backend/src/controllers/training-session/TrainingSessionAdminController.ts index 66c7858..680ced3 100644 --- a/backend/src/controllers/training-session/TrainingSessionAdminController.ts +++ b/backend/src/controllers/training-session/TrainingSessionAdminController.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; -import _TrainingSessionAdminValidator from "./_TrainingSessionAdminValidator"; import { Course } from "../../models/Course"; import { TrainingSession } from "../../models/TrainingSession"; import { generateUUID } from "../../utility/UUID"; @@ -14,18 +13,18 @@ import { TrainingType } from "../../models/TrainingType"; import { TrainingLog } from "../../models/TrainingLog"; import { UsersBelongsToCourses } from "../../models/through/UsersBelongsToCourses"; import { sequelize } from "../../core/Sequelize"; -import { MentorGroup } from "../../models/MentorGroup"; import JobLibrary, { JobTypeEnum } from "../../libraries/JobLibrary"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; -import Logger, { LogLevels } from "../../utility/Logger"; import { Op } from "sequelize"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; /** * Creates a new training session with one user and one mentor + * @param request + * @param response + * @param next */ -async function createTrainingSession(request: Request, response: Response) { - // TODO: Check if the mentor of the course is even allowed to create such a session! - +async function createTrainingSession(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user as User; const body = request.body as { @@ -41,8 +40,9 @@ async function createTrainingSession(request: Request, response: Response) { user_ids: [ValidationTypeEnum.VALID_JSON], // Parses to number[] }); - // TODO: - // user.isMentorInCourse(body.course_uuid) + if (!(await user.isMentorInCourse(body.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course."); + } // 1. Find out which of these users is actually enrolled in the course. To do this, query the course and it's members, and check against the array of user_ids. Create a new actual array with only those people // that are actually enrolled in this course. @@ -152,12 +152,20 @@ async function createTrainingSession(request: Request, response: Response) { }); } } catch (e: any) { - Logger.log(LogLevels.LOG_WARN, "createTrainingSession >> " + e.message); + next(e); } } +/** + * Updates a request for a given UUID + * Should only be done by the mentor + * @param request + * @param response + * @param next + */ async function updateByUUID(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; const params = request.params as { uuid: string }; const body = request.body as { date?: string; mentor_id?: string; training_station_id?: string }; @@ -172,6 +180,7 @@ async function updateByUUID(request: Request, response: Response, next: NextFunc const session = await TrainingSession.findOne({ where: { uuid: params.uuid, + mentor_id: user.id, }, }); @@ -196,28 +205,15 @@ async function updateByUUID(request: Request, response: Response, next: NextFunc /** * Deletes a training session by a mentor * All users that are participants of this course are placed back in the queue + * @param request + * @param response + * @param next */ -async function deleteTrainingSession(request: Request, response: Response) { - const user: User = response.locals.user; - const data = request.body as { training_session_id: number }; - - const session = await TrainingSession.findOne({ - where: { - id: data.training_session_id, - mentor_id: user.id, - }, - include: [TrainingSession.associations.users, TrainingSession.associations.course], - }); - - if (session == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } - - for (const participant of session?.users ?? []) { +async function deleteTrainingSession(request: Request, response: Response, next: NextFunction) { + async function notifyUser(mentor: User, participant: User, session: TrainingSession) { await NotificationLibrary.sendUserNotification({ user_id: participant.id, - author_id: user.id, + author_id: mentor.id, message_de: `Deine Session im Kurs ${session.course?.name} am ${dayjs .utc(session.date) .format(Config.DATE_FORMAT)} wurde gelöscht. Dein Request wurde wieder in der Warteliste plaziert.`, @@ -229,198 +225,280 @@ async function deleteTrainingSession(request: Request, response: Response) { }); } - // Update training requests to reflect the now non-existent session - await TrainingRequest.update( - { - status: "requested", - training_session_id: null, - expires: dayjs.utc().add(2, "months"), - }, - { + try { + const user: User = response.locals.user; + const data = request.body as { training_session_id: number }; + + const session = await TrainingSession.findOne({ where: { - training_session_id: session.id, + id: data.training_session_id, + mentor_id: user.id, }, + include: [TrainingSession.associations.users, TrainingSession.associations.course], + }); + + if (session == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; } - ); - // Destroying the session also destroys the related training_session_belongs_to_users - // entries with this course, due to their foreign relationships - await session.destroy(); + for (const participant of session?.users ?? []) { + await notifyUser(user, participant, session); + } - response.sendStatus(HttpStatusCode.NoContent); + // Update training requests to reflect the now non-existent session + await TrainingRequest.update( + { + status: "requested", + training_session_id: null, + expires: dayjs.utc().add(2, "months"), + }, + { + where: { + training_session_id: session.id, + }, + } + ); + + // Destroying the session also destroys the related training_session_belongs_to_users + // entries with this course, due to their foreign relationships + await session.destroy(); + + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } -async function getByUUID(request: Request, response: Response) { - const user: User = response.locals.user; - const params = request.params as { uuid: string }; +/** + * Returns the session by UUID + * @param request + * @param response + * @param next + */ +async function getByUUID(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { uuid: string }; - const id = await TrainingSession.getIDFromUUID(params.uuid); + const id = await TrainingSession.getIDFromUUID(params.uuid); - const trainingSession = await TrainingSession.findOne({ - where: { - id: id, - }, - include: [ - TrainingSession.associations.course, - TrainingSession.associations.cpt, - { - association: TrainingSession.associations.users, - through: { - attributes: [], - }, - include: [ - { - association: User.associations.training_logs, - through: { - where: { - training_session_id: id, + const trainingSession = await TrainingSession.findOne({ + where: { + id: id, + }, + include: [ + TrainingSession.associations.course, + TrainingSession.associations.cpt, + { + association: TrainingSession.associations.users, + through: { + attributes: [], + }, + include: [ + { + association: User.associations.training_logs, + through: { + where: { + training_session_id: id, + }, + attributes: ["passed"], }, - attributes: ["passed"], + attributes: ["uuid"], }, - attributes: ["uuid"], - }, - ], - }, - { - association: TrainingSession.associations.training_type, - include: [ - { - association: TrainingType.associations.training_stations, - through: { - attributes: [], + ], + }, + { + association: TrainingSession.associations.training_type, + include: [ + { + association: TrainingType.associations.training_stations, + through: { + attributes: [], + }, }, - }, - ], - }, - TrainingSession.associations.training_station, - ], - }); + ], + }, + TrainingSession.associations.training_station, + ], + }); - if (trainingSession == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } + if (trainingSession == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } + + if (!trainingSession.userCanRead(user)) { + throw new ForbiddenException("You are not allowed to view this training session"); + } - response.send(trainingSession); + response.send(trainingSession); + } catch (e) { + next(e); + } } /** * Returns all the planned sessions of the current user as either mentor or CPT examiner * The differentiation between mentor & examiner must be done in the frontend - * @param request + * @param _request * @param response + * @param next */ -async function getPlanned(request: Request, response: Response) { - const user: User = response.locals.user; +async function getPlanned(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; - let trainingSession = await TrainingSession.findAll({ - where: { - mentor_id: user.id, - completed: false, - }, - order: [["date", "asc"]], - include: [ - TrainingSession.associations.course, - TrainingSession.associations.training_type, - { - association: TrainingSession.associations.training_session_belongs_to_users, - attributes: ["log_id"], + let trainingSession = await TrainingSession.findAll({ + where: { + mentor_id: user.id, + completed: false, }, - ], - }); - - response.send(trainingSession); -} - -async function getParticipants(request: Request, response: Response) { - const user: User = response.locals.user; - const params = request.params as { uuid: string }; - - const session = await TrainingSession.findOne({ - where: { - uuid: params.uuid, - mentor_id: user.id, - }, - include: [ - { - association: TrainingSession.associations.users, - through: { - attributes: [], + order: [["date", "asc"]], + include: [ + TrainingSession.associations.course, + TrainingSession.associations.training_type, + { + association: TrainingSession.associations.training_session_belongs_to_users, + attributes: ["log_id"], }, - }, - ], - }); + ], + }); - if (session == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; + response.send(trainingSession); + } catch (e) { + next(e); } - - response.send(session.users ?? []); } -async function getLogTemplate(request: Request, response: Response) { - const user: User = response.locals.user; - const params = request.params as { uuid: string }; +/** + * Gets all participants of a training session by UUID. + * Can only be viewed by the mentor + * @param request + * @param response + * @param next + */ +async function getParticipants(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { uuid: string }; - const session = await TrainingSession.findOne({ - where: { - uuid: params.uuid, - mentor_id: user.id, - }, - include: { - association: TrainingSession.associations.training_type, + const trainingSession = await TrainingSession.findOne({ + where: { + uuid: params.uuid, + }, include: [ { - association: TrainingType.associations.log_template, + association: TrainingSession.associations.users, + through: { + attributes: [], + }, }, ], - }, - }); + }); - if (session == null || session.training_type?.log_template == null) { - response.sendStatus(HttpStatusCode.NotFound); - return; - } + if (trainingSession == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } + + if (!trainingSession.userCanRead(user)) { + throw new ForbiddenException("You are not allowed to view this training session"); + } - response.send(session.training_type?.log_template); + response.send(trainingSession.users ?? []); + } catch (e) { + next(e); + } } -async function getCourseTrainingTypes(request: Request, response: Response) { - const user: User = response.locals.user; - const params = request.params as { uuid: string }; +/** + * Gets the log template for a specific training session. Used for creating log entries + * Can be viewed by all mentors + * @param request + * @param response + * @param next + */ +async function getLogTemplate(request: Request, response: Response, next: NextFunction) { + try { + const params = request.params as { uuid: string }; - const session = await TrainingSession.findOne({ - where: { - uuid: params.uuid, - }, - include: [ - { - association: TrainingSession.associations.course, + const session = await TrainingSession.findOne({ + where: { + uuid: params.uuid, + }, + include: { + association: TrainingSession.associations.training_type, include: [ { - association: Course.associations.training_types, - attributes: ["id", "name", "type"], - through: { - attributes: [], - }, + association: TrainingType.associations.log_template, }, ], }, - ], - }); + }); + + if (session?.training_type?.log_template == null) { + response.sendStatus(HttpStatusCode.NotFound); + return; + } - if (session == null || session.course?.training_types == null) { - response.sendStatus(HttpStatusCode.NotFound); - return; + response.send(session.training_type.log_template); + } catch (e) { + next(e); } +} + +/** + * Gets a list of training types associated with a course. Used for creating log entries (selecting next training type) + * Can be viewed by all mentors + * @param request + * @param response + * @param next + */ +async function getCourseTrainingTypes(request: Request, response: Response, next: NextFunction) { + try { + const params = request.params as { uuid: string }; + + const session = await TrainingSession.findOne({ + where: { + uuid: params.uuid, + }, + include: [ + { + association: TrainingSession.associations.course, + include: [ + { + association: Course.associations.training_types, + attributes: ["id", "name", "type"], + through: { + attributes: [], + }, + }, + ], + }, + ], + }); + + if (session?.course?.training_types == null) { + response.sendStatus(HttpStatusCode.NotFound); + return; + } - response.send(session.course?.training_types); + response.send(session.course.training_types); + } catch (e) { + next(e); + } } +/** + * Creates the log entries for all participants of the session + * @param request + * @param response + * @param next + */ async function createTrainingLogs(request: Request, response: Response, next: NextFunction) { // All of these steps MUST complete, else we are left in an undefined state - const t = await sequelize.transaction(); + const transaction = await sequelize.transaction(); try { const user: User = response.locals.user; @@ -433,17 +511,21 @@ async function createTrainingLogs(request: Request, response: Response, next: Ne user_log: any[]; }[]; + //////////////// + // VALIDATION // + //////////////// + if (body == null || body.length == 0) { response.sendStatus(HttpStatusCode.BadRequest); return; } - // Validate body for (const entry of body) { Validator.validate(entry, { user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], passed: [ValidationTypeEnum.NON_NULL], user_log: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.VALID_JSON], + course_completed: [ValidationTypeEnum.NON_NULL], }); for (const logEntry of entry.user_log) { @@ -455,6 +537,10 @@ async function createTrainingLogs(request: Request, response: Response, next: Ne } } + /////////////// + // APP LOGIC // + /////////////// + const session = await TrainingSession.findOne({ where: { uuid: params.uuid, @@ -464,37 +550,43 @@ async function createTrainingLogs(request: Request, response: Response, next: Ne if (session == null) { response.sendStatus(HttpStatusCode.InternalServerError); - return; + } + if (!session?.userCanCreateLogs(user)) { + throw new ForbiddenException("You are not allowed to create logs for this session"); } - for (let i = 0; i < body.length; i++) { - const user_id = body[i].user_id; + // Loop through all log entries + for (const logEntry of body) { + const user_id = logEntry.user_id; + // Create the training log with the respective content const trainingLog = await TrainingLog.create( { uuid: generateUUID(), - content: body[i].user_log, + content: logEntry.user_log, author_id: user.id, }, { - transaction: t, + transaction: transaction, } ); + // Add the training log to the user (this could be done automatically with sequelize, but is a little finicky) await TrainingSessionBelongsToUsers.update( { log_id: trainingLog.id, - passed: body[i].passed, + passed: logEntry.passed, }, { where: { - user_id: body[i].user_id, + user_id: logEntry.user_id, training_session_id: session?.id, }, - transaction: t, + transaction: transaction, } ); + // Mark the request as completed await TrainingRequest.update( { status: "completed", @@ -504,27 +596,28 @@ async function createTrainingLogs(request: Request, response: Response, next: Ne user_id: user_id, training_session_id: session.id, }, - transaction: t, + transaction: transaction, } ); - // If the course is marked as completed, we need to update accordingly - if (body[i].course_completed) { - await UsersBelongsToCourses.update( - { - completed: true, - next_training_type: null, + // Set the course as completed (or not) depending on the course_completion flag + await UsersBelongsToCourses.update( + { + completed: logEntry.course_completed, + next_training_type: logEntry.course_completed ? null : logEntry.next_training_id, + }, + { + where: { + user_id: logEntry.user_id, + course_id: session.course?.id ?? -1, + completed: false, }, - { - where: { - user_id: body[i].user_id, - course_id: session.course?.id ?? -1, - completed: false, - }, - transaction: t, - } - ); + transaction: transaction, + } + ); + // If the course is marked as completed, let the user know! + if (logEntry.course_completed) { await NotificationLibrary.sendUserNotification({ user_id: user_id, author_id: user.id, @@ -533,20 +626,6 @@ async function createTrainingLogs(request: Request, response: Response, next: Ne icon: "check", severity: "success", }); - } else { - await UsersBelongsToCourses.update( - { - next_training_type: body[i].next_training_id, - }, - { - where: { - user_id: body[i].user_id, - course_id: session.course?.id ?? -1, - completed: false, - }, - transaction: t, - } - ); } } @@ -558,21 +637,20 @@ async function createTrainingLogs(request: Request, response: Response, next: Ne where: { id: session.id, }, - transaction: t, + transaction: transaction, } ); - await t.commit(); - - response.sendStatus(HttpStatusCode.Ok); + await transaction.commit(); + response.sendStatus(HttpStatusCode.NoContent); } catch (e) { - await t.rollback(); + await transaction.rollback(); next(e); } } /** - * Returns all the available + * Returns all the available mentors within a course from a training session * @param request * @param response * @param next @@ -586,34 +664,18 @@ async function getAvailableMentorsByUUID(request: Request, response: Response, n where: { uuid: params.uuid, }, - include: [ - { - association: TrainingSession.associations.course, - include: [ - { - association: Course.associations.mentor_groups, - include: [ - { - association: MentorGroup.associations.users, - through: { - attributes: [], - }, - }, - ], - }, - ], - }, - ], }); - if (trainingSession == null || trainingSession.course == null) { + if (trainingSession == null) { response.sendStatus(HttpStatusCode.NotFound); return; } + const mentorGroups = await trainingSession.getAvailableMentorGroups(); + let mentors: any[] = []; - for (const mentorGroup of trainingSession.course.mentor_groups ?? []) { + for (const mentorGroup of mentorGroups) { for (const user of mentorGroup.users ?? []) { if (mentors.find(u => u.id == user.id) == null) { mentors.push(user); @@ -627,7 +689,13 @@ async function getAvailableMentorsByUUID(request: Request, response: Response, n } } -async function getMyTrainingSessions(request: Request, response: Response, next: NextFunction) { +/** + * Returns all training sessions in which the current user is marked as the mentor + * @param _request + * @param response + * @param next + */ +async function getMyTrainingSessions(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; @@ -644,24 +712,41 @@ async function getMyTrainingSessions(request: Request, response: Response, next: } } -async function getAllUpcoming(request: Request, response: Response, next: NextFunction) { +/** + * Gets all upcoming training sessions (even those from other mentors) + * Useful to check for date conflicts + * Accessible by all mentors + * @param _request + * @param response + * @param next + */ +async function getAllUpcoming(_request: Request, response: Response, next: NextFunction) { try { - const user: User = response.locals.user; - - let sessions = await TrainingSession.findAll({ + let sessions: TrainingSession[] = await TrainingSession.findAll({ where: { date: { [Op.gt]: dayjs.utc().toDate(), }, }, - include: [TrainingSession.associations.training_type, TrainingSession.associations.course], + attributes: ["uuid", "mentor_id", "date"], + include: [ + { + association: TrainingSession.associations.training_type, + attributes: ["id", "name", "type"], + through: { + attributes: [], + }, + }, + { + association: TrainingSession.associations.course, + attributes: ["uuid", "name"], + through: { + attributes: [], + }, + }, + ], }); - if (sessions == null || sessions.length == 0) { - response.sendStatus(HttpStatusCode.InternalServerError); - return; - } - const res = sessions.map(session => ({ date: session.date, training_type: session.training_type, diff --git a/backend/src/controllers/training-session/TrainingSessionController.ts b/backend/src/controllers/training-session/TrainingSessionController.ts index 4ab83c8..f7129c0 100644 --- a/backend/src/controllers/training-session/TrainingSessionController.ts +++ b/backend/src/controllers/training-session/TrainingSessionController.ts @@ -10,11 +10,11 @@ import { ForbiddenException } from "../../exceptions/ForbiddenException"; /** * Gets a list of upcoming training sessions - * @param request + * @param _request * @param response * @param next */ -async function getUpcoming(request: Request, response: Response, next: NextFunction) { +async function getUpcoming(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; @@ -28,7 +28,13 @@ async function getUpcoming(request: Request, response: Response, next: NextFunct } } -async function getCompleted(request: Request, response: Response, next: NextFunction) { +/** + * Gets a list of completed trainings + * @param _request + * @param response + * @param next + */ +async function getCompleted(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; @@ -57,139 +63,158 @@ async function getCompleted(request: Request, response: Response, next: NextFunc } /** - * [User] * Gets all the associated data of a training session * @param request * @param response + * @param next */ -async function getByUUID(request: Request, response: Response) { - const user: User = response.locals.user; - const sessionUUID: string = request.params.uuid; - - const session: TrainingSession | null = await TrainingSession.findOne({ - where: { - uuid: sessionUUID, - }, - include: [ - { - association: TrainingSession.associations.users, - attributes: ["id"], - through: { attributes: [] }, - }, - { - association: TrainingSession.associations.mentor, - }, - { - association: TrainingSession.associations.cpt, - }, - { - association: TrainingSession.associations.training_type, - attributes: ["id", "name", "type"], - }, - { - association: TrainingSession.associations.training_station, - attributes: ["id", "callsign", "frequency"], +async function getByUUID(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const sessionUUID: string = request.params.uuid; + + const session: TrainingSession | null = await TrainingSession.findOne({ + where: { + uuid: sessionUUID, }, - { - association: TrainingSession.associations.course, - attributes: ["uuid", "name", "name_en"], + include: [ + { + association: TrainingSession.associations.users, + attributes: ["id"], + through: { attributes: [] }, + }, + { + association: TrainingSession.associations.mentor, + }, + { + association: TrainingSession.associations.cpt, + }, + { + association: TrainingSession.associations.training_type, + attributes: ["id", "name", "type"], + }, + { + association: TrainingSession.associations.training_station, + attributes: ["id", "callsign", "frequency"], + }, + { + association: TrainingSession.associations.course, + attributes: ["uuid", "name", "name_en"], + }, + ], + }); + + // Check if the session exists + if (session == null) { + response.sendStatus(HttpStatusCode.InternalServerError); + return; + } + + // Check if the user even exists in this session, else deny the request + if (!session.isUserParticipant(user)) { + throw new ForbiddenException("You don't have permission to view this session", true); + } + + const requestingUserPassed = await TrainingSessionBelongsToUsers.findOne({ + where: { + user_id: user.id, + training_session_id: session.id, }, - ], - }); + include: [ + { + association: TrainingSessionBelongsToUsers.associations.training_log, + attributes: ["uuid"], + }, + ], + }); - // Check if the session exists - if (session == null) { - response.sendStatus(HttpStatusCode.InternalServerError); - return; + response.send({ + ...session.toJSON(), + user_passed: requestingUserPassed == null ? null : requestingUserPassed.passed, + log_id: requestingUserPassed?.training_log?.uuid ?? null, + }); + } catch (e) { + next(e); } +} - // Check if the user even exists in this session, else deny the request - if (session?.users?.find((u: User) => u.id == user.id) == null) { - throw new ForbiddenException("You don't have permission to view this log", true); - } +/** + * Removes the requesting user from a session by its UUID + * If the user is the only participant, deletes the entire training session + * @param request + * @param response + * @param next + */ +async function withdrawFromSessionByUUID(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const sessionUUID: string = request.params.uuid; - const requestingUserPassed = await TrainingSessionBelongsToUsers.findOne({ - where: { - user_id: user.id, - training_session_id: session.id, - }, - include: [ - { - association: TrainingSessionBelongsToUsers.associations.training_log, - attributes: ["uuid"], + const session: TrainingSession | null = await TrainingSession.findOne({ + where: { + uuid: sessionUUID, }, - ], - }); - - response.send({ - ...session.toJSON(), - user_passed: requestingUserPassed == null ? null : requestingUserPassed.passed, - log_id: requestingUserPassed?.training_log?.uuid ?? null, - }); -} + include: [TrainingSession.associations.users, TrainingSession.associations.training_type], + }); -async function withdrawFromSessionByUUID(request: Request, response: Response) { - const user: User = response.locals.user; - const sessionUUID: string = request.params.uuid; + if (session == null) { + response.sendStatus(HttpStatusCode.InternalServerError); + return; + } - const session: TrainingSession | null = await TrainingSession.findOne({ - where: { - uuid: sessionUUID, - }, - include: [TrainingSession.associations.users, TrainingSession.associations.training_type], - }); + if (!session.isUserParticipant(user)) { + throw new ForbiddenException("You are not a participant of this session"); + } - if (session == null) { - response.status(404).send({ message: "Session with this UUID not found" }); - return; - } + // Update the request to reflect this change + await TrainingRequest.update( + { + status: "requested", + training_session_id: null, + expires: dayjs().add(2, "months").toDate(), + }, + { + where: { + user_id: user.id, + training_session_id: session.id, + }, + } + ); - // Update the request to reflect this change - await TrainingRequest.update( - { - status: "requested", - training_session_id: null, - expires: dayjs().add(2, "months").toDate(), - }, - { + // Delete the association between trainee and session + // only if the session hasn't been completed (i.e. passed == null && log_id == null) + await TrainingSessionBelongsToUsers.destroy({ where: { user_id: user.id, training_session_id: session.id, + passed: null, + log_id: null, }, + }); + + // Check if we can delete the entire session, or only the user + if (session.users?.length == 1) { + await session.destroy(); } - ); - - // Delete the association between trainee and session - // only if the session hasn't been completed (i.e. passed == null && log_id == null) - await TrainingSessionBelongsToUsers.destroy({ - where: { - user_id: user.id, - training_session_id: session.id, - passed: null, - log_id: null, - }, - }); - - // Check if we can delete the entire session, or only the user - if (session.users?.length == 1) { - await session.destroy(); - } - if (session.mentor_id) { - await NotificationLibrary.sendUserNotification({ - user_id: session.mentor_id, - message_de: `${user.first_name} ${user.last_name} (${user.id}) hat sich von der geplanten Session (${session.training_type?.name}) am ${dayjs( - session.date - ).format("DD.MM.YYYY")} abgemeldet`, - message_en: `${user.first_name} ${user.last_name} (${user.id}) withdrew from the planned session (${session.training_type?.name}) on ${dayjs( - session.date - ).format("DD.MM.YYYY")}`, - severity: "danger", - icon: "door-exit", - }); - } + if (session.mentor_id) { + await NotificationLibrary.sendUserNotification({ + user_id: session.mentor_id, + message_de: `${user.first_name} ${user.last_name} (${user.id}) hat sich von der geplanten Session (${session.training_type?.name}) am ${dayjs( + session.date + ).format("DD.MM.YYYY")} abgemeldet`, + message_en: `${user.first_name} ${user.last_name} (${user.id}) withdrew from the planned session (${session.training_type?.name}) on ${dayjs( + session.date + ).format("DD.MM.YYYY")}`, + severity: "danger", + icon: "door-exit", + }); + } - response.sendStatus(HttpStatusCode.NoContent); + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/training-session/_TrainingSessionAdminValidator.ts b/backend/src/controllers/training-session/_TrainingSessionAdminValidator.ts deleted file mode 100644 index 96fb31e..0000000 --- a/backend/src/controllers/training-session/_TrainingSessionAdminValidator.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ValidationException } from "../../exceptions/ValidationException"; - -function validateCreateSessionRequest(data: any) { - // return ValidationHelper.validate([ - // { - // name: "user_ids", - // validationObject: data.user_ids, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // { - // name: "course_uuid", - // validationObject: data.course_uuid, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // { - // name: "date", - // validationObject: data.date, - // toValidate: [{ val: ValidationOptions.VALID_DATE }], - // }, - // { - // name: "training_type_id", - // validationObject: data.training_type_id, - // toValidate: [{ val: ValidationOptions.NUMBER }], - // }, - // ]); -} - -function validateUpdateRequest(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "date", - // validationObject: data.date, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.VALID_DATE }], - // }, - // { - // name: "mentor_id", - // validationObject: data.mentor_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // { - // name: "training_station_id", - // validationObject: data.training_station_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -export default { - validateCreateSessionRequest, - validateUpdateRequest, -}; diff --git a/backend/src/controllers/training-station/TrainingStationAdminController.ts b/backend/src/controllers/training-station/TrainingStationAdminController.ts index c7fa6af..87d46f1 100644 --- a/backend/src/controllers/training-station/TrainingStationAdminController.ts +++ b/backend/src/controllers/training-station/TrainingStationAdminController.ts @@ -1,12 +1,17 @@ import { NextFunction, Response, Request } from "express"; -import axios, { HttpStatusCode } from "axios"; -import { sequelize } from "../../core/Sequelize"; import { TrainingStation } from "../../models/TrainingStation"; -import _TrainingStationAdminValidator from "./_TrainingStationAdmin.validator"; -import { Config } from "../../core/Config"; import { updateTrainingStations } from "../../libraries/vatger/GithubLibrary"; +import { User } from "../../models/User"; +import PermissionHelper from "../../utility/helper/PermissionHelper"; -async function getAll(request: Request, response: Response, next: NextFunction) { +/** + * Returns a list of all training stations (from the DataHub) + * Required by all mentors that can create courses, endorsement groups, etc. + * @param _request + * @param response + * @param next + */ +async function getAll(_request: Request, response: Response, next: NextFunction) { try { const trainingStations = await TrainingStation.findAll(); response.send(trainingStations); @@ -15,29 +20,17 @@ async function getAll(request: Request, response: Response, next: NextFunction) } } -async function getByID(request: Request, response: Response, next: NextFunction) { +/** + * Syncs the training stations with the DataHub + * @param _request + * @param response + * @param next + */ +async function syncStations(_request: Request, response: Response, next: NextFunction) { try { - const params = request.params as { id: string }; + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "atd.training_stations.sync"); - const trainingStation = await TrainingStation.findOne({ - where: { - id: params.id, - }, - }); - - if (trainingStation == null) { - response.sendStatus(HttpStatusCode.NotFound); - return; - } - - response.send(trainingStation); - } catch (e) { - next(e); - } -} - -async function syncStations(request: Request, response: Response, next: NextFunction) { - try { await updateTrainingStations(); const newStations = await TrainingStation.findAll(); response.send(newStations); @@ -48,6 +41,5 @@ async function syncStations(request: Request, response: Response, next: NextFunc export default { getAll, - getByID, syncStations, }; diff --git a/backend/src/controllers/training-station/_TrainingStationAdmin.validator.ts b/backend/src/controllers/training-station/_TrainingStationAdmin.validator.ts deleted file mode 100644 index 0324623..0000000 --- a/backend/src/controllers/training-station/_TrainingStationAdmin.validator.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ValidationException } from "../../exceptions/ValidationException"; - -function validateCreateStations(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "body", - // validationObject: data, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.LENGTH_GT, value: 0 }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -function validateUpdateStation(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "callsign", - // validationObject: data.callsign, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // { - // name: "frequency", - // validationObject: data.frequency, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -export default { - validateCreateStations, - validateUpdateStation, -}; diff --git a/backend/src/controllers/training-type/TrainingTypeAdminController.ts b/backend/src/controllers/training-type/TrainingTypeAdminController.ts index 5ad5233..2aed0f4 100644 --- a/backend/src/controllers/training-type/TrainingTypeAdminController.ts +++ b/backend/src/controllers/training-type/TrainingTypeAdminController.ts @@ -5,9 +5,12 @@ import { TrainingStationBelongsToTrainingType } from "../../models/through/Train import { HttpStatusCode } from "axios"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; import { TRAINING_TYPES_TABLE_TYPES } from "../../../db/migrations/20221115171246-create-training-types-table"; +import { User } from "../../models/User"; +import PermissionHelper from "../../utility/helper/PermissionHelper"; /** * Gets all training types + * Useful for all mentors when creating courses, etc. * @param _request * @param response * @param next @@ -70,6 +73,9 @@ async function getByID(request: Request, response: Response, next: NextFunction) */ async function create(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.training_types.create"); + const body = request.body as { name: string; type: (typeof TRAINING_TYPES_TABLE_TYPES)[number]; @@ -109,6 +115,9 @@ async function create(request: Request, response: Response, next: NextFunction) */ async function update(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.training_types.edit"); + const training_type_id = request.params.id; const body = request.body as { name: string; @@ -152,10 +161,18 @@ async function update(request: Request, response: Response, next: NextFunction) } } +/** + * Adds a station to a training type + * @param request + * @param response + * @param next + */ async function addStation(request: Request, response: Response, next: NextFunction) { try { - const body = request.body as { training_station_id: string; training_type_id: string }; + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.training_types.edit"); + const body = request.body as { training_station_id: string; training_type_id: string }; Validator.validate(body, { training_station_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], training_type_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], @@ -189,8 +206,17 @@ async function addStation(request: Request, response: Response, next: NextFuncti } } +/** + * Removes a station from a training type + * @param request + * @param response + * @param next + */ async function removeStation(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.training_types.edit"); + const body = request.body as { training_station_id?: string; training_type_id?: string }; Validator.validate(body, { training_station_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], diff --git a/backend/src/controllers/training-type/TrainingTypeController.ts b/backend/src/controllers/training-type/TrainingTypeController.ts index 21bdfa4..0fa58ca 100644 --- a/backend/src/controllers/training-type/TrainingTypeController.ts +++ b/backend/src/controllers/training-type/TrainingTypeController.ts @@ -2,6 +2,12 @@ import { NextFunction, Request, Response } from "express"; import { TrainingType } from "../../models/TrainingType"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; +/** + * Returns a training type by its ID + * @param request + * @param response + * @param next + */ async function getByID(request: Request, response: Response, next: NextFunction) { try { const params = request.params as { id: string }; diff --git a/backend/src/controllers/user-course-progress/UserCourseProgressAdministrationController.ts b/backend/src/controllers/user-course-progress/UserCourseProgressAdministrationController.ts index b7b9b4a..867089a 100644 --- a/backend/src/controllers/user-course-progress/UserCourseProgressAdministrationController.ts +++ b/backend/src/controllers/user-course-progress/UserCourseProgressAdministrationController.ts @@ -1,5 +1,4 @@ import { NextFunction, Request, Response } from "express"; -import _UserCourseProgressAdministrationValidator from "./_UserCourseProgressAdministration.validator"; import { User } from "../../models/User"; import { HttpStatusCode } from "axios"; import { Course } from "../../models/Course"; @@ -7,6 +6,7 @@ import { TrainingRequest } from "../../models/TrainingRequest"; import { TrainingSession } from "../../models/TrainingSession"; import { UsersBelongsToCourses } from "../../models/through/UsersBelongsToCourses"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; /** * Returns information about the progress of a user within the specified course. @@ -21,21 +21,19 @@ import Validator, { ValidationTypeEnum } from "../../utility/Validator"; */ async function getInformation(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; const query = request.query as { course_uuid: string; user_id: string }; Validator.validate(query, { course_uuid: [ValidationTypeEnum.NON_NULL], user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); - // TODO: Return the relevant information for this course ONLY! - // TODO(2): Limit the amount of data in the return (attribute: [...]) - // Currently, the controller returns ALL Requests and Histories for the user with this ID, not only those in the specified course! - + if (!(await user.isMentorInCourse(query.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course"); + } const course_id = await Course.getIDFromUUID(query.course_uuid); - console.log("Course id: " + course_id); - - const user = await User.findOne({ + const dbUser = await User.findOne({ where: { id: query.user_id, }, @@ -109,7 +107,7 @@ async function getInformation(request: Request, response: Response, next: NextFu ], }); - if (user == null || user.courses == null || user.courses.length == 0) { + if (dbUser == null || dbUser.courses == null || dbUser.courses.length == 0) { response.sendStatus(HttpStatusCode.NotFound); return; } @@ -128,12 +126,21 @@ async function getInformation(request: Request, response: Response, next: NextFu */ async function updateInformation(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; const body = request.body as { course_completed: "0" | "1"; user_id: string; course_uuid: string; next_training_type_id?: string }; - _UserCourseProgressAdministrationValidator.validateUpdateRequest(body); + Validator.validate(body, { + course_completed: [ValidationTypeEnum.NON_NULL], + user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + course_uuid: [ValidationTypeEnum.NON_NULL], + }); + + if (!(await user.isMentorInCourse(body.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course"); + } - const course = await Course.findOne({ where: { uuid: body.course_uuid } }); + const courseId = await Course.getIDFromUUID(body.course_uuid); - if (course == null) { + if (courseId == -1) { response.sendStatus(HttpStatusCode.NotFound); return; } @@ -144,7 +151,7 @@ async function updateInformation(request: Request, response: Response, next: Nex completed: body.course_completed == "1", }, { - where: { course_id: course.id, user_id: body.user_id }, + where: { course_id: courseId, user_id: body.user_id }, } ); diff --git a/backend/src/controllers/user-course-progress/_UserCourseProgressAdministration.validator.ts b/backend/src/controllers/user-course-progress/_UserCourseProgressAdministration.validator.ts deleted file mode 100644 index 3246db6..0000000 --- a/backend/src/controllers/user-course-progress/_UserCourseProgressAdministration.validator.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ValidationException } from "../../exceptions/ValidationException"; - -function validateGetAllRequest(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "course_uuid", - // validationObject: data.course_uuid, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // { - // name: "user_id", - // validationObject: data.user_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -function validateUpdateRequest(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "course_completed", - // validationObject: data.course_completed, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // { - // name: "user_id", - // validationObject: data.user_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // { - // name: "course_uuid", - // validationObject: data.course_uuid, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -export default { - validateGetAllRequest, - validateUpdateRequest, -}; diff --git a/backend/src/controllers/user/GDPRController.ts b/backend/src/controllers/user/GDPRController.ts index cc0a657..3c5651f 100644 --- a/backend/src/controllers/user/GDPRController.ts +++ b/backend/src/controllers/user/GDPRController.ts @@ -2,11 +2,16 @@ import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; import { EndorsementGroup } from "../../models/EndorsementGroup"; -async function getData(request: Request, response: Response, next: NextFunction) { +/** + * Returns all data that we store on a user and returns it as JSON + * @param _request + * @param response + * @param next + */ +async function getData(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; - // TODO: Add some includes here const foundUser = await User.scope("sensitive").findOne({ where: { id: user.id, diff --git a/backend/src/controllers/user/UserAdminController.ts b/backend/src/controllers/user/UserAdminController.ts index 84a4186..190f1cf 100644 --- a/backend/src/controllers/user/UserAdminController.ts +++ b/backend/src/controllers/user/UserAdminController.ts @@ -5,28 +5,24 @@ import PermissionHelper from "../../utility/helper/PermissionHelper"; /** * Gets all users including their user data (VATSIM Data) - * @param request + * Can be accessed by all mentors (privileged access!) + * @param _request * @param response + * @param next */ -async function getAll(request: Request, response: Response) { - const users = await User.findAll({ - include: [User.associations.user_data], - }); +async function getAll(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "users.list"); - response.send(users); -} + const users = await User.findAll({ + include: [User.associations.user_data], + }); -/** - * Gets all users including their sensitive data (email, etc.) - * @param request - * @param response - */ -async function getAllSensitive(request: Request, response: Response) { - const users = await User.findAll({ - include: [User.associations.user_data], - }); - - response.send(users); + response.send(users); + } catch (e) { + next(e); + } } /** @@ -40,12 +36,11 @@ async function getAllSensitive(request: Request, response: Response) { async function getAllUsersMinimalData(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; - const query = request.query as { users?: string }; + PermissionHelper.checkUserHasPermission(user, "users.list"); - PermissionHelper.checkUserHasPermission(user, "mentor.view"); + const query = request.query as { users?: string }; let users: User[]; - if (query.users == null) { users = await User.findAll({ attributes: ["id", "first_name", "last_name"], @@ -68,6 +63,5 @@ async function getAllUsersMinimalData(request: Request, response: Response, next export default { getAll, - getAllSensitive, getAllUsersMinimalData, }; diff --git a/backend/src/controllers/user/UserCourseAdminController.ts b/backend/src/controllers/user/UserCourseAdminController.ts index 9f64675..4f4b066 100644 --- a/backend/src/controllers/user/UserCourseAdminController.ts +++ b/backend/src/controllers/user/UserCourseAdminController.ts @@ -4,51 +4,53 @@ import { MentorGroup } from "../../models/MentorGroup"; import { Course } from "../../models/Course"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; import { UsersBelongsToCourses } from "../../models/through/UsersBelongsToCourses"; +import { HttpStatusCode } from "axios"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; /** * Returns all the user's courses that the requesting user is also a mentor of * Courses that the user is not a mentor of will be filtered out * @param request * @param response + * @param next */ -async function getUserCourseMatch(request: Request, response: Response) { - const reqUser: User = response.locals.user; - const userID = request.query.user_id; - const mentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); - - if (userID == null) { - response.status(404).send({ message: "No User ID supplied" }); - return; - } +async function getUserCourseMatch(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const query = request.query as { user_id: string }; + const mentorGroups: MentorGroup[] = await user.getMentorGroupsAndCourses(); - const user: User | null = await User.findOne({ - where: { - id: userID.toString(), - }, - include: [User.associations.courses], - }); + const dbUser: User | null = await User.findOne({ + where: { + id: query.user_id.toString(), + }, + include: [User.associations.courses], + }); - if (user == null) { - response.status(404).send({ message: "User with this ID not found" }); - return; - } + if (dbUser == null) { + response.status(404).send({ message: "User with this ID not found" }); + return; + } - let courses: Course[] | undefined = user.courses?.filter((course: Course) => { - for (const mG of mentorGroups) { - if (mG.courses?.find((c: Course) => c.id == course.id) != null) { - return true; + let courses: Course[] | undefined = dbUser.courses?.filter((course: Course) => { + for (const mentorGroup of mentorGroups) { + if (mentorGroup.courses?.find((c: Course) => c.id == course.id) != null) { + return true; + } } - } - return false; - }); + return false; + }); - if (courses == null) { - response.status(500).send(); - return; - } + if (courses == null) { + response.sendStatus(HttpStatusCode.InternalServerError); + return; + } - response.send(courses); + response.send(courses); + } catch (e) { + next(e); + } } /** @@ -60,18 +62,22 @@ async function getUserCourseMatch(request: Request, response: Response) { async function enrolUser(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; - const body = request.body as { user_id: string; course_id: string }; - - // TODO: Check Permissions + const body = request.body as { user_id: string; course_id: string }; Validator.validate(body, { user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], course_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER, { option: ValidationTypeEnum.NOT_EQUAL, value: -1 }], }); + const courseUUID = await Course.getUUIDFromID(body.course_id); + if (!(await user.isMentorInCourse(courseUUID))) { + throw new ForbiddenException("You are not a mentor of this course"); + } + const course = await Course.findByPk(body.course_id); if (course == null) { - throw new Error("Course with specified id couldn't be found"); + response.sendStatus(HttpStatusCode.NotFound); + return; } await UsersBelongsToCourses.findOrCreate({ diff --git a/backend/src/controllers/user/UserCourseController.ts b/backend/src/controllers/user/UserCourseController.ts index a89b5d1..713a56b 100644 --- a/backend/src/controllers/user/UserCourseController.ts +++ b/backend/src/controllers/user/UserCourseController.ts @@ -8,62 +8,49 @@ import { MentorGroup } from "../../models/MentorGroup"; import { TrainingType } from "../../models/TrainingType"; import { HttpStatusCode } from "axios"; import { ForbiddenException } from "../../exceptions/ForbiddenException"; -import PermissionHelper from "../../utility/helper/PermissionHelper"; +import { sequelize } from "../../core/Sequelize"; /** - * Returns courses that are available to the current user (i.e. not enrolled in course) - * @param request + * Returns courses that are available to the requesting user (i.e. not enrolled in course) + * @param _request * @param response + * @param next */ -async function getAvailableCourses(request: Request, response: Response) { - const user: User = response.locals.user; +async function getAvailableCourses(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; - const myCourses = await user.getCourses(); - const allCourses = await Course.findAll({ - where: { - is_active: true, - }, - }); + const myCourses = await user.getCourses(); + const allCourses = await Course.findAll({ + where: { + is_active: true, + }, + }); - const filteredCourses = allCourses.filter((course: Course) => { - return myCourses.find((myCourse: Course) => myCourse.id === course.id) == null; - }); + const filteredCourses = allCourses.filter((course: Course) => { + return myCourses.find((myCourse: Course) => myCourse.id === course.id) == null; + }); - response.send(filteredCourses); + response.send(filteredCourses); + } catch (e) { + next(e); + } } /** - * Gets courses that are active and associated to the current user (i.e. not completed) - * @param request + * Gets courses that are active and associated to the requesting user (i.e. not completed) + * @param _request * @param response + * @param next */ -async function getActiveCourses(request: Request, response: Response) { - const reqUser: User = response.locals.user; - - const userInCourses = await UsersBelongsToCourses.findAll({ - where: { - user_id: reqUser.id, - completed: 0, - }, - include: [UsersBelongsToCourses.associations.course], - }); - - let courses: Course[] = []; - for (const c of userInCourses) { - if (c.course != null) courses.push(c.course); - } - - response.send(courses); -} - -async function getCompletedCourses(request: Request, response: Response, next: NextFunction) { +async function getActiveCourses(_request: Request, response: Response, next: NextFunction) { try { - const reqUser: User = response.locals.user; + const user: User = response.locals.user; const userInCourses = await UsersBelongsToCourses.findAll({ where: { - user_id: reqUser.id, - completed: true, + user_id: user.id, + completed: 0, }, include: [UsersBelongsToCourses.associations.course], }); @@ -80,100 +67,122 @@ async function getCompletedCourses(request: Request, response: Response, next: N } /** - * Returns all courses that are associated to the current user (i.e. enrolled in course or completed) - * @param request + * Gets all courses the requesting user has completed + * @param _request * @param response + * @param next */ -async function getMyCourses(request: Request, response: Response) { - const reqUser: User = response.locals.user; - - const user = await User.findOne({ - where: { - id: reqUser.id, - }, - include: { - association: User.associations.courses, - through: { - as: "through", +async function getCompletedCourses(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + + const userInCourses = await UsersBelongsToCourses.findAll({ + where: { + user_id: user.id, + completed: true, }, - }, - }); + include: [UsersBelongsToCourses.associations.course], + }); + + let courses: Course[] = []; + for (const c of userInCourses) { + if (c.course != null) courses.push(c.course); + } - response.send(user?.courses ?? []); + response.send(courses); + } catch (e) { + next(e); + } } /** * Enrol the current user in the course * @param request * @param response + * @param next */ -async function enrolInCourse(request: Request, response: Response) { - const user: User = response.locals.user; - const body = request.body as { course_uuid: string }; - - Validator.validate(body, { - course_uuid: [ValidationTypeEnum.NON_NULL], - }); - - // Get course in question - const course: Course | null = await Course.findOne({ - where: { - uuid: body.course_uuid, - }, - include: [Course.associations.training_type], - }); - - // If Course-Instance couldn't be found, throw an error (caught locally) - if (course == null) { - throw Error("Course with id " + body.course_uuid + " could not be found!"); - } +async function enrolInCourse(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { course_uuid: string }; + + Validator.validate(body, { + course_uuid: [ValidationTypeEnum.NON_NULL], + }); + + // Get course in question + const course: Course | null = await Course.findOne({ + where: { + uuid: body.course_uuid, + }, + include: [Course.associations.training_type], + }); - // Enrol user in course - const userBelongsToCourses = await UsersBelongsToCourses.findOrCreate({ - where: { - user_id: user.id, - course_id: course.id, - }, - defaults: { - user_id: user.id, - course_id: course.id, - completed: false, - next_training_type: course?.training_type?.id ?? null, - }, - }); - - response.send(userBelongsToCourses); + // If Course-Instance couldn't be found, throw an error (caught locally) + if (course == null) { + response.sendStatus(HttpStatusCode.NotFound); + return; + } + + // Enrol user in course + const userBelongsToCourses = await UsersBelongsToCourses.findOrCreate({ + where: { + user_id: user.id, + course_id: course.id, + }, + defaults: { + user_id: user.id, + course_id: course.id, + completed: false, + next_training_type: course?.training_type?.id ?? null, + }, + }); + + response.send(userBelongsToCourses); + } catch (e) { + next(e); + } } /** - * + * Withdraws (un-enrolls) from a course by its UUID * @param request * @param response + * @param next */ -async function withdrawFromCourseByUUID(request: Request, response: Response) { - const user: User = response.locals.user; - const courseID = request.body.course_id; +async function withdrawFromCourseByUUID(request: Request, response: Response, next: NextFunction) { + const transaction = await sequelize.transaction(); - if (courseID == null) { - response.send(404); - return; - } + try { + const user: User = response.locals.user; + const body = request.body as { course_id: string }; + + Validator.validate(body, { + course_id: [ValidationTypeEnum.NON_NULL], + }); + + await UsersBelongsToCourses.destroy({ + where: { + course_id: body.course_id, + user_id: user.id, + }, + transaction: transaction, + }); + + await TrainingRequest.destroy({ + where: { + course_id: body.course_id, + user_id: user.id, + }, + transaction: transaction, + }); - await UsersBelongsToCourses.destroy({ - where: { - course_id: courseID, - user_id: user.id, - }, - }); - - await TrainingRequest.destroy({ - where: { - course_id: courseID, - user_id: user.id, - }, - }); - - response.send({ message: "OK" }); + await transaction.commit(); + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + await transaction.rollback(); + next(e); + } } /** @@ -254,7 +263,7 @@ async function getMentorable(_request: Request, response: Response, next: NextFu async function getEditable(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; - if (!user.isMentor()) { + if (!(await user.isMentor())) { throw new ForbiddenException("You are not a mentor"); } @@ -306,7 +315,6 @@ export default { getAvailableCourses, getActiveCourses, getCompletedCourses, - getMyCourses, enrolInCourse, withdrawFromCourseByUUID, getMentorable, diff --git a/backend/src/controllers/user/UserEndorsementAdminController.ts b/backend/src/controllers/user/UserEndorsementAdminController.ts index faabdfd..969760e 100644 --- a/backend/src/controllers/user/UserEndorsementAdminController.ts +++ b/backend/src/controllers/user/UserEndorsementAdminController.ts @@ -1,91 +1,103 @@ import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; -import _UserAdminValidator from "./_UserAdmin.validator"; import { EndorsementGroupsBelongsToUsers } from "../../models/through/EndorsementGroupsBelongsToUsers"; import { HttpStatusCode } from "axios"; import { EndorsementGroup } from "../../models/EndorsementGroup"; import { createEndorsement, removeEndorsement } from "../../libraries/vateud/VateudCoreLibrary"; - +import Validator, { ValidationTypeEnum } from "../../utility/Validator"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; + +/** + * Adds an endorsement to a user + * @param request + * @param response + * @param next + */ async function addEndorsement(request: Request, response: Response, next: NextFunction) { try { const requestingUser: User = response.locals.user; const body = request.body as { user_id: string; endorsement_group_id: string }; - _UserAdminValidator.validateCreateRequest(body); - const user = await User.findOne({ - where: { - id: Number(body.user_id), - }, - include: [User.associations.endorsement_groups], + Validator.validate(body, { + user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + endorsement_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], }); - if (!user) { - throw new Error(); - } const endorsementGroup = await EndorsementGroup.findOne({ where: { id: Number(body.endorsement_group_id), }, }); - if (!endorsementGroup) { - throw new Error(); + + if (endorsementGroup == null) { + response.sendStatus(HttpStatusCode.NotFound); + return; + } + + if (!(await endorsementGroup.userCanEndorse(requestingUser))) { + throw new ForbiddenException("You are not allowed to endorse this group"); } const userEndorsement = await EndorsementGroupsBelongsToUsers.create({ - user_id: user.id, + user_id: Number(body.user_id), endorsement_group_id: endorsementGroup.id, created_by: requestingUser.id, }); - const success = await createEndorsement(userEndorsement, endorsementGroup); - - if (!success) { + if (!(await createEndorsement(userEndorsement, endorsementGroup))) { await userEndorsement.destroy(); throw new Error(); } - response.status(HttpStatusCode.Created).send(user?.endorsement_groups ?? []); + response.status(HttpStatusCode.Created).send(endorsementGroup); } catch (e) { next(e); } } +// TODO: Paul (keine Ahnung, was du hier probierst :D) async function deleteEndorsement(request: Request, response: Response, next: NextFunction) { try { const requestingUser: User = response.locals.user; - const body = request.body as { user_endorsement_id: string }; - _UserAdminValidator.validateCreateRequest(body); - - - const userEndorsement = await EndorsementGroupsBelongsToUsers.findOne({where: { - id: Number(body.user_endorsement_id), - },}) + const body = request.body as { user_id: string; endorsement_group_id: string }; + Validator.validate(body, { + user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + endorsement_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + }); - const user = await User.findOne({ + const userEndorsement = await EndorsementGroupsBelongsToUsers.findOne({ where: { - id: userEndorsement?.user_id ?? -1, + user_id: body.user_id, + endorsement_group_id: body.endorsement_group_id, }, - include: [User.associations.endorsement_groups], }); - - const endorsementGroup = await EndorsementGroup.findOne({where: { + const endorsementGroup = await EndorsementGroup.findOne({ + where: { id: userEndorsement?.endorsement_group_id ?? -1, - },}) + }, + }); + if (endorsementGroup == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } + if (!(await endorsementGroup.userCanEndorse(requestingUser))) { + throw new ForbiddenException("You are not allowed to endorse this group"); + } const success = await removeEndorsement(userEndorsement, endorsementGroup); - if(success){ + if (success) { throw new Error("Could not delete endorsement in VATEUD CORE."); } await userEndorsement?.destroy(); - - response.status(HttpStatusCode.Ok).send(user?.endorsement_groups ?? []); + // Don't send anything back, we'll just remove it from the frontend + response.sendStatus(HttpStatusCode.Ok); } catch (e) { next(e); } diff --git a/backend/src/controllers/user/UserInformationAdminController.ts b/backend/src/controllers/user/UserInformationAdminController.ts index f272d4c..3195e47 100644 --- a/backend/src/controllers/user/UserInformationAdminController.ts +++ b/backend/src/controllers/user/UserInformationAdminController.ts @@ -16,6 +16,7 @@ async function getUserDataByID(request: Request, response: Response, next: NextF const query = request.query as { user_id: string }; Validator.validate(query, { user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] }); + PermissionHelper.checkUserHasPermission(user, "users.view"); if (Number(query.user_id) == user.id) { PermissionHelper.checkUserHasPermission(user, "mentor.acc.manage.own"); @@ -43,69 +44,37 @@ async function getUserDataByID(request: Request, response: Response, next: NextF } } -async function getBasicUserDataByID(request: Request, response: Response) { - const query = request.query as { user_id: string }; - - const user = await User.findOne({ - where: { - id: query.user_id, - }, - attributes: { - exclude: ["createdAt", "updatedAt"], - }, - }); - - if (user == null) { - response.status(404).send(); - return; - } - - response.send(user); -} - /** - * Returns the user data for a user with id request.query.user_id + * Returns basic user data by the id of the user (not entirely sure what the difference is to the above function) * @param request * @param response + * @param next */ -async function getSensitiveUserDataByID(request: Request, response: Response) { - const user_id: string | undefined = request.query.user_id?.toString(); - const user: User = response.locals.user; - - if (user_id == null) { - response.status(404).send({ error: 'Parameter "user_id" is required' }); - return; - } - //TODO: Change this to proper permission to access sensitive data - //Potentially wrong, should user id be equal or not equal? (using logical or instead) - PermissionHelper.checkUserHasPermission(user, "mentor.acc.manage.own"); - if (user_id == user.id.toString()) return; +async function getBasicUserDataByID(request: Request, response: Response, next: NextFunction) { + try { + const query = request.query as { user_id: string }; - const data = await User.scope("sensitive").findOne({ - where: { - id: user_id, - }, - include: [ - { - association: User.associations.user_data, - }, - { - association: User.associations.mentor_groups, + const user = await User.findOne({ + where: { + id: query.user_id, }, - { - association: User.associations.courses, - through: { - as: "through", - }, + attributes: { + exclude: ["createdAt", "updatedAt"], }, - ], - }); + }); - response.send(data); + if (user == null) { + response.status(404).send(); + return; + } + + response.send(user); + } catch (e) { + next(e); + } } export default { getUserDataByID, getBasicUserDataByID, - getSensitiveUserDataByID, }; diff --git a/backend/src/controllers/user/UserInformationController.ts b/backend/src/controllers/user/UserInformationController.ts index 6e3de9e..46ef892 100644 --- a/backend/src/controllers/user/UserInformationController.ts +++ b/backend/src/controllers/user/UserInformationController.ts @@ -5,7 +5,13 @@ import { EndorsementGroup } from "../../models/EndorsementGroup"; import { TrainingSession } from "../../models/TrainingSession"; import dayjs from "dayjs"; -async function getOverviewStatistics(request: Request, response: Response, next: NextFunction) { +/** + * Returns an overview of all statistics (such as endorsement groups, next trainings, etc.) + * @param _request + * @param response + * @param next + */ +async function getOverviewStatistics(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; diff --git a/backend/src/controllers/user/UserMentorGroupController.ts b/backend/src/controllers/user/UserMentorGroupController.ts index ed99c82..a1b15a2 100644 --- a/backend/src/controllers/user/UserMentorGroupController.ts +++ b/backend/src/controllers/user/UserMentorGroupController.ts @@ -1,53 +1,63 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; import { UserBelongToMentorGroups } from "../../models/through/UserBelongToMentorGroups"; import { MentorGroup } from "../../models/MentorGroup"; /** * Gets all mentor groups that are associated with the current user - * @param request + * @param _request * @param response + * @param next */ -async function getMentorGroups(request: Request, response: Response) { - const reqUser: User = response.locals.user; - - const userInMentorGroups = await UserBelongToMentorGroups.findAll({ - where: { - user_id: reqUser.id, - }, - include: [UserBelongToMentorGroups.associations.mentor_group], - }); - - let mentorGroups: MentorGroup[] = []; - for (const u of userInMentorGroups) { - if (u.mentor_group != null) mentorGroups.push(u.mentor_group); - } +async function getMentorGroups(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + + const userInMentorGroups = await UserBelongToMentorGroups.findAll({ + where: { + user_id: user.id, + }, + include: [UserBelongToMentorGroups.associations.mentor_group], + }); - response.send(mentorGroups); + let mentorGroups: MentorGroup[] = []; + for (const u of userInMentorGroups) { + if (u.mentor_group != null) mentorGroups.push(u.mentor_group); + } + + response.send(mentorGroups); + } catch (e) { + next(e); + } } /** * Gets all mentor groups in which the current user can manage (i.e. can_manage_course flag set) - * @param request + * @param _request * @param response + * @param next */ -async function getCourseManagerMentorGroups(request: Request, response: Response) { - const reqUser: User = response.locals.user; - - const userInMentorGroups = await UserBelongToMentorGroups.findAll({ - where: { - user_id: reqUser.id, - can_manage_course: true, - }, - include: [UserBelongToMentorGroups.associations.mentor_group], - }); - - let mentorGroups: MentorGroup[] = []; - for (const u of userInMentorGroups) { - if (u.mentor_group != null) mentorGroups.push(u.mentor_group); - } +async function getCourseManagerMentorGroups(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + + const userInMentorGroups = await UserBelongToMentorGroups.findAll({ + where: { + user_id: user.id, + can_manage_course: true, + }, + include: [UserBelongToMentorGroups.associations.mentor_group], + }); - response.send(mentorGroups); + let mentorGroups: MentorGroup[] = []; + for (const u of userInMentorGroups) { + if (u.mentor_group != null) mentorGroups.push(u.mentor_group); + } + + response.send(mentorGroups); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/user/UserNoteAdminController.ts b/backend/src/controllers/user/UserNoteAdminController.ts index 97a2eaf..aa59334 100644 --- a/backend/src/controllers/user/UserNoteAdminController.ts +++ b/backend/src/controllers/user/UserNoteAdminController.ts @@ -4,63 +4,94 @@ import { User } from "../../models/User"; import { generateUUID } from "../../utility/UUID"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; import { HttpStatusCode } from "axios"; +import PermissionHelper from "../../utility/helper/PermissionHelper"; +import { Course } from "../../models/Course"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; /** * Gets the specified user's notes that are not linked to a course, i.e. all those, that all mentors can see * @param request * @param response + * @param next */ -async function getGeneralUserNotes(request: Request, response: Response) { - const query = request.query as { user_id: string }; - - // const validation = ValidationHelper.validate([ - // { - // name: "user_id", - // validationObject: query.user_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // ]); - - const notes: UserNote[] = await UserNote.findAll({ - where: { - user_id: query.user_id, - course_id: null, - }, - include: { - association: UserNote.associations.author, - attributes: ["id", "first_name", "last_name"], - }, - }); - - response.send(notes); +async function getGeneralUserNotes(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const query = request.query as { user_id: string }; + + PermissionHelper.checkUserHasPermission(user, "notes.view"); + + const notes: UserNote[] = await UserNote.findAll({ + where: { + user_id: query.user_id, + course_id: null, + }, + include: { + association: UserNote.associations.author, + attributes: ["id", "first_name", "last_name"], + }, + }); + + response.send(notes); + } catch (e) { + next(e); + } } /** * Gets all the notes of the requested user by the specified course_id + * @param request + * @param response + * @param next */ -async function getNotesByCourseID(request: Request, response: Response) { - const courseID = request.query.courseID; - const userID = request.query.userID; - - const notes: UserNote[] = await UserNote.findAll({ - where: { - user_id: userID?.toString(), - course_id: courseID?.toString(), - }, - include: { - association: UserNote.associations.author, - attributes: ["id", "first_name", "last_name"], - }, - }); - - response.send(notes); +async function getNotesByCourseID(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const query = request.query as { courseID: string; userID: string }; + + PermissionHelper.checkUserHasPermission(user, "notes.view"); + const courseUUID = await Course.getUUIDFromID(query.courseID); + if (!(await user.isMentorInCourse(courseUUID))) { + throw new ForbiddenException("You are not a mentor of this course"); + } + + const notes: UserNote[] = await UserNote.findAll({ + where: { + user_id: query.userID?.toString(), + course_id: query.courseID?.toString(), + }, + include: { + association: UserNote.associations.author, + attributes: ["id", "first_name", "last_name"], + }, + }); + + response.send(notes); + } catch (e) { + next(e); + } } +/** + * Creates a new user note + * @param request + * @param response + * @param next + */ async function createUserNote(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; const body = request.body as { user_id: string; content: string; course_id?: string }; + PermissionHelper.checkUserHasPermission(user, "notes.create"); + + if (body.course_id != null) { + const courseUUID = await Course.getUUIDFromID(body.course_id); + if (!(await user.isMentorInCourse(courseUUID))) { + throw new ForbiddenException("You are not a mentor of this course"); + } + } + Validator.validate(body, { user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], content: [ValidationTypeEnum.NON_NULL], diff --git a/backend/src/controllers/user/UserNotificationController.ts b/backend/src/controllers/user/UserNotificationController.ts index 7113d17..c709206 100644 --- a/backend/src/controllers/user/UserNotificationController.ts +++ b/backend/src/controllers/user/UserNotificationController.ts @@ -1,140 +1,178 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; import { Notification } from "../../models/Notification"; -import { Op } from "sequelize"; import { HttpStatusCode } from "axios"; -async function getNotifications(request: Request, response: Response) { - const user: User = response.locals.user; +/** + * Gets all notifications for a specific user + * @param _request + * @param response + * @param next + */ +async function getNotifications(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; - const notifications: Notification[] = await Notification.findAll({ - where: { - user_id: user.id, - }, - order: [["createdAt", "desc"]], - include: [ - { - association: Notification.associations.author, - attributes: ["id", "first_name", "last_name"], + const notifications: Notification[] = await Notification.findAll({ + where: { + user_id: user.id, }, - ], - }); - - response.send(notifications); + order: [["createdAt", "desc"]], + include: [ + { + association: Notification.associations.author, + attributes: ["id", "first_name", "last_name"], + }, + ], + }); + + response.send(notifications); + } catch (e) { + next(e); + } } /** * Returns all unread notifications for the requesting user - * @param request + * @param _request * @param response + * @param next */ -async function getUnreadNotifications(request: Request, response: Response) { - const user: User = response.locals.user; - - const notifications: Notification[] = await Notification.findAll({ - where: { - user_id: user.id, - read: false, - }, - order: [["createdAt", "desc"]], - include: [ - { - association: Notification.associations.author, - attributes: ["id", "first_name", "last_name"], - }, - ], - }); +async function getUnreadNotifications(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; - response.send(notifications); + const notifications: Notification[] = await Notification.findAll({ + where: { + user_id: user.id, + read: false, + }, + order: [["createdAt", "desc"]], + include: [ + { + association: Notification.associations.author, + attributes: ["id", "first_name", "last_name"], + }, + ], + }); + + response.send(notifications); + } catch (e) { + next(e); + } } /** * Marks all notifications as read - * @param request + * @param _request * @param response + * @param next */ -async function markAllNotificationsRead(request: Request, response: Response) { - const user: User = response.locals.user; - - await Notification.update( - { - read: true, - }, - { - where: { - user_id: user.id, - }, - } - ); +async function markAllNotificationsRead(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; - response.sendStatus(HttpStatusCode.NoContent); + await Notification.update( + { + read: true, + }, + { + where: { + user_id: user.id, + }, + } + ); + + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** * Marks a single notification as read + * @param request + * @param response + * @param next */ -async function markNotificationRead(request: Request, response: Response) { - const user: User = response.locals.user; - const body = request.body as { notification_id: string }; - - await Notification.update( - { - read: true, - }, - { - where: { - id: body.notification_id, - user_id: user.id, // Just an additional sanity check, not really needed since the id is PK - }, - } - ); +async function markNotificationRead(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { notification_id: string }; - response.sendStatus(HttpStatusCode.NoContent); + await Notification.update( + { + read: true, + }, + { + where: { + id: body.notification_id, + user_id: user.id, // Just an additional sanity check, not really needed since the id is PK + }, + } + ); + + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** * Deletes a single notification + * @param request + * @param response + * @param next */ -async function deleteNotification(request: Request, response: Response) { - const user: User = response.locals.user; - const body = request.body as { notification_id: string }; - - await Notification.destroy({ - where: { - id: body.notification_id, - user_id: user.id, - }, - }); - - response.sendStatus(HttpStatusCode.NoContent); +async function deleteNotification(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { notification_id: string }; + + await Notification.destroy({ + where: { + id: body.notification_id, + user_id: user.id, + }, + }); + + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** * Toggles the read flag of a notification * @param request * @param response + * @param next */ -async function toggleMarkNotificationRead(request: Request, response: Response) { - const user: User = response.locals.user; - const body = request.body as { notification_id: string }; - - const notification = await Notification.findOne({ - where: { - id: body.notification_id, - user_id: user.id, - }, - }); - - if (notification == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } +async function toggleMarkNotificationRead(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { notification_id: string }; - await notification.update({ - read: !notification.read, - }); + const notification = await Notification.findOne({ + where: { + id: body.notification_id, + user_id: user.id, + }, + }); + + if (notification == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } + + await notification.update({ + read: !notification.read, + }); - response.sendStatus(HttpStatusCode.NoContent); + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/user/UserSettingsController.ts b/backend/src/controllers/user/UserSettingsController.ts index 8493e0b..a51289f 100644 --- a/backend/src/controllers/user/UserSettingsController.ts +++ b/backend/src/controllers/user/UserSettingsController.ts @@ -3,6 +3,12 @@ import { User } from "../../models/User"; import { UserSettings } from "../../models/UserSettings"; import { HttpStatusCode } from "axios"; +/** + * Updates the settings for the requesting user + * @param request + * @param response + * @param next + */ async function updateSettings(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; diff --git a/backend/src/controllers/user/UserStatisticsController.ts b/backend/src/controllers/user/UserStatisticsController.ts index edce4e3..301fbb8 100644 --- a/backend/src/controllers/user/UserStatisticsController.ts +++ b/backend/src/controllers/user/UserStatisticsController.ts @@ -1,29 +1,40 @@ import { NextFunction, Request, Response } from "express"; -import axios, { AxiosError, HttpStatusCode } from "axios"; +import axios from "axios"; import { User } from "../../models/User"; -import Logger, { LogLevels } from "../../utility/Logger"; import { Config } from "../../core/Config"; +import { localCache } from "../../core/Cache"; // TODO: The entire statistics controller should ideally cache the data for at least a day (the hours really don't change that much). // That way we can drastically reduce load times and the chance of getting blocked by VATSIM (again). async function getUserRatingTimes(request: Request, response: Response, next: NextFunction) { - const user: User = response.locals.user; - let userID = user.id; + try { + const user: User = response.locals.user; + let userID = user.id; - if (Config.APP_DEBUG) { - userID = 1373921; - } + if (Config.APP_DEBUG) { + userID = 1373921; + } + + if (localCache.has(userID)) { + response.send(localCache.get(userID)); + return; + } - try { const res = await axios.get(`https://api.vatsim.net/v2/members/${userID}/stats`); + localCache.set(userID, res.data); response.send(res.data); - } catch (e: any) { - Logger.log(LogLevels.LOG_WARN, `Failed to retrieve API Statistics for ${userID}: ${e.message}`); - response.sendStatus(HttpStatusCode.InternalServerError); + } catch (e) { + next(e); } } +/** + * Returns the number of sessions of the requesting user (including planned afaik) + * @param request + * @param response + * @param next + */ async function getUserTrainingSessionCount(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; diff --git a/backend/src/controllers/user/UserTrainingController.ts b/backend/src/controllers/user/UserTrainingController.ts index 1cae916..8739dfd 100644 --- a/backend/src/controllers/user/UserTrainingController.ts +++ b/backend/src/controllers/user/UserTrainingController.ts @@ -1,85 +1,104 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; import { TrainingRequest } from "../../models/TrainingRequest"; import { Course } from "../../models/Course"; import { HttpStatusCode } from "axios"; -import { Op } from "sequelize"; /** * Get a list of all training-requests made by this user * @param request * @param response + * @param next */ -async function getRequests(request: Request, response: Response) { - const reqUser: User = response.locals.user; - const user = await User.findOne({ - where: { - id: reqUser.id, - }, - include: [User.associations.training_requests], - }); +async function getRequests(request: Request, response: Response, next: NextFunction) { + try { + const reqUser: User = response.locals.user; + const user = await User.findOne({ + where: { + id: reqUser.id, + }, + include: [User.associations.training_requests], + }); - response.send(user?.training_requests ?? []); + response.send(user?.training_requests ?? []); + } catch (e) { + next(e); + } } /** * Get a list of all training-requests made by this user for a specified course * @param request * @param response + * @param next */ -async function getRequestsByUUID(request: Request, response: Response) { - const reqUser: User = response.locals.user; - const course_uuid: string | undefined = request.params?.course_uuid?.toString(); +async function getRequestsByUUID(request: Request, response: Response, next: NextFunction) { + try { + const reqUser: User = response.locals.user; + const course_uuid: string | undefined = request.params?.course_uuid?.toString(); - const course_id = await Course.getIDFromUUID(course_uuid); - if (course_id == -1) { - response.status(500).send("Some error"); - return; - } + const course_id = await Course.getIDFromUUID(course_uuid); + if (course_id == -1) { + response.status(500).send("Some error"); + return; + } - const trainingRequests: TrainingRequest[] = await TrainingRequest.findAll({ - where: { - user_id: reqUser.id, - course_id: course_id, - }, - include: [TrainingRequest.associations.training_type, TrainingRequest.associations.training_station], - }); + const trainingRequests: TrainingRequest[] = await TrainingRequest.findAll({ + where: { + user_id: reqUser.id, + course_id: course_id, + }, + include: [TrainingRequest.associations.training_type, TrainingRequest.associations.training_station], + }); - response.send(trainingRequests); + response.send(trainingRequests); + } catch (e) { + next(e); + } } -async function getActiveRequestsByUUID(request: Request, response: Response) { - const user: User = response.locals.user; - const params = request.params as { course_uuid: string }; +/** + * Gets information on an active training requests by its UUID + * @param request + * @param response + * @param next + */ +async function getActiveRequestsByUUID(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { course_uuid: string }; + + const course = await Course.findOne({ + where: { + uuid: params.course_uuid, + }, + }); - const course = await Course.findOne({ - where: { - uuid: params.course_uuid, - }, - }); + if (course == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } - if (course == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } + const trainingRequests = await TrainingRequest.findAll({ + where: { + user_id: user.id, + course_id: course.id, + }, + include: [TrainingRequest.associations.training_type, TrainingRequest.associations.training_station], + }); - const trainingRequests = await TrainingRequest.findAll({ - where: { - user_id: user.id, - course_id: course.id, - }, - include: [TrainingRequest.associations.training_type, TrainingRequest.associations.training_station], - }); + for (const trainingRequest of trainingRequests) { + await trainingRequest.appendNumberInQueue(); + } - for (const trainingRequest of trainingRequests) { - await trainingRequest.appendNumberInQueue(); + response.send( + trainingRequests.filter(t => { + return t.status == "requested" || t.status == "planned"; + }) + ); + } catch (e) { + next(e); } - - response.send( - trainingRequests.filter(t => { - return t.status == "requested" || t.status == "planned"; - }) - ); } export default { diff --git a/backend/src/controllers/user/_UserAdmin.validator.ts b/backend/src/controllers/user/_UserAdmin.validator.ts deleted file mode 100644 index 642e64b..0000000 --- a/backend/src/controllers/user/_UserAdmin.validator.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ValidationException } from "../../exceptions/ValidationException"; - -function validateCreateRequest(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "user_id", - // validationObject: data.user_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // { - // name: "endorsement_group_id", - // validationObject: data.endorsement_group_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -export default { - validateCreateRequest, -}; diff --git a/backend/src/core/Cache.ts b/backend/src/core/Cache.ts new file mode 100644 index 0000000..8a4ba98 --- /dev/null +++ b/backend/src/core/Cache.ts @@ -0,0 +1,7 @@ +import NodeCache from "node-cache"; + +const cacheOptions: NodeCache.Options = { + stdTTL: 60 * 60 * 24, +}; + +export const localCache: NodeCache = new NodeCache(cacheOptions); diff --git a/backend/src/exceptions/GenericException.ts b/backend/src/exceptions/GenericException.ts index 2744dfa..6bc19d3 100644 --- a/backend/src/exceptions/GenericException.ts +++ b/backend/src/exceptions/GenericException.ts @@ -15,4 +15,4 @@ export class GenericException extends Error { public getMessage() { return this.message_; } -} \ No newline at end of file +} diff --git a/backend/src/libraries/vateud/VateudCoreLibrary.ts b/backend/src/libraries/vateud/VateudCoreLibrary.ts index bcec29c..880c9ab 100644 --- a/backend/src/libraries/vateud/VateudCoreLibrary.ts +++ b/backend/src/libraries/vateud/VateudCoreLibrary.ts @@ -5,7 +5,6 @@ import { VateudCoreSoloCreateResponseT, VateudCoreSoloCreateT, VateudCoreSoloRemoveResponseT, - VateudCoreSoloRemoveT, VateudCoreTypeEnum, } from "./VateudCoreLibraryTypes"; import { Config } from "../../core/Config"; @@ -172,9 +171,9 @@ export async function createEndorsement(userEndorsement: EndorsementGroupsBelong * Removes a Tier 1 or 2 endorsement. * On success, it updates the corresponding endorsement */ -export async function removeEndorsement(userEndorsement: EndorsementGroupsBelongsToUsers|null, endorsementGroup: EndorsementGroup | null) { - if(!endorsementGroup || !userEndorsement) return false; - if(!userEndorsement.vateud_id) return true; +export async function removeEndorsement(userEndorsement: EndorsementGroupsBelongsToUsers | null, endorsementGroup: EndorsementGroup | null) { + if (!endorsementGroup || !userEndorsement) return false; + if (!userEndorsement.vateud_id) return true; const res = await _send({ endpoint: `facility/endorsements/tier-${endorsementGroup.tier}/${userEndorsement.vateud_id}`, diff --git a/backend/src/middlewares/ExceptionInterceptorMiddleware.ts b/backend/src/middlewares/ExceptionInterceptorMiddleware.ts index d9f7d6b..b874527 100644 --- a/backend/src/middlewares/ExceptionInterceptorMiddleware.ts +++ b/backend/src/middlewares/ExceptionInterceptorMiddleware.ts @@ -98,7 +98,7 @@ export async function exceptionInterceptorMiddleware(error: any, request: Reques method: request.method, code: HttpStatusCode.BadRequest, error_code: error.getCode(), - message: error.getMessage() + message: error.getMessage(), }); return; } diff --git a/backend/src/models/EndorsementGroup.ts b/backend/src/models/EndorsementGroup.ts index 40c740c..8ca5c13 100644 --- a/backend/src/models/EndorsementGroup.ts +++ b/backend/src/models/EndorsementGroup.ts @@ -3,6 +3,7 @@ import { sequelize } from "../core/Sequelize"; import { User } from "./User"; import { TrainingStation } from "./TrainingStation"; import { ENDORSEMENT_GROUPS_TABLE_ATTRIBUTES, ENDORSEMENT_GROUPS_TABLE_NAME } from "../../db/migrations/20221115171254-create-endorsement-groups-table"; +import EndorsementGroupExtensions from "./extensions/EndorsementGroupExtensions"; export interface IEndorsementGroup { id: number; @@ -38,6 +39,8 @@ export class EndorsementGroup extends Model, I users: Association; stations: Association; }; + + userCanEndorse = EndorsementGroupExtensions.userCanEndorse.bind(this); } EndorsementGroup.init(ENDORSEMENT_GROUPS_TABLE_ATTRIBUTES, { diff --git a/backend/src/models/TrainingSession.ts b/backend/src/models/TrainingSession.ts index de207f9..06fae8f 100644 --- a/backend/src/models/TrainingSession.ts +++ b/backend/src/models/TrainingSession.ts @@ -1,5 +1,4 @@ import { Model, InferAttributes, CreationOptional, InferCreationAttributes, NonAttribute, Association, ForeignKey } from "sequelize"; -import { DataType } from "sequelize-typescript"; import { sequelize } from "../core/Sequelize"; import { User } from "./User"; import { TrainingType } from "./TrainingType"; @@ -9,6 +8,7 @@ import { TrainingStation } from "./TrainingStation"; import { TrainingSessionBelongsToUsers } from "./through/TrainingSessionBelongsToUsers"; import { TRAINING_SESSION_TABLE_ATTRIBUTES, TRAINING_SESSION_TABLE_NAME } from "../../db/migrations/20221115171253-create-training-sessions-table"; import { CptSession } from "./CptSession"; +import TrainingSessionExtensions from "./extensions/TrainingSessionExtensions"; export class TrainingSession extends Model, InferCreationAttributes> { // @@ -54,6 +54,11 @@ export class TrainingSession extends Model, Inf cpt: Association; }; + userCanRead = TrainingSessionExtensions.userCanRead.bind(this); + userCanCreateLogs = TrainingSessionExtensions.userCanCreateLogs.bind(this); + getAvailableMentorGroups = TrainingSessionExtensions.getAvailableMentorGroups.bind(this); + isUserParticipant = TrainingSessionExtensions.isUserParticipant.bind(this); + static async getIDFromUUID(uuid: string) { const trainingSession = await TrainingSession.findOne({ where: { diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 10915eb..81a7c18 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -97,6 +97,7 @@ export class User extends Model, InferCreationAttributes } export default { - canUserView -} \ No newline at end of file + canUserView, +}; diff --git a/backend/src/models/extensions/TrainingSessionExtensions.ts b/backend/src/models/extensions/TrainingSessionExtensions.ts new file mode 100644 index 0000000..5d908fe --- /dev/null +++ b/backend/src/models/extensions/TrainingSessionExtensions.ts @@ -0,0 +1,81 @@ +import { TrainingSession } from "../TrainingSession"; +import { User } from "../User"; +import { Course } from "../Course"; +import { MentorGroup } from "../MentorGroup"; + +/** + * Checks if the user is allowed to view the training session + * @param user + */ +function userCanRead(this: TrainingSession, user: User): boolean { + if (user.hasPermission("atd.override")) { + return true; + } + + return this.mentor_id == user.id; +} + +/** + * Checks if the user is allowed to create training logs for this session + * @param user + */ +function userCanCreateLogs(this: TrainingSession, user: User): boolean { + if (user.hasPermission("atd.override")) { + return true; + } + + return this.mentor_id == user.id; +} + +/** + * Returns all available mentors that could also mentor this session. + * (for passing the training on) + */ +async function getAvailableMentorGroups(this: TrainingSession): Promise { + const trainingSession = await TrainingSession.findOne({ + where: { + uuid: this.uuid, + }, + include: [ + { + association: TrainingSession.associations.course, + through: { + attributes: [], + }, + include: [ + { + association: Course.associations.mentor_groups, + through: { + attributes: [], + }, + include: [ + { + association: MentorGroup.associations.users, + through: { + attributes: [], + }, + }, + ], + }, + ], + }, + ], + }); + + return trainingSession?.course?.mentor_groups ?? []; +} + +/** + * Checks whether the user is a participant of the training session + * @param user + */ +function isUserParticipant(this: TrainingSession, user: User): boolean { + return this.users?.find(participant => participant.id == user.id) != null; +} + +export default { + userCanRead, + userCanCreateLogs, + getAvailableMentorGroups, + isUserParticipant, +}; diff --git a/backend/src/models/extensions/UserExtensions.ts b/backend/src/models/extensions/UserExtensions.ts index 7bca4a4..9ef5e69 100644 --- a/backend/src/models/extensions/UserExtensions.ts +++ b/backend/src/models/extensions/UserExtensions.ts @@ -3,7 +3,6 @@ import { MentorGroup } from "../MentorGroup"; import { Role } from "../Role"; import { Permission } from "../Permission"; import { Course } from "../Course"; -import { TrainingSessionBelongsToUsers } from "../through/TrainingSessionBelongsToUsers"; /** * Checks if the user as the specified role @@ -50,6 +49,34 @@ async function getMentorGroups(this: User): Promise { return user?.mentor_groups ?? []; } +async function getMentorGroupsAndEndorsementGroups(this: User): Promise { + const user = await User.findOne({ + where: { + id: this.id, + }, + include: [ + { + association: User.associations.mentor_groups, + attributes: ["id", "name"], + through: { + attributes: [], + }, + include: [ + { + association: MentorGroup.associations.endorsement_groups, + attributes: ["id", "name"], + through: { + attributes: [], + }, + }, + ], + }, + ], + }); + + return user?.mentor_groups ?? []; +} + /** * Returns a list of mentor groups this user is a group admin in */ @@ -76,7 +103,9 @@ async function getGroupAdminMentorGroups(this: User): Promise { * course and is, by extension, allowed to mentor it. * @param uuid */ -async function isMentorInCourse(this: User, uuid: string): Promise { +async function isMentorInCourse(this: User, uuid?: string): Promise { + if (uuid == null) return false; + const mentorGroups = await this.getMentorGroupsAndCourses(); for (const mentorGroup of mentorGroups) { @@ -189,6 +218,7 @@ export default { hasRole, hasPermission, getMentorGroups, + getMentorGroupsAndEndorsementGroups, getGroupAdminMentorGroups, getCourseCreatorMentorGroups, canManageCourseInMentorGroup, @@ -196,5 +226,5 @@ export default { getCourses, getCoursesWithInformation, isMentorInCourse, - isMentor + isMentor, }; diff --git a/frontend/src/components/template/SideNav.tsx b/frontend/src/components/template/SideNav.tsx index 9048808..2c0e73a 100644 --- a/frontend/src/components/template/SideNav.tsx +++ b/frontend/src/components/template/SideNav.tsx @@ -1,7 +1,7 @@ import vaccLogo from "../../assets/img/vacc_logo.png"; import vaccLogoDark from "../../assets/img/vacc_logo_dark.png"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { MenuItem } from "../ui/MenuItem/MenuItem"; import { TbAdjustments, @@ -220,7 +220,10 @@ export function SideNav() { Fast-Tracks - }> + }> Trainingsstationen diff --git a/frontend/src/components/ui/Input/InputGroup.tsx b/frontend/src/components/ui/Input/InputGroup.tsx index aa13cc2..6a4ac0c 100644 --- a/frontend/src/components/ui/Input/InputGroup.tsx +++ b/frontend/src/components/ui/Input/InputGroup.tsx @@ -4,10 +4,6 @@ import { ReactElement } from "react"; * Creates a grid of inputs to allow coherence between pages * @constructor */ -export function InputGroup({children}: {children: ReactElement | ReactElement[]}) { - return ( -
- {children} -
- ) -} \ No newline at end of file +export function InputGroup({ children }: { children: ReactElement | ReactElement[] }) { + return
{children}
; +} diff --git a/frontend/src/pages/administration/lm/endorsement-group/create/EndorsementGroupCreate.view.tsx b/frontend/src/pages/administration/lm/endorsement-group/create/EndorsementGroupCreate.view.tsx index b5dbb50..08e7f10 100644 --- a/frontend/src/pages/administration/lm/endorsement-group/create/EndorsementGroupCreate.view.tsx +++ b/frontend/src/pages/administration/lm/endorsement-group/create/EndorsementGroupCreate.view.tsx @@ -104,7 +104,9 @@ export function EndorsementGroupCreateView() { name={"name_vateud"} type={"text"} maxLength={70} - description={"Dies ist der Name, welcher bei VATEUD angezeigt wird, sobald einem Benutzer die Freigabegruppe zugewiesen wurde. Dieser Name kann NICHT mehr geändert werden!"} + description={ + "Dies ist der Name, welcher bei VATEUD angezeigt wird, sobald einem Benutzer die Freigabegruppe zugewiesen wurde. Dieser Name kann NICHT mehr geändert werden!" + } labelSmall className={"mt-5"} placeholder={"EDDF_GNDDEL"} @@ -123,9 +125,8 @@ export function EndorsementGroupCreateView() { required className={"mt-5"} value={selectedTrainingStation} - preIcon={} - description={"Dieser Wert kann NICHT mehr geändert werden."} - > + preIcon={} + description={"Dieser Wert kann NICHT mehr geändert werden."}> diff --git a/frontend/src/pages/administration/mentor/users/view/_modals/UVAddEndorsement.modal.tsx b/frontend/src/pages/administration/mentor/users/view/_modals/UVAddEndorsement.modal.tsx index 97fbf1a..45084b9 100644 --- a/frontend/src/pages/administration/mentor/users/view/_modals/UVAddEndorsement.modal.tsx +++ b/frontend/src/pages/administration/mentor/users/view/_modals/UVAddEndorsement.modal.tsx @@ -8,8 +8,6 @@ import React, { Dispatch, FormEvent, useState } from "react"; import { TableColumn } from "react-data-table-component"; import { TrainingStationModel } from "@/models/TrainingStationModel"; import { UserModel } from "@/models/UserModel"; -import { Separator } from "@/components/ui/Separator/Separator"; -import { Checkbox } from "@/components/ui/Checkbox/Checkbox"; import { Button } from "@/components/ui/Button/Button"; import { TbPlus } from "react-icons/tb"; import { COLOR_OPTS } from "@/assets/theme.config"; @@ -63,8 +61,8 @@ export function UVAddEndorsementModal({ .post("/administration/endorsement", FormHelper.toJSON(formData)) .then((res: AxiosResponse) => { ToastHelper.success("Freigabe erfolgreich erstellt"); - const endorsements = res.data as EndorsementGroupModel[]; - setUser({ ...user, endorsement_groups: endorsements }); + const endorsements = res.data as EndorsementGroupModel; + setUser({ ...user, endorsement_groups: [...(userEndorsementGroups ?? []), endorsements] }); onClose(); }) diff --git a/frontend/src/pages/administration/mentor/users/view/_modals/UVDeleteEndorsement.modal.tsx b/frontend/src/pages/administration/mentor/users/view/_modals/UVDeleteEndorsement.modal.tsx index 3cec2a5..ff515c2 100644 --- a/frontend/src/pages/administration/mentor/users/view/_modals/UVDeleteEndorsement.modal.tsx +++ b/frontend/src/pages/administration/mentor/users/view/_modals/UVDeleteEndorsement.modal.tsx @@ -1,21 +1,12 @@ import { Modal } from "@/components/ui/Modal/Modal"; -import { UserModel, UserSoloModel } from "@/models/UserModel"; -import { Input } from "@/components/ui/Input/Input"; -import { getAtcRatingCombined } from "@/utils/helper/vatsim/AtcRatingHelper"; +import { UserModel } from "@/models/UserModel"; import { Separator } from "@/components/ui/Separator/Separator"; -import { Select } from "@/components/ui/Select/Select"; -import dayjs from "dayjs"; import React, { Dispatch, FormEvent, useState } from "react"; import { Button } from "@/components/ui/Button/Button"; -import { TbPlaylistAdd, TbTrack, TbTrash } from "react-icons/tb"; +import { TbTrash } from "react-icons/tb"; import { COLOR_OPTS } from "@/assets/theme.config"; -import FormHelper from "@/utils/helper/FormHelper"; import { axiosInstance } from "@/utils/network/AxiosInstance"; import ToastHelper from "@/utils/helper/ToastHelper"; -import { AxiosResponse } from "axios"; -import { EndorsementGroupModel } from "@/models/EndorsementGroupModel"; -import useApi from "@/utils/hooks/useApi"; -import { MapArray } from "@/components/conditionals/MapArray"; export function UVDeleteSoloModal({ show, onClose, user, setUser }: { show: boolean; onClose: () => any; user?: UserModel; setUser: Dispatch }) { const [deletingSolo, setDeletingSolo] = useState(false); @@ -63,7 +54,6 @@ export function UVDeleteSoloModal({ show, onClose, user, setUser }: { show: bool Löschen }> -

diff --git a/package-lock.json b/package-lock.json index 9a84d91..5689c39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "handlebars": "^4.7.8", "multer": "^1.4.5-lts.1", "mysql2": "^2.3.3", + "node-cache": "^5.1.2", "node-cron": "^3.0.2", "nodemailer": "^6.9.7", "pretty-bytes": "^6.0.0", @@ -1739,6 +1740,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -4007,6 +4016,17 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-cron": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", diff --git a/package.json b/package.json index 1de9b71..377f72f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "handlebars": "^4.7.8", "multer": "^1.4.5-lts.1", "mysql2": "^2.3.3", + "node-cache": "^5.1.2", "node-cron": "^3.0.2", "nodemailer": "^6.9.7", "pretty-bytes": "^6.0.0",