From 0ec83d7e8577cd6f7e6b8ca67a3272ed13de8ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 30 Aug 2023 21:14:20 +0200 Subject: [PATCH] start controller refactor --- misc/Permissions.txt | 14 ++ src/Router.ts | 22 +-- .../course/CourseAdminController.ts | 37 +++-- .../course/CourseAdministrationController.ts | 114 ++++++++++++++ .../CourseInformationAdminController.ts | 101 ++++++------ .../course/_CourseAdministration.validator.ts | 51 ++++++ .../_CourseInformationAdmin.validator.ts} | 52 +------ .../MentorGroupAdminController.ts | 24 +-- src/models/MentorGroup.ts | 6 + src/models/User.ts | 65 +++----- src/models/associations/RoleAssociations.ts | 4 +- src/models/extensions/UserExtensions.ts | 145 ++++++++++++++++++ src/utility/helper/ValidationHelper.ts | 11 ++ 13 files changed, 473 insertions(+), 173 deletions(-) create mode 100644 misc/Permissions.txt create mode 100644 src/controllers/course/CourseAdministrationController.ts create mode 100644 src/controllers/course/_CourseAdministration.validator.ts rename src/controllers/{_validators/CourseInformationAdminValidator.ts => course/_CourseInformationAdmin.validator.ts} (53%) create mode 100644 src/models/extensions/UserExtensions.ts diff --git a/misc/Permissions.txt b/misc/Permissions.txt new file mode 100644 index 0000000..b206f9e --- /dev/null +++ b/misc/Permissions.txt @@ -0,0 +1,14 @@ +############ +## COURSE ## +############ +COURSE.CREATE +COURSE.UPDATE + +########## +## Tech ## +########## +TECH.SYSLOG.VIEW +TECH.PERMISSIONS.VIEW +TECH.ROLES.VIEW +... + diff --git a/src/Router.ts b/src/Router.ts index 9f40340..112e936 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -14,7 +14,7 @@ import UserInformationAdminController from "./controllers/user/UserInformationAd import UserNoteAdminController from "./controllers/user/UserNoteAdminController"; import UserController from "./controllers/user/UserAdminController"; import TrainingRequestAdminController from "./controllers/training-request/TrainingRequestAdminController"; -import CourseAdministrationController from "./controllers/course/CourseAdminController"; +import CourseAdminController from "./controllers/course/CourseAdminController"; import CourseInformationAdministrationController from "./controllers/course/CourseInformationAdminController"; import SkillTemplateAdministrationController from "./controllers/skill-template/SkillTemplateAdminController"; import TrainingTypeAdministrationController from "./controllers/training-type/TrainingTypeAdminController"; @@ -30,6 +30,7 @@ import TrainingSessionController from "./controllers/training-session/TrainingSe import UserCourseAdminController from "./controllers/user/UserCourseAdminController"; import SessionController from "./controllers/login/SessionController"; import UserSettingsController from "./controllers/user/UserSettingsController"; +import CourseAdministrationController from "./controllers/course/CourseAdministrationController"; const routerGroup = (callback: (router: Router) => void) => { const router = Router(); @@ -175,17 +176,19 @@ router.use( r.use( "/course", routerGroup((r: Router) => { - r.get("/", CourseAdministrationController.getAll); - r.put("/", CourseAdministrationController.create); + r.post("/", CourseAdministrationController.createCourse); + r.patch("/", CourseAdministrationController.updateCourse); - r.get("/mentorable", CourseAdministrationController.getMentorable); - r.get("/editable", CourseAdministrationController.getEditable); + // TODO: REFACTOR THIS INTO THE COURSEADMINISTRATIONCONTROLLER! + r.get("/", CourseAdminController.getAll); - r.get("/info/", CourseInformationAdministrationController.getByUUID); + r.get("/mentorable", CourseAdminController.getMentorable); + r.get("/editable", CourseAdminController.getEditable); + + r.get("/info", CourseInformationAdministrationController.getByUUID); r.get("/info/mentor-group", CourseInformationAdministrationController.getMentorGroups); r.get("/info/user", CourseInformationAdministrationController.getUsers); - r.patch("/info/update", CourseInformationAdministrationController.update); r.delete("/info/user", CourseInformationAdministrationController.deleteUser); r.put("/info/mentor-group", CourseInformationAdministrationController.addMentorGroup); r.delete("/info/mentor-group", CourseInformationAdministrationController.deleteMentorGroup); @@ -193,7 +196,7 @@ router.use( ); r.use( - "/course-skill-template", + "/skill-template", routerGroup((r: Router) => { r.get("/", SkillTemplateAdministrationController.getAll); }) @@ -234,11 +237,10 @@ router.use( r.use( "/mentor-group", routerGroup((r: Router) => { + r.get("/", MentorGroupAdministrationController.getAll); r.post("/", MentorGroupAdministrationController.create); r.patch("/", MentorGroupAdministrationController.update); - r.get("/", MentorGroupAdministrationController.getAll); - r.get("/admin", MentorGroupAdministrationController.getAllAdmin); r.get("/members", MentorGroupAdministrationController.getMembers); r.put("/member", MentorGroupAdministrationController.addMember); diff --git a/src/controllers/course/CourseAdminController.ts b/src/controllers/course/CourseAdminController.ts index 76296e6..7e36c6f 100644 --- a/src/controllers/course/CourseAdminController.ts +++ b/src/controllers/course/CourseAdminController.ts @@ -7,6 +7,10 @@ import CourseAdminValidator from "../_validators/CourseAdminValidator"; import { ValidatorType } from "../_validators/ValidatorType"; import { HttpStatusCode } from "axios"; import { TrainingType } from "../../models/TrainingType"; +import _CourseInformationAdminValidator from "./_CourseInformationAdmin.validator"; + +// DEPRECATED +// TODO REMOVE THIS CONTROLLER --> CourseAdministrationController /** * Gets all courses @@ -69,28 +73,29 @@ async function getEditable(request: Request, response: Response) { * Creates a new course */ async function create(request: Request, response: Response) { - const data = request.body.data as { + const user = request.body.user; + const data = request.body as { course_uuid: string; name_de: string; name_en: string; description_de: string; description_en: string; - active: string; - self_enrol: string; - training_id: string; + active: boolean; + self_enrol_enabled: boolean; + training_type_id: string; skill_template_id: string; mentor_group_id: string; }; - const validation: ValidatorType = CourseAdminValidator.validateCreateRequest(data); - - if (validation.invalid) { - response.status(HttpStatusCode.BadRequest).send({ - validation: validation.message, - validation_failed: true, - }); - return; - } + // const validation: ValidatorType = _CourseInformationAdminValidator.validateUpdateOrCreateRequest(data); + // + // if (validation.invalid) { + // response.status(HttpStatusCode.BadRequest).send({ + // validation: validation.message, + // validation_failed: true, + // }); + // return; + // } const course: Course = await Course.create({ uuid: data.course_uuid, @@ -98,9 +103,9 @@ async function create(request: Request, response: Response) { name_en: data.name_en, description: data.description_de, description_en: data.description_en, - is_active: Number(data.active) == 1, - self_enrollment_enabled: Number(data.self_enrol) == 1, - initial_training_type: Number(data.training_id), + is_active: data.active, + self_enrollment_enabled: data.self_enrol_enabled, + initial_training_type: Number(data.training_type_id), skill_template_id: Number(data.skill_template_id) == 0 || isNaN(Number(data.skill_template_id)) ? null : Number(data.skill_template_id), }); if (course == null) { diff --git a/src/controllers/course/CourseAdministrationController.ts b/src/controllers/course/CourseAdministrationController.ts new file mode 100644 index 0000000..be7548a --- /dev/null +++ b/src/controllers/course/CourseAdministrationController.ts @@ -0,0 +1,114 @@ +import { Request, Response } from "express"; +import { User } from "../../models/User"; +import _CourseAdministrationValidator from "./_CourseAdministration.validator"; +import ValidationHelper from "../../utility/helper/ValidationHelper"; +import { Course } from "../../models/Course"; +import { MentorGroupsBelongsToCourses } from "../../models/through/MentorGroupsBelongsToCourses"; +import { HttpStatusCode } from "axios"; + +// TODO: Move all course related things into this controller + +/** + * The ICourseBody Interface is the type which all requests that wish to create or update a course must satisfy + */ +interface ICourseBody { + course_uuid: string; + name_de: string; + name_en: string; + description_de: string; + description_en: string; + active: boolean; + self_enrol_enabled: boolean; + training_type_id: string; + mentor_group_id: string; + skill_template_id?: string; +} + +/** + * Validates and creates a new course based on the request + * @param request + * @param response + */ +async function createCourse(request: Request, response: Response) { + const user: User = request.body.user; + const body: ICourseBody = request.body as ICourseBody; + + const validation = _CourseAdministrationValidator.validateUpdateOrCreateRequest(body); + if (validation.invalid) { + ValidationHelper.sendValidationErrorResponse(response, validation); + return; + } + + if (!user.hasPermission("course.create") || !(await user.canManageCourseInMentorGroup(Number(body.mentor_group_id)))) { + response.sendStatus(HttpStatusCode.Forbidden); + return; + } + + const skillTemplateID = isNaN(Number(body.skill_template_id)) || body.skill_template_id == "-1" ? null : Number(body.skill_template_id); + const course = await Course.create({ + uuid: body.course_uuid, + name: body.name_de, + name_en: body.name_en, + description: body.description_de, + description_en: body.description_en, + is_active: body.active, + self_enrollment_enabled: body.self_enrol_enabled, + initial_training_type: Number(body.training_type_id), + skill_template_id: skillTemplateID, + }); + + await MentorGroupsBelongsToCourses.create({ + mentor_group_id: Number(body.mentor_group_id), + course_id: course.id, + can_edit_course: true, + }); + + response.status(HttpStatusCode.Created).send({ uuid: course.uuid }); +} + +/** + * Validates and updates a course based on the request + * @param request + * @param response + */ +async function updateCourse(request: Request, response: Response) { + const user: User = request.body.user; + const body: ICourseBody = request.body as ICourseBody; + + const validation = _CourseAdministrationValidator.validateUpdateOrCreateRequest(body); + if (validation.invalid) { + ValidationHelper.sendValidationErrorResponse(response, validation); + return; + } + + if (!(await user.canEditCourse(body.course_uuid))) { + response.sendStatus(HttpStatusCode.Forbidden); + return; + } + + const skillTemplateID = isNaN(Number(body.skill_template_id)) || body.skill_template_id == "-1" ? null : Number(body.skill_template_id); + await Course.update( + { + name: body.name_de, + name_en: body.name_en, + description: body.description_de, + description_en: body.description_en, + is_active: body.active, + self_enrollment_enabled: body.self_enrol_enabled, + initial_training_type: Number(body.training_type_id), + skill_template_id: skillTemplateID, + }, + { + where: { + uuid: body.course_uuid, + }, + } + ); + + response.sendStatus(HttpStatusCode.NoContent); +} + +export default { + createCourse, + updateCourse, +}; diff --git a/src/controllers/course/CourseInformationAdminController.ts b/src/controllers/course/CourseInformationAdminController.ts index 816c387..a6e0ae8 100644 --- a/src/controllers/course/CourseInformationAdminController.ts +++ b/src/controllers/course/CourseInformationAdminController.ts @@ -3,13 +3,12 @@ import { Request, Response } from "express"; import { MentorGroupsBelongsToCourses } from "../../models/through/MentorGroupsBelongsToCourses"; import { UsersBelongsToCourses } from "../../models/through/UsersBelongsToCourses"; import { MentorGroup } from "../../models/MentorGroup"; -import CourseInformationAdminValidator from "../_validators/CourseInformationAdminValidator"; import { ValidatorType } from "../_validators/ValidatorType"; import { TrainingRequest } from "../../models/TrainingRequest"; -import { TrainingSession } from "../../models/TrainingSession"; -import { TrainingSessionBelongsToUsers } from "../../models/through/TrainingSessionBelongsToUsers"; import { User } from "../../models/User"; import { HttpStatusCode } from "axios"; +import ValidationHelper from "../../utility/helper/ValidationHelper"; +import _CourseInformationAdminValidator from "./_CourseInformationAdmin.validator"; /** * Gets the basic course information associated with this course @@ -38,7 +37,7 @@ async function getByUUID(request: Request, response: Response) { * @param response */ async function getMentorGroups(request: Request, response: Response) { - const validation: ValidatorType = CourseInformationAdminValidator.validateGetMentorGroupsRequest(request.query.uuid); + const validation: ValidatorType = _CourseInformationAdminValidator.validateGetMentorGroupsRequest(request.query.uuid); if (validation.invalid) { response.status(400).send({ validation: validation.message, validation_failed: validation.invalid }); return; @@ -76,7 +75,7 @@ async function getMentorGroups(request: Request, response: Response) { * @param response */ async function deleteMentorGroup(request: Request, response: Response) { - const validation: ValidatorType = CourseInformationAdminValidator.validateDeleteMentorGroupRequest(request.body.data); + const validation: ValidatorType = _CourseInformationAdminValidator.validateDeleteMentorGroupRequest(request.body.data); if (validation.invalid) { response.status(400).send({ validation: validation.message, validation_failed: validation.invalid }); return; @@ -99,7 +98,7 @@ async function deleteMentorGroup(request: Request, response: Response) { async function getUsers(request: Request, response: Response) { const uuid: string | undefined = request.query?.uuid?.toString(); - const validation: ValidatorType = CourseInformationAdminValidator.validateGetUsersRequest(uuid); + const validation: ValidatorType = _CourseInformationAdminValidator.validateGetUsersRequest(uuid); if (validation.invalid) { response.status(400).send({ validation: validation.message, validation_failed: validation.invalid }); return; @@ -119,74 +118,88 @@ async function getUsers(request: Request, response: Response) { response.send(course?.users ?? []); } +/** + * Removes a user from a course including all associated training requests + * @param request + * @param response + */ async function deleteUser(request: Request, response: Response) { - const requestData = request.body.data; + const body = request.body as { course_uuid: string; user_id: number }; - const validation: ValidatorType = CourseInformationAdminValidator.validateDeleteUserRequest(requestData); + const validation = _CourseInformationAdminValidator.validateDeleteUserRequest(body); if (validation.invalid) { - response.status(400).send({ validation: validation.message, validation_failed: validation.invalid }); + ValidationHelper.sendValidationErrorResponse(response, validation); + return; + } + + const course = await Course.findOne({ + where: { + uuid: body.course_uuid, + }, + }); + + if (course == null) { + response.sendStatus(HttpStatusCode.InternalServerError); return; } await UsersBelongsToCourses.destroy({ where: { - user_id: requestData.user_id, - course_id: requestData.course_id, + user_id: body.user_id, + course_id: course.id, }, }); await TrainingRequest.destroy({ where: { - user_id: requestData.user_id, - course_id: requestData.course_id, + user_id: body.user_id, + course_id: course.id, }, }); - response.send({ message: "OK" }); + response.sendStatus(HttpStatusCode.NoContent); } /** * Updates the course's information */ -async function update(request: Request, response: Response) { - const data = request.body.data; +async function updateCourse(request: Request, response: Response) { + const body = request.body as { + course_uuid: string; + active: boolean; + self_enrol_enabled: boolean; + description_de: string; + description_en: string; + name_de: string; + name_en: string; + training_type_id: string; + skill_template_id?: string; + }; - const validation: ValidatorType = CourseInformationAdminValidator.validateUpdateRequest(data); - if (validation.invalid) { - response.status(400).send({ validation: validation.message, validation_failed: validation.invalid }); - return; - } + // const validation: ValidatorType = _CourseInformationAdminValidator.validateUpdateOrCreateRequest(body); + // if (validation.invalid) { + // ValidationHelper.sendValidationErrorResponse(response, validation); + // return; + // } const updateObject = { - name: data.name_de, - name_en: data.name_en, - description: data.description_de, - description_en: data.description_en, - is_active: Number(data.active) == 1, - self_enrollment_enabled: Number(data.self_enrol) == 1, - initial_training_type: Number(data.training_id), - skill_template_id: Number(data.skill_template_id) == 0 || isNaN(Number(data.skill_template_id)) ? null : data.skill_template_id, + name: body.name_de, + name_en: body.name_en, + description: body.description_de, + description_en: body.description_en, + is_active: body.active, + self_enrollment_enabled: body.self_enrol_enabled, + initial_training_type: Number(body.training_type_id), + skill_template_id: body.skill_template_id != null && !isNaN(Number(body.skill_template_id)) ? Number(body.skill_template_id) : null, }; await Course.update(updateObject, { where: { - uuid: data.course_uuid, + uuid: body.course_uuid, }, }); - const course: Course | null = await Course.findOne({ - where: { - uuid: data.course_uuid, - }, - include: [Course.associations.training_type, Course.associations.skill_template], - }); - - if (course == null) { - response.status(500).send(); - return; - } - - response.send(course); + response.sendStatus(HttpStatusCode.NoContent); } async function addMentorGroup(request: Request, response: Response) { @@ -209,5 +222,5 @@ export default { deleteMentorGroup, getUsers, deleteUser, - update, + updateCourse, }; diff --git a/src/controllers/course/_CourseAdministration.validator.ts b/src/controllers/course/_CourseAdministration.validator.ts new file mode 100644 index 0000000..5123653 --- /dev/null +++ b/src/controllers/course/_CourseAdministration.validator.ts @@ -0,0 +1,51 @@ +import { ValidatorType } from "../_validators/ValidatorType"; +import ValidationHelper, { ValidationOptions } from "../../utility/helper/ValidationHelper"; + +function validateUpdateOrCreateRequest(data: any): ValidatorType { + return ValidationHelper.validate([ + { + name: "course_uuid", + validationObject: data.course_uuid, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "name_de", + validationObject: data.name_de, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "name_en", + validationObject: data.name_en, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "description_de", + validationObject: data.description_de, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "description_en", + validationObject: data.description_en, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "self_enrol_enabled", + validationObject: data.self_enrol_enabled, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "active", + validationObject: data.active, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "training_type_id", + validationObject: data.training_type_id, + toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], + }, + ]); +} + +export default { + validateUpdateOrCreateRequest, +}; diff --git a/src/controllers/_validators/CourseInformationAdminValidator.ts b/src/controllers/course/_CourseInformationAdmin.validator.ts similarity index 53% rename from src/controllers/_validators/CourseInformationAdminValidator.ts rename to src/controllers/course/_CourseInformationAdmin.validator.ts index e334425..4f2ade0 100644 --- a/src/controllers/_validators/CourseInformationAdminValidator.ts +++ b/src/controllers/course/_CourseInformationAdmin.validator.ts @@ -1,5 +1,5 @@ -import { ValidatorType } from "./ValidatorType"; import ValidationHelper, { ValidationOptions } from "../../utility/helper/ValidationHelper"; +import { ValidatorType } from "../_validators/ValidatorType"; function validateGetMentorGroupsRequest(course_uuid: any): ValidatorType { return ValidationHelper.validate([ @@ -37,21 +37,6 @@ function validateGetUsersRequest(uuid: any): ValidatorType { } function validateDeleteUserRequest(data: any): ValidatorType { - return ValidationHelper.validate([ - { - name: "course_id", - validationObject: data.course_id, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "user_id", - validationObject: data.user_id, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - ]); -} - -function validateUpdateRequest(data: any): ValidatorType { return ValidationHelper.validate([ { name: "course_uuid", @@ -59,38 +44,8 @@ function validateUpdateRequest(data: any): ValidatorType { toValidate: [{ val: ValidationOptions.NON_NULL }], }, { - name: "name_de", - validationObject: data.name_de, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "name_en", - validationObject: data.name_en, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "description_de", - validationObject: data.description_de, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "description_en", - validationObject: data.description_en, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "self_enrol", - validationObject: data.self_enrol, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "active", - validationObject: data.active, - toValidate: [{ val: ValidationOptions.NON_NULL }], - }, - { - name: "training_id", - validationObject: data.training_id, + name: "user_id", + validationObject: data.user_id, toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], }, ]); @@ -101,5 +56,4 @@ export default { validateDeleteMentorGroupRequest, validateGetUsersRequest, validateDeleteUserRequest, - validateUpdateRequest, }; diff --git a/src/controllers/mentor-group/MentorGroupAdminController.ts b/src/controllers/mentor-group/MentorGroupAdminController.ts index f55b3ca..28387c7 100644 --- a/src/controllers/mentor-group/MentorGroupAdminController.ts +++ b/src/controllers/mentor-group/MentorGroupAdminController.ts @@ -120,13 +120,14 @@ async function getAll(request: Request, response: Response) { * @param response */ async function getByID(request: Request, response: Response) { - const mentorGroupID = request.params.mentor_group_id; + const user: User = request.body.user; + const params = request.params; const validation = ValidationHelper.validate([ { name: "id", - validationObject: mentorGroupID, - toValidate: [{ val: ValidationOptions.NON_NULL }], + validationObject: params.mentor_group_id, + toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], }, ]); @@ -135,9 +136,14 @@ async function getByID(request: Request, response: Response) { return; } + if (!(await user.isMentorGroupAdmin(Number(params.mentor_group_id)))) { + response.sendStatus(HttpStatusCode.Forbidden); + return; + } + const mentorGroup = await MentorGroup.findOne({ where: { - id: mentorGroupID, + id: params.mentor_group_id, }, include: [ { @@ -255,14 +261,14 @@ async function addMember(request: Request, response: Response) { const validation = _MentorGroupAdminValidator.validateAddUser(body); if (validation.invalid) { - response.status(HttpStatusCode.BadRequest).send({ - validation: validation.message, - validation_failed: true, - }); + ValidationHelper.sendValidationErrorResponse(response, validation); return; } - // TODO: Add utility function to check if user is allowed to perform this action + if (!(await user.isMentorGroupAdmin(Number(body.mentor_group_id)))) { + response.sendStatus(HttpStatusCode.Forbidden); + return; + } try { await UserBelongToMentorGroups.create({ diff --git a/src/models/MentorGroup.ts b/src/models/MentorGroup.ts index 795d884..c8dabb1 100644 --- a/src/models/MentorGroup.ts +++ b/src/models/MentorGroup.ts @@ -3,6 +3,7 @@ import { User } from "./User"; import { DataType } from "sequelize-typescript"; import { sequelize } from "../core/Sequelize"; import { Course } from "./Course"; +import { UserBelongToMentorGroups } from "./through/UserBelongToMentorGroups"; export class MentorGroup extends Model, InferCreationAttributes> { // @@ -24,6 +25,11 @@ export class MentorGroup extends Model, InferCreati declare users?: NonAttribute; declare courses?: NonAttribute; + // + // Through Association Placeholders + // + declare UserBelongToMentorGroups?: NonAttribute; + declare static associations: { users: Association; courses: Association; diff --git a/src/models/User.ts b/src/models/User.ts index c3233f2..3fa8444 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -12,6 +12,8 @@ import { Role } from "./Role"; import { TrainingLog } from "./TrainingLog"; import { UsersBelongsToCourses } from "./through/UsersBelongsToCourses"; import { TrainingRequest } from "./TrainingRequest"; +import { UserBelongToMentorGroups } from "./through/UserBelongToMentorGroups"; +import UserExtensions from "./extensions/UserExtensions"; export class User extends Model, InferCreationAttributes> { // @@ -46,6 +48,11 @@ export class User extends Model, InferCreationAttributes; declare roles?: NonAttribute; + // + // Through Association Placeholders + // + declare UserBelongToMentorGroups?: NonAttribute; + declare static associations: { user_data: Association; user_settings: Association; @@ -61,42 +68,14 @@ export class User extends Model, InferCreationAttributes; }; - /** - * Checks if the user has a specific role - */ - hasRole(role: string): boolean { - let roles: string[] = []; - this.roles?.forEach(role => { - roles.push(role.name); - }); - - return roles.includes(role); - } - - /** - * Checks if the user has a specific permission - */ - hasPermission(permission: string): boolean { - let permissions: string[] = []; - this.roles?.forEach(role => { - role.permissions?.forEach(perm => { - permissions.push(perm.name.toLowerCase()); - }); - }); - - return permissions.includes(permission.toLowerCase()); - } - - async getCourses(): Promise { - const user: User | null = await User.findOne({ - where: { - id: this.id, - }, - include: [User.associations.courses], - }); - - return user?.courses ?? []; - } + hasRole = UserExtensions.hasRole.bind(this); + hasPermission = UserExtensions.hasPermission.bind(this); + getMentorGroups = UserExtensions.getMentorGroups.bind(this); + getGroupAdminMentorGroups = UserExtensions.getGroupAdminMentorGroups.bind(this); + getCourseCreatorMentorGroups = UserExtensions.getCourseCreatorMentorGroups.bind(this); + getCourses = UserExtensions.getCourses.bind(this); + canManageCourseInMentorGroup = UserExtensions.canManageCourseInMentorGroup.bind(this); + canEditCourse = UserExtensions.canEditCourse.bind(this); async isMemberOfCourse(uuid: string): Promise { const course = await Course.findOne({ @@ -134,17 +113,17 @@ export class User extends Model, InferCreationAttributes { - const user = await User.findOne({ + async isMentorGroupAdmin(mentorGroupID: number): Promise { + // Find mentor group by ID and select the users + const userInMentorGroup = await UserBelongToMentorGroups.findOne({ where: { - id: this.id, - }, - include: { - association: User.associations.mentor_groups, + user_id: this.id, + group_id: mentorGroupID, + group_admin: true, }, }); - return user?.mentor_groups ?? []; + return userInMentorGroup != null; } async getMentorGroupsAndCourses(): Promise { diff --git a/src/models/associations/RoleAssociations.ts b/src/models/associations/RoleAssociations.ts index 1290d54..b375c87 100644 --- a/src/models/associations/RoleAssociations.ts +++ b/src/models/associations/RoleAssociations.ts @@ -5,7 +5,7 @@ import { Permission } from "../Permission"; export function registerRoleAssociations() { // - // Role -> Permissions + // Role -> Permissions.txt // Role.belongsToMany(Permission, { as: "permissions", @@ -15,7 +15,7 @@ export function registerRoleAssociations() { }); // - // Permissions -> Role + // Permissions.txt -> Role // Permission.belongsToMany(Role, { as: "roles", diff --git a/src/models/extensions/UserExtensions.ts b/src/models/extensions/UserExtensions.ts new file mode 100644 index 0000000..5539a7b --- /dev/null +++ b/src/models/extensions/UserExtensions.ts @@ -0,0 +1,145 @@ +import { User } from "../User"; +import { MentorGroup } from "../MentorGroup"; +import { Role } from "../Role"; +import { Permission } from "../Permission"; +import { Course } from "../Course"; + +/** + * Checks if the user as the specified role + * @param role + */ +function hasRole(this: User, role: string): boolean { + const roles: string[] = []; + this.roles?.forEach((role: Role) => { + roles.push(role.name); + }); + + return roles.includes(role); +} + +/** + * Checks if the user has the specified permission + * @param permission + */ +function hasPermission(this: User, permission: string): boolean { + const permissions: string[] = []; + this.roles?.forEach((role: Role) => { + role.permissions?.forEach((perm: Permission) => { + permissions.push(perm.name.toLowerCase()); + }); + }); + + return permissions.includes(permission.toLowerCase()); +} + +/** + * Returns a list of mentor groups this user is associated with + * Note: Permissions.txt within this mentor group are not considered! + */ +async function getMentorGroups(this: User): Promise { + const user = await User.findOne({ + where: { + id: this.id, + }, + include: { + association: User.associations.mentor_groups, + }, + }); + + return user?.mentor_groups ?? []; +} + +/** + * Returns a list of mentor groups this user is a group admin in + */ +async function getGroupAdminMentorGroups(this: User): Promise { + const user = await User.findOne({ + where: { + id: this.id, + }, + include: { + association: User.associations.mentor_groups, + through: { + where: { + group_admin: true, + }, + }, + }, + }); + + return user?.mentor_groups ?? []; +} + +/** + * Returns a list of mentor groups this user can manage courses + * Manage in this context means, create, update, delete - although this might be reevaluated in the future + */ +async function getCourseCreatorMentorGroups(this: User): Promise { + const user = await User.findOne({ + where: { + id: this.id, + }, + include: { + association: User.associations.mentor_groups, + through: { + where: { + can_manage_course: true, + }, + }, + }, + }); + + return user?.mentor_groups ?? []; +} + +/** + * Checks if the current user can create a course in the specified mentor group + * @param mentorGroupID + */ +async function canManageCourseInMentorGroup(this: User, mentorGroupID: number): Promise { + const mentorGroups = await this.getCourseCreatorMentorGroups(); + for (const mentorGroup of mentorGroups) { + if (mentorGroup.id == mentorGroupID) { + return true; + } + } + + return false; +} + +/** + * Checks if the user can edit the course with the specified UUID + * This is the case if and only if: + * 1. A user is in a mentor group of a course and has the 'can_manage_course' attribute + * 2. This mentor group has the 'can_edit_course' attribute set + * @param courseUUID + */ +async function canEditCourse(this: User, courseUUID: string): Promise { + // TODO: Find an overlap between 1 & 2 (above) and return a boolean if the case is true + return true; +} + +/** + * Gets all courses which the current user is associated with (enrolled, completed, ...) + */ +async function getCourses(this: User): Promise { + const user: User | null = await User.findOne({ + where: { + id: this.id, + }, + include: [User.associations.courses], + }); + + return user?.courses ?? []; +} + +export default { + hasRole, + hasPermission, + getMentorGroups, + getGroupAdminMentorGroups, + getCourseCreatorMentorGroups, + canManageCourseInMentorGroup, + canEditCourse, + getCourses, +}; diff --git a/src/utility/helper/ValidationHelper.ts b/src/utility/helper/ValidationHelper.ts index 7c11477..cb64bc5 100644 --- a/src/utility/helper/ValidationHelper.ts +++ b/src/utility/helper/ValidationHelper.ts @@ -1,4 +1,7 @@ import dayjs from "dayjs"; +import { Response } from "express"; +import { ValidatorType } from "../../controllers/_validators/ValidatorType"; +import { HttpStatusCode } from "axios"; export function validateObject(object: any, keys: string[], checkStringNull = false) { let malformedKeys: any[] = []; @@ -113,6 +116,14 @@ function validate(options: ValidationType[]): { invalid: boolean; message: any[] }; } +function sendValidationErrorResponse(response: Response, validation: ValidatorType) { + response.status(HttpStatusCode.BadRequest).send({ + validation: validation.message, + validation_failed: true, + }); +} + export default { validate, + sendValidationErrorResponse, };