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/.prettierignore b/.prettierignore index 9299f61..a7ad98d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ node_modules dist +.github package.json package-lock.json @@ -14,6 +15,7 @@ docker-compose.yml .prettierignore .gitlab-ci.yml +CONTRIBUTING.md README.md .env .env.example 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/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/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..5a79fb2 100644 --- a/db/migrations/20221115171243-create-user-session-table.js +++ b/db/migrations/20221115171243-create-user-session-table.js @@ -10,6 +10,14 @@ const DataModelAttributes = { type: DataType.UUID, allowNull: false, }, + browser_uuid: { + type: DataType.UUID, + allowNull: false, + }, + client: { + type: DataType.STRING(100), + allowNull: true, + }, 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/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/docker-compose.yml b/docker-compose.yml index de1cb6a..9697fa6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,14 @@ version: "3.9" services: backend: - build: . + image: ghcr.io/vatger/trainingcenter-backend:latest depends_on: mysql: condition: service_started - env_file: - - .env environment: - APP_DEBUG - APP_LOG_SQL - - APP_PORT=8001 + - APP_PORT=80 - APP_HOST - APP_KEY - APP_VERSION @@ -32,13 +30,17 @@ services: - DB_USERNAME - DB_PASSWORD ports: - - "8001:8001" + - "5001:80" + frontend: + image: ghcr.io/vatger/trainingcenter-frontend:latest + ports: + - "5002:80" mysql: image: mysql restart: always ports: - - "3306:3306" + - "5000:3306" environment: MYSQL_ROOT_PASSWORD: example MYSQL_USER: trainingcenter diff --git a/package-lock.json b/package-lock.json index 7da8b53..a02e84e 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": { @@ -95,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": "*" } @@ -134,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", @@ -149,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", @@ -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", @@ -508,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", @@ -554,17 +561,20 @@ } }, "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": { - "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", @@ -1059,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" @@ -1091,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": { @@ -1107,7 +1117,7 @@ "js-beautify": "js/bin/js-beautify.js" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/jsonfile": { @@ -1616,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", @@ -1677,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", @@ -1936,6 +1946,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 +1955,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 bee5aea..fcfc324 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 .", @@ -24,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", @@ -37,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/Router.ts b/src/Router.ts index e4c804a..c0c43f9 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -24,6 +24,10 @@ 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"; +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(); @@ -51,6 +55,7 @@ router.use( r.use(authMiddleware); r.get("/gdpr", GDPRController.getData); + r.get("/notifications", UserNotificationController.getUnreadNotifications); r.use( "/course", @@ -60,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); @@ -85,10 +91,19 @@ router.use( r.delete("/", TrainingRequestController.destroy); r.get("/open", TrainingRequestController.getOpen); + r.get("/planned", TrainingRequestController.getPlanned); r.get("/:request_uuid", TrainingRequestController.getByUUID); }) ); + r.use( + "/training-session", + routerGroup((r: Router) => { + r.get("/:uuid", TrainingSessionController.getByUUID); + r.delete("/withdraw/:uuid", TrainingSessionController.withdrawFromSessionByUUID); + }) + ); + r.use( "/training-type", routerGroup((r: Router) => { @@ -107,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); }) ); @@ -119,7 +138,18 @@ 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"); }) ); 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..724d35e 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); } @@ -54,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, }, @@ -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, }, @@ -94,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, }, @@ -103,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/login/LoginController.ts b/src/controllers/login/LoginController.ts index 16dbe68..c37c738 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; } @@ -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-request/TrainingRequestAdminController.ts b/src/controllers/training-request/TrainingRequestAdminController.ts index b1a3a48..0ae2b6d 100644 --- a/src/controllers/training-request/TrainingRequestAdminController.ts +++ b/src/controllers/training-request/TrainingRequestAdminController.ts @@ -3,16 +3,14 @@ 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 - * @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: { @@ -26,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[] = []; @@ -51,17 +118,64 @@ 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({ + 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..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 @@ -105,6 +107,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], }); @@ -112,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; @@ -150,5 +182,6 @@ export default { create, destroy, getOpen, + getPlanned, getByUUID, }; 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/training-session/TrainingSessionController.ts b/src/controllers/training-session/TrainingSessionController.ts new file mode 100644 index 0000000..c4769cc --- /dev/null +++ b/src/controllers/training-session/TrainingSessionController.ts @@ -0,0 +1,103 @@ +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] + * 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); +} + +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, + withdrawFromSessionByUUID, +}; 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/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/controllers/user/UserNotificationController.ts b/src/controllers/user/UserNotificationController.ts new file mode 100644 index 0000000..5717f2e --- /dev/null +++ b/src/controllers/user/UserNotificationController.ts @@ -0,0 +1,29 @@ +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 + * @param request + * @param response + */ +async function getUnreadNotifications(request: Request, response: Response) { + const user: User = request.body.user; + + const notifications: Notification[] = await Notification.findAll({ + where: { + [Op.and]: { + user_id: user.id, + read: false, + }, + }, + include: [Notification.associations.author], + }); + + response.send(notifications); +} + +export default { + getUnreadNotifications, +}; diff --git a/src/exceptions/VatsimConnectException.ts b/src/exceptions/VatsimConnectException.ts index 3f4f8e9..8c4f5fa 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/notification/NotificationLibrary.ts b/src/libraries/notification/NotificationLibrary.ts new file mode 100644 index 0000000..be84159 --- /dev/null +++ b/src/libraries/notification/NotificationLibrary.ts @@ -0,0 +1,32 @@ +import { Notification } from "../../models/Notification"; +import { generateUUID } from "../../utility/UUID"; + +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: 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, + }); +} + +export default { + sendUserNotification, +}; diff --git a/src/libraries/session/SessionLibrary.ts b/src/libraries/session/SessionLibrary.ts index e7f9d7f..6efc0f6 100644 --- a/src/libraries/session/SessionLibrary.ts +++ b/src/libraries/session/SessionLibrary.ts @@ -4,18 +4,25 @@ 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 + * @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 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(); + if (browserUUID == null) return false; + const cookie_options: CookieOptions = { signed: true, httpOnly: true, @@ -25,14 +32,16 @@ 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, + client: `${userAgent.os.name} / ${userAgent.browser.name} ${userAgent.browser.version}`, }); 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 +56,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 +77,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/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..49e112f 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; @@ -39,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" }, }); } @@ -86,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; - const user_response_data: VatsimUserData | undefined = user_response.data as VatsimUserData; + 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; if (user_response_data == null) { throw new VatsimConnectException(); @@ -134,17 +141,20 @@ export class VatsimConnectLibrary { } private async handleSessionChange() { - if (this.m_response == 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, }, }); // 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 +168,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(); @@ -209,7 +220,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/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/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/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/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/UserNote.ts b/src/models/UserNote.ts index dc526ea..6131f9f 100644 --- a/src/models/UserNote.ts +++ b/src/models/UserNote.ts @@ -1,4 +1,4 @@ -import { Model, InferAttributes, CreationOptional, InferCreationAttributes, NonAttribute, Association } from "sequelize"; +import { Association, CreationOptional, InferAttributes, InferCreationAttributes, Model, NonAttribute } from "sequelize"; import { DataType } from "sequelize-typescript"; import { sequelize } from "../core/Sequelize"; import { User } from "./User"; @@ -33,6 +33,15 @@ export class UserNote extends Model, 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 2a49381..b58fce6 100644 --- a/src/models/UserSession.ts +++ b/src/models/UserSession.ts @@ -8,6 +8,8 @@ export class UserSession extends Model, InferCreati // Attributes // declare uuid: string; + declare browser_uuid: string; + declare client: string; declare user_id: ForeignKey; declare expires_at: Date; declare expires_latest: Date; @@ -37,6 +39,14 @@ UserSession.init( type: DataType.UUID, allowNull: false, }, + browser_uuid: { + type: DataType.UUID, + allowNull: false, + }, + client: { + type: DataType.STRING(100), + allowNull: true, + }, user_id: { type: DataType.INTEGER, allowNull: false, 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(); } 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/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; 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, } );