From 9410c28df109c23ef8881e5b299cea08ec148776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Mon, 20 May 2024 18:26:42 +0200 Subject: [PATCH] More permissions and changes to endorsement group creation. --- backend/db/config/options.ts | 6 +- ...5171254-create-endorsement-groups-table.ts | 4 + .../20221121101837-PermissionSeeder.ts | 7 +- backend/src/Router.ts | 2 +- .../EndorsementGroupAdminController.ts | 4 +- .../LogTemplateAdminController.ts | 1 - .../permission/PermissionAdminController.ts | 92 ++++-- .../permission/RoleAdminController.ts | 64 ++-- .../controllers/solo/SoloAdminController.ts | 98 ++++-- .../controllers/solo/_SoloAdmin.validator.ts | 74 ----- .../training-log/TrainingLogController.ts | 14 + .../TrainingRequestAdminController.ts | 176 ++++++----- .../TrainingRequestController.ts | 297 ++++++++++-------- .../TrainingSessionAdminController.ts | 3 + backend/src/exceptions/GenericException.ts | 18 ++ .../ExceptionInterceptorMiddleware.ts | 12 + backend/src/models/EndorsementGroup.ts | 10 + backend/src/models/TrainingLog.ts | 3 + backend/src/models/TrainingRequest.ts | 3 + .../extensions/TrainingLogExtensions.ts | 28 ++ .../extensions/TrainingRequestExtensions.ts | 12 + .../src/models/extensions/UserExtensions.ts | 8 +- frontend/src/components/template/SideNav.tsx | 2 +- .../src/components/ui/Input/InputGroup.tsx | 13 + .../TrainingSessionBelongsToUser.model.ts | 1 - .../create/EndorsementGroupCreate.view.tsx | 37 ++- .../view/_subpages/EGVSettings.subpage.tsx | 26 +- .../TrainingSessionLogsCreate.view.tsx | 13 - package.json | 6 +- 29 files changed, 605 insertions(+), 429 deletions(-) delete mode 100644 backend/src/controllers/solo/_SoloAdmin.validator.ts create mode 100644 backend/src/exceptions/GenericException.ts create mode 100644 backend/src/models/extensions/TrainingLogExtensions.ts create mode 100644 backend/src/models/extensions/TrainingRequestExtensions.ts create mode 100644 frontend/src/components/ui/Input/InputGroup.tsx diff --git a/backend/db/config/options.ts b/backend/db/config/options.ts index 1edf621..e413633 100644 --- a/backend/db/config/options.ts +++ b/backend/db/config/options.ts @@ -3,7 +3,7 @@ import path from "path"; const dir = process.cwd(); module.exports = { - config: path.join(dir, "dist/db/config/config.js"), - "migrations-path": path.join(dir, "dist/db/migrations"), - "seeders-path": path.join(dir, "dist/db/seeders"), + config: path.join(dir, "../_build/backend/db/config/config.js"), + "migrations-path": path.join(dir, "../_build/backend/db/migrations"), + "seeders-path": path.join(dir, "../_build/backend/db/seeders"), }; diff --git a/backend/db/migrations/20221115171254-create-endorsement-groups-table.ts b/backend/db/migrations/20221115171254-create-endorsement-groups-table.ts index 34378c7..b46cffd 100644 --- a/backend/db/migrations/20221115171254-create-endorsement-groups-table.ts +++ b/backend/db/migrations/20221115171254-create-endorsement-groups-table.ts @@ -14,6 +14,10 @@ export const ENDORSEMENT_GROUPS_TABLE_ATTRIBUTES = { type: DataType.STRING(70), allowNull: false, }, + name_vateud: { + type: DataType.STRING(70), + allowNull: false, + }, tier: { type: DataType.SMALLINT, allowNull: false, diff --git a/backend/db/seeders/20221121101837-PermissionSeeder.ts b/backend/db/seeders/20221121101837-PermissionSeeder.ts index 484dfcf..3b7f1d1 100644 --- a/backend/db/seeders/20221121101837-PermissionSeeder.ts +++ b/backend/db/seeders/20221121101837-PermissionSeeder.ts @@ -34,11 +34,8 @@ const allPerms = [ "tech.view", "tech.syslog.view", - "tech.permissions.view", - "tech.permissions.role.edit", - "tech.permissions.role.view", - "tech.permissions.perm.edit", - "tech.permissions.perm.view", + "tech.role_management.view", + "tech.role_management.edit", "tech.appsettings.view", "tech.joblog.view", ]; diff --git a/backend/src/Router.ts b/backend/src/Router.ts index 532c7f4..f9a61ad 100644 --- a/backend/src/Router.ts +++ b/backend/src/Router.ts @@ -234,8 +234,8 @@ router.use( r.get("/mentorable", EndorsementGroupAdminController.getMentorable); r.get("/", EndorsementGroupAdminController.getAll); - r.get("/with-stations", EndorsementGroupAdminController.getAllWithStations); r.post("/", EndorsementGroupAdminController.createEndorsementGroup); + r.get("/with-stations", EndorsementGroupAdminController.getAllWithStations); r.get("/:id", EndorsementGroupAdminController.getByID); r.patch("/:id", EndorsementGroupAdminController.updateByID); diff --git a/backend/src/controllers/endorsement-group/EndorsementGroupAdminController.ts b/backend/src/controllers/endorsement-group/EndorsementGroupAdminController.ts index 7721f35..a5e05d7 100644 --- a/backend/src/controllers/endorsement-group/EndorsementGroupAdminController.ts +++ b/backend/src/controllers/endorsement-group/EndorsementGroupAdminController.ts @@ -352,17 +352,19 @@ async function removeUserByID(request: Request, response: Response, next: NextFu async function createEndorsementGroup(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; - const body = request.body as { name: string; tier: number; training_station_ids: number[] }; + const body = request.body as { name: string; name_vateud: string; tier: number; training_station_ids: number[] }; PermissionHelper.checkUserHasPermission(user, "lm.endorsement_groups.create"); Validator.validate(body, { name: [ValidationTypeEnum.NON_NULL], + name_vateud: [ValidationTypeEnum.NON_NULL], training_station_ids: [ValidationTypeEnum.IS_ARRAY, ValidationTypeEnum.VALID_JSON], }); const endorsementGroup = await EndorsementGroup.create({ name: body.name, + name_vateud: body.name_vateud, tier: body.tier, }); diff --git a/backend/src/controllers/log-template/LogTemplateAdminController.ts b/backend/src/controllers/log-template/LogTemplateAdminController.ts index 97dff21..27d4f27 100644 --- a/backend/src/controllers/log-template/LogTemplateAdminController.ts +++ b/backend/src/controllers/log-template/LogTemplateAdminController.ts @@ -3,7 +3,6 @@ import { TrainingLogTemplate } from "../../models/TrainingLogTemplate"; import { HttpStatusCode } from "axios"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; import { User } from "../../models/User"; -import { ForbiddenException } from "../../exceptions/ForbiddenException"; import PermissionHelper from "../../utility/helper/PermissionHelper"; /** diff --git a/backend/src/controllers/permission/PermissionAdminController.ts b/backend/src/controllers/permission/PermissionAdminController.ts index 528d163..3b7cb25 100644 --- a/backend/src/controllers/permission/PermissionAdminController.ts +++ b/backend/src/controllers/permission/PermissionAdminController.ts @@ -1,64 +1,88 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { Permission } from "../../models/Permission"; +import { User } from "../../models/User"; +import PermissionHelper from "../../utility/helper/PermissionHelper"; +import Validator, { ValidationTypeEnum } from "../../utility/Validator"; +import { GenericException } from "../../exceptions/GenericException"; +import { HttpStatusCode } from "axios"; /** * Gets all permissions - * @param request + * @param _request * @param response + * @param next */ -async function getAll(request: Request, response: Response) { - const permissions = await Permission.findAll(); - response.send(permissions); +async function getAll(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "tech.role_management.view"); + + const permissions = await Permission.findAll(); + response.send(permissions); + } catch (e) { + next(e); + } } /** * Creates a new permission. If the name of this permission exists, returns a 400 error * @param request * @param response + * @param next */ -async function create(request: Request, response: Response) { - const name = request.body.name; +async function create(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit"); - if (name == null || name.length == 0) { - response.status(400).send({ code: "VAL_ERR", error: "No name supplied" }); - return; - } + const body = request.body as { name: string }; + Validator.validate(body, { + name: [ValidationTypeEnum.NON_NULL] + }); - const [perm, created] = await Permission.findOrCreate({ - where: { name: name }, - defaults: { - name: name, - }, - }); + const [perm, created] = await Permission.findOrCreate({ + where: { name: body.name }, + defaults: { + name: body.name, + }, + }); - if (!created) { - response.status(400).send({ code: "DUP_ENTRY", error: "Duplicate entry for column name" }); - return; - } + if (!created) { + throw new GenericException("DUP_ENTRY", "Permission with this name already exists"); + } - response.send(perm); + response.send(perm); + } catch (e) { + next(e); + } } /** * Deletes a permission specified by request.body.perm_id * @param request * @param response + * @param next */ -async function destroy(request: Request, response: Response) { - const perm_id = request.body.perm_id; +async function destroy(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit"); - if (perm_id == null || perm_id == -1) { - response.status(400).send({ code: "VAL_ERR", error: "No permission supplied" }); - return; - } + const body = request.body as {perm_id: string}; + Validator.validate(body, { + perm_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + }); - const res = await Permission.destroy({ - where: { - id: perm_id, - }, - }); + const res = await Permission.destroy({ + where: { + id: body.perm_id, + }, + }); - response.send({ message: "OK", rows: res }); + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/permission/RoleAdminController.ts b/backend/src/controllers/permission/RoleAdminController.ts index 065e297..af46a3f 100644 --- a/backend/src/controllers/permission/RoleAdminController.ts +++ b/backend/src/controllers/permission/RoleAdminController.ts @@ -9,19 +9,27 @@ import { RoleBelongsToUsers } from "../../models/through/RoleBelongsToUsers"; /** * Gets all roles - * @param request + * @param _request * @param response + * @param next */ -async function getAll(request: Request, response: Response) { - const roles = await Role.findAll(); - response.send(roles); +async function getAll(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "tech.role_management.view"); + + const roles = await Role.findAll(); + response.send(roles); + } catch(e) { + next(e); + } } async function create(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; const body = request.body as { name: string }; - PermissionHelper.checkUserHasPermission(user, "tech.permissions.role.edit"); + PermissionHelper.checkUserHasPermission(user, "tech.role_management.role.edit"); Validator.validate(body, { name: [ValidationTypeEnum.NON_NULL], @@ -41,7 +49,7 @@ async function addUser(request: Request, response: Response, next: NextFunction) try { const user: User = response.locals.user; const body = request.body as { role_id: string; user_id: string }; - PermissionHelper.checkUserHasPermission(user, "tech.permissions.role.edit"); + PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit"); Validator.validate(body, { role_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], @@ -65,7 +73,7 @@ async function removeUser(request: Request, response: Response, next: NextFuncti try { const user: User = response.locals.user; const body = request.body as { role_id: string; user_id: string }; - PermissionHelper.checkUserHasPermission(user, "tech.permissions.role.edit"); + PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit"); Validator.validate(body, { role_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], @@ -158,7 +166,7 @@ async function removePermission(request: Request, response: Response) { const params = request.params; const body = request.body; - PermissionHelper.checkUserHasPermission(user, "tech.permissions.role.edit", true); + PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit", true); // const validation = ValidationHelper.validate([ // { @@ -193,33 +201,29 @@ async function removePermission(request: Request, response: Response) { * Adds a permission to a role * @param request * @param response + * @param next */ -async function addPermission(request: Request, response: Response) { - const user: User = response.locals.user; - const params = request.params; - const body = request.body; +async function addPermission(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params; + const body = request.body as {permission_id?: string}; - PermissionHelper.checkUserHasPermission(user, "tech.permissions.role.edit", true); + PermissionHelper.checkUserHasPermission(user, "tech.role_management.edit", true); - // const validate = ValidationHelper.validate([ - // { - // name: "role_id", - // validationObject: role_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // { - // name: "permission_id", - // validationObject: permission_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); + Validator.validate(body, { + permission_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] + }); - const res = await RoleHasPermissions.create({ - role_id: Number(params.role_id), - permission_id: Number(body.permission_id), - }); + const res = await RoleHasPermissions.create({ + role_id: Number(params.role_id), + permission_id: Number(body.permission_id), + }); - response.send(res); + response.send(res); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/solo/SoloAdminController.ts b/backend/src/controllers/solo/SoloAdminController.ts index 592eb54..a033041 100644 --- a/backend/src/controllers/solo/SoloAdminController.ts +++ b/backend/src/controllers/solo/SoloAdminController.ts @@ -1,5 +1,4 @@ import { NextFunction, Request, Response } from "express"; -import _SoloAdminValidator from "./_SoloAdmin.validator"; import { UserSolo } from "../../models/UserSolo"; import dayjs from "dayjs"; import { HttpStatusCode } from "axios"; @@ -7,8 +6,13 @@ 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"; type CreateSoloRequestBody = { solo_duration: string; @@ -20,6 +24,9 @@ type CreateSoloRequestBody = { type Optional = Pick, K> & Omit; type UpdateSoloRequestBody = Optional; +// TODO: Do we need to validate if a mentor is allowed to assign a solo to a specific user? +// TODO: i.e. only if the user is in a course of the mentor, or do we trust mentors? :) + /** * Create a new Solo * @param request @@ -27,10 +34,17 @@ type UpdateSoloRequestBody = Optional dayjs.utc().subtract(20, "days").startOf("day").toDate() && @@ -215,7 +240,7 @@ async function extendSolo(request: Request, response: Response, next: NextFuncti // Here, both cases are valid, we can extend the solo no problem! const solo = await UserSolo.findOne({ where: { - user_id: user.id, + user_id: user?.id, }, }); @@ -235,21 +260,22 @@ async function extendSolo(request: Request, response: Response, next: NextFuncti } async function deleteSolo(request: Request, response: Response, next: NextFunction) { + const transaction = await sequelize.transaction(); try { const user: User = response.locals.user; - const body = request.body as { trainee_id: string; solo_id: string }; - PermissionHelper.checkUserHasPermission(user, "atd.solo.delete", true); - if (body.solo_id == null || body.trainee_id == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } + 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] + }); const solo = await UserSolo.findOne({ where: { id: body.solo_id, }, + transaction: transaction }); // 1. Delete all endorsements that are linked to the solo. @@ -257,6 +283,7 @@ async function deleteSolo(request: Request, response: Response, next: NextFuncti where: { solo_id: body.solo_id, }, + transaction: transaction }); // 2. Delete the VATEUD Core Solo @@ -266,6 +293,7 @@ async function deleteSolo(request: Request, response: Response, next: NextFuncti where: { id: body.solo_id, }, + transaction: transaction }); const returnUser = await User.findOne({ @@ -275,8 +303,10 @@ async function deleteSolo(request: Request, response: Response, next: NextFuncti include: [User.associations.endorsement_groups], }); + await transaction.commit(); response.send(returnUser); } catch (e) { + await transaction.rollback(); next(e); } } diff --git a/backend/src/controllers/solo/_SoloAdmin.validator.ts b/backend/src/controllers/solo/_SoloAdmin.validator.ts deleted file mode 100644 index 753c725..0000000 --- a/backend/src/controllers/solo/_SoloAdmin.validator.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ValidationException } from "../../exceptions/ValidationException"; - -function validateCreateRequest(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "solo_duration", - // validationObject: data.solo_duration, - // 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 }], - // }, - // { - // name: "solo_start", - // validationObject: data.solo_start, - // toValidate: [{ val: ValidationOptions.VALID_DATE }], - // }, - // { - // name: "trainee_id", - // validationObject: data.trainee_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: "solo_duration", - // validationObject: data.solo_duration, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // { - // name: "solo_start", - // validationObject: data.solo_start, - // toValidate: [{ val: ValidationOptions.VALID_DATE }], - // }, - // { - // name: "trainee_id", - // validationObject: data.trainee_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -function validateExtensionRequest(data: any) { - // const validation = ValidationHelper.validate([ - // { - // name: "trainee_id", - // validationObject: data.trainee_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], - // }, - // ]); - // - // if (validation.invalid) { - // throw new ValidationException(validation); - // } -} - -export default { - validateCreateRequest, - validateUpdateRequest, - validateExtensionRequest, -}; diff --git a/backend/src/controllers/training-log/TrainingLogController.ts b/backend/src/controllers/training-log/TrainingLogController.ts index d4ca8f6..323bb6e 100644 --- a/backend/src/controllers/training-log/TrainingLogController.ts +++ b/backend/src/controllers/training-log/TrainingLogController.ts @@ -1,9 +1,19 @@ import { NextFunction, Request, Response } from "express"; import { TrainingLog } from "../../models/TrainingLog"; import { HttpStatusCode } from "axios"; +import { User } from "../../models/User"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; +/** + * Gets a training log by its UUID. Includes the content and the author of the log. + * Should only be viewable by the trainee in question, or by mentors. + * @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 trainingLog = await TrainingLog.findOne({ @@ -18,6 +28,10 @@ async function getByUUID(request: Request, response: Response, next: NextFunctio return; } + if (!await trainingLog.userCanRead(user)) { + throw new ForbiddenException("You are not permitted to view this training log."); + } + response.send(trainingLog); } catch (e) { next(e); diff --git a/backend/src/controllers/training-request/TrainingRequestAdminController.ts b/backend/src/controllers/training-request/TrainingRequestAdminController.ts index f6d59bd..2c05d1a 100644 --- a/backend/src/controllers/training-request/TrainingRequestAdminController.ts +++ b/backend/src/controllers/training-request/TrainingRequestAdminController.ts @@ -5,8 +5,6 @@ import { TrainingRequest } from "../../models/TrainingRequest"; import { Op } from "sequelize"; import NotificationLibrary from "../../libraries/notification/NotificationLibrary"; import { TrainingType } from "../../models/TrainingType"; -import { TrainingSession } from "../../models/TrainingSession"; -import { Course } from "../../models/Course"; import { HttpStatusCode } from "axios"; /** @@ -30,125 +28,145 @@ async function _getOpenTrainingRequests(): Promise { /** * Returns all training requests that the current user is able to mentor based on his mentor groups * DOESN'T RETURN CPT REQUESTS! - * @param request + * @param _request * @param response + * @param next */ -async function getOpen(request: Request, response: Response) { - const reqUser: User = response.locals.user; - const reqUserMentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); - let trainingRequests: TrainingRequest[] = await _getOpenTrainingRequests(); +async function getOpen(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const reqUserMentorGroups: MentorGroup[] = await user.getMentorGroupsAndCourses(); + let trainingRequests: TrainingRequest[] = await _getOpenTrainingRequests(); - // Store course IDs that a user can mentor in - const courseIDs: number[] = []; + // Store course IDs that a user can mentor in + const courseIDs: number[] = []; - for (const mentorGroup of reqUserMentorGroups) { - for (const course of mentorGroup.courses ?? []) { - if (!courseIDs.includes(course.id)) courseIDs.push(course.id); + for (const mentorGroup of reqUserMentorGroups) { + for (const course of mentorGroup.courses ?? []) { + if (!courseIDs.includes(course.id)) courseIDs.push(course.id); + } } - } - trainingRequests = trainingRequests.filter((req: TrainingRequest) => { - return courseIDs.includes(req.course_id) && req.training_type?.type != "cpt"; - }); + trainingRequests = trainingRequests.filter((req: TrainingRequest) => { + return courseIDs.includes(req.course_id) && req.training_type?.type != "cpt"; + }); - response.send(trainingRequests); + response.send(trainingRequests); + } catch (e) { + next(e); + } } /** * Returns all training requests that the current user is able to mentor based on his mentor groups * Only returns Trainings (not lessons) - * @param request + * @param _request * @param response + * @param next */ -async function getOpenTrainingRequests(request: Request, response: Response) { - const reqUser: User = response.locals.user; - const reqUserMentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); - let trainingRequests: TrainingRequest[] = (await _getOpenTrainingRequests()).filter((trainingRequest: TrainingRequest) => { - return trainingRequest.training_type?.type != "lesson"; - }); +async function getOpenTrainingRequests(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const reqUserMentorGroups: MentorGroup[] = await user.getMentorGroupsAndCourses(); + let trainingRequests: TrainingRequest[] = (await _getOpenTrainingRequests()).filter((trainingRequest: TrainingRequest) => { + return trainingRequest.training_type?.type != "lesson"; + }); - // Store course IDs that a user can mentor in - const courseIDs: number[] = []; + // Store course IDs that a user can mentor in + const courseIDs: number[] = []; - for (const mentorGroup of reqUserMentorGroups) { - for (const course of mentorGroup.courses ?? []) { - if (!courseIDs.includes(course.id)) courseIDs.push(course.id); + for (const mentorGroup of reqUserMentorGroups) { + for (const course of mentorGroup.courses ?? []) { + if (!courseIDs.includes(course.id)) courseIDs.push(course.id); + } } - } - trainingRequests = trainingRequests.filter((req: TrainingRequest) => { - return courseIDs.includes(req.course_id); - }); + trainingRequests = trainingRequests.filter((req: TrainingRequest) => { + return courseIDs.includes(req.course_id); + }); - response.send(trainingRequests); + response.send(trainingRequests); + } catch (e) { + next(e); + } } /** * Returns all training requests that the current user is able to mentor based on his mentor groups * Only returns Lessons (not anything else) - * @param request + * @param _request * @param response + * @param next */ -async function getOpenLessonRequests(request: Request, response: Response) { - const reqUser: User = response.locals.user; - const reqUserMentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); - let trainingRequests: TrainingRequest[] = (await _getOpenTrainingRequests()).filter((trainingRequest: TrainingRequest) => { - return trainingRequest.training_type?.type == "lesson"; - }); +async function getOpenLessonRequests(_request: Request, response: Response, next: NextFunction) { + try { + const reqUser: User = response.locals.user; + const reqUserMentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); + let trainingRequests: TrainingRequest[] = (await _getOpenTrainingRequests()).filter((trainingRequest: TrainingRequest) => { + return trainingRequest.training_type?.type == "lesson"; + }); - // Store course IDs that a user can mentor in - const courseIDs: number[] = []; + // Store course IDs that a user can mentor in + const courseIDs: number[] = []; - for (const mentorGroup of reqUserMentorGroups) { - for (const course of mentorGroup.courses ?? []) { - if (!courseIDs.includes(course.id)) courseIDs.push(course.id); + for (const mentorGroup of reqUserMentorGroups) { + for (const course of mentorGroup.courses ?? []) { + if (!courseIDs.includes(course.id)) courseIDs.push(course.id); + } } - } - trainingRequests = trainingRequests.filter((req: TrainingRequest) => { - return courseIDs.includes(req.course_id); - }); + trainingRequests = trainingRequests.filter((req: TrainingRequest) => { + return courseIDs.includes(req.course_id); + }); - response.send(trainingRequests); + response.send(trainingRequests); + } catch (e) { + next(e); + } } /** * Returns training request information by its UUID * @param request * @param response + * @param next */ -async function getByUUID(request: Request, response: Response) { - const trainingRequestUUID = request.params.uuid; +async function getByUUID(request: Request, response: Response, next: NextFunction) { + try { + const trainingRequestUUID = request.params.uuid; - const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ - where: { - uuid: trainingRequestUUID, - }, - include: [ - TrainingRequest.associations.user, - TrainingRequest.associations.training_station, - TrainingRequest.associations.course, - { - association: TrainingRequest.associations.training_type, - include: [ - { - association: TrainingType.associations.training_stations, - attributes: ["id", "callsign"], - through: { - attributes: [], - }, - }, - ], + const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ + where: { + uuid: trainingRequestUUID, }, - ], - }); + include: [ + TrainingRequest.associations.user, + TrainingRequest.associations.training_station, + TrainingRequest.associations.course, + { + association: TrainingRequest.associations.training_type, + include: [ + { + association: TrainingType.associations.training_stations, + attributes: ["id", "callsign"], + through: { + attributes: [], + }, + }, + ], + }, + ], + }); - if (trainingRequest == null) { - response.status(404).send({ message: "Training request with this UUID not found" }); - return; - } + if (trainingRequest == null) { + response.status(404).send({ message: "Training request with this UUID not found" }); + return; + } - response.send(trainingRequest); + response.send(trainingRequest); + } catch (e) { + next(e); + } } /** diff --git a/backend/src/controllers/training-request/TrainingRequestController.ts b/backend/src/controllers/training-request/TrainingRequestController.ts index f2d8808..23f17a1 100644 --- a/backend/src/controllers/training-request/TrainingRequestController.ts +++ b/backend/src/controllers/training-request/TrainingRequestController.ts @@ -6,178 +6,209 @@ import { TrainingSession } from "../../models/TrainingSession"; import dayjs from "dayjs"; import { TrainingSessionBelongsToUsers } from "../../models/through/TrainingSessionBelongsToUsers"; import { HttpStatusCode } from "axios"; +import Validator, { ValidationTypeEnum } from "../../utility/Validator"; +import { GenericException } from "../../exceptions/GenericException"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; +import { ConversionUtils } from "turbocommons-ts"; /** * Creates a new training request * @param request * @param response + * @param next */ -async function create(request: Request, response: Response) { - const body = request.body as { - course_id: number; - training_type_id: number; - comment?: string; - training_station_id?: number; - }; - const user: User = response.locals.user; - - // const validation = ValidationHelper.validate([ - // { - // name: "course_id", - // validationObject: requestData.course_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // { - // name: "training_type_id", - // validationObject: requestData.training_type_id, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // ]); - - const trainingRequest = await TrainingRequest.create({ - uuid: generateUUID(), - user_id: user.id, - training_type_id: body.training_type_id, - course_id: body.course_id ?? -1, - training_station_id: body.training_station_id ?? null, - status: "requested", - comment: body.comment?.length == 0 ? null : body.comment, - expires: dayjs().add(2, "month").toDate(), // Expires in 2 months from now - }); - - if (trainingRequest == null) { - response.status(500).send({ message: "Failed to create training request" }); - return; - } +async function create(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { + course_id: number; + training_type_id: number; + comment?: string; + training_station_id?: number; + }; + + Validator.validate(body, { + course_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + training_type_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + }); + + const trainingRequest = await TrainingRequest.create({ + uuid: generateUUID(), + user_id: user.id, + training_type_id: body.training_type_id, + course_id: body.course_id ?? -1, + training_station_id: body.training_station_id ?? null, + status: "requested", + comment: body.comment?.length == 0 ? null : body.comment, + expires: dayjs().add(2, "month").toDate(), // Expires in 2 months from now + }); - const trainingRequestTrainingType = await trainingRequest.getTrainingType(); - const trainingRequestTrainingStation = await trainingRequest.getTrainingStation(); - response.send({ - ...trainingRequest.dataValues, - training_type: trainingRequestTrainingType, - training_station: trainingRequestTrainingStation, - }); + if (trainingRequest == null) { + response.status(500).send({ message: "Failed to create training request" }); + return; + } + + const trainingRequestTrainingType = await trainingRequest.getTrainingType(); + const trainingRequestTrainingStation = await trainingRequest.getTrainingStation(); + response.send({ + ...trainingRequest.dataValues, + training_type: trainingRequestTrainingType, + training_station: trainingRequestTrainingStation, + }); + } catch (e) { + next(e); + } } /** * Destroys a training request based on the UUID and user id (CID) * @param request * @param response + * @param next */ -async function destroy(request: Request, response: Response) { - const user: User = response.locals.user; - const body = request.body as { uuid: string }; - - // const validation = ValidationHelper.validate([ - // { - // name: "training_request_uuid", - // validationObject: training_request_uuid, - // toValidate: [{ val: ValidationOptions.NON_NULL }], - // }, - // ]); - - const trainingRequest = await TrainingRequest.findOne({ - where: { - uuid: body.uuid, - user_id: user.id, - }, - }); - if (trainingRequest == null) { - response.status(500).send({ message: "Training request could not be found, or is not linked to the requesting user." }); - return; - } +async function destroy(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { uuid: string }; + + Validator.validate(body, { + uuid: [ValidationTypeEnum.NON_NULL] + }); + + const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ + where: { + uuid: body.uuid, + user_id: user.id, + }, + }); + if (trainingRequest == null) { + throw new GenericException("NOT_FOUND", "Training request could not be found."); + } - await trainingRequest.destroy(); - response.send({ message: "OK" }); + await trainingRequest.destroy(); + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** * Gets all training requests for the currently logged-in user - * @param request + * @param _request * @param response + * @param next */ -async function getOpen(request: Request, response: Response) { - const reqUser: User = response.locals.user; +async function getOpen(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; - const trainingRequests = await TrainingRequest.findAll({ - where: { - user_id: reqUser.id, - status: "requested", - }, - include: [TrainingRequest.associations.training_type, TrainingRequest.associations.course], - }); + const trainingRequests = await TrainingRequest.findAll({ + where: { + user_id: user.id, + status: "requested", + }, + include: [TrainingRequest.associations.training_type, TrainingRequest.associations.course], + }); - for (const trainingRequest of trainingRequests) { - await trainingRequest.appendNumberInQueue(); - } + for (const trainingRequest of trainingRequests) { + await trainingRequest.appendNumberInQueue(); + } - response.send(trainingRequests); + response.send(trainingRequests); + } catch (e) { + next(e); + } } /** * Gets all planned training sessions for the requesting user - * @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; - const sessions: TrainingSessionBelongsToUsers[] = await TrainingSessionBelongsToUsers.findAll({ - where: { - user_id: user.id, - passed: null, - }, - attributes: ["id", "user_id", "createdAt"], - include: [ - { - association: TrainingSessionBelongsToUsers.associations.training_session, - include: [ - TrainingSession.associations.mentor, - { - association: TrainingSession.associations.training_station, - attributes: ["id", "callsign", "frequency"], - }, - { - association: TrainingSession.associations.training_type, - attributes: ["id", "name"], - }, - ], - attributes: ["uuid", "mentor_id", "date", "createdAt"], + const sessions: TrainingSessionBelongsToUsers[] = await TrainingSessionBelongsToUsers.findAll({ + where: { + user_id: user.id, + passed: null, }, - ], - }); + attributes: ["id", "user_id", "createdAt"], + include: [ + { + association: TrainingSessionBelongsToUsers.associations.training_session, + include: [ + TrainingSession.associations.mentor, + { + association: TrainingSession.associations.training_station, + attributes: ["id", "callsign", "frequency"], + }, + { + association: TrainingSession.associations.training_type, + attributes: ["id", "name"], + }, + ], + attributes: ["uuid", "mentor_id", "date", "createdAt"], + }, + ], + }); - response.send(sessions); + response.send(sessions); + } catch (e) { + next(e); + } } -async function getByUUID(request: Request, response: Response) { - const params = request.params as { uuid?: string }; - - const trainingRequest = await TrainingRequest.findOne({ - where: { - uuid: params.uuid?.toString(), - }, - include: [ - TrainingRequest.associations.training_type, - TrainingRequest.associations.course, - TrainingRequest.associations.training_station, - { - association: TrainingRequest.associations.training_session, - include: [TrainingSession.associations.mentor], +/** + * Returns the information of a training request by its 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 trainingRequest = await TrainingRequest.findOne({ + where: { + uuid: params.uuid?.toString(), }, - ], - }); + include: [ + TrainingRequest.associations.training_type, + TrainingRequest.associations.course, + TrainingRequest.associations.training_station, + { + association: TrainingRequest.associations.training_session, + include: [TrainingSession.associations.mentor], + }, + ], + }); - response.send(trainingRequest); + if (!await trainingRequest?.canUserView(user)) { + throw new ForbiddenException("You are not allowed to view this training request"); + } + + response.send(trainingRequest); + } catch (e) { + next(e); + } } +/** + * Confirms the interest for a training request + * @param request + * @param response + * @param next + */ async function confirmInterest(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; const body = request.body as { token: string }; - const token = atob(body.token).split("."); + const token = ConversionUtils.base64ToString(body.token).split("."); - console.log(token); if (body.token == null || token.length != 3) { response.sendStatus(HttpStatusCode.BadRequest); return; @@ -189,16 +220,12 @@ async function confirmInterest(request: Request, response: Response, next: NextF const trainingRequest = await TrainingRequest.findOne({ where: { uuid: trainingRequestUUID, + user_id: user.id }, }); - if (trainingRequest == null) { - response.sendStatus(HttpStatusCode.NotFound); - return; - } - - const djsExpires = dayjs.utc(trainingRequest.expires); - if (trainingRequest.user_id != cid || djsExpires.unix() != timestamp || djsExpires.isAfter(dayjs.utc())) { + const djsExpires = dayjs.utc(trainingRequest?.expires); + if (trainingRequest == null || trainingRequest.user_id != cid || djsExpires.unix() != timestamp || djsExpires.isAfter(dayjs.utc())) { // Check if the CID doesn't match or // the timestamps don't match // or the expiry is already in the future, in which case the token was reverse-engineered (it isn't really that difficult tbh :D) and not to be used diff --git a/backend/src/controllers/training-session/TrainingSessionAdminController.ts b/backend/src/controllers/training-session/TrainingSessionAdminController.ts index 4f38b5b..66c7858 100644 --- a/backend/src/controllers/training-session/TrainingSessionAdminController.ts +++ b/backend/src/controllers/training-session/TrainingSessionAdminController.ts @@ -41,6 +41,9 @@ async function createTrainingSession(request: Request, response: Response) { user_ids: [ValidationTypeEnum.VALID_JSON], // Parses to number[] }); + // TODO: + // user.isMentorInCourse(body.course_uuid) + // 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. let courseParticipants: number[] = []; diff --git a/backend/src/exceptions/GenericException.ts b/backend/src/exceptions/GenericException.ts new file mode 100644 index 0000000..2744dfa --- /dev/null +++ b/backend/src/exceptions/GenericException.ts @@ -0,0 +1,18 @@ +export class GenericException extends Error { + public readonly code_: string; + public readonly message_: string; + + constructor(code: string, message: string) { + super(); + this.code_ = code; + this.message_ = message; + } + + public getCode() { + return this.code_; + } + + public getMessage() { + return this.message_; + } +} \ No newline at end of file diff --git a/backend/src/middlewares/ExceptionInterceptorMiddleware.ts b/backend/src/middlewares/ExceptionInterceptorMiddleware.ts index d655096..d9f7d6b 100644 --- a/backend/src/middlewares/ExceptionInterceptorMiddleware.ts +++ b/backend/src/middlewares/ExceptionInterceptorMiddleware.ts @@ -7,6 +7,7 @@ import { VatsimConnectException } from "../exceptions/VatsimConnectException"; import { MissingPermissionException } from "../exceptions/MissingPermissionException"; import { SysLog } from "../models/SysLog"; import { User } from "../models/User"; +import { GenericException } from "../exceptions/GenericException"; const sequelizeErrors = ["SequelizeValidationError", "SequelizeForeignKeyConstraintError", "SequelizeUniqueConstraintError"]; @@ -91,6 +92,17 @@ export async function exceptionInterceptorMiddleware(error: any, request: Reques return; } + if (error instanceof GenericException) { + response.status(HttpStatusCode.BadRequest).send({ + path: request.url, + method: request.method, + code: HttpStatusCode.BadRequest, + error_code: error.getCode(), + message: error.getMessage() + }); + return; + } + response.status(HttpStatusCode.InternalServerError).send({ path: request.url, method: request.method, diff --git a/backend/src/models/EndorsementGroup.ts b/backend/src/models/EndorsementGroup.ts index 26d6595..40c740c 100644 --- a/backend/src/models/EndorsementGroup.ts +++ b/backend/src/models/EndorsementGroup.ts @@ -4,11 +4,21 @@ 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"; +export interface IEndorsementGroup { + id: number; + name: string; + name_vateud: string; + tier: number; + createdAt: Date; + updatedAt?: Date; +} + export class EndorsementGroup extends Model, InferCreationAttributes> { // // Attributes // declare name: string; + declare name_vateud: string; declare tier: number; // diff --git a/backend/src/models/TrainingLog.ts b/backend/src/models/TrainingLog.ts index b6f507e..b41b673 100644 --- a/backend/src/models/TrainingLog.ts +++ b/backend/src/models/TrainingLog.ts @@ -2,6 +2,7 @@ import { Model, InferAttributes, CreationOptional, InferCreationAttributes, NonA import { User } from "./User"; import { sequelize } from "../core/Sequelize"; import { TRAINING_LOG_TABLE_ATTRIBUTES, TRAINING_LOG_TABLE_NAME } from "../../db/migrations/20221115171257-create-training-log-table"; +import TrainingLogExtensions from "./extensions/TrainingLogExtensions"; export class TrainingLog extends Model, InferCreationAttributes> { // @@ -26,6 +27,8 @@ export class TrainingLog extends Model, InferCreati declare static associations: { author: Association; }; + + userCanRead = TrainingLogExtensions.userCanRead.bind(this); } TrainingLog.init(TRAINING_LOG_TABLE_ATTRIBUTES, { diff --git a/backend/src/models/TrainingRequest.ts b/backend/src/models/TrainingRequest.ts index 183f590..f2bde1a 100644 --- a/backend/src/models/TrainingRequest.ts +++ b/backend/src/models/TrainingRequest.ts @@ -10,6 +10,7 @@ import { TRAINING_REQUEST_TABLE_NAME, TRAINING_REQUEST_TABLE_STATUS_TYPES, } from "../../db/migrations/20221115171256-create-training-request-table"; +import TrainingRequestExtensions from "./extensions/TrainingRequestExtensions"; export class TrainingRequest extends Model, InferCreationAttributes> { // @@ -50,6 +51,8 @@ export class TrainingRequest extends Model, Inf training_station: Association; }; + canUserView = TrainingRequestExtensions.canUserView.bind(this); + async getTrainingType(): Promise { return await TrainingType.findOne({ where: { diff --git a/backend/src/models/extensions/TrainingLogExtensions.ts b/backend/src/models/extensions/TrainingLogExtensions.ts new file mode 100644 index 0000000..bc7adb3 --- /dev/null +++ b/backend/src/models/extensions/TrainingLogExtensions.ts @@ -0,0 +1,28 @@ +import { TrainingLog } from "../TrainingLog"; +import { User } from "../User"; +import { TrainingSessionBelongsToUsers } from "../through/TrainingSessionBelongsToUsers"; + +/** + * Checks if the given user is permitted to read this training log + * @param user + */ +async function userCanRead(this: TrainingLog, user: User) { + if (await user.isMentor()) { + return true; + } + + if (await TrainingSessionBelongsToUsers.count({ + where: { + log_id: this.id, + user_id: user.id + } + }) == 0) { + return false; + } + + return true; +} + +export default { + userCanRead +} \ No newline at end of file diff --git a/backend/src/models/extensions/TrainingRequestExtensions.ts b/backend/src/models/extensions/TrainingRequestExtensions.ts new file mode 100644 index 0000000..20355d3 --- /dev/null +++ b/backend/src/models/extensions/TrainingRequestExtensions.ts @@ -0,0 +1,12 @@ +import { TrainingRequest } from "../TrainingRequest"; +import { User } from "../User"; + +async function canUserView(this: TrainingRequest, user: User): Promise { + if (await user.isMentor()) return true; + + return this.user_id == user.id; +} + +export default { + canUserView +} \ No newline at end of file diff --git a/backend/src/models/extensions/UserExtensions.ts b/backend/src/models/extensions/UserExtensions.ts index b609e45..7bca4a4 100644 --- a/backend/src/models/extensions/UserExtensions.ts +++ b/backend/src/models/extensions/UserExtensions.ts @@ -3,6 +3,7 @@ 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 @@ -176,7 +177,10 @@ async function getCoursesWithInformation(this: User): Promise { return user?.courses ?? []; } -async function isMentor(this: User) { +/** + * Checks if the user is a mentor (i.e. is a member of any mentor group) + */ +async function isMentor(this: User): Promise { const mentorGroups = await this.getMentorGroups(); return mentorGroups.length > 0; } @@ -192,5 +196,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 b18a715..9048808 100644 --- a/frontend/src/components/template/SideNav.tsx +++ b/frontend/src/components/template/SideNav.tsx @@ -238,7 +238,7 @@ export function SideNav() { }> Joblogs - }> + }> Rechteverwaltung }> diff --git a/frontend/src/components/ui/Input/InputGroup.tsx b/frontend/src/components/ui/Input/InputGroup.tsx new file mode 100644 index 0000000..aa13cc2 --- /dev/null +++ b/frontend/src/components/ui/Input/InputGroup.tsx @@ -0,0 +1,13 @@ +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 diff --git a/frontend/src/models/TrainingSessionBelongsToUser.model.ts b/frontend/src/models/TrainingSessionBelongsToUser.model.ts index 905f0e9..6a6402b 100644 --- a/frontend/src/models/TrainingSessionBelongsToUser.model.ts +++ b/frontend/src/models/TrainingSessionBelongsToUser.model.ts @@ -18,7 +18,6 @@ export type TrainingLogModel = { id: number; uuid: string; content: object; - log_public: boolean; author_id: number; createdAt?: Date; updatedAt?: Date; 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 fe62e92..b5dbb50 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 @@ -2,11 +2,11 @@ import { PageHeader } from "@/components/ui/PageHeader/PageHeader"; import { Card } from "@/components/ui/Card/Card"; import React, { FormEvent, useState } from "react"; import { Input } from "@/components/ui/Input/Input"; -import { TbFilePlus, TbId, TbPlus } from "react-icons/tb"; +import { TbFilePlus, TbId, TbListNumbers, TbPlus } from "react-icons/tb"; import { Separator } from "@/components/ui/Separator/Separator"; import { CommonRegexp } from "@/core/Config"; import { Select } from "@/components/ui/Select/Select"; -import { COLOR_OPTS, SIZE_OPTS } from "@/assets/theme.config"; +import { COLOR_OPTS, ICON_SIZE_OPTS, SIZE_OPTS } from "@/assets/theme.config"; import useApi from "@/utils/hooks/useApi"; import { TrainingStationModel } from "@/models/TrainingStationModel"; import { MapArray } from "@/components/conditionals/MapArray"; @@ -22,8 +22,6 @@ import { AxiosResponse } from "axios"; import { useNavigate } from "react-router-dom"; import { useDebounce } from "@/utils/hooks/useDebounce"; import { useFilter } from "@/utils/hooks/useFilter"; -import FuzzySearch from "fuzzy-search"; -import { fuzzySearch } from "@/utils/helper/fuzzysearch/FuzzySearchHelper"; function filterStations(element: TrainingStationModel, searchValue: string) { return element.callsign.startsWith(searchValue.toUpperCase()); @@ -102,9 +100,34 @@ export function EndorsementGroupCreateView() { preIcon={} /> - } + /> + + diff --git a/frontend/src/pages/administration/lm/endorsement-group/view/_subpages/EGVSettings.subpage.tsx b/frontend/src/pages/administration/lm/endorsement-group/view/_subpages/EGVSettings.subpage.tsx index 7cdcf42..fe96fe7 100644 --- a/frontend/src/pages/administration/lm/endorsement-group/view/_subpages/EGVSettings.subpage.tsx +++ b/frontend/src/pages/administration/lm/endorsement-group/view/_subpages/EGVSettings.subpage.tsx @@ -2,18 +2,19 @@ import useApi from "@/utils/hooks/useApi"; import { EndorsementGroupModel } from "@/models/EndorsementGroupModel"; import { useParams } from "react-router-dom"; import { Input } from "@/components/ui/Input/Input"; -import { TbCalendarTime, TbEdit, TbId } from "react-icons/tb"; +import { TbCalendarTime, TbEdit, TbId, TbListNumbers } from "react-icons/tb"; import dayjs from "dayjs"; import { CommonRegexp, Config } from "@/core/Config"; import React, { FormEvent, useState } from "react"; import { Separator } from "@/components/ui/Separator/Separator"; -import { COLOR_OPTS } from "@/assets/theme.config"; +import { COLOR_OPTS, ICON_SIZE_OPTS } from "@/assets/theme.config"; import { Button } from "@/components/ui/Button/Button"; import FormHelper from "@/utils/helper/FormHelper"; import { axiosInstance } from "@/utils/network/AxiosInstance"; import ToastHelper from "@/utils/helper/ToastHelper"; import { RenderIf } from "@/components/conditionals/RenderIf"; import { EGVSettingsSkeleton } from "@/pages/administration/lm/endorsement-group/view/_skeletons/EGVSettings.skeleton"; +import { IEndorsementGroup } from "@models/EndorsementGroup"; export function EGVSettingsSubpage() { const { id } = useParams(); @@ -22,7 +23,7 @@ export function EGVSettingsSubpage() { loading: loadingEndorsementGroup, data: endorsementGroup, setData: setEndorsementGroup, - } = useApi({ + } = useApi({ url: `/administration/endorsement-group/${id}`, method: "get", }); @@ -54,12 +55,29 @@ export function EGVSettingsSubpage() { } value={dayjs.utc(endorsementGroup?.updatedAt).format(Config.DATETIME_FORMAT)} /> + } + value={endorsementGroup?.name_vateud} + /> + + } + value={endorsementGroup?.tier.toString()} + /> +
diff --git a/frontend/src/pages/administration/mentor/training-session/session-log-create/TrainingSessionLogsCreate.view.tsx b/frontend/src/pages/administration/mentor/training-session/session-log-create/TrainingSessionLogsCreate.view.tsx index 0c3b8ac..e4daa6d 100644 --- a/frontend/src/pages/administration/mentor/training-session/session-log-create/TrainingSessionLogsCreate.view.tsx +++ b/frontend/src/pages/administration/mentor/training-session/session-log-create/TrainingSessionLogsCreate.view.tsx @@ -23,7 +23,6 @@ export type ParticipantStatus = { user_id: number; user_log: LogTemplateElement[]; passed: boolean; - log_public: boolean; next_training_id?: number; course_completed: boolean; _uuid: string; // This is used internally only! @@ -50,7 +49,6 @@ export function TrainingSessionLogsCreateView() { user_id: participant.id, user_log: [], passed: true, - log_public: true, next_training_id: undefined, course_completed: false, _uuid: generateUUID(), @@ -146,17 +144,6 @@ export function TrainingSessionLogsCreateView() { Bestanden - { - const p = [...participantValues]; - p[index].log_public = e; - setParticipantValues(p); - }}> - Log Öffentlich - Für den Trainee sichtbar - -