From 227ca6f57bd554e733170ce7f3171b2e5988e442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Fri, 9 Jun 2023 19:39:45 +0200 Subject: [PATCH 01/16] Ready for public release --- db/config/config.js | 12 ++-- .../_validators/CourseAdminValidator.ts | 15 ++--- .../CourseInformationAdminValidator.ts | 4 +- src/controllers/_validators/ValidatorType.ts | 4 +- .../course/CourseAdminController.ts | 2 +- .../course/CourseInformationController.ts | 10 ++- src/controllers/login/LoginController.ts | 2 +- .../TrainingTypeAdminController.ts | 62 +++++++++---------- src/libraries/session/UserSessionLibrary.ts | 5 +- src/libraries/vatsim/ConnectLibrary.ts | 2 +- src/models/CourseInformation.ts | 22 +++---- src/models/CourseSkillTemplate.ts | 10 +-- src/models/Job.ts | 20 +++--- src/models/Permission.ts | 8 +-- src/models/SysLog.ts | 14 ++--- src/models/TrainingLogTemplate.ts | 10 +-- src/models/TrainingStation.ts | 21 +++---- src/models/User.ts | 2 +- .../EndorsementGroupsBelongsToUsers.ts | 20 +++--- .../through/MentorGroupsBelongsToCourses.ts | 16 ++--- .../TrainingStationBelongsToTrainingType.ts | 24 +++---- .../through/TrainingTypesBelongsToCourses.ts | 14 ++--- 22 files changed, 138 insertions(+), 161 deletions(-) diff --git a/db/config/config.js b/db/config/config.js index a6e41a8..163aa4c 100644 --- a/db/config/config.js +++ b/db/config/config.js @@ -1,4 +1,4 @@ -const dotenv = require('dotenv'); +const dotenv = require("dotenv"); dotenv.config(); @@ -15,9 +15,9 @@ module.exports = { username: process.env.CI_DB_USERNAME, password: process.env.CI_DB_PASSWORD, database: process.env.CI_DB_NAME, - host: '127.0.0.1', + host: "127.0.0.1", port: 3306, - dialect: 'mysql', + dialect: "mysql", }, production: { username: process.env.PROD_DB_USERNAME, @@ -25,6 +25,6 @@ module.exports = { database: process.env.PROD_DB_NAME, host: process.env.PROD_DB_HOSTNAME, port: process.env.PROD_DB_PORT, - dialect: 'mysql', - } -}; \ No newline at end of file + dialect: "mysql", + }, +}; diff --git a/src/controllers/_validators/CourseAdminValidator.ts b/src/controllers/_validators/CourseAdminValidator.ts index 2818222..54228ec 100644 --- a/src/controllers/_validators/CourseAdminValidator.ts +++ b/src/controllers/_validators/CourseAdminValidator.ts @@ -11,10 +11,7 @@ function validateCreateRequest(data: any): ValidatorType { { name: "mentor_group_id", validationObject: data.mentor_group_id, - toValidate: [ - { val: ValidationOptions.NON_NULL }, - { val: ValidationOptions.NUMBER } - ], + toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], }, { name: "name_de", @@ -49,15 +46,11 @@ function validateCreateRequest(data: any): ValidatorType { { name: "training_id", validationObject: data.training_id, - toValidate: [ - { val: ValidationOptions.NON_NULL }, - { val: ValidationOptions.NUMBER }, - { val: ValidationOptions.NOT_EQUAL_NUM, value: 0 } - ], + toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }, { val: ValidationOptions.NOT_EQUAL_NUM, value: 0 }], }, ]); } export default { - validateCreateRequest -} \ No newline at end of file + validateCreateRequest, +}; diff --git a/src/controllers/_validators/CourseInformationAdminValidator.ts b/src/controllers/_validators/CourseInformationAdminValidator.ts index 79b4a93..e334425 100644 --- a/src/controllers/_validators/CourseInformationAdminValidator.ts +++ b/src/controllers/_validators/CourseInformationAdminValidator.ts @@ -101,5 +101,5 @@ export default { validateDeleteMentorGroupRequest, validateGetUsersRequest, validateDeleteUserRequest, - validateUpdateRequest -} \ No newline at end of file + validateUpdateRequest, +}; diff --git a/src/controllers/_validators/ValidatorType.ts b/src/controllers/_validators/ValidatorType.ts index b0672b6..8ea3851 100644 --- a/src/controllers/_validators/ValidatorType.ts +++ b/src/controllers/_validators/ValidatorType.ts @@ -1,4 +1,4 @@ export type ValidatorType = { invalid: boolean; - message: any[] -} \ No newline at end of file + message: any[]; +}; diff --git a/src/controllers/course/CourseAdminController.ts b/src/controllers/course/CourseAdminController.ts index 2437980..94cc477 100644 --- a/src/controllers/course/CourseAdminController.ts +++ b/src/controllers/course/CourseAdminController.ts @@ -74,7 +74,7 @@ async function create(request: Request, response: Response) { if (validation.invalid) { response.status(400).send({ validation: validation.message, - validation_failed: validation.invalid + validation_failed: validation.invalid, }); return; } diff --git a/src/controllers/course/CourseInformationController.ts b/src/controllers/course/CourseInformationController.ts index 4036082..029ce08 100644 --- a/src/controllers/course/CourseInformationController.ts +++ b/src/controllers/course/CourseInformationController.ts @@ -10,6 +10,9 @@ import { TrainingSession } from "../../models/TrainingSession"; */ async function getInformationByUUID(request: Request, response: Response) { const uuid: string = request.query.uuid?.toString() ?? ""; + const user: User = request.body.user; + + const userCourses: Course[] = await user.getCourses(); const course: Course | null = await Course.findOne({ where: { @@ -36,6 +39,11 @@ async function getInformationByUUID(request: Request, response: Response) { return; } + if (userCourses.find((c: Course) => c.uuid == course.uuid) != null) { + response.send({ ...course.toJSON(), enrolled: true }); + return; + } + response.send(course); } @@ -85,7 +93,7 @@ async function getCourseTrainingInformationByUUID(request: Request, response: Re return; } - const data = await User.findOne({ + const data: User | null = await User.findOne({ where: { id: user.id, }, diff --git a/src/controllers/login/LoginController.ts b/src/controllers/login/LoginController.ts index 16dbe68..761a3f2 100644 --- a/src/controllers/login/LoginController.ts +++ b/src/controllers/login/LoginController.ts @@ -110,7 +110,7 @@ async function getUserData(request: Request, response: Response) { } async function validateSessionToken(request: Request, response: Response) { - response.send(await SessionLibrary.validateSessionToken(request) != null); + response.send((await SessionLibrary.validateSessionToken(request)) != null); } export default { diff --git a/src/controllers/training-type/TrainingTypeAdminController.ts b/src/controllers/training-type/TrainingTypeAdminController.ts index 2fba147..73460c9 100644 --- a/src/controllers/training-type/TrainingTypeAdminController.ts +++ b/src/controllers/training-type/TrainingTypeAdminController.ts @@ -27,8 +27,8 @@ async function getByID(request: Request, response: Response) { { name: "id", validationObject: requestID, - toValidate: [{ val: ValidationOptions.NON_NULL }] - } + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, ]); if (validation.invalid) { @@ -38,22 +38,22 @@ async function getByID(request: Request, response: Response) { const trainingType = await TrainingType.findOne({ where: { - id: requestID?.toString() + id: requestID?.toString(), }, include: [ { association: TrainingType.associations.log_template, attributes: { - exclude: ["content"] - } + exclude: ["content"], + }, }, { association: TrainingType.associations.training_stations, through: { - attributes: [] - } - } - ] + attributes: [], + }, + }, + ], }); response.send(trainingType); @@ -71,13 +71,13 @@ async function create(request: Request, response: Response) { { name: "name", validationObject: requestData.name, - toValidate: [{ val: ValidationOptions.NON_NULL }] + toValidate: [{ val: ValidationOptions.NON_NULL }], }, { name: "type", validationObject: requestData.type, - toValidate: [{ val: ValidationOptions.NON_NULL }] - } + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, ]); if (validation.invalid) { @@ -88,7 +88,7 @@ async function create(request: Request, response: Response) { const trainingType = await TrainingType.create({ name: requestData.name, type: requestData.type, - log_template_id: !isNaN(requestData.log_template_id) && Number(requestData.log_template_id) == -1 ? null : Number(requestData.log_template_id) + log_template_id: !isNaN(requestData.log_template_id) && Number(requestData.log_template_id) == -1 ? null : Number(requestData.log_template_id), }); response.send(trainingType); @@ -107,18 +107,18 @@ async function update(request: Request, response: Response) { { name: "id", validationObject: training_type_id, - toValidate: [{ val: ValidationOptions.NON_NULL }] + toValidate: [{ val: ValidationOptions.NON_NULL }], }, { name: "name", validationObject: requestData.name, - toValidate: [{ val: ValidationOptions.NON_NULL }] + toValidate: [{ val: ValidationOptions.NON_NULL }], }, { name: "type", validationObject: requestData.type, - toValidate: [{ val: ValidationOptions.NON_NULL }] - } + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, ]); if (validation.invalid) { @@ -130,20 +130,20 @@ async function update(request: Request, response: Response) { ( await TrainingType.findOne({ where: { - id: training_type_id - } + id: training_type_id, + }, }) )?.update({ name: requestData.name, type: requestData.type, - log_template_id: !isNaN(requestData.log_template_id) && Number(requestData.log_template_id) == -1 ? null : Number(requestData.log_template_id) + log_template_id: !isNaN(requestData.log_template_id) && Number(requestData.log_template_id) == -1 ? null : Number(requestData.log_template_id), }); const trainingType = await TrainingType.findOne({ where: { - id: training_type_id + id: training_type_id, }, - include: [TrainingType.associations.log_template, TrainingType.associations.training_stations] + include: [TrainingType.associations.log_template, TrainingType.associations.training_stations], }); response.send(trainingType); @@ -159,13 +159,13 @@ async function addStation(request: Request, response: Response) { { name: "training_type_id", validationObject: requestData.training_type_id, - toValidate: [{ val: ValidationOptions.NON_NULL }] + toValidate: [{ val: ValidationOptions.NON_NULL }], }, { name: "training_station_id", validationObject: requestData.training_station_id, - toValidate: [{ val: ValidationOptions.NON_NULL }] - } + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, ]); if (validation.invalid) { @@ -175,14 +175,14 @@ async function addStation(request: Request, response: Response) { const station = await TrainingStation.findOne({ where: { - id: requestData.training_station_id - } + id: requestData.training_station_id, + }, }); const trainingType = await TrainingType.findOne({ where: { - id: requestData.training_type_id - } + id: requestData.training_type_id, + }, }); if (station == null || trainingType == null) { @@ -192,7 +192,7 @@ async function addStation(request: Request, response: Response) { await TrainingStationBelongsToTrainingType.create({ training_station_id: requestData.training_station_id, - training_type_id: requestData.training_type_id + training_type_id: requestData.training_type_id, }); response.send(station); @@ -210,5 +210,5 @@ export default { create, update, addStation, - removeStation + removeStation, }; diff --git a/src/libraries/session/UserSessionLibrary.ts b/src/libraries/session/UserSessionLibrary.ts index 067205e..43ca492 100644 --- a/src/libraries/session/UserSessionLibrary.ts +++ b/src/libraries/session/UserSessionLibrary.ts @@ -10,8 +10,7 @@ async function getUserIdFromSession(request: Request): Promise { if (session_token == null || session_token == false) return 0; const sessionCurrent: UserSession | null = await SessionLibrary.validateSessionToken(request); - if (sessionCurrent == null) - return 0; + if (sessionCurrent == null) return 0; return sessionCurrent.user_id; } @@ -46,4 +45,4 @@ async function getUserFromSession(request: Request): Promise { export default { getUserFromSession, getUserIdFromSession, -} \ No newline at end of file +}; diff --git a/src/libraries/vatsim/ConnectLibrary.ts b/src/libraries/vatsim/ConnectLibrary.ts index fe6496d..0f86e42 100644 --- a/src/libraries/vatsim/ConnectLibrary.ts +++ b/src/libraries/vatsim/ConnectLibrary.ts @@ -209,7 +209,7 @@ export class VatsimConnectLibrary { }, }); - const user = await User.findOne({ + const user: User | null = await User.scope("sensitive").findOne({ where: { id: this.m_userData?.data.cid, }, diff --git a/src/models/CourseInformation.ts b/src/models/CourseInformation.ts index 112d149..9fb6992 100644 --- a/src/models/CourseInformation.ts +++ b/src/models/CourseInformation.ts @@ -1,12 +1,4 @@ -import { - Association, - CreationOptional, - ForeignKey, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute -} from "sequelize"; +import { Association, CreationOptional, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute } from "sequelize"; import { DataType } from "sequelize-typescript"; import { sequelize } from "../core/Sequelize"; import { Course } from "./Course"; @@ -39,27 +31,27 @@ CourseInformation.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, course_id: { type: DataType.INTEGER, allowNull: false, references: { model: "courses", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, data: { type: DataType.JSON, - allowNull: false + allowNull: false, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "course_information", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/CourseSkillTemplate.ts b/src/models/CourseSkillTemplate.ts index 695b0ec..ccb36d0 100644 --- a/src/models/CourseSkillTemplate.ts +++ b/src/models/CourseSkillTemplate.ts @@ -22,21 +22,21 @@ CourseSkillTemplate.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, name: { type: DataType.STRING, - allowNull: false + allowNull: false, }, content: { type: DataType.JSON, - allowNull: false + allowNull: false, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "course_skill_templates", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/Job.ts b/src/models/Job.ts index 10ac227..72949f5 100644 --- a/src/models/Job.ts +++ b/src/models/Job.ts @@ -27,38 +27,38 @@ Job.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, uuid: { type: DataType.UUID, - allowNull: false + allowNull: false, }, job_type: { - type: DataType.ENUM("email") + type: DataType.ENUM("email"), }, payload: { type: DataType.JSON, - comment: "Payload for the job, includes json data for the job to execute" + comment: "Payload for the job, includes json data for the job to execute", }, attempts: { type: DataType.TINYINT({ unsigned: true }), - allowNull: false + allowNull: false, }, available_at: { - type: DataType.DATE + type: DataType.DATE, }, last_executed: { - type: DataType.DATE + type: DataType.DATE, }, status: { type: DataType.ENUM("queued", "running", "completed"), - allowNull: false + allowNull: false, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "jobs", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/Permission.ts b/src/models/Permission.ts index 085b4fe..3a39414 100644 --- a/src/models/Permission.ts +++ b/src/models/Permission.ts @@ -21,18 +21,18 @@ Permission.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, name: { type: DataType.STRING(70), allowNull: false, - unique: true + unique: true, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "permissions", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/SysLog.ts b/src/models/SysLog.ts index e1d1a19..3bcfb71 100644 --- a/src/models/SysLog.ts +++ b/src/models/SysLog.ts @@ -21,25 +21,25 @@ SysLog.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, user_id: { - type: DataType.STRING + type: DataType.STRING, }, path: { - type: DataType.STRING + type: DataType.STRING, }, method: { - type: DataType.STRING(10) + type: DataType.STRING(10), }, remote_addr: { - type: DataType.STRING + type: DataType.STRING, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "syslog", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/TrainingLogTemplate.ts b/src/models/TrainingLogTemplate.ts index 5d1b95d..1ba3178 100644 --- a/src/models/TrainingLogTemplate.ts +++ b/src/models/TrainingLogTemplate.ts @@ -22,21 +22,21 @@ TrainingLogTemplate.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, name: { type: DataType.STRING, - allowNull: false + allowNull: false, }, content: { type: DataType.JSON, - allowNull: false + allowNull: false, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "training_log_templates", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/TrainingStation.ts b/src/models/TrainingStation.ts index bb5dc59..32672cf 100644 --- a/src/models/TrainingStation.ts +++ b/src/models/TrainingStation.ts @@ -1,11 +1,4 @@ -import { - Association, - CreationOptional, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute -} from "sequelize"; +import { Association, CreationOptional, InferAttributes, InferCreationAttributes, Model, NonAttribute } from "sequelize"; import { DataType } from "sequelize-typescript"; import { sequelize } from "../core/Sequelize"; import { TrainingRequest } from "./TrainingRequest"; @@ -39,25 +32,25 @@ TrainingStation.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, callsign: { type: DataType.STRING(15), - allowNull: false + allowNull: false, }, frequency: { type: DataType.FLOAT(6, 3), - allowNull: false + allowNull: false, }, deactivated: { type: DataType.BOOLEAN, - allowNull: false + allowNull: false, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "training_stations", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/User.ts b/src/models/User.ts index 6ff17f0..c3233f2 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -88,7 +88,7 @@ export class User extends Model, InferCreationAttributes { - const user = await User.findOne({ + const user: User | null = await User.findOne({ where: { id: this.id, }, diff --git a/src/models/through/EndorsementGroupsBelongsToUsers.ts b/src/models/through/EndorsementGroupsBelongsToUsers.ts index a2e1949..c19646a 100644 --- a/src/models/through/EndorsementGroupsBelongsToUsers.ts +++ b/src/models/through/EndorsementGroupsBelongsToUsers.ts @@ -28,43 +28,43 @@ EndorsementGroupsBelongsToUsers.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, endorsement_group_id: { type: DataType.INTEGER, allowNull: false, references: { model: "endorsement_groups", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, user_id: { type: DataType.INTEGER, allowNull: false, references: { model: "users", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, solo: { type: DataType.BOOLEAN, - allowNull: false + allowNull: false, }, solo_expires: { - type: DataType.DATE + type: DataType.DATE, }, solo_extension_count: { - type: DataType.INTEGER + type: DataType.INTEGER, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "endorsement_groups_belong_to_users", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/through/MentorGroupsBelongsToCourses.ts b/src/models/through/MentorGroupsBelongsToCourses.ts index c73eee9..4feab3b 100644 --- a/src/models/through/MentorGroupsBelongsToCourses.ts +++ b/src/models/through/MentorGroupsBelongsToCourses.ts @@ -25,7 +25,7 @@ MentorGroupsBelongsToCourses.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, mentor_group_id: { type: DataType.INTEGER, @@ -33,10 +33,10 @@ MentorGroupsBelongsToCourses.init( allowNull: false, references: { model: "mentor_groups", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, course_id: { type: DataType.INTEGER, @@ -44,22 +44,22 @@ MentorGroupsBelongsToCourses.init( allowNull: false, references: { model: "courses", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, can_edit_course: { type: DataType.BOOLEAN, comment: "If true, ALL users of this mentor group can edit the course assuming the can_manage_course flag is set for the user on users_belong_to_mentor_groups.", - allowNull: false + allowNull: false, }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "mentor_groups_belong_to_courses", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/through/TrainingStationBelongsToTrainingType.ts b/src/models/through/TrainingStationBelongsToTrainingType.ts index ad41b1e..2613809 100644 --- a/src/models/through/TrainingStationBelongsToTrainingType.ts +++ b/src/models/through/TrainingStationBelongsToTrainingType.ts @@ -1,12 +1,4 @@ -import { - Association, - CreationOptional, - ForeignKey, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute -} from "sequelize"; +import { Association, CreationOptional, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute } from "sequelize"; import { TrainingType } from "../TrainingType"; import { DataType } from "sequelize-typescript"; import { sequelize } from "../../core/Sequelize"; @@ -45,33 +37,33 @@ TrainingStationBelongsToTrainingType.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, training_type_id: { type: DataType.INTEGER, allowNull: false, references: { model: "training_types", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, training_station_id: { type: DataType.INTEGER, allowNull: false, references: { model: "training_stations", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "training_types_belong_to_training_stations", - sequelize: sequelize + sequelize: sequelize, } ); diff --git a/src/models/through/TrainingTypesBelongsToCourses.ts b/src/models/through/TrainingTypesBelongsToCourses.ts index bb9c859..07123ab 100644 --- a/src/models/through/TrainingTypesBelongsToCourses.ts +++ b/src/models/through/TrainingTypesBelongsToCourses.ts @@ -27,33 +27,33 @@ TrainingTypesBelongsToCourses.init( id: { type: DataType.INTEGER, primaryKey: true, - autoIncrement: true + autoIncrement: true, }, training_type_id: { type: DataType.INTEGER, allowNull: false, references: { model: "training_types", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, course_id: { type: DataType.INTEGER, allowNull: false, references: { model: "courses", - key: "id" + key: "id", }, onUpdate: "cascade", - onDelete: "cascade" + onDelete: "cascade", }, createdAt: DataType.DATE, - updatedAt: DataType.DATE + updatedAt: DataType.DATE, }, { tableName: "training_types_belongs_to_courses", - sequelize: sequelize + sequelize: sequelize, } ); From ae4f1c11dac7f35d79fbdf4d18d8a40a3d2b74cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Sat, 10 Jun 2023 01:16:53 +0200 Subject: [PATCH 02/16] Add notification support --- .prettierignore | 1 + ...221115171265-create-notification-tables.js | 70 ++++++++++++ src/Router.ts | 3 + .../TrainingRequestAdminController.ts | 48 ++++++++- .../user/UserNotificationController.ts | 26 +++++ .../notification/NotificationLibrary.ts | 18 ++++ src/models/Notification.ts | 102 ++++++++++++++++++ .../associations/NotificationAssociations.ts | 43 ++++++++ .../associations/_RegisterAssociations.ts | 2 + 9 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 db/migrations/20221115171265-create-notification-tables.js create mode 100644 src/controllers/user/UserNotificationController.ts create mode 100644 src/libraries/notification/NotificationLibrary.ts create mode 100644 src/models/Notification.ts create mode 100644 src/models/associations/NotificationAssociations.ts diff --git a/.prettierignore b/.prettierignore index 9299f61..3d581d3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,6 +14,7 @@ docker-compose.yml .prettierignore .gitlab-ci.yml +CONTRIBUTING.md README.md .env .env.example diff --git a/db/migrations/20221115171265-create-notification-tables.js b/db/migrations/20221115171265-create-notification-tables.js new file mode 100644 index 0000000..8c3a4ac --- /dev/null +++ b/db/migrations/20221115171265-create-notification-tables.js @@ -0,0 +1,70 @@ +const { DataType } = require("sequelize-typescript"); + +const CourseInformationModelAttributes = { + id: { + type: DataType.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + uuid: { + type: DataType.UUID, + allowNull: false, + }, + user_id: { + type: DataType.INTEGER, + allowNull: false, + references: { + model: "users", + key: "id", + }, + onUpdate: "cascade", + onDelete: "cascade", + }, + author_id: { + type: DataType.INTEGER, + allowNull: true, + references: { + model: "users", + key: "id", + }, + onUpdate: "cascade", + onDelete: "set null", + }, + content_de: { + type: DataType.TEXT("medium"), + allowNull: false, + }, + content_en: { + type: DataType.TEXT("medium"), + allowNull: false, + }, + link: { + type: DataType.STRING(255), + allowNull: true, + }, + icon: { + type: DataType.STRING(50), + allowNull: true, + }, + severity: { + type: DataType.ENUM("default", "info", "success", "danger"), + allowNull: true, + }, + read: { + type: DataType.BOOLEAN, + allowNull: false, + default: true, + }, + createdAt: DataType.DATE, + updatedAt: DataType.DATE, +}; + +module.exports = { + async up(queryInterface) { + await queryInterface.createTable("notifications", CourseInformationModelAttributes); + }, + + async down(queryInterface) { + await queryInterface.dropTable("notifications"); + }, +}; diff --git a/src/Router.ts b/src/Router.ts index e4c804a..5c41cb9 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -24,6 +24,7 @@ import MentorGroupAdministrationController from "./controllers/mentor-group/Ment import SyslogAdminController from "./controllers/syslog/SyslogAdminController"; import PermissionAdministrationController from "./controllers/permission/PermissionAdminController"; import RoleAdministrationController from "./controllers/permission/RoleAdminController"; +import UserNotificationController from "./controllers/user/UserNotificationController"; const routerGroup = (callback: (router: Router) => void) => { const router = Router(); @@ -51,6 +52,7 @@ router.use( r.use(authMiddleware); r.get("/gdpr", GDPRController.getData); + r.get("/notifications", UserNotificationController.getUnreadNotifications); r.use( "/course", @@ -120,6 +122,7 @@ router.use( routerGroup((r: Router) => { r.get("/", TrainingRequestAdminController.getOpen); r.get("/:uuid", TrainingRequestAdminController.getByUUID); + r.delete("/:uuid", TrainingRequestAdminController.destroyByUUID); }) ); diff --git a/src/controllers/training-request/TrainingRequestAdminController.ts b/src/controllers/training-request/TrainingRequestAdminController.ts index b1a3a48..9023625 100644 --- a/src/controllers/training-request/TrainingRequestAdminController.ts +++ b/src/controllers/training-request/TrainingRequestAdminController.ts @@ -3,6 +3,7 @@ import { User } from "../../models/User"; import { MentorGroup } from "../../models/MentorGroup"; import { TrainingRequest } from "../../models/TrainingRequest"; import { Op } from "sequelize"; +import NotificationLibrary from "../../libraries/notification/NotificationLibrary"; /** * Returns all training requests that the current user is able to mentor based on his mentor groups @@ -51,17 +52,60 @@ async function getOpen(request: Request, response: Response) { async function getByUUID(request: Request, response: Response) { const trainingRequestUUID = request.params.uuid; - const trainingRequest = await TrainingRequest.findOne({ + const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ where: { uuid: trainingRequestUUID, }, - include: [TrainingRequest.associations.user, TrainingRequest.associations.training_type, TrainingRequest.associations.training_station], + include: [ + TrainingRequest.associations.user, + TrainingRequest.associations.training_type, + TrainingRequest.associations.training_station, + TrainingRequest.associations.course, + ], }); + if (trainingRequest == null) { + response.status(404).send({ message: "Training request with this UUID not found" }); + return; + } + response.send(trainingRequest); } +/** + * Allows a mentor (or above) to delete the training request of a user based on its UUID. + * @param request + * @param response + */ +async function destroyByUUID(request: Request, response: Response) { + const trainingRequestUUID: string = request.params.uuid; + + const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ + where: { + uuid: trainingRequestUUID, + }, + include: [TrainingRequest.associations.training_type], + }); + + if (trainingRequest == null) { + response.status(404).send({ message: "Training request with this UUID not found" }); + return; + } + + await trainingRequest.destroy(); + + await NotificationLibrary.sendUserNotification( + trainingRequest.user_id, + `Deine Trainingsanfrage für "${trainingRequest.training_type?.name}" wurde von $author gelöscht`, + `$author has deleted your training request for "${trainingRequest.training_type?.name}"`, + request.body.user.id + ); + + response.send({ message: "OK" }); +} + export default { getOpen, getByUUID, + destroyByUUID, }; diff --git a/src/controllers/user/UserNotificationController.ts b/src/controllers/user/UserNotificationController.ts new file mode 100644 index 0000000..6c27c83 --- /dev/null +++ b/src/controllers/user/UserNotificationController.ts @@ -0,0 +1,26 @@ +import { Request, Response } from "express"; +import { User } from "../../models/User"; +import { Notification } from "../../models/Notification"; + +/** + * Returns all unread notifications for the requesting user + * @param request + * @param response + */ +async function getUnreadNotifications(request: Request, response: Response) { + const user: User = request.body.user; + + const notifications: Notification[] = await Notification.findAll({ + where: { + user_id: user.id, + read: false, + }, + include: [Notification.associations.author], + }); + + response.send(notifications); +} + +export default { + getUnreadNotifications, +}; diff --git a/src/libraries/notification/NotificationLibrary.ts b/src/libraries/notification/NotificationLibrary.ts new file mode 100644 index 0000000..6fc814f --- /dev/null +++ b/src/libraries/notification/NotificationLibrary.ts @@ -0,0 +1,18 @@ +import { Notification } from "../../models/Notification"; +import { generateUUID } from "../../utility/UUID"; + +async function sendUserNotification(user_id: number, message_de: string, message_en: string, author_id?: number, link?: string) { + await Notification.create({ + uuid: generateUUID(), + user_id: user_id, + content_de: message_de, + content_en: message_en, + link: link ?? null, + author_id: author_id ?? null, + read: false, + }); +} + +export default { + sendUserNotification, +}; diff --git a/src/models/Notification.ts b/src/models/Notification.ts new file mode 100644 index 0000000..f5501aa --- /dev/null +++ b/src/models/Notification.ts @@ -0,0 +1,102 @@ +import { Model, InferAttributes, CreationOptional, InferCreationAttributes, NonAttribute, Association, ForeignKey } from "sequelize"; +import { DataType } from "sequelize-typescript"; +import { sequelize } from "../core/Sequelize"; +import { ActionRequirement } from "./ActionRequirement"; +import { TrainingStation } from "./TrainingStation"; +import { Course } from "./Course"; +import { TrainingLogTemplate } from "./TrainingLogTemplate"; +import { User } from "./User"; + +export class Notification extends Model, InferCreationAttributes> { + // + // Attributes + // + declare uuid: string; + declare user_id: ForeignKey; + declare content_de: string; + declare content_en: string; + declare read: boolean; + + // + // Optional Attributes + // + declare id: CreationOptional; + declare author_id: CreationOptional> | null; + declare link: CreationOptional | null; + declare icon: CreationOptional | null; + declare severity: CreationOptional<"default" | "info" | "success" | "danger"> | null; + declare createdAt: CreationOptional | null; + declare updatedAt: CreationOptional | null; + + declare user?: NonAttribute; + declare author?: NonAttribute; + + declare static associations: { + user: Association; + author: Association; + }; +} + +Notification.init( + { + id: { + type: DataType.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + uuid: { + type: DataType.UUID, + allowNull: false, + }, + user_id: { + type: DataType.INTEGER, + allowNull: false, + references: { + model: "user", + key: "id", + }, + onUpdate: "cascade", + onDelete: "cascade", + }, + author_id: { + type: DataType.INTEGER, + allowNull: true, + references: { + model: "user", + key: "id", + }, + onUpdate: "cascade", + onDelete: "setNull", + }, + content_de: { + type: DataType.TEXT("medium"), + allowNull: false, + }, + content_en: { + type: DataType.TEXT("medium"), + allowNull: false, + }, + link: { + type: DataType.STRING(255), + allowNull: true, + }, + icon: { + type: DataType.STRING(50), + allowNull: true, + }, + severity: { + type: DataType.ENUM("default", "info", "success", "danger"), + allowNull: true, + }, + read: { + type: DataType.BOOLEAN, + allowNull: false, + }, + createdAt: DataType.DATE, + updatedAt: DataType.DATE, + }, + { + tableName: "notifications", + sequelize: sequelize, + } +); diff --git a/src/models/associations/NotificationAssociations.ts b/src/models/associations/NotificationAssociations.ts new file mode 100644 index 0000000..8a31875 --- /dev/null +++ b/src/models/associations/NotificationAssociations.ts @@ -0,0 +1,43 @@ +import { Notification } from "../Notification"; +import { User } from "../User"; +import Logger, { LogLevels } from "../../utility/Logger"; + +export function registerNotificationAssociations() { + // + // Notification -> User + // + Notification.belongsTo(User, { + as: "user", + foreignKey: "user_id", + targetKey: "id", + }); + + // + // User -> Notification + // + User.hasMany(Notification, { + as: "notifications", + foreignKey: "user_id", + sourceKey: "id", + }); + + // + // Notification -> Author + // + Notification.belongsTo(User, { + as: "author", + foreignKey: "author_id", + targetKey: "id", + }); + + // + // Author -> Notification + // + User.hasMany(Notification, { + as: "author", + foreignKey: "author_id", + sourceKey: "id", + }); + + Logger.log(LogLevels.LOG_INFO, "[NotificationAssociations]"); +} diff --git a/src/models/associations/_RegisterAssociations.ts b/src/models/associations/_RegisterAssociations.ts index f1678f0..709b455 100644 --- a/src/models/associations/_RegisterAssociations.ts +++ b/src/models/associations/_RegisterAssociations.ts @@ -10,6 +10,7 @@ import { registerTrainingRequestAssociations } from "./TrainingRequestAssociatio import { registerFastTrackRequestAssociations } from "./FastTrackRequestAssociations"; import { registerRoleAssociations } from "./RoleAssociations"; import { registerTrainingStationAssociations } from "./TrainingStationAssociations"; +import { registerNotificationAssociations } from "./NotificationAssociations"; export function registerAssociations() { registerUserAssociations(); @@ -24,4 +25,5 @@ export function registerAssociations() { registerFastTrackRequestAssociations(); registerRoleAssociations(); registerTrainingStationAssociations(); + registerNotificationAssociations(); } From 7b6c2885c0724a24bfbb13aca7ba85af661e293f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Sun, 11 Jun 2023 00:50:57 +0200 Subject: [PATCH 03/16] Create session (trainings) --- src/Router.ts | 11 +++ .../TrainingRequestAdminController.ts | 96 ++++++++++++++++--- .../TrainingRequestController.ts | 1 + .../TrainingSessionAdminController.ts | 56 +++++++++++ .../user/UserNotificationController.ts | 7 +- .../notification/NotificationLibrary.ts | 26 +++-- src/models/TrainingSession.ts | 2 +- .../through/TrainingSessionBelongsToUsers.ts | 2 +- 8 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 src/controllers/training-session/TrainingSessionAdminController.ts diff --git a/src/Router.ts b/src/Router.ts index 5c41cb9..50d8968 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -25,6 +25,7 @@ import SyslogAdminController from "./controllers/syslog/SyslogAdminController"; import PermissionAdministrationController from "./controllers/permission/PermissionAdminController"; import RoleAdministrationController from "./controllers/permission/RoleAdminController"; import UserNotificationController from "./controllers/user/UserNotificationController"; +import TrainingSessionAdminController from "./controllers/training-session/TrainingSessionAdminController"; const routerGroup = (callback: (router: Router) => void) => { const router = Router(); @@ -121,11 +122,21 @@ router.use( "/training-request", routerGroup((r: Router) => { r.get("/", TrainingRequestAdminController.getOpen); + r.get("/training", TrainingRequestAdminController.getOpenTrainingRequests); + r.get("/lesson", TrainingRequestAdminController.getOpenLessonRequests); r.get("/:uuid", TrainingRequestAdminController.getByUUID); r.delete("/:uuid", TrainingRequestAdminController.destroyByUUID); }) ); + r.use( + "/training-session", + routerGroup((r: Router) => { + r.put("/training", TrainingSessionAdminController.createTrainingSession); + // TODO r.put("/lesson"); + }) + ); + r.use( "/course", routerGroup((r: Router) => { diff --git a/src/controllers/training-request/TrainingRequestAdminController.ts b/src/controllers/training-request/TrainingRequestAdminController.ts index 9023625..0ae2b6d 100644 --- a/src/controllers/training-request/TrainingRequestAdminController.ts +++ b/src/controllers/training-request/TrainingRequestAdminController.ts @@ -6,14 +6,11 @@ import { Op } from "sequelize"; import NotificationLibrary from "../../libraries/notification/NotificationLibrary"; /** - * Returns all training requests that the current user is able to mentor based on his mentor groups - * @param request - * @param response + * Returns all currently open training requests + * Method should not be called from router */ -async function getOpen(request: Request, response: Response) { - const reqUser: User = request.body.user; - const reqUserMentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); - let trainingRequests: TrainingRequest[] = await TrainingRequest.findAll({ +async function _getOpenTrainingRequests(): Promise { + return await TrainingRequest.findAll({ where: { [Op.and]: { expires: { @@ -27,6 +24,75 @@ async function getOpen(request: Request, response: Response) { }, include: [TrainingRequest.associations.training_station, TrainingRequest.associations.training_type, TrainingRequest.associations.user], }); +} + +/** + * Returns all training requests that the current user is able to mentor based on his mentor groups + * @param request + * @param response + */ +async function getOpen(request: Request, response: Response) { + const reqUser: User = request.body.user; + const reqUserMentorGroups: MentorGroup[] = await reqUser.getMentorGroupsAndCourses(); + let trainingRequests: TrainingRequest[] = await _getOpenTrainingRequests(); + + // 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); + } + } + + trainingRequests = trainingRequests.filter((req: TrainingRequest) => { + return courseIDs.includes(req.course_id); + }); + + response.send(trainingRequests); +} + +/** + * 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 response + */ +async function getOpenTrainingRequests(request: Request, response: Response) { + const reqUser: User = request.body.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[] = []; + + 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); + }); + + response.send(trainingRequests); +} + +/** + * 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 response + */ +async function getOpenLessonRequests(request: Request, response: Response) { + const reqUser: User = request.body.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[] = []; @@ -94,18 +160,22 @@ async function destroyByUUID(request: Request, response: Response) { await trainingRequest.destroy(); - await NotificationLibrary.sendUserNotification( - trainingRequest.user_id, - `Deine Trainingsanfrage für "${trainingRequest.training_type?.name}" wurde von $author gelöscht`, - `$author has deleted your training request for "${trainingRequest.training_type?.name}"`, - request.body.user.id - ); + await NotificationLibrary.sendUserNotification({ + user_id: trainingRequest.user_id, + message_de: `Deine Trainingsanfrage für "${trainingRequest.training_type?.name}" wurde von $author gelöscht`, + message_en: `$author has deleted your training request for "${trainingRequest.training_type?.name}"`, + author_id: request.body.user.id, + severity: "default", + icon: "trash", + }); response.send({ message: "OK" }); } export default { getOpen, + getOpenTrainingRequests, + getOpenLessonRequests, getByUUID, destroyByUUID, }; diff --git a/src/controllers/training-request/TrainingRequestController.ts b/src/controllers/training-request/TrainingRequestController.ts index 35d8dbe..a73c801 100644 --- a/src/controllers/training-request/TrainingRequestController.ts +++ b/src/controllers/training-request/TrainingRequestController.ts @@ -105,6 +105,7 @@ async function getOpen(request: Request, response: Response) { const trainingRequests = await TrainingRequest.findAll({ where: { user_id: reqUser.id, + status: "requested", }, include: [TrainingRequest.associations.training_type, TrainingRequest.associations.course], }); diff --git a/src/controllers/training-session/TrainingSessionAdminController.ts b/src/controllers/training-session/TrainingSessionAdminController.ts new file mode 100644 index 0000000..fb392da --- /dev/null +++ b/src/controllers/training-session/TrainingSessionAdminController.ts @@ -0,0 +1,56 @@ +import { Request, Response } from "express"; +import { User } from "../../models/User"; +import { TrainingRequest } from "../../models/TrainingRequest"; +import { TrainingSession } from "../../models/TrainingSession"; +import { generateUUID } from "../../utility/UUID"; +import { TrainingSessionBelongsToUsers } from "../../models/through/TrainingSessionBelongsToUsers"; +import dayjs from "dayjs"; + +/** + * Creates a new training session with one user and one mentor + */ +async function createTrainingSession(request: Request, response: Response) { + const mentor: User = request.body.user as User; + const data = request.body.data as { user_id: number; uuid: string; date: string }; + + const trainingRequest: TrainingRequest | null = await TrainingRequest.findOne({ + where: { + uuid: data.uuid, + }, + }); + + if (trainingRequest == null) { + response.status(404).send({ message: "TrainingRequest with this UUID not found." }); + return; + } + + const session: TrainingSession = await TrainingSession.create({ + uuid: generateUUID(), + mentor_id: mentor.id, + date: dayjs(data.date).toDate(), + training_type_id: trainingRequest.training_type_id, + training_station_id: trainingRequest.training_station_id ?? null, + course_id: trainingRequest.course_id, + }); + + await TrainingSessionBelongsToUsers.create({ + training_session_id: session.id, + user_id: data.user_id, + }); + + await trainingRequest.update({ + status: "planned", + training_session_id: session.id, + }); + + response.send(session); +} + +/** + * TODO + */ +async function createLessonSession() {} + +export default { + createTrainingSession, +}; diff --git a/src/controllers/user/UserNotificationController.ts b/src/controllers/user/UserNotificationController.ts index 6c27c83..5717f2e 100644 --- a/src/controllers/user/UserNotificationController.ts +++ b/src/controllers/user/UserNotificationController.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { User } from "../../models/User"; import { Notification } from "../../models/Notification"; +import { Op } from "sequelize"; /** * Returns all unread notifications for the requesting user @@ -12,8 +13,10 @@ async function getUnreadNotifications(request: Request, response: Response) { const notifications: Notification[] = await Notification.findAll({ where: { - user_id: user.id, - read: false, + [Op.and]: { + user_id: user.id, + read: false, + }, }, include: [Notification.associations.author], }); diff --git a/src/libraries/notification/NotificationLibrary.ts b/src/libraries/notification/NotificationLibrary.ts index 6fc814f..be84159 100644 --- a/src/libraries/notification/NotificationLibrary.ts +++ b/src/libraries/notification/NotificationLibrary.ts @@ -1,14 +1,28 @@ import { Notification } from "../../models/Notification"; import { generateUUID } from "../../utility/UUID"; -async function sendUserNotification(user_id: number, message_de: string, message_en: string, author_id?: number, link?: string) { +type Severity = "default" | "info" | "success" | "danger"; + +type UserNotificationType = { + user_id: number; + message_de: string; + message_en: string; + severity?: Severity; + icon?: string; + author_id?: number; + link?: string; +}; + +async function sendUserNotification(notificationType: UserNotificationType) { await Notification.create({ uuid: generateUUID(), - user_id: user_id, - content_de: message_de, - content_en: message_en, - link: link ?? null, - author_id: author_id ?? null, + user_id: notificationType.user_id, + content_de: notificationType.message_de, + content_en: notificationType.message_en, + link: notificationType.link ?? null, + icon: notificationType.icon ?? null, + severity: notificationType.severity ?? "default", + author_id: notificationType.author_id ?? null, read: false, }); } diff --git a/src/models/TrainingSession.ts b/src/models/TrainingSession.ts index 5ee5eb5..99f252e 100644 --- a/src/models/TrainingSession.ts +++ b/src/models/TrainingSession.ts @@ -13,7 +13,6 @@ export class TrainingSession extends Model, Inf // declare uuid: string; declare mentor_id: number; - declare cpt_examiner_id: number; declare training_type_id: number; declare course_id: number; @@ -22,6 +21,7 @@ export class TrainingSession extends Model, Inf // declare id: CreationOptional; declare date: CreationOptional | null; + declare cpt_examiner_id: CreationOptional | null; declare cpt_atsim_passed: CreationOptional | null; declare training_station_id: CreationOptional> | null; declare createdAt: CreationOptional | null; diff --git a/src/models/through/TrainingSessionBelongsToUsers.ts b/src/models/through/TrainingSessionBelongsToUsers.ts index 7dd80af..347da1f 100644 --- a/src/models/through/TrainingSessionBelongsToUsers.ts +++ b/src/models/through/TrainingSessionBelongsToUsers.ts @@ -12,13 +12,13 @@ export class TrainingSessionBelongsToUsers extends Model< // // Attributes // - declare id: number; declare user_id: ForeignKey; declare training_session_id: ForeignKey; // // Optional Attributes // + declare id: CreationOptional; declare log_id: CreationOptional> | null; declare passed: CreationOptional | null; declare createdAt: CreationOptional | null; From 87b8f6e34169ebbf57f0f368b3f46ea1b0df65dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Mon, 12 Jun 2023 16:34:03 +0200 Subject: [PATCH 04/16] Add view planned training --- src/Router.ts | 1 + .../course/CourseInformationController.ts | 9 ++---- .../TrainingRequestController.ts | 32 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Router.ts b/src/Router.ts index 50d8968..41780bc 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -88,6 +88,7 @@ router.use( r.delete("/", TrainingRequestController.destroy); r.get("/open", TrainingRequestController.getOpen); + r.get("/planned", TrainingRequestController.getPlanned); r.get("/:request_uuid", TrainingRequestController.getByUUID); }) ); diff --git a/src/controllers/course/CourseInformationController.ts b/src/controllers/course/CourseInformationController.ts index 029ce08..724d35e 100644 --- a/src/controllers/course/CourseInformationController.ts +++ b/src/controllers/course/CourseInformationController.ts @@ -62,7 +62,7 @@ async function getUserCourseInformationByUUID(request: Request, response: Respon return; } - const user = await User.findOne({ + const user: User | null = await User.findOne({ where: { id: reqUser.id, }, @@ -102,8 +102,7 @@ async function getCourseTrainingInformationByUUID(request: Request, response: Re association: User.associations.training_sessions, as: "training_sessions", through: { - as: "through", - attributes: ["passed"], + attributes: ["passed", "log_id"], where: { user_id: user.id, }, @@ -111,14 +110,12 @@ async function getCourseTrainingInformationByUUID(request: Request, response: Re include: [ { association: TrainingSession.associations.training_logs, - attributes: ["uuid", "log_public"], - as: "training_logs", + attributes: ["uuid", "log_public", "id"], through: { attributes: [] }, }, { association: TrainingSession.associations.training_type, attributes: ["id", "name", "type"], - as: "training_type", }, { association: TrainingSession.associations.course, diff --git a/src/controllers/training-request/TrainingRequestController.ts b/src/controllers/training-request/TrainingRequestController.ts index a73c801..7fd36f1 100644 --- a/src/controllers/training-request/TrainingRequestController.ts +++ b/src/controllers/training-request/TrainingRequestController.ts @@ -5,6 +5,8 @@ import { TrainingRequest } from "../../models/TrainingRequest"; import { generateUUID } from "../../utility/UUID"; import { TrainingSession } from "../../models/TrainingSession"; import dayjs from "dayjs"; +import { TrainingSessionBelongsToUsers } from "../../models/through/TrainingSessionBelongsToUsers"; +import { Op } from "sequelize"; /** * Creates a new training request @@ -113,6 +115,35 @@ async function getOpen(request: Request, response: Response) { response.send(trainingRequests); } +/** + * Gets all planned training sessions for the requesting user + * @param request + * @param response + */ +async function getPlanned(request: Request, response: Response) { + const user: User = request.body.user; + + const sessions: TrainingSessionBelongsToUsers[] = await TrainingSessionBelongsToUsers.findAll({ + where: { + user_id: user.id, + passed: null, + }, + include: [ + { + association: TrainingSessionBelongsToUsers.associations.training_session, + include: [TrainingSession.associations.mentor, TrainingSession.associations.training_station], + where: { + date: { + [Op.gte]: new Date(), + }, + }, + }, + ], + }); + + response.send(sessions); +} + async function getByUUID(request: Request, response: Response) { const reqData = request.params; @@ -151,5 +182,6 @@ export default { create, destroy, getOpen, + getPlanned, getByUUID, }; From ed3d0ff324584a802575ad7797ff24d9592cef95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Tue, 13 Jun 2023 11:25:43 +0200 Subject: [PATCH 05/16] Add view planned session for users --- src/Router.ts | 8 +++ .../TrainingSessionController.ts | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/controllers/training-session/TrainingSessionController.ts diff --git a/src/Router.ts b/src/Router.ts index 41780bc..a89cf20 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -26,6 +26,7 @@ import PermissionAdministrationController from "./controllers/permission/Permiss import RoleAdministrationController from "./controllers/permission/RoleAdminController"; import UserNotificationController from "./controllers/user/UserNotificationController"; import TrainingSessionAdminController from "./controllers/training-session/TrainingSessionAdminController"; +import TrainingSessionController from "./controllers/training-session/TrainingSessionController"; const routerGroup = (callback: (router: Router) => void) => { const router = Router(); @@ -93,6 +94,13 @@ router.use( }) ); + r.use( + "/training-session", + routerGroup((r: Router) => { + r.get("/:uuid", TrainingSessionController.getByUUID) + }) + ) + r.use( "/training-type", routerGroup((r: Router) => { diff --git a/src/controllers/training-session/TrainingSessionController.ts b/src/controllers/training-session/TrainingSessionController.ts new file mode 100644 index 0000000..49862d7 --- /dev/null +++ b/src/controllers/training-session/TrainingSessionController.ts @@ -0,0 +1,49 @@ +import { Request, Response } from "express"; +import { User } from "../../models/User"; +import { TrainingSession } from "../../models/TrainingSession"; + +/** + * [User] + * Gets all the associated data of a training session + * @param request + * @param response + */ +async function getByUUID(request: Request, response: Response) { + const user: User = request.body.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: []} + }, + TrainingSession.associations.mentor, + TrainingSession.associations.cpt_examiner, + TrainingSession.associations.training_type, + TrainingSession.associations.training_station, + TrainingSession.associations.course + ] + }); + + // Check if the user even exists in this session, else deny the request + if (session?.users?.find((u: User) => u.id == user.id) == null) { + response.status(403).send(); + return; + } + + if (session == null) { + response.status(404).send(); + return; + } + + response.send(session); +} + +export default { + getByUUID +} \ No newline at end of file From 9c335a07853da8cf930516c437c17b7cda1c2cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 11:59:19 +0200 Subject: [PATCH 06/16] add withdraw from session - TODO: Create a notification for the user, when his/her session is deleted / a user leaves --- src/Router.ts | 3 +- .../TrainingSessionController.ts | 54 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Router.ts b/src/Router.ts index a89cf20..a07a83a 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -97,7 +97,8 @@ router.use( r.use( "/training-session", routerGroup((r: Router) => { - r.get("/:uuid", TrainingSessionController.getByUUID) + r.get("/:uuid", TrainingSessionController.getByUUID); + r.delete("/withdraw/:uuid", TrainingSessionController.withdrawFromSessionByUUID); }) ) diff --git a/src/controllers/training-session/TrainingSessionController.ts b/src/controllers/training-session/TrainingSessionController.ts index 49862d7..f0ee95d 100644 --- a/src/controllers/training-session/TrainingSessionController.ts +++ b/src/controllers/training-session/TrainingSessionController.ts @@ -1,6 +1,9 @@ import { Request, Response } from "express"; import { User } from "../../models/User"; import { TrainingSession } from "../../models/TrainingSession"; +import { TrainingSessionBelongsToUsers } from "../../models/through/TrainingSessionBelongsToUsers"; +import { TrainingRequest } from "../../models/TrainingRequest"; +import dayjs from "dayjs"; /** * [User] @@ -44,6 +47,55 @@ async function getByUUID(request: Request, response: Response) { response.send(session); } +async function withdrawFromSessionByUUID(request: Request, response: Response) { + const user: User = request.body.user; + const sessionUUID: string = request.params.uuid; + + const session: TrainingSession | null = await TrainingSession.findOne({ + where: { + uuid: sessionUUID + }, + include: [TrainingSession.associations.users] + }); + + if (session == null) { + response.status(404).send({message: "Session with this UUID not found"}); + return; + } + + // 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(); + } + + // Update the request to reflect this change + await TrainingRequest.update({ + status: "requested", + training_session_id: null, + expires: dayjs().add(1, 'month').toDate() + }, { + where: { + user_id: user.id, + training_session_id: session.id, + } + }); + + response.send({message: "OK"}); +} + + export default { - getByUUID + getByUUID, + withdrawFromSessionByUUID } \ No newline at end of file From 0838e560b116c3152131bd2585b855e4fc44c345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 15:21:17 +0200 Subject: [PATCH 07/16] add docker support --- .dockerignore | 5 +++- .github/workflows/dev.yml | 32 ++++++++++++++++++++++++ docker-compose.yml | 51 --------------------------------------- 3 files changed, 36 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/dev.yml delete mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore index 76add87..77e94df 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,5 @@ node_modules -dist \ No newline at end of file +dist + +README.md +CONTRIBUTING.md \ No newline at end of file diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..7c75623 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,32 @@ +name: Docker CI/CD + +on: + push: + branches: [ "dev" ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + run: | + docker build --pull -t ${{ env.REGISTRY }}/${{ github.repository }}:latest . + docker push ${{ env.REGISTRY }}/${{ github.repository }}:latest \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index de1cb6a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: "3.9" -services: - backend: - build: . - depends_on: - mysql: - condition: service_started - env_file: - - .env - environment: - - APP_DEBUG - - APP_LOG_SQL - - APP_PORT=8001 - - APP_HOST - - APP_KEY - - APP_VERSION - - SESSION_COOKIE_NAME - - FILE_STORAGE_LOCATION - - FILE_TMP_LOCATION - - VATSIM_API_BASE - - VATGER_API_BASE - - MOODLE_API_BASE - - CONNECT_BASE - - CONNECT_REDIRECT_URI - - CONNECT_SCOPE - - CONNECT_CLIENT_ID - - CONNECT_SECRET - - DB_DIALECT - - DB_HOST - - DB_PORT - - DB_DATABASE_NAME - - DB_USERNAME - - DB_PASSWORD - ports: - - "8001:8001" - - mysql: - image: mysql - restart: always - ports: - - "3306:3306" - environment: - MYSQL_ROOT_PASSWORD: example - MYSQL_USER: trainingcenter - MYSQL_PASSWORD: example - MYSQL_DATABASE: trainingcenter - volumes: - - trainingcenter-sql:/var/lib/mysql - -volumes: - trainingcenter-sql: \ No newline at end of file From 589355170ce1684ca7f94312fd6a09b1ceb4ca38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 15:43:49 +0200 Subject: [PATCH 08/16] update docker --- Dockerfile | 4 +++- docker-compose.yml | 53 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ src/Application.ts | 2 +- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 1849373..f7b7e70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,6 @@ RUN npm install --quiet --unsafe-perm --no-progress --no-audit --include=dev COPY . . -CMD npm run dev \ No newline at end of file +RUN npm run build + +CMD npm run run \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9697fa6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: "3.9" +services: + backend: + image: ghcr.io/vatger/trainingcenter-backend:latest + depends_on: + mysql: + condition: service_started + environment: + - APP_DEBUG + - APP_LOG_SQL + - APP_PORT=80 + - APP_HOST + - APP_KEY + - APP_VERSION + - SESSION_COOKIE_NAME + - FILE_STORAGE_LOCATION + - FILE_TMP_LOCATION + - VATSIM_API_BASE + - VATGER_API_BASE + - MOODLE_API_BASE + - CONNECT_BASE + - CONNECT_REDIRECT_URI + - CONNECT_SCOPE + - CONNECT_CLIENT_ID + - CONNECT_SECRET + - DB_DIALECT + - DB_HOST + - DB_PORT + - DB_DATABASE_NAME + - DB_USERNAME + - DB_PASSWORD + ports: + - "5001:80" + + frontend: + image: ghcr.io/vatger/trainingcenter-frontend:latest + ports: + - "5002:80" + mysql: + image: mysql + restart: always + ports: + - "5000:3306" + environment: + MYSQL_ROOT_PASSWORD: example + MYSQL_USER: trainingcenter + MYSQL_PASSWORD: example + MYSQL_DATABASE: trainingcenter + volumes: + - trainingcenter-sql:/var/lib/mysql + +volumes: + trainingcenter-sql: \ No newline at end of file diff --git a/package.json b/package.json index bee5aea..003d3ce 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ }, "scripts": { "dev": "tsc && node ./dist/Application.js", + "build": "tsc", + "run": "node ./dist/Application.js", "seq": "npx sequelize-cli --options-path=db/config/options.js", "prettier:check": "npx prettier --check **/*.{ts,js}", "prettier:write": "npx prettier --write .", diff --git a/src/Application.ts b/src/Application.ts index aba9bfb..e7f6864 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -20,7 +20,7 @@ initializeApplication() application.use( cors({ credentials: true, - origin: "http://localhost:8000", + origin: "https://tc-dev.vatsim-germany.org", }) ); application.use(cookieParser(Config.APP_KEY)); From f46f2085bccffc683e6c6912e1ef66f66993edb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 22:44:44 +0200 Subject: [PATCH 09/16] add unique browser token for the user session --- .../20221115171242-create-user-table.js | 2 ++ ...0221115171243-create-user-session-table.js | 4 +++ ...15171256-create-training-requests-table.js | 4 +-- src/Application.ts | 2 +- src/controllers/login/LoginController.ts | 4 +-- src/libraries/session/SessionLibrary.ts | 29 ++++++++++++------- src/libraries/vatsim/ConnectLibrary.ts | 10 ++++--- src/models/UserSession.ts | 5 ++++ 8 files changed, 41 insertions(+), 19 deletions(-) diff --git a/db/migrations/20221115171242-create-user-table.js b/db/migrations/20221115171242-create-user-table.js index 1068023..87edfe4 100644 --- a/db/migrations/20221115171242-create-user-table.js +++ b/db/migrations/20221115171242-create-user-table.js @@ -1,5 +1,7 @@ const { DataType } = require("sequelize-typescript"); +//TODO: Check relationships (explicitly on delete and on update). These should not all be cascade! + const DataModelAttributes = { id: { type: DataType.INTEGER, diff --git a/db/migrations/20221115171243-create-user-session-table.js b/db/migrations/20221115171243-create-user-session-table.js index 76975a8..f33f51b 100644 --- a/db/migrations/20221115171243-create-user-session-table.js +++ b/db/migrations/20221115171243-create-user-session-table.js @@ -10,6 +10,10 @@ const DataModelAttributes = { type: DataType.UUID, allowNull: false, }, + browser_uuid: { + type: DataType.UUID, + allowNull: false, + }, user_id: { type: DataType.INTEGER, allowNull: false, diff --git a/db/migrations/20221115171256-create-training-requests-table.js b/db/migrations/20221115171256-create-training-requests-table.js index c614704..0464690 100644 --- a/db/migrations/20221115171256-create-training-requests-table.js +++ b/db/migrations/20221115171256-create-training-requests-table.js @@ -50,7 +50,7 @@ const DataModelAttributes = { key: "id", }, onUpdate: "cascade", - onDelete: "cascade", + onDelete: "set null", }, comment: { type: DataType.TEXT, @@ -73,7 +73,7 @@ const DataModelAttributes = { key: "id", }, onUpdate: "cascade", - onDelete: "cascade", + onDelete: "set null", }, createdAt: DataType.DATE, updatedAt: DataType.DATE, diff --git a/src/Application.ts b/src/Application.ts index e7f6864..aba9bfb 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -20,7 +20,7 @@ initializeApplication() application.use( cors({ credentials: true, - origin: "https://tc-dev.vatsim-germany.org", + origin: "http://localhost:8000", }) ); application.use(cookieParser(Config.APP_KEY)); diff --git a/src/controllers/login/LoginController.ts b/src/controllers/login/LoginController.ts index 761a3f2..1f09c1b 100644 --- a/src/controllers/login/LoginController.ts +++ b/src/controllers/login/LoginController.ts @@ -51,7 +51,7 @@ async function login(request: Request, response: Response) { const vatsimConnectLibrary = new VatsimConnectLibrary(connect_options, remember); try { - await vatsimConnectLibrary.login(response, code); + await vatsimConnectLibrary.login(request, response, code); } catch (e: any) { if (e instanceof VatsimConnectException) { Logger.log(LogLevels.LOG_ERROR, e.message); @@ -72,7 +72,7 @@ async function logout(request: Request, response: Response) { } async function getUserData(request: Request, response: Response) { - if (!(await SessionLibrary.validateSessionToken(request))) { + if (await SessionLibrary.validateSessionToken(request) == null) { response.status(401).send({ message: "Session token invalid" }); return; } diff --git a/src/libraries/session/SessionLibrary.ts b/src/libraries/session/SessionLibrary.ts index e7f9d7f..7758733 100644 --- a/src/libraries/session/SessionLibrary.ts +++ b/src/libraries/session/SessionLibrary.ts @@ -7,15 +7,19 @@ import dayjs from "dayjs"; /** * Creates and stores a new session token in the database + * @param request * @param response * @param user_id * @param remember */ -export async function createSessionToken(response: Response, user_id: number, remember: boolean = false): Promise { - const session_uuid: string = generateUUID(); +export async function createSessionToken(request: Request, response: Response, user_id: number, remember: boolean = false): Promise { + const sessionUUID: string = generateUUID(); + const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; const expiration: Date = remember ? dayjs().add(7, "day").toDate() : dayjs().add(20, "minute").toDate(); const expiration_latest: Date = remember ? dayjs().add(1, "month").toDate() : dayjs().add(1, "hour").toDate(); + if (browserUUID == null) return false; + const cookie_options: CookieOptions = { signed: true, httpOnly: true, @@ -25,14 +29,15 @@ export async function createSessionToken(response: Response, user_id: number, re }; const session: UserSession = await UserSession.create({ - uuid: session_uuid.toString(), + uuid: sessionUUID.toString(), + browser_uuid: browserUUID.toString(), user_id: user_id, expires_at: expiration, expires_latest: expiration_latest, }); if (session != null) { - response.cookie(Config.SESSION_COOKIE_NAME, session_uuid, cookie_options); + response.cookie(Config.SESSION_COOKIE_NAME, sessionUUID, cookie_options); return true; } @@ -47,12 +52,14 @@ export async function createSessionToken(response: Response, user_id: number, re * @param response */ export async function removeSessionToken(request: Request, response: Response) { - const session_token = request.signedCookies[Config.SESSION_COOKIE_NAME]; + const sessionUUID = request.signedCookies[Config.SESSION_COOKIE_NAME]; + const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; - if (session_token != null) { + if (sessionUUID != null && browserUUID != null) { await UserSession.destroy({ where: { - uuid: session_token, + uuid: sessionUUID, + browser_uuid: browserUUID }, }); } @@ -66,16 +73,18 @@ export async function removeSessionToken(request: Request, response: Response) { * @returns true if the current session is valid, false if the current session is invalid (or doesn't exist) */ async function validateSessionToken(request: Request): Promise { - const session_token = request.signedCookies[Config.SESSION_COOKIE_NAME]; + const sessionToken = request.signedCookies[Config.SESSION_COOKIE_NAME]; + const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; const now = dayjs(); // Check if token is present - if (session_token == null || session_token == false) return null; + if (sessionToken == null || sessionToken == false || browserUUID == null) return null; // Get session from Database const session: UserSession | null = await UserSession.findOne({ where: { - uuid: session_token, + uuid: sessionToken, + browser_uuid: browserUUID }, }); diff --git a/src/libraries/vatsim/ConnectLibrary.ts b/src/libraries/vatsim/ConnectLibrary.ts index 0f86e42..cee8e6e 100644 --- a/src/libraries/vatsim/ConnectLibrary.ts +++ b/src/libraries/vatsim/ConnectLibrary.ts @@ -1,5 +1,5 @@ import axios, { Axios, AxiosResponse } from "axios"; -import { Response } from "express"; +import { Request, Response } from "express"; import { VatsimOauthToken, VatsimScopes, VatsimUserData } from "./ConnectTypes"; import { ConnectLibraryErrors, VatsimConnectException } from "../../exceptions/VatsimConnectException"; import { checkIsUserBanned } from "../../utility/helper/MembershipHelper"; @@ -30,6 +30,7 @@ export class VatsimConnectLibrary { private m_userData: VatsimUserData | undefined = undefined; private m_response: Response | undefined = undefined; + private m_request: Request | undefined = undefined; constructor(connectOptions: ConnectOptions, remember: boolean) { this.m_connectOptions = connectOptions; @@ -134,7 +135,7 @@ export class VatsimConnectLibrary { } private async handleSessionChange() { - if (this.m_response == null || this.m_userData?.data.cid == null) throw new VatsimConnectException(); + if (this.m_response == null || this.m_request == null || this.m_userData?.data.cid == null) throw new VatsimConnectException(); // Remove old session await UserSession.destroy({ @@ -144,7 +145,7 @@ export class VatsimConnectLibrary { }); // Create new session - return await createSessionToken(this.m_response, this.m_userData?.data.cid, this.m_remember); + return await createSessionToken(this.m_request, this.m_response, this.m_userData?.data.cid, this.m_remember); } /** @@ -158,10 +159,11 @@ export class VatsimConnectLibrary { * Handle the login flow * @throws VatsimConnectException */ - public async login(response: Response, code: string | undefined) { + public async login(request: Request, response: Response, code: string | undefined) { if (code == null) throw new VatsimConnectException(ConnectLibraryErrors.ERR_NO_CODE); this.m_response = response; + this.m_request = request; await this.queryAccessTokens(code); await this.queryUserData(); diff --git a/src/models/UserSession.ts b/src/models/UserSession.ts index 2a49381..6cbfe91 100644 --- a/src/models/UserSession.ts +++ b/src/models/UserSession.ts @@ -8,6 +8,7 @@ export class UserSession extends Model, InferCreati // Attributes // declare uuid: string; + declare browser_uuid: string; declare user_id: ForeignKey; declare expires_at: Date; declare expires_latest: Date; @@ -37,6 +38,10 @@ UserSession.init( type: DataType.UUID, allowNull: false, }, + browser_uuid: { + type: DataType.UUID, + allowNull: false, + }, user_id: { type: DataType.INTEGER, allowNull: false, From 9807da2c13cd09671bebd873cd478671d1de1934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 22:45:24 +0200 Subject: [PATCH 10/16] add unique browser token for the user session --- src/Application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.ts b/src/Application.ts index aba9bfb..c9cf8fc 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -20,7 +20,7 @@ initializeApplication() application.use( cors({ credentials: true, - origin: "http://localhost:8000", + origin: "https://tc-api-dev.vatsim-germany.org", }) ); application.use(cookieParser(Config.APP_KEY)); From 48780fadf87d4527d1429533b52f5b68d2bd04ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= <48025581+ngoerlitz@users.noreply.github.com> Date: Wed, 14 Jun 2023 22:50:04 +0200 Subject: [PATCH 11/16] Update Application.ts --- src/Application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.ts b/src/Application.ts index c9cf8fc..e7f6864 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -20,7 +20,7 @@ initializeApplication() application.use( cors({ credentials: true, - origin: "https://tc-api-dev.vatsim-germany.org", + origin: "https://tc-dev.vatsim-germany.org", }) ); application.use(cookieParser(Config.APP_KEY)); From 7029acf5e3793dce766b707978fe6f36f773a110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 22:57:12 +0200 Subject: [PATCH 12/16] update ConnectLibrary.ts --- src/libraries/vatsim/ConnectLibrary.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/vatsim/ConnectLibrary.ts b/src/libraries/vatsim/ConnectLibrary.ts index cee8e6e..bbf65b1 100644 --- a/src/libraries/vatsim/ConnectLibrary.ts +++ b/src/libraries/vatsim/ConnectLibrary.ts @@ -135,12 +135,15 @@ export class VatsimConnectLibrary { } private async handleSessionChange() { - if (this.m_response == null || this.m_request == null || this.m_userData?.data.cid == null) throw new VatsimConnectException(); + const browserUUID: string | string[] | undefined = this.m_request?.headers['unique-browser-token']; + + if (this.m_response == null || this.m_request == null || browserUUID == null || this.m_userData?.data.cid == null) throw new VatsimConnectException(); // Remove old session await UserSession.destroy({ where: { - user_id: this.m_userData?.data.cid, + user_id: this.m_userData.data.cid, + browser_uuid: browserUUID }, }); From 58caff676244f196f17b60230c4f00b0c9780b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Wed, 14 Jun 2023 23:04:07 +0200 Subject: [PATCH 13/16] Catch potential axios timeout when polling vatsim --- src/exceptions/VatsimConnectException.ts | 1 + src/libraries/vatsim/ConnectLibrary.ts | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/exceptions/VatsimConnectException.ts b/src/exceptions/VatsimConnectException.ts index 3f4f8e9..e79a8e9 100644 --- a/src/exceptions/VatsimConnectException.ts +++ b/src/exceptions/VatsimConnectException.ts @@ -8,6 +8,7 @@ export enum ConnectLibraryErrors { ERR_AUTH_REVOKED, ERR_INV_CODE, ERR_NO_AUTH_RESPONSE, + ERR_AXIOS_TIMEOUT } export class VatsimConnectException extends Error { diff --git a/src/libraries/vatsim/ConnectLibrary.ts b/src/libraries/vatsim/ConnectLibrary.ts index bbf65b1..6838466 100644 --- a/src/libraries/vatsim/ConnectLibrary.ts +++ b/src/libraries/vatsim/ConnectLibrary.ts @@ -40,7 +40,7 @@ export class VatsimConnectLibrary { this.m_axiosInstance = axios.create({ baseURL: this.m_connectOptions.base_uri, - timeout: 2000, + timeout: 5000, headers: { "Accept-Encoding": "gzip,deflate,compress" }, }); } @@ -87,14 +87,20 @@ export class VatsimConnectLibrary { private async queryUserData() { if (this.m_accessToken == null) return null; - const user_response = await this.m_axiosInstance.get("/api/user", { - headers: { - Authorization: `Bearer ${this.m_accessToken}`, - Accept: "application/json", - }, - }); + let user_response: AxiosResponse | undefined = undefined; + + try { + user_response = await this.m_axiosInstance.get("/api/user", { + headers: { + Authorization: `Bearer ${this.m_accessToken}`, + Accept: "application/json", + }, + }); + } catch (e) { + throw new VatsimConnectException(ConnectLibraryErrors.ERR_AXIOS_TIMEOUT); + } - const user_response_data: VatsimUserData | undefined = user_response.data as VatsimUserData; + const user_response_data: VatsimUserData | undefined = user_response?.data as VatsimUserData; if (user_response_data == null) { throw new VatsimConnectException(); From b151fd0e29b59e6443adef35527cc6b7122a353f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Thu, 15 Jun 2023 10:25:34 +0200 Subject: [PATCH 14/16] Add UA to session --- ...0221115171243-create-user-session-table.js | 4 +++ package-lock.json | 36 ++++++++++++++++--- package.json | 6 ++-- src/libraries/session/SessionLibrary.ts | 4 +++ src/models/UserSession.ts | 5 +++ 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/db/migrations/20221115171243-create-user-session-table.js b/db/migrations/20221115171243-create-user-session-table.js index f33f51b..6969dce 100644 --- a/db/migrations/20221115171243-create-user-session-table.js +++ b/db/migrations/20221115171243-create-user-session-table.js @@ -14,6 +14,10 @@ const DataModelAttributes = { type: DataType.UUID, allowNull: false, }, + client: { + type: DataType.STRING(100), + allowNull: true + }, user_id: { type: DataType.INTEGER, allowNull: false, diff --git a/package-lock.json b/package-lock.json index 7da8b53..3983970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@types/node": "^18.11.9", "@types/node-cron": "^3.0.6", "@types/sequelize": "^4.28.14", + "@types/ua-parser-js": "^0.7.36", "@types/uuid": "^8.3.4", "axios": "^1.1.3", "body-parser": "^1.20.1", @@ -29,14 +30,15 @@ "node-cron": "^3.0.2", "sequelize": "^6.25.5", "sequelize-typescript": "^2.1.5", - "typescript": "^4.8.4", + "ua-parser-js": "^1.0.35", "uuid": "^9.0.0" }, "devDependencies": { "@types/cors": "^2.8.12", "cors": "^2.8.5", "prettier": "^2.7.1", - "sequelize-cli": "^6.5.2" + "sequelize-cli": "^6.5.2", + "typescript": "^4.8.4" } }, "node_modules/@types/bluebird": { @@ -197,6 +199,11 @@ "@types/node": "*" } }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz", + "integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -562,9 +569,9 @@ } }, "node_modules/dottie": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.3.tgz", - "integrity": "sha512-4liA0PuRkZWQFQjwBypdxPfZaRWiv5tkhMXY2hzsa2pNf5s7U3m9cwUchfNKe8wZQxdGPQQzO6Rm2uGe0rvohQ==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, "node_modules/editorconfig": { "version": "0.15.3", @@ -1936,6 +1943,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1944,6 +1952,24 @@ "node": ">=4.2.0" } }, + "node_modules/ua-parser-js": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", + "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", diff --git a/package.json b/package.json index 003d3ce..fcfc324 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/node": "^18.11.9", "@types/node-cron": "^3.0.6", "@types/sequelize": "^4.28.14", + "@types/ua-parser-js": "^0.7.36", "@types/uuid": "^8.3.4", "axios": "^1.1.3", "body-parser": "^1.20.1", @@ -39,13 +40,14 @@ "node-cron": "^3.0.2", "sequelize": "^6.25.5", "sequelize-typescript": "^2.1.5", + "ua-parser-js": "^1.0.35", "uuid": "^9.0.0" }, "devDependencies": { "@types/cors": "^2.8.12", "cors": "^2.8.5", "prettier": "^2.7.1", - "typescript": "^4.8.4", - "sequelize-cli": "^6.5.2" + "sequelize-cli": "^6.5.2", + "typescript": "^4.8.4" } } diff --git a/src/libraries/session/SessionLibrary.ts b/src/libraries/session/SessionLibrary.ts index 7758733..d04d685 100644 --- a/src/libraries/session/SessionLibrary.ts +++ b/src/libraries/session/SessionLibrary.ts @@ -4,6 +4,7 @@ import { generateUUID } from "../../utility/UUID"; import { Config } from "../../core/Config"; import Logger, { LogLevels } from "../../utility/Logger"; import dayjs from "dayjs"; +import UAParser from "ua-parser-js"; /** * Creates and stores a new session token in the database @@ -15,6 +16,8 @@ import dayjs from "dayjs"; export async function createSessionToken(request: Request, response: Response, user_id: number, remember: boolean = false): Promise { const sessionUUID: string = generateUUID(); const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; + const userAgent = UAParser(request.headers["user-agent"]); + const expiration: Date = remember ? dayjs().add(7, "day").toDate() : dayjs().add(20, "minute").toDate(); const expiration_latest: Date = remember ? dayjs().add(1, "month").toDate() : dayjs().add(1, "hour").toDate(); @@ -34,6 +37,7 @@ export async function createSessionToken(request: Request, response: Response, u user_id: user_id, expires_at: expiration, expires_latest: expiration_latest, + client: `${userAgent.os.name} / ${userAgent.browser.name} ${userAgent.browser.version}` }); if (session != null) { diff --git a/src/models/UserSession.ts b/src/models/UserSession.ts index 6cbfe91..87087ab 100644 --- a/src/models/UserSession.ts +++ b/src/models/UserSession.ts @@ -9,6 +9,7 @@ export class UserSession extends Model, InferCreati // declare uuid: string; declare browser_uuid: string; + declare client: string; declare user_id: ForeignKey; declare expires_at: Date; declare expires_latest: Date; @@ -42,6 +43,10 @@ UserSession.init( type: DataType.UUID, allowNull: false, }, + client: { + type: DataType.STRING(100), + allowNull: true + }, user_id: { type: DataType.INTEGER, allowNull: false, From 6bc07d8a1ad50b0d072a8b25b9538cd2d45dfa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Thu, 15 Jun 2023 10:27:43 +0200 Subject: [PATCH 15/16] update package version - update dottie from 2.0.3 -> 2.0.6 (vulnerability) --- package-lock.json | 61 +++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3983970..a02e84e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,9 +97,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", "dependencies": { "@types/ms": "*" } @@ -136,9 +136,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.194", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", - "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==" + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" }, "node_modules/@types/mime": { "version": "1.3.2", @@ -151,9 +151,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "18.16.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.10.tgz", - "integrity": "sha512-sMo3EngB6QkMBlB9rBe1lFdKSLqljyWPPWv6/FzSxh/IDlyVWSzE9RiF4eAuerQHybrWdqBgAGb03PM89qOasA==" + "version": "18.16.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz", + "integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==" }, "node_modules/@types/node-cron": { "version": "3.0.7", @@ -515,9 +515,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" }, "node_modules/debug": { "version": "2.6.9", @@ -561,11 +561,14 @@ } }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.1.4.tgz", + "integrity": "sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/dottie": { @@ -1066,9 +1069,9 @@ } }, "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -1098,14 +1101,14 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" }, "node_modules/js-beautify": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.7.tgz", - "integrity": "sha512-5SOX1KXPFKx+5f6ZrPsIPEY7NwKeQz47n3jm2i+XeHx9MoRsfQenlOP13FQhWvg8JRS0+XLO6XYUQ2GX+q+T9A==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.8.tgz", + "integrity": "sha512-4S7HFeI9YfRvRgKnEweohs0tgJj28InHVIj4Nl8Htf96Y6pHg3+tJrmo4ucAM9f7l4SHbFI3IvFAZ2a1eQPbyg==", "dev": true, "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^0.15.3", - "glob": "^8.0.3", + "glob": "^8.1.0", "nopt": "^6.0.0" }, "bin": { @@ -1114,7 +1117,7 @@ "js-beautify": "js/bin/js-beautify.js" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/jsonfile": { @@ -1623,9 +1626,9 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/sequelize": { - "version": "6.31.1", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.1.tgz", - "integrity": "sha512-cahWtRrYLjqoZP/aurGBoaxn29qQCF4bxkAUPEQ/ozjJjt6mtL4Q113S3N39mQRmX5fgxRbli+bzZARP/N51eg==", + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.32.0.tgz", + "integrity": "sha512-gMd1M6kPANyrCeU/vtgEP5gnse7sVsiKbJyz7p4huuW8zZcRopj47UlglvdrMuIoqksZmsUPfApmMo6ZlJpcvg==", "funding": [ { "type": "opencollective", @@ -1684,9 +1687,9 @@ } }, "node_modules/sequelize-cli": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.0.tgz", - "integrity": "sha512-FwTClhGRvXKanFRHMZbgfXOBV8UC2B3VkE0WOdW1n39/36PF4lWyurF95f246une/V4eaO3a7/Ywvy++3r+Jmg==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.1.tgz", + "integrity": "sha512-C3qRpy1twBsFa855qOQFSYWer8ngiaZP05/OAsT1QCUwtc6UxVNNiQ0CGUt98T9T1gi5D3TGWL6le8HWUKELyw==", "dev": true, "dependencies": { "cli-color": "^2.0.3", From d34d3138f33264533de14e861b01837592dfc5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Thu, 15 Jun 2023 21:00:57 +0200 Subject: [PATCH 16/16] add user notes, more profile, etc. --- .prettierignore | 1 + ...0221115171243-create-user-session-table.js | 2 +- src/Application.ts | 2 +- src/Router.ts | 8 ++- src/controllers/login/LoginController.ts | 2 +- .../TrainingSessionController.ts | 46 +++++++------- .../user/UserCourseAdminController.ts | 54 ++++++++++++++++ src/controllers/user/UserCourseController.ts | 33 ++++++++++ .../user/UserNoteAdminController.ts | 63 ++++++++++++++++++- src/exceptions/VatsimConnectException.ts | 2 +- src/libraries/session/SessionLibrary.ts | 12 ++-- src/libraries/vatsim/ConnectLibrary.ts | 4 +- src/models/UserNote.ts | 11 +++- src/models/UserSession.ts | 2 +- 14 files changed, 203 insertions(+), 39 deletions(-) create mode 100644 src/controllers/user/UserCourseAdminController.ts diff --git a/.prettierignore b/.prettierignore index 3d581d3..a7ad98d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ node_modules dist +.github package.json package-lock.json diff --git a/db/migrations/20221115171243-create-user-session-table.js b/db/migrations/20221115171243-create-user-session-table.js index 6969dce..5a79fb2 100644 --- a/db/migrations/20221115171243-create-user-session-table.js +++ b/db/migrations/20221115171243-create-user-session-table.js @@ -16,7 +16,7 @@ const DataModelAttributes = { }, client: { type: DataType.STRING(100), - allowNull: true + allowNull: true, }, user_id: { type: DataType.INTEGER, diff --git a/src/Application.ts b/src/Application.ts index e7f6864..aba9bfb 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -20,7 +20,7 @@ initializeApplication() application.use( cors({ credentials: true, - origin: "https://tc-dev.vatsim-germany.org", + origin: "http://localhost:8000", }) ); application.use(cookieParser(Config.APP_KEY)); diff --git a/src/Router.ts b/src/Router.ts index a07a83a..c0c43f9 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -27,6 +27,7 @@ import RoleAdministrationController from "./controllers/permission/RoleAdminCont import UserNotificationController from "./controllers/user/UserNotificationController"; import TrainingSessionAdminController from "./controllers/training-session/TrainingSessionAdminController"; import TrainingSessionController from "./controllers/training-session/TrainingSessionController"; +import UserCourseAdminController from "./controllers/user/UserCourseAdminController"; const routerGroup = (callback: (router: Router) => void) => { const router = Router(); @@ -64,6 +65,7 @@ router.use( r.get("/available", UserCourseController.getAvailableCourses); r.get("/active", UserCourseController.getActiveCourses); r.put("/enrol", UserCourseController.enrolInCourse); + r.delete("/withdraw", UserCourseController.withdrawFromCourseByUUID); r.get("/info", CourseInformationController.getInformationByUUID); r.get("/info/my", CourseInformationController.getUserCourseInformationByUUID); @@ -100,7 +102,7 @@ router.use( r.get("/:uuid", TrainingSessionController.getByUUID); r.delete("/withdraw/:uuid", TrainingSessionController.withdrawFromSessionByUUID); }) - ) + ); r.use( "/training-type", @@ -120,11 +122,15 @@ router.use( r.get("/data/", UserInformationAdminController.getUserDataByID); r.get("/data/sensitive", UserInformationAdminController.getSensitiveUserDataByID); + r.put("/note", UserNoteAdminController.createUserNote); r.get("/notes", UserNoteAdminController.getGeneralUserNotes); + r.get("/notes/course", UserNoteAdminController.getNotesByCourseID); r.get("/", UserController.getAll); r.get("/min", UserController.getAllUsersMinimalData); r.get("/sensitive", UserController.getAllSensitive); + + r.get("/course/match", UserCourseAdminController.getUserCourseMatch); }) ); diff --git a/src/controllers/login/LoginController.ts b/src/controllers/login/LoginController.ts index 1f09c1b..c37c738 100644 --- a/src/controllers/login/LoginController.ts +++ b/src/controllers/login/LoginController.ts @@ -72,7 +72,7 @@ async function logout(request: Request, response: Response) { } async function getUserData(request: Request, response: Response) { - if (await SessionLibrary.validateSessionToken(request) == null) { + if ((await SessionLibrary.validateSessionToken(request)) == null) { response.status(401).send({ message: "Session token invalid" }); return; } diff --git a/src/controllers/training-session/TrainingSessionController.ts b/src/controllers/training-session/TrainingSessionController.ts index f0ee95d..c4769cc 100644 --- a/src/controllers/training-session/TrainingSessionController.ts +++ b/src/controllers/training-session/TrainingSessionController.ts @@ -17,20 +17,20 @@ async function getByUUID(request: Request, response: Response) { const session: TrainingSession | null = await TrainingSession.findOne({ where: { - uuid: sessionUUID + uuid: sessionUUID, }, include: [ { association: TrainingSession.associations.users, attributes: ["id"], - through: {attributes: []} + through: { attributes: [] }, }, TrainingSession.associations.mentor, TrainingSession.associations.cpt_examiner, TrainingSession.associations.training_type, TrainingSession.associations.training_station, - TrainingSession.associations.course - ] + TrainingSession.associations.course, + ], }); // Check if the user even exists in this session, else deny the request @@ -53,13 +53,13 @@ async function withdrawFromSessionByUUID(request: Request, response: Response) { const session: TrainingSession | null = await TrainingSession.findOne({ where: { - uuid: sessionUUID + uuid: sessionUUID, }, - include: [TrainingSession.associations.users] + include: [TrainingSession.associations.users], }); if (session == null) { - response.status(404).send({message: "Session with this UUID not found"}); + response.status(404).send({ message: "Session with this UUID not found" }); return; } @@ -70,8 +70,8 @@ async function withdrawFromSessionByUUID(request: Request, response: Response) { user_id: user.id, training_session_id: session.id, passed: null, - log_id: null - } + log_id: null, + }, }); // Check if we can delete the entire session, or only the user @@ -80,22 +80,24 @@ async function withdrawFromSessionByUUID(request: Request, response: Response) { } // Update the request to reflect this change - await TrainingRequest.update({ - status: "requested", - training_session_id: null, - expires: dayjs().add(1, 'month').toDate() - }, { - where: { - user_id: user.id, - training_session_id: session.id, + await TrainingRequest.update( + { + status: "requested", + training_session_id: null, + expires: dayjs().add(1, "month").toDate(), + }, + { + where: { + user_id: user.id, + training_session_id: session.id, + }, } - }); + ); - response.send({message: "OK"}); + response.send({ message: "OK" }); } - export default { getByUUID, - withdrawFromSessionByUUID -} \ No newline at end of file + withdrawFromSessionByUUID, +}; diff --git a/src/controllers/user/UserCourseAdminController.ts b/src/controllers/user/UserCourseAdminController.ts new file mode 100644 index 0000000..85b4f1a --- /dev/null +++ b/src/controllers/user/UserCourseAdminController.ts @@ -0,0 +1,54 @@ +import { Request, Response } from "express"; +import { User } from "../../models/User"; +import { MentorGroup } from "../../models/MentorGroup"; +import { Course } from "../../models/Course"; + +/** + * 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 + */ +async function getUserCourseMatch(request: Request, response: Response) { + const reqUser: User = request.body.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; + } + + const user: User | null = await User.findOne({ + where: { + id: userID.toString(), + }, + include: [User.associations.courses], + }); + + if (user == 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; + } + } + + return false; + }); + + if (courses == null) { + response.status(500).send(); + return; + } + + response.send(courses); +} + +export default { + getUserCourseMatch, +}; diff --git a/src/controllers/user/UserCourseController.ts b/src/controllers/user/UserCourseController.ts index e6f9303..b2d36fd 100644 --- a/src/controllers/user/UserCourseController.ts +++ b/src/controllers/user/UserCourseController.ts @@ -3,6 +3,7 @@ import { Request, Response } from "express"; import { Course } from "../../models/Course"; import { UsersBelongsToCourses } from "../../models/through/UsersBelongsToCourses"; import ValidationHelper, { ValidationOptions } from "../../utility/helper/ValidationHelper"; +import { TrainingRequest } from "../../models/TrainingRequest"; /** * Returns courses that are available to the current user (i.e. not enrolled in course) @@ -120,9 +121,41 @@ async function enrolInCourse(request: Request, response: Response) { response.send(userBelongsToCourses); } +/** + * + * @param request + * @param response + */ +async function withdrawFromCourseByUUID(request: Request, response: Response) { + const user: User = request.body.user; + const courseID = request.body.course_id; + + if (courseID == null) { + response.send(404); + return; + } + + 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" }); +} + export default { getAvailableCourses, getActiveCourses, getMyCourses, enrolInCourse, + withdrawFromCourseByUUID, }; diff --git a/src/controllers/user/UserNoteAdminController.ts b/src/controllers/user/UserNoteAdminController.ts index af3dde6..9cd236f 100644 --- a/src/controllers/user/UserNoteAdminController.ts +++ b/src/controllers/user/UserNoteAdminController.ts @@ -1,6 +1,8 @@ import { Request, Response } from "express"; import ValidationHelper, { ValidationOptions } from "../../utility/helper/ValidationHelper"; import { UserNote } from "../../models/UserNote"; +import { User } from "../../models/User"; +import { generateUUID } from "../../utility/UUID"; /** * Gets the specified user's notes that are not linked to a course, i.e. all those, that all mentors can see @@ -23,13 +25,13 @@ async function getGeneralUserNotes(request: Request, response: Response) { return; } - const notes = await UserNote.findAll({ + const notes: UserNote[] = await UserNote.findAll({ where: { user_id: user_id, course_id: null, }, include: { - association: UserNote.associations.user, + association: UserNote.associations.author, attributes: ["id", "first_name", "last_name"], }, }); @@ -37,6 +39,63 @@ async function getGeneralUserNotes(request: Request, response: Response) { response.send(notes); } +/** + * Gets all the notes of the requested user by the specified course_id + */ +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 createUserNote(request: Request, response: Response) { + const reqUser: User = request.body.user; + + const validation = ValidationHelper.validate([ + { + name: "user_id", + validationObject: request.body.user_id, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + { + name: "content", + validationObject: request.body.content, + toValidate: [{ val: ValidationOptions.NON_NULL }], + }, + ]); + + if (validation.invalid) { + response.status(400).send({ validation: validation.message, validation_failed: validation.invalid }); + return; + } + + const note: UserNote = await UserNote.create({ + uuid: generateUUID(), + user_id: request.body.user_id, + course_id: request.body.course_id == "-1" ? null : request.body.course_id, + content: request.body.content.toString(), + author_id: reqUser.id, + }); + + const noteWithAuthor: UserNote | null = await note.getAuthor(); + + response.send(noteWithAuthor); +} + export default { getGeneralUserNotes, + createUserNote, + getNotesByCourseID, }; diff --git a/src/exceptions/VatsimConnectException.ts b/src/exceptions/VatsimConnectException.ts index e79a8e9..8c4f5fa 100644 --- a/src/exceptions/VatsimConnectException.ts +++ b/src/exceptions/VatsimConnectException.ts @@ -8,7 +8,7 @@ export enum ConnectLibraryErrors { ERR_AUTH_REVOKED, ERR_INV_CODE, ERR_NO_AUTH_RESPONSE, - ERR_AXIOS_TIMEOUT + ERR_AXIOS_TIMEOUT, } export class VatsimConnectException extends Error { diff --git a/src/libraries/session/SessionLibrary.ts b/src/libraries/session/SessionLibrary.ts index d04d685..6efc0f6 100644 --- a/src/libraries/session/SessionLibrary.ts +++ b/src/libraries/session/SessionLibrary.ts @@ -15,7 +15,7 @@ import UAParser from "ua-parser-js"; */ export async function createSessionToken(request: Request, response: Response, user_id: number, remember: boolean = false): Promise { const sessionUUID: string = generateUUID(); - const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; + const browserUUID: string | string[] | undefined = request.headers["unique-browser-token"]; const userAgent = UAParser(request.headers["user-agent"]); const expiration: Date = remember ? dayjs().add(7, "day").toDate() : dayjs().add(20, "minute").toDate(); @@ -37,7 +37,7 @@ export async function createSessionToken(request: Request, response: Response, u user_id: user_id, expires_at: expiration, expires_latest: expiration_latest, - client: `${userAgent.os.name} / ${userAgent.browser.name} ${userAgent.browser.version}` + client: `${userAgent.os.name} / ${userAgent.browser.name} ${userAgent.browser.version}`, }); if (session != null) { @@ -57,13 +57,13 @@ export async function createSessionToken(request: Request, response: Response, u */ export async function removeSessionToken(request: Request, response: Response) { const sessionUUID = request.signedCookies[Config.SESSION_COOKIE_NAME]; - const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; + const browserUUID: string | string[] | undefined = request.headers["unique-browser-token"]; if (sessionUUID != null && browserUUID != null) { await UserSession.destroy({ where: { uuid: sessionUUID, - browser_uuid: browserUUID + browser_uuid: browserUUID, }, }); } @@ -78,7 +78,7 @@ export async function removeSessionToken(request: Request, response: Response) { */ async function validateSessionToken(request: Request): Promise { const sessionToken = request.signedCookies[Config.SESSION_COOKIE_NAME]; - const browserUUID: string | string[] | undefined = request.headers['unique-browser-token']; + const browserUUID: string | string[] | undefined = request.headers["unique-browser-token"]; const now = dayjs(); // Check if token is present @@ -88,7 +88,7 @@ async function validateSessionToken(request: Request): Promise, InferCreationAttr author: Association; course: Association; }; + + async getAuthor(): Promise { + return await UserNote.findOne({ + where: { + uuid: this.uuid, + }, + include: [UserNote.associations.author], + }); + } } UserNote.init( diff --git a/src/models/UserSession.ts b/src/models/UserSession.ts index 87087ab..b58fce6 100644 --- a/src/models/UserSession.ts +++ b/src/models/UserSession.ts @@ -45,7 +45,7 @@ UserSession.init( }, client: { type: DataType.STRING(100), - allowNull: true + allowNull: true, }, user_id: { type: DataType.INTEGER,