diff --git a/.env.example b/.env.example index ed4a805b..68cd6889 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ DB_PORT=5432 DB_USERNAME=tanulo DB_PASSWORD=tanulo DB_DATABASE=tanulo + +# Don't touch this +DB_URL="postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}?schema=public" diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 22db5445..56fd5f68 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -46,7 +46,7 @@ jobs: with: # Specify Browser since container image is compile with Firefox browser: chrome - wait-on: yarn migrate + wait-on: yarn prisma:migrate build: yarn build:prod start: yarn start env: diff --git a/migration-porter.sh b/migration-porter.sh new file mode 100755 index 00000000..864b7772 --- /dev/null +++ b/migration-porter.sh @@ -0,0 +1,55 @@ +#!/usr/bin/bash + +source './.env' + +# basenames of knex migration files +# migrations=`ls ./migrations/*.ts | cut -c 14- | rev | cut -c 4- | rev` + +migrations=( + '20200328131352_initial_schema' #1 + '20200328173956_add_owner_id_to_group' #2 + '20200408124626_add_non_nullable' #3 + '20200510001427_add_floor_to_profile' #4 + '20200527121145_rename_column_in_groups' #5 + '20200721213650_update_description_lengths' #6 + '20200828113656_migration_add_status_for_tickets' #7 + '20201012224257_add_max_attendees' #8 + '20201013225752_add_role_to_user' #9 + '20210418152354_add_user_id_to_tickets' #10 + '20210611222535_add_place_and_link_to_groups' #11 + '20210626192847_add_wantemail_to_user' #12 + '20210829032204_insert_mocked_prisma_migration_logs' #13 +) + +if [[ "$1" = "--help" ]] +then + echo 'Convert knex migrations into prisma migrations.' + echo 'Usage:' + echo ' ./migration-porter.sh [start from = 1]' + exit 0 +fi + +START_FROM_IDX="${1:-1}" + +for i in `seq $START_FROM_IDX ${#migrations[@]}` +do + F="${migrations[$i-1]}" + + echo "Porting migration $F" + docker run --name 'tanulo-migration' -e POSTGRES_USER="$DB_USERNAME" -e POSTGRES_PASSWORD="$DB_PASSWORD" -e POSTGRES_DB="$tanulo" --rm --net host -d postgres + + sleep 10 + + for j in `seq $i` + do + npx knex migrate:up + done + + npx prisma db pull + echo 'y' | npx prisma migrate dev --create-only -n "$F" + mv ./prisma/migrations/*"$F" -n ./prisma/migrations/"$F" + echo -e "\n-- Objectionjs migration\nINSERT INTO \"migrationTable\" (\"migration_time\", \"name\", \"batch\")\nVALUES (\n CURRENT_TIMESTAMP,\n '$F.ts',\n 1);\n" >> "./prisma/migrations/$F/migration.sql" + + docker kill 'tanulo-migration' + sleep 3 +done diff --git a/migrations/20210829032204_insert_mocked_prisma_migration_logs.ts b/migrations/20210829032204_insert_mocked_prisma_migration_logs.ts new file mode 100644 index 00000000..3fb2de70 --- /dev/null +++ b/migrations/20210829032204_insert_mocked_prisma_migration_logs.ts @@ -0,0 +1,127 @@ +import * as Knex from 'knex' + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('_prisma_migrations', (table) => { + table.string('id', 36).primary() + table.string('checksum', 64).notNullable() + table.timestamp('finished_at') + table.string('migration_name', 255).notNullable() + table.string('logs') + table.timestamp('rolled_back_at') + table.timestamp('started_at').notNullable() + table.integer('applied_steps_count') + }) + + return knex.table('_prisma_migrations').insert([ + { + id: 'ec605277-3fd7-436b-814d-7a7806f23b3f', + checksum: '66517a5cc09d205d5e0ce783c6a66829c4991ae1aaa29d7b05dc3734aeb709d1', + migration_name: '20200328131352_initial_schema', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '46631492-c804-4f1a-85b8-a909b62837fc', + checksum: 'afee0d4c885c333b31f4b016fa7602d8c6c2420b3fb2a6512f94fec03fd4b5af', + migration_name: '20200328173956_add_owner_id_to_group', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '23daa180-0ef6-4e32-8e8a-3c38d611f0de', + checksum: 'c1d348b0b81e2f43c3bd5e048eb78b03098c51533053ac0b072a50a453a15f2f', + migration_name: '20200408124626_add_non_nullable', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '164d5175-407a-4534-b9b5-74f362d02260', + checksum: '2cbb2f78fd00d6da87010bfc68779afd4fa10e2beba797422789ae6949ab6be1', + migration_name: '20200510001427_add_floor_to_profile', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: 'a2c12db3-ef64-4c7f-9a75-a35df6c8c27d', + checksum: 'adc769d24b44e029aeadfc17e5239bd4dc9fe140830b1d3907df07a103f59a3d', + migration_name: '20200527121145_rename_column_in_groups', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '945f1416-bae3-43ac-8c79-7eba59c6b70e', + checksum: 'b9ea38b901440a825c37b4344d85f6c869a3e32fc021576d4436066aee01b6de', + migration_name: '20200721213650_update_description_lengths', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '62fd4180-f9a4-4763-848b-530c22508a07', + checksum: '9bccdccb9d26d0ce4fbb40cf15e2a96e8b92205db377d9c89d7094e05e00c235', + migration_name: '20200828113656_migration_add_status_for_tickets', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '0cc14462-e876-4ac6-9e3b-f8785df7d61f', + checksum: '64c27a4a9419cffb52cb11fc7b3c1faec6d563f90952663f97563d2b32b78d1a', + migration_name: '20201012224257_add_max_attendees', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '99837065-b062-4da7-be83-f66085485b49', + checksum: '20c03bfcf56907fd44992d0b090fd178e6fd755df0c056aea0988c24b7ee11ca', + migration_name: '20201013225752_add_role_to_user', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '74f11458-b655-4076-9392-586967faafa4', + checksum: '33c7c37d36bd1976edb134eb0e538bed166ccce6cc49dd5a0226444c9f4d948a', + migration_name: '20210418152354_add_user_id_to_tickets', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '07df2122-3d22-43d5-a0b0-439bf656b8f7', + checksum: 'ac35a78a746ad8b4fcbc87c0ed2e7ccbd29e081b4cfb80ee225a67f9695ea936', + migration_name: '20210611222535_add_place_and_link_to_groups', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '70e518c8-91ae-4af0-9570-78094b14ea2b', + checksum: 'e47909787202b22822dd4395b865fcde2eb24b3d43dcc263cd78dde8ae4853c8', + migration_name: '20210626192847_add_wantemail_to_user', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + { + id: '66fdb87d-2b6a-4567-b455-70e27323de2c', + checksum: '374da65652aaf3e425b0cde5169cfeb799853e9ac958119fc46a3673085406fd', + migration_name: '20210829032204_insert_mocked_prisma_migration_logs', + applied_steps_count: 1, + started_at: new Date(), + finished_at: new Date() + }, + ]) +} + + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('_prisma_migrations') +} + diff --git a/package.json b/package.json index c91027db..f3219a2e 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,16 @@ "build": "yarn build:ts && yarn build:css && yarn lint && yarn copy:static-assets", "build:prod": "yarn build:ts && yarn build:css && yarn copy:static-assets", "serve": "node dist/src/server.js", - "migrate": "knex migrate:latest", - "seed": "knex seed:run", + "migrate:upgrade": "knex migrate:latest", + "prisma:migrate": "npx prisma migrate deploy", + "prisma:migrate:new": "npx prisma migrate dev --create-only --preview-feature", + "prisma:pull": "npx prisma db pull", + "prisma:build": "npx prisma generate", + "prisma:seed": "npx prisma db seed --preview-feature", "watch:node": "nodemon", "watch": "concurrently -k -p \"[{name}]\" -n \"Css,Static,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"yarn watch:css\" \"yarn watch:static-assets\" \"yarn watch:node\"", "test": "yarn run cypress open", - "build:ts": "etsc", + "build:ts": "yarn prisma:build && etsc", "build:css": "NODE_ENV=production postcss public/css/tailwind.css -o dist/public/css/styles.css", "watch:css": "NODE_ENV=development postcss public/css/tailwind.css -o dist/public/css/styles.css -w", "watch:fe": "concurrently -k -p \"[{name}]\" -n \"Css,Static\" -c \"yellow.bold,cyan.bold\" \"yarn watch:css\" \"yarn watch:static-assets\"", @@ -21,10 +25,11 @@ "copy:static-assets": "node -r esm copyStaticAssets.js", "watch:static-assets": "chokidar \"views/**/*.pug\" \"public/js/**/*.js\" -c \"yarn copy:static-assets\"", "debug": "yarn build && yarn watch:debug", - "serve:debug": "nodemon --inspect", + "serve:debug": "nodemon --exec 'tsc && node --inspect dist/src/server.js'", "watch:debug": "concurrently -k -p \"[{name}]\" -n \"Css,Static,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"yarn watch:css\" \"yarn watch:static-assets\" \"yarn serve:debug\"" }, "dependencies": { + "@prisma/client": "^2.30.0", "@tailwindcss/forms": "0.3.3", "@tailwindcss/typography": "0.4.1", "autoprefixer": "10.3.1", diff --git a/prisma/migrations/20200328131352_initial_schema/migration.sql b/prisma/migrations/20200328131352_initial_schema/migration.sql new file mode 100644 index 00000000..43e84e03 --- /dev/null +++ b/prisma/migrations/20200328131352_initial_schema/migration.sql @@ -0,0 +1,78 @@ +-- CreateTable +CREATE TABLE "groups" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(255), + "subject" VARCHAR(255), + "description" VARCHAR(255), + "start_date" TIMESTAMPTZ(6), + "end_date" TIMESTAMPTZ(6), + "room" INTEGER, + "do_not_disturb" BOOLEAN, + "created_at" TIMESTAMPTZ(6), + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "migrationTable" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(255), + "batch" INTEGER, + "migration_time" TIMESTAMPTZ(6), + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "migrationTable_lock" ( + "index" SERIAL NOT NULL, + "is_locked" INTEGER, + + PRIMARY KEY ("index") +); + +-- CreateTable +CREATE TABLE "tickets" ( + "id" SERIAL NOT NULL, + "description" VARCHAR(255), + "room_number" INTEGER, + "created_at" TIMESTAMPTZ(6), + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(255), + "email" VARCHAR(255), + "auth_sch_id" VARCHAR(255), + "admin" BOOLEAN, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users_groups" ( + "id" SERIAL NOT NULL, + "user_id" INTEGER, + "group_id" INTEGER, + + PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "users_groups_groupid_index" ON "users_groups"("group_id"); + +-- CreateIndex +CREATE INDEX "users_groups_userid_index" ON "users_groups"("user_id"); + +-- AddForeignKey +ALTER TABLE "users_groups" ADD FOREIGN KEY ("group_id") REFERENCES "groups"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "users_groups" ADD FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20200328131352_initial_schema.ts', 1); diff --git a/prisma/migrations/20200328173956_add_owner_id_to_group/migration.sql b/prisma/migrations/20200328173956_add_owner_id_to_group/migration.sql new file mode 100644 index 00000000..d4d3c45d --- /dev/null +++ b/prisma/migrations/20200328173956_add_owner_id_to_group/migration.sql @@ -0,0 +1,12 @@ +-- AlterTable +ALTER TABLE "groups" ADD COLUMN "owner_id" INTEGER; + +-- CreateIndex +CREATE INDEX "groups_ownerid_index" ON "groups"("owner_id"); + +-- AddForeignKey +ALTER TABLE "groups" ADD FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20200328173956_add_owner_id_to_group.ts', 1); diff --git a/prisma/migrations/20200408124626_add_non_nullable/migration.sql b/prisma/migrations/20200408124626_add_non_nullable/migration.sql new file mode 100644 index 00000000..4ccbcd56 --- /dev/null +++ b/prisma/migrations/20200408124626_add_non_nullable/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - Made the column `name` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `start_date` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `end_date` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `room` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `do_not_disturb` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `description` on table `tickets` required. This step will fail if there are existing NULL values in that column. + - Made the column `room_number` on table `tickets` required. This step will fail if there are existing NULL values in that column. + - Made the column `name` on table `users` required. This step will fail if there are existing NULL values in that column. + - Made the column `email` on table `users` required. This step will fail if there are existing NULL values in that column. + - Made the column `auth_sch_id` on table `users` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "groups" ALTER COLUMN "name" SET NOT NULL, +ALTER COLUMN "start_date" SET NOT NULL, +ALTER COLUMN "end_date" SET NOT NULL, +ALTER COLUMN "room" SET NOT NULL, +ALTER COLUMN "do_not_disturb" SET NOT NULL; + +-- AlterTable +ALTER TABLE "tickets" ALTER COLUMN "description" SET NOT NULL, +ALTER COLUMN "room_number" SET NOT NULL; + +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "name" SET NOT NULL, +ALTER COLUMN "email" SET NOT NULL, +ALTER COLUMN "auth_sch_id" SET NOT NULL; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20200408124626_add_non_nullable.ts', 1); diff --git a/prisma/migrations/20200510001427_add_floor_to_profile/migration.sql b/prisma/migrations/20200510001427_add_floor_to_profile/migration.sql new file mode 100644 index 00000000..288aeefd --- /dev/null +++ b/prisma/migrations/20200510001427_add_floor_to_profile/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "floor" INTEGER; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20200510001427_add_floor_to_profile.ts', 1); diff --git a/prisma/migrations/20200527121145_rename_column_in_groups/migration.sql b/prisma/migrations/20200527121145_rename_column_in_groups/migration.sql new file mode 100644 index 00000000..662eafb0 --- /dev/null +++ b/prisma/migrations/20200527121145_rename_column_in_groups/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "groups" +RENAME COLUMN "subject" TO "tags"; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20200527121145_rename_column_in_groups.ts', 1); diff --git a/prisma/migrations/20200721213650_update_description_lengths/migration.sql b/prisma/migrations/20200721213650_update_description_lengths/migration.sql new file mode 100644 index 00000000..38cb639b --- /dev/null +++ b/prisma/migrations/20200721213650_update_description_lengths/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable +ALTER TABLE "groups" ALTER COLUMN "description" SET DATA TYPE VARCHAR(500); + +-- AlterTable +ALTER TABLE "tickets" ALTER COLUMN "description" DROP NOT NULL, +ALTER COLUMN "description" SET DATA TYPE VARCHAR(500); + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP,'20200721213650_update_description_lengths.ts', 1); diff --git a/prisma/migrations/20200828113656_migration_add_status_for_tickets/migration.sql b/prisma/migrations/20200828113656_migration_add_status_for_tickets/migration.sql new file mode 100644 index 00000000..c5d8d726 --- /dev/null +++ b/prisma/migrations/20200828113656_migration_add_status_for_tickets/migration.sql @@ -0,0 +1,9 @@ +-- CreateEnum +CREATE TYPE "status_type" AS ENUM ('SENT', 'IN_PROGRESS', 'DONE', 'ARCHIVED'); + +-- AlterTable +ALTER TABLE "tickets" ADD COLUMN "status" "status_type" NOT NULL DEFAULT E'SENT'; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20200828113656_migration_add_status_for_tickets.ts', 1); diff --git a/prisma/migrations/20201012224257_add_max_attendees/migration.sql b/prisma/migrations/20201012224257_add_max_attendees/migration.sql new file mode 100644 index 00000000..dd57a7e3 --- /dev/null +++ b/prisma/migrations/20201012224257_add_max_attendees/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "groups" ADD COLUMN "max_attendees" INTEGER DEFAULT 100; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20201012224257_add_max_attendees.ts', 1); diff --git a/prisma/migrations/20201013225752_add_role_to_user/migration.sql b/prisma/migrations/20201013225752_add_role_to_user/migration.sql new file mode 100644 index 00000000..5098eb75 --- /dev/null +++ b/prisma/migrations/20201013225752_add_role_to_user/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `admin` on the `users` table. All the data in the column will be lost. + +*/ +-- CreateEnum +CREATE TYPE "role_type" AS ENUM ('ADMIN', 'TICKET_ADMIN', 'USER'); + +-- AlterTable +ALTER TABLE "users" +ADD COLUMN "role" "role_type" NOT NULL DEFAULT E'USER'; + +-- Transfer data +UPDATE "users" +SET "role" = E'ADMIN' +WHERE "admin"=true; + +-- Drop column +ALTER TABLE "users" +DROP COLUMN "admin"; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20201013225752_add_role_to_user.ts', 1); diff --git a/prisma/migrations/20210418152354_add_user_id_to_tickets/migration.sql b/prisma/migrations/20210418152354_add_user_id_to_tickets/migration.sql new file mode 100644 index 00000000..ba728697 --- /dev/null +++ b/prisma/migrations/20210418152354_add_user_id_to_tickets/migration.sql @@ -0,0 +1,12 @@ +-- AlterTable +ALTER TABLE "tickets" ADD COLUMN "user_id" INTEGER; + +-- CreateIndex +CREATE INDEX "tickets_userid_index" ON "tickets"("user_id"); + +-- AddForeignKey +ALTER TABLE "tickets" ADD FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20210418152354_add_user_id_to_tickets.ts', 1); diff --git a/prisma/migrations/20210611222535_add_place_and_link_to_groups/migration.sql b/prisma/migrations/20210611222535_add_place_and_link_to_groups/migration.sql new file mode 100644 index 00000000..6a22c0c8 --- /dev/null +++ b/prisma/migrations/20210611222535_add_place_and_link_to_groups/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "groups" ADD COLUMN "link" VARCHAR(255), +ADD COLUMN "place" VARCHAR(255), +ALTER COLUMN "room" DROP NOT NULL; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20210611222535_add_place_and_link_to_groups.ts', 1); diff --git a/prisma/migrations/20210626192847_add_wantemail_to_user/migration.sql b/prisma/migrations/20210626192847_add_wantemail_to_user/migration.sql new file mode 100644 index 00000000..ea7c7777 --- /dev/null +++ b/prisma/migrations/20210626192847_add_wantemail_to_user/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "want_email" BOOLEAN DEFAULT true; + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20210626192847_add_wantemail_to_user.ts', 1); diff --git a/prisma/migrations/20210829032204_insert_mocked_prisma_migration_logs/migration.sql b/prisma/migrations/20210829032204_insert_mocked_prisma_migration_logs/migration.sql new file mode 100644 index 00000000..f5625284 --- /dev/null +++ b/prisma/migrations/20210829032204_insert_mocked_prisma_migration_logs/migration.sql @@ -0,0 +1,6 @@ +-- This is an empty migration. +-- This is initial + +-- Objectionjs migration +INSERT INTO "migrationTable" ("migration_time", "name", "batch") +VALUES (CURRENT_TIMESTAMP, '20210829032204_insert_mocked_prisma_migration_logs.ts', 1); diff --git a/prisma/migrations/20210829033425_add_default_for_created_at/migration.sql b/prisma/migrations/20210829033425_add_default_for_created_at/migration.sql new file mode 100644 index 00000000..43a08765 --- /dev/null +++ b/prisma/migrations/20210829033425_add_default_for_created_at/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "groups" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "tickets" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20210829044240_remove_knex_migration_logs/migration.sql b/prisma/migrations/20210829044240_remove_knex_migration_logs/migration.sql new file mode 100644 index 00000000..3dcf17cf --- /dev/null +++ b/prisma/migrations/20210829044240_remove_knex_migration_logs/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the `migrationTable` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `migrationTable_lock` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "migrationTable"; + +-- DropTable +DROP TABLE "migrationTable_lock"; diff --git a/prisma/migrations/20210829174948_add_missing_non_nulls/migration.sql b/prisma/migrations/20210829174948_add_missing_non_nulls/migration.sql new file mode 100644 index 00000000..5e174ac3 --- /dev/null +++ b/prisma/migrations/20210829174948_add_missing_non_nulls/migration.sql @@ -0,0 +1,36 @@ +/* + Warnings: + + - Made the column `tags` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `description` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `created_at` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `owner_id` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `max_attendees` on table `groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `description` on table `tickets` required. This step will fail if there are existing NULL values in that column. + - Made the column `created_at` on table `tickets` required. This step will fail if there are existing NULL values in that column. + - Made the column `user_id` on table `tickets` required. This step will fail if there are existing NULL values in that column. + - Made the column `want_email` on table `users` required. This step will fail if there are existing NULL values in that column. + - Made the column `user_id` on table `users_groups` required. This step will fail if there are existing NULL values in that column. + - Made the column `group_id` on table `users_groups` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "groups" ALTER COLUMN "tags" SET NOT NULL, +ALTER COLUMN "tags" SET DEFAULT E'', +ALTER COLUMN "description" SET NOT NULL, +ALTER COLUMN "description" SET DEFAULT E'', +ALTER COLUMN "created_at" SET NOT NULL, +ALTER COLUMN "owner_id" SET NOT NULL, +ALTER COLUMN "max_attendees" SET NOT NULL; + +-- AlterTable +ALTER TABLE "tickets" ALTER COLUMN "description" SET NOT NULL, +ALTER COLUMN "created_at" SET NOT NULL, +ALTER COLUMN "user_id" SET NOT NULL; + +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "want_email" SET NOT NULL; + +-- AlterTable +ALTER TABLE "users_groups" ALTER COLUMN "user_id" SET NOT NULL, +ALTER COLUMN "group_id" SET NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..7b8b0d6f --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,86 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DB_URL") +} + +model Group { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + tags String @default("") @db.VarChar(255) + description String @default("") @db.VarChar(500) + startDate DateTime @map("start_date") @db.Timestamptz(6) + endDate DateTime @map("end_date") @db.Timestamptz(6) + room Int? + doNotDisturb Boolean @map("do_not_disturb") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + ownerId Int @map("owner_id") + maxAttendees Int @default(100) @map("max_attendees") + link String? @db.VarChar(255) + place String? @db.VarChar(255) + owner User @relation(fields: [ownerId], references: [id]) + users Membership[] + + @@index([ownerId], name: "groups_ownerid_index") + @@map("groups") +} + +model Ticket { + id Int @id @default(autoincrement()) + description String @db.VarChar(500) + roomNumber Int @map("room_number") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + status StatusType @default(Sent) + ownerId Int @map("user_id") + owner User @relation(fields: [ownerId], references: [id]) + + @@index([ownerId], name: "tickets_userid_index") + @@map("tickets") +} + +model User { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + email String @db.VarChar(255) + authSchId String @map("auth_sch_id") @db.VarChar(255) + floor Int? + role RoleType @default(User) + wantEmail Boolean @default(true) @map("want_email") + ownedGroups Group[] + tickets Ticket[] + groups Membership[] + + @@map("users") +} + +model Membership { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + groupId Int @map("group_id") + group Group @relation(fields: [groupId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@index([groupId], name: "users_groups_groupid_index") + @@index([userId], name: "users_groups_userid_index") + @@map("users_groups") +} + +enum StatusType { + Sent @map("SENT") + InProgress @map("IN_PROGRESS") + Done @map("DONE") + Archived @map("ARCHIVED") + + @@map("status_type") +} + +enum RoleType { + Admin @map("ADMIN") + TicketAdmin @map("TICKET_ADMIN") + User @map("USER") + + @@map("role_type") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 00000000..e422452d --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,142 @@ +import { PrismaClient, RoleType, StatusType, Prisma } from '@prisma/client' +import faker from 'faker' + +const prisma = new PrismaClient() + +const USERS_COUNT = 16 + +async function addUsers() { + await prisma.user.deleteMany() + + const userArray: Prisma.UserCreateManyInput[] = [] + for (let i = 0; i < USERS_COUNT; ++i) { + const user = { + name: faker.name.findName(), + email: faker.internet.email(), + authSchId: faker.datatype.uuid(), + role: RoleType.User, + wantEmail: faker.datatype.boolean(), + floor: faker.datatype.boolean() ? faker.datatype.number(18 - 3) + 3 : null + } + console.log('\x1b[33m%s\x1b[0m', `User: #${i + 1} ${user.name}`) + userArray.push(user) + } + + await prisma.user.createMany({ data: userArray }) +} + +async function addGroups() { + const startingFloor = 3 + const floorCount = 16 + const groupsPerFloor = 4 + const groupArray: Prisma.GroupCreateManyInput[] = [] + const connectArray: Prisma.MembershipCreateManyInput[] = [] + let connectCountSum = 0 + for (let i = 0; i < floorCount; ++i) { + for (let j = 0; j < groupsPerFloor; ++j) { + // Compose description + let description = faker.datatype.boolean() ? + ('## ' + faker.random.words(5)) : faker.random.words(6) + + description += '\n' + + for (let k = 0; k < faker.datatype.number(3) + 2; ++k) { + description += '* ' + faker.lorem.words(3) + '\n' + } + + + // Compose group + const startSeed = faker.datatype.number(500) * 1_000_000 * (faker.datatype.boolean() ? -1 : 1) + const maxTwoHours = faker.datatype.number(6600) * 1000 + 600_000 + const ownerId = (i * groupsPerFloor + j) % 16 + 1 + const groupId = (i * groupsPerFloor + j) + 1 + + const group = { + name: faker.company.catchPhrase(), + tags: faker.random.words(faker.datatype.number(5)).split(' ').join(','), + description, + startDate: (new Date(Date.now() + startSeed * (j + 1))), + endDate: (new Date(Date.now() + startSeed * (j + 1) + maxTwoHours)), + room: i + startingFloor, + doNotDisturb: (i * groupsPerFloor + j) % 16 == 1, + maxAttendees: 100, + createdAt: new Date(), + ownerId + } + console.log('\x1b[33m%s\x1b[0m', + `Group: #${groupId} ${group.name}, floor: ${group.room}, owner: ${group.ownerId}`) + groupArray.push(group) + + // Connect groups and users + const connectOwner: Prisma.MembershipCreateManyInput = { + userId: (i * groupsPerFloor + j) % 16 + 1, + groupId: (i * groupsPerFloor + j) + 1, + } + console.log(`Connect: owner #${connectOwner.userId} to #${connectOwner.groupId}`) + connectArray.push(connectOwner) + connectCountSum++ + + for (let k = 1; k < ownerId; ++k) { + const connect = { + userId: k, + groupId + } + console.log(`Connect: user #${connect.userId} to #${connect.groupId}`) + connectArray.push(connect) + connectCountSum++ + } + } + } + + // Inserts seed entries + await prisma.group.createMany({ data: groupArray }) + await prisma.membership.createMany({ data: connectArray }) + console.log('\x1b[32m%s\x1b[0m', `Inserted ${groupsPerFloor * floorCount} groups into db.`) + console.log('\x1b[32m%s\x1b[0m', `Inserted ${connectCountSum} users_groups into db.`) +} + +async function addTickets() { + const statuses = Object.keys(StatusType) + const ticketCount = 12 + const ticketArray: Prisma.TicketCreateManyInput[] = [] + for (let i = 0; i < ticketCount; ++i) { + const ticket = { + description: faker.lorem.sentences(5), + roomNumber: faker.datatype.number(15) + 3, + createdAt: (new Date(Date.now() - faker.datatype.number(500) * 1_000_000)), + status: statuses[faker.datatype.number(3)] as StatusType, + ownerId: faker.datatype.number(USERS_COUNT - 1) + 1, + } + console.log('\x1b[33m%s\x1b[0m', `Ticket: #${i + 1}`) + ticketArray.push(ticket) + } + + // Inserts seed entries + await prisma.ticket.createMany({ data: ticketArray }) + console.log('\x1b[32m%s\x1b[0m', `Inserted ${ticketCount} tickets into db.`) +} + +async function main() { + await prisma.$transaction([ + prisma.membership.deleteMany(), + prisma.ticket.deleteMany(), + prisma.group.deleteMany(), + prisma.user.deleteMany(), + ]) + + await prisma.$executeRaw('TRUNCATE tickets, users_groups, groups, users RESTART IDENTITY CASCADE') + + + await addUsers() + await addGroups() + await addTickets() +} + +main() + .catch((e) => { + console.error(e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/public/js/user.js b/public/js/user.js index 1a58b01f..df7da82e 100644 --- a/public/js/user.js +++ b/public/js/user.js @@ -44,7 +44,7 @@ function updateRole(id) { const roleEl = document.getElementById('role') const role = roleEl.value console.log(role) - if (role == '' || !(role == 'ADMIN' || role == 'TICKET_ADMIN' || role == 'USER')) { + if (role === '' || !(role === 'Admin' || role === 'TicketAdmin' || role === 'User')) { displayMessage('A felhasználói jogkör nem megfelelő.') } else { fetch(`/users/${id}/role`, { diff --git a/seeds/01_add_users.ts b/seeds/01_add_users.ts deleted file mode 100644 index 97ee849f..00000000 --- a/seeds/01_add_users.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as Knex from 'knex' -import faker from 'faker' -import { RoleType } from '../src/components/users/user' - -export async function seed(knex: Knex): Promise { - // Deletes ALL existing entries not considering FK constraints - await knex.raw('TRUNCATE tickets, users_groups, groups, users RESTART IDENTITY CASCADE') - - const userCount = 16 - const userArray = [] - for (let i = 0; i < userCount; ++i) { - const user = { - name: faker.name.findName(), - email: faker.internet.email(), - authSchId: faker.datatype.uuid(), - role: RoleType.USER, - } - console.log('\x1b[33m%s\x1b[0m', `User: #${i+1} ${user.name}`) - userArray.push(user) - } - - // Inserts seed entries - await knex('users').insert(userArray) - console.log('\x1b[32m%s\x1b[0m', `Inserted ${userCount} users into db.`) -} diff --git a/seeds/02_add_groups.ts b/seeds/02_add_groups.ts deleted file mode 100644 index 215d2480..00000000 --- a/seeds/02_add_groups.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as Knex from 'knex' -import faker from 'faker' - -export async function seed(knex: Knex): Promise { - - const startingFloor = 3 - const floorCount = 16 - const groupsPerFloor = 4 - const groupArray = [] - const connectArray = [] - let connectCountSum = 0 - for (let i = 0; i < floorCount; ++i) { - for (let j = 0; j < groupsPerFloor; ++j) { - // Compose description - let description = faker.datatype.boolean()? - ('## ' + faker.random.words(5)) : faker.random.words(6) - - description += '\n' - - for (let k = 0; k < faker.datatype.number(3) + 2; ++k) { - description += '* ' + faker.lorem.words(3) + '\n' - } - - - // Compose group - const startSeed = faker.datatype.number(500) * 1_000_000 * (faker.datatype.boolean()? -1 : 1) - const maxTwoHours = faker.datatype.number(6600) * 1000 + 600_000 - const ownerId = (i * groupsPerFloor + j) % 16 + 1 - const groupId = (i * groupsPerFloor + j) + 1 - - const group = { - name: faker.company.catchPhrase(), - tags: faker.random.words(faker.datatype.number(5)).split(' ').join(','), - description, - startDate: (new Date( Date.now() + startSeed * (j + 1) )), - endDate: (new Date( Date.now() + startSeed * (j + 1) + maxTwoHours )), - room: i + startingFloor, - doNotDisturb: (i * groupsPerFloor + j) % 16 == 1, - maxAttendees: 100, - createdAt: new Date(), - ownerId - } - console.log('\x1b[33m%s\x1b[0m', - `Group: #${groupId} ${group.name}, floor: ${group.room}, owner: ${group.ownerId}`) - groupArray.push(group) - - // Connect groups and users - const connectOwner = { - userId: (i * groupsPerFloor + j) % 16 + 1, - groupId: (i * groupsPerFloor + j) + 1, - } - console.log(`Connect: owner #${connectOwner.userId} to #${connectOwner.groupId}`) - connectArray.push(connectOwner) - connectCountSum++ - - for (let k = 1; k < ownerId; ++k) { - const connect = { - userId: k, - groupId - } - console.log(`Connect: user #${connect.userId} to #${connect.groupId}`) - connectArray.push(connect) - connectCountSum++ - } - } - } - - // Inserts seed entries - await knex('groups').insert(groupArray) - await knex('users_groups').insert(connectArray) - console.log('\x1b[32m%s\x1b[0m', `Inserted ${groupsPerFloor * floorCount} groups into db.`) - console.log('\x1b[32m%s\x1b[0m', `Inserted ${connectCountSum} users_groups into db.`) -} diff --git a/seeds/03_add_tickets.ts b/seeds/03_add_tickets.ts deleted file mode 100644 index da4c3536..00000000 --- a/seeds/03_add_tickets.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as Knex from 'knex' -import faker from 'faker' - -export async function seed(knex: Knex): Promise { - - const statuses = ['SENT', 'IN_PROGRESS', 'DONE', 'ARCHIVED'] - const ticketCount = 12 - const ticketArray = [] - for (let i = 0; i < ticketCount; ++i) { - const ticket = { - description: faker.lorem.sentences(5), - roomNumber: faker.datatype.number(15) + 3, - createdAt: (new Date( Date.now() - faker.datatype.number(500) * 1_000_000 )), - status: statuses[faker.datatype.number(3)] - } - console.log('\x1b[33m%s\x1b[0m', `Ticket: #${i+1}`) - ticketArray.push(ticket) - } - - // Inserts seed entries - await knex('tickets').insert(ticketArray) - console.log('\x1b[32m%s\x1b[0m', `Inserted ${ticketCount} tickets into db.`) -} diff --git a/src/app.ts b/src/app.ts index c633b841..34592b7f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,14 +2,11 @@ import express from 'express' import compression from 'compression' import cookieParser from 'cookie-parser' import session from 'express-session' -import Knex from 'knex' import lusca from 'lusca' -import { Model } from 'objection' import path from 'path' import passport from 'passport' import RateLimit from 'express-rate-limit' -import dbConfig = require('../knexfile') import { SESSION_SECRET } from './util/secrets' import userRouter from './components/users/user.routes' @@ -17,10 +14,6 @@ import ticketRouter from './components/tickets/ticket.routes' import roomRouter, { index } from './components/rooms/room.routes' import groupRouter from './components/groups/group.routes' -const knex = Knex(dbConfig) - -Model.knex(knex) - // Create Express server const app = express() @@ -45,7 +38,7 @@ app.use(lusca.xssProtection(true)) // set up rate limiter: maximum requests per minute const limiter = new RateLimit({ - windowMs: 1*60*1000, // 1 minute + windowMs: 1 * 60 * 1000, // 1 minute max: 1000 // max number of requests }) // apply rate limiter to all requests @@ -107,6 +100,6 @@ app.get('/auth/oauth/callback', /** * Error routes */ -app.use('*', (req, res) => res.render('error/not-found')) +app.use('*', (req, res) => res.status(404).render('error/not-found')) export default app diff --git a/src/components/groups/group.middlewares.ts b/src/components/groups/group.middlewares.ts index 10f3c76b..8af9ed37 100644 --- a/src/components/groups/group.middlewares.ts +++ b/src/components/groups/group.middlewares.ts @@ -5,11 +5,11 @@ import * as ics from 'ics' import winston from 'winston' import { differenceInMinutes } from 'date-fns' -import { RoleType, User } from '../users/user' -import { Group } from './group' import { asyncWrapper } from '../../util/asyncWrapper' import sendMessage from '../../util/sendMessage' import { sendEmail } from '../../util/sendEmail' +import { prisma } from '../../prisma' +import { Group, RoleType } from '@prisma/client' export const joinGroup = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { const user = req.user @@ -17,29 +17,33 @@ export const joinGroup = asyncWrapper(async (req: Request, res: Response, next: // Join group if not already in it, and it's not closed or it's the owner who joins. // We only join the group if it is not full already - if (group.doNotDisturb && (user.id !== group.ownerId)){ + if (group.doNotDisturb && (user.id !== group.ownerId)) { sendMessage(res, 'Ez egy privát csoport!') - } else if (group.users?.find(it => it.id === user.id)) { + } else if (group.users?.find(it => it.userId === user.id)) { sendMessage(res, 'Már tagja vagy ennek a csoportnak!') } else if ((group.users?.length || 0) >= group.maxAttendees) { sendMessage(res, 'Ez a csoport már tele van!') } else if (group.endDate < new Date()) { sendMessage(res, 'Ez a csoport már véget ért!') } else { - await Group.relatedQuery('users') - .for(group.id) - .relate(user.id) + await prisma.membership.create({ + data: { + userId: user.id, + groupId: group.id + }, + select: { id: true } + }) return next() } res.redirect(`/groups/${req.params.id}`) }) export const sendEmailToOwner = asyncWrapper( - async (req: Request, res: Response, next: NextFunction) => { + async (req: Request, res: Response, next: NextFunction) => { const user = req.user const group = req.group - const emailRecepient = await User.query().findOne({ id: group.ownerId }) + const emailRecepient = await prisma.user.findFirst({ where: { id: group.ownerId } }) sendEmail([emailRecepient], { subject: 'Csatlakoztak egy csoportodba!', body: `${user.name} csatlakozott a(z) ${group.name} csoportodba!`, @@ -49,37 +53,39 @@ export const sendEmailToOwner = asyncWrapper( next() }) export const leaveGroup = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - await Group.relatedQuery('users') - .for(req.group.id) - .unrelate() - .where('user_id', req.user.id) + await prisma.membership.deleteMany({ + where: { userId: req.user.id, groupId: req.group.id }, + }) next() }) export const isMemberInGroup = -asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - const kickableUser = await Group.relatedQuery('users').for(req.group.id) - .findOne({ userId: parseInt(req.params.userid) }) - if (kickableUser) { - next() - } else { - res.redirect('/not-found') - } -}) + asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { + const kickableUser = await prisma.membership.findFirst({ + where: { groupId: req.group.id, userId: parseInt(req.params.userid) }, + select: { id: true } + }) + if (kickableUser) { + next() + } else { + res.status(404).redirect('/not-found') + } + }) export const kickMember = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - await Group.relatedQuery('users') - .for(req.group.id) - .unrelate() - .where('user_id', req.params.userid) + await prisma.membership.deleteMany({ + where: { userId: parseInt(req.params.userid), groupId: req.group.id }, + }) next() }) export const sendEmailToMember = asyncWrapper( - async (req: Request, res: Response, next: NextFunction) => { - const emailRecepient = await User.query().findOne({ id: req.params.userid }) + async (req: Request, res: Response, next: NextFunction) => { + const emailRecepient = await prisma.user.findFirst({ + where: { id: parseInt(req.params.userid) } + }) sendEmail([emailRecepient], { subject: 'Kirúgtak egy csoportból!', body: `A(z) ${req.group.name} csoport szervezője vagy egy admin kirúgott a csoportból.`, @@ -103,7 +109,7 @@ export const isGroupOwner = asyncWrapper( export const isGroupOwnerOrAdmin = asyncWrapper( async (req: Request, res: Response, next: NextFunction) => { if ((req.user?.id === req.group.ownerId) - || (req.user?.role == RoleType.ADMIN)) { + || (req.user?.role == RoleType.Admin)) { next() } else { res.render('error/forbidden') @@ -162,12 +168,12 @@ function isValidHttpsUrl(str) { return false } // not catching bad top lvl domain (1 character) - const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string - '(\\#[-a-z\\d_]*)?$','i') // fragment locator + const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i') // fragment locator // not allowing '(' and ')' // catching 1 character TLD @@ -211,7 +217,7 @@ export const validateGroup = (): ValidationChain[] => { .custom((value, { req }) => new Date(value).getTime() < new Date(req.body.endDate).getTime()) .withMessage('A kezdés nem lehet korábban, mint a befejezés') .custom((value, { req }) => - differenceInMinutes(new Date(req.body.endDate), new Date(value)) <= 5*60) + differenceInMinutes(new Date(req.body.endDate), new Date(value)) <= 5 * 60) .withMessage('A foglalás időtartama nem lehet hosszabb 5 óránál'), check('endDate', 'A befejezés időpontja kötelező') .exists({ checkFalsy: true, checkNull: true }), @@ -230,7 +236,7 @@ export const checkValidMaxAttendeeLimit = asyncWrapper( if (req.group.users.length > (req.body.maxAttendees || 100)) { res.status(400).json( { - errors: [{msg: 'Nem lehet kisebb a maximum jelenlét, mint a jelenlegi'}] + errors: [{ msg: 'Nem lehet kisebb a maximum jelenlét, mint a jelenlegi' }] } ) } else { @@ -241,28 +247,26 @@ export const checkValidMaxAttendeeLimit = asyncWrapper( export const checkConflicts = asyncWrapper( async (req: Request, res: Response, next: NextFunction) => { - const { type, ...group } = req.body as Group & { type: string } + const { type, ...group } = req.body as { + [x in keyof Group]: string + } & { type: string } if (type !== 'floor') { return next() } - group.startDate = new Date(req.body.startDate) - group.endDate = new Date(req.body.endDate) - const conflictingGroups = await Group.query() - .where({ room: group.room }) - .andWhere(builder => { - builder - .where(bld => { - bld - .where('startDate', '<', group.endDate) - .andWhere('endDate', '>=', group.endDate) - }) - .orWhere(bld => { - bld - .where('endDate', '>', group.startDate) - .andWhere('endDate', '<=', group.endDate) - }) - }) - .andWhereNot({ id: req.params.id ?? null }) + const id = parseInt(req.params.id) + const startDate = new Date(req.body.startDate) + const endDate = new Date(req.body.endDate) + const conflictingGroups = await prisma.group.findMany({ + where: { + room: parseInt(group.room), + OR: [ + { startDate: { lt: endDate }, endDate: { gte: endDate } }, + { endDate: { gt: startDate, lte: endDate } } + ], + NOT: Number.isFinite(id) ? { id } : {} + }, + select: { name: true } + }) if (conflictingGroups.length) { res.status(400).json( diff --git a/src/components/groups/group.routes.ts b/src/components/groups/group.routes.ts index 021c2857..283a3510 100644 --- a/src/components/groups/group.routes.ts +++ b/src/components/groups/group.routes.ts @@ -1,3 +1,4 @@ +import { RoleType } from '@prisma/client' import { format, formatDistanceToNowStrict, @@ -10,7 +11,6 @@ import multer from 'multer' import { isAuthenticated } from '../../config/passport' import { DATE_FORMAT, ROOMS } from '../../util/constants' import { handleValidationError, checkIdParam } from '../../util/validators' -import { RoleType } from '../users/user' import { joinGroup, sendEmailToOwner, @@ -68,9 +68,9 @@ router.get('/:id', checkIdParam, getGroup, (req, res) => { - const joined = req.group.users.some(u => u.id === req.user.id) + const joined = req.group.users.some(u => u.userId === req.user.id) const isOwner = req.group.ownerId === req.user.id - const isAdmin = req.user.role == RoleType.ADMIN + const isAdmin = req.user.role == RoleType.Admin res.render('group/show', { group: req.group, joined, isOwner, format, DATE_FORMAT, isAdmin }) diff --git a/src/components/groups/group.service.ts b/src/components/groups/group.service.ts index bb4ac8cb..4df4aa13 100644 --- a/src/components/groups/group.service.ts +++ b/src/components/groups/group.service.ts @@ -1,72 +1,79 @@ import { Request, Response, NextFunction } from 'express' -import { Group } from './group' import { formatMdToSafeHTML } from '../../util/convertMarkdown' import { asyncWrapper } from '../../util/asyncWrapper' +import { prisma } from '../../prisma' export const getGroups = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { const page = isNaN(Number(req.query.page)) ? 0 : Number(req.query.page) const limit = 20 - const pageObject = req.query.past === 'true' ? - await Group.query().where('endDate', '<', new Date()) - .orderBy('startDate', 'DESC').page(page, limit) : - await Group.query().where('endDate', '>=', new Date()) - .orderBy('startDate', 'ASC').page(page, limit) - req.groups = pageObject.results.map(group => { + const endDate = req.query.past === 'true' + ? { 'lt': new Date() } + : { 'gte': new Date() } + const [pageObject, total] = await prisma.$transaction([ + prisma.group.findMany({ + where: { endDate }, + orderBy: { startDate: 'asc' }, + take: limit, + skip: page * limit, + }), + prisma.group.count({ where: { endDate } }) + ]) + req.groups = pageObject.map(group => { const raw = group.description.slice(0, 50) + (group.description.length > 50 ? ' ...' : '') group.description = formatMdToSafeHTML(raw) return group }) req.paginationOptions = { - pageNum: Math.ceil(pageObject.total / limit), + pageNum: Math.ceil(total / limit), current: page } next() }) export const getGroup = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - const group = await Group.query() - .findOne({ id: parseInt(req.params.id) }) - .withGraphFetched('users') + const group = await prisma.group.findFirst({ + where: { id: parseInt(req.params.id) }, + include: { users: { include: { user: true } } } + }) if (group) { // Getting raw description for /copy and /edit pages - if (/\/copy|\/edit/.test(req.path)) - req.group = group - else - req.group = { ...group, description: formatMdToSafeHTML(group.description) } as Group + if (!/\/copy|\/edit/.test(req.path)) { + group.description = formatMdToSafeHTML(group.description) + } + req.group = group next() } else { - res.render('error/not-found') + res.status(404).render('error/not-found') } }) export const createGroup = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - req.group = await Group.query() - .insert( - { - name: req.body.name, - tags: req.body.tags ?? '', - room: req.body.room ? parseInt(req.body.room) : null, - link: req.body.link, - place: req.body.place, - description: req.body.description, - doNotDisturb: !!req.body.doNotDisturb, - startDate: new Date(req.body.startDate), - endDate: new Date(req.body.endDate), - ownerId: req.user.id, - maxAttendees: parseInt(req.body.maxAttendees) || 100 - } - ) - + const group = await prisma.group.create({ + data: { + name: req.body.name, + tags: req.body.tags ?? '', + room: req.body.room ? parseInt(req.body.room) : null, + link: req.body.link, + place: req.body.place, + description: req.body.description, + doNotDisturb: !!req.body.doNotDisturb, + startDate: new Date(req.body.startDate), + endDate: new Date(req.body.endDate), + ownerId: req.user.id, + maxAttendees: parseInt(req.body.maxAttendees) || 100 + } + }) + req.group = { users: [], ...group } next() }) export const updateGroup = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - - await Group.query() - .patch({ + await prisma.group.update({ + where: { id: parseInt(req.params.id) }, + data: { name: req.body.name, tags: req.body.tags ?? '', room: req.body.room ? parseInt(req.body.room) : null, @@ -77,25 +84,22 @@ export const updateGroup = asyncWrapper(async (req: Request, res: Response, next startDate: new Date(req.body.startDate), endDate: new Date(req.body.endDate), maxAttendees: parseInt(req.body.maxAttendees) || 100 - }) - .findById(req.params.id) - .catch((err) => { - console.log(err) - return next(err) - }) + }, + select: { id: true } + }) next() }) export const removeGroup = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - await Group.transaction(async trx => { - await Group.relatedQuery('users', trx) - .for(req.group.id) - .unrelate() - - await Group.query(trx).deleteById(req.group.id) - }) - + await prisma.$transaction([ + prisma.membership.deleteMany({ + where: { groupId: req.group.id } + }), + prisma.group.delete({ + where: { id: req.group.id }, + }), + ]) next() }) diff --git a/src/components/groups/group.ts b/src/components/groups/group.ts deleted file mode 100644 index 598d2469..00000000 --- a/src/components/groups/group.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Model } from 'objection' - -import { User } from '../users/user' - -export class Group extends Model { - id!: number - name: string - tags: string - description: string - startDate: Date - endDate: Date - room?: number - link?: string - place?: string - doNotDisturb: boolean - ownerId: number - users: User[] - createdAt: Date - maxAttendees: number - - $beforeInsert(): void { - this.createdAt = new Date() - } - - static get tableName(): string { - return 'groups' - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - static get relationMappings(): Record { - return { - users: { - relation: Model.ManyToManyRelation, - modelClass: User, - - join: { - from: 'groups.id', - // ManyToMany relation needs the `through` object to describe the join table. - through: { - from: 'users_groups.groupId', - to: 'users_groups.userId' - }, - to: 'users.id' - } - } - } - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - static get jsonSchema(): Record { - return { - type: 'object', - required: ['name', 'description', 'doNotDisturb', 'tags', 'startDate', 'endDate'], - - properties: { - id: { type: 'integer' }, - name: { type: 'string', minLength: 1, maxLength: 255 }, - description: { type: 'string' }, - doNotDisturb: { type: 'boolean' }, - tags: { type: 'string' }, - room: { type: ['number', 'null'] }, - link: { type: 'string' }, - place: { type: 'string' }, - startDate: { type: 'datetime' }, - endDate: { type: 'datetime' }, - maxAttendees: { type: 'integer' } - } - } - } -} diff --git a/src/components/rooms/rawusagedata.ts b/src/components/rooms/rawusagedata.ts index 69a82c4f..688d4d27 100644 --- a/src/components/rooms/rawusagedata.ts +++ b/src/components/rooms/rawusagedata.ts @@ -1,9 +1,7 @@ -import { Group } from '../groups/group' - /** * The number of events in a room on a given day */ -export class RawUsageData extends Group { +export class RawUsageData { room: number day: Date count: number diff --git a/src/components/rooms/room.routes.ts b/src/components/rooms/room.routes.ts index 73a112f6..4d8260d6 100644 --- a/src/components/rooms/room.routes.ts +++ b/src/components/rooms/room.routes.ts @@ -15,7 +15,7 @@ const show = asyncWrapper(async (req: Request, res: Response) => { if (+req.params.id <= 18 && +req.params.id >= 3) { res.render('room/calendar', { room: req.params.id, ROOMS }) } else { - res.redirect('/not-found') + res.status(404).redirect('/not-found') } }) diff --git a/src/components/rooms/room.service.ts b/src/components/rooms/room.service.ts index 2608c448..c0e7f2af 100644 --- a/src/components/rooms/room.service.ts +++ b/src/components/rooms/room.service.ts @@ -1,8 +1,8 @@ import { startOfDay, addDays, addWeeks, differenceInDays } from 'date-fns' -import { Group } from '../groups/group' import { DAYS_OF_WEEK, ROOMS } from '../../util/constants' import { RawUsageData } from './rawusagedata' -import { raw } from 'objection' +import { prisma } from '../../prisma' +import { Group } from '@prisma/client' type ParsedUsageData = Map => { const currentTime = new Date() - return (await Group.query() - .where('startDate', '<', currentTime) - .andWhere('endDate', '>', currentTime)) + return await prisma.group.findMany({ + where: { + startDate: { lt: currentTime }, + endDate: { gt: currentTime } + } + }) } export const getEventsForRoom = async (roomId: number): Promise => - await Group.query().where({ room: roomId }) + await prisma.group.findMany({ where: { room: roomId } }) const fetchUsageData = async (start: Date, end: Date) => { //! unhandled case: // meeting starts before midnight, ends after // The event will not be registrated for the next day - return RawUsageData - .query() - .select('room') - .select({ day: raw('date(start_date)') }) - .count() - .where('endDate', '>=', start) - .andWhere('startDate', '<', end) - .groupBy('room') - .groupBy('day') as Promise + return [] + // TODO + // .query() + // .select('room') + // .select({ day: raw('date(start_date)') }) + // .count() + // .where('endDate', '>=', start) + // .andWhere('startDate', '<', end) + // .groupBy('room') + // .groupBy('day') as Promise } const parseUsageData = (rawData: RawUsageData[], today: Date): ParsedUsageData => { diff --git a/src/components/tickets/ticket.routes.ts b/src/components/tickets/ticket.routes.ts index 3a223594..bf1042a4 100644 --- a/src/components/tickets/ticket.routes.ts +++ b/src/components/tickets/ticket.routes.ts @@ -9,7 +9,7 @@ import { createTicket, sendEmailToTicketAdmins, getOtherTickets, getMyTickets, moveTicket, removeTicket, checkTicketOwner } from './ticket.service' import { handleValidationError } from '../../util/validators' -import { RoleType } from '../users/user' +import { RoleType } from '@prisma/client' const router = Router() @@ -20,6 +20,7 @@ router.get('/', res.render('ticket/index', { otherTickets: req.otherTickets, myTickets: req.myTickets, + RoleType, format, DATE_FORMAT, STATUSES @@ -29,7 +30,7 @@ router.get('/new', isAuthenticated, (_req, res) => res.render('ticket/new', { RO router.put('/:id', isAuthenticated, - requireRoles(RoleType.ADMIN, RoleType.TICKET_ADMIN), + requireRoles(RoleType.Admin, RoleType.TicketAdmin), multer().none(), [ check('status') @@ -61,7 +62,7 @@ router.post('/', router.delete('/:id', isAuthenticated, - requireRoles(RoleType.ADMIN, RoleType.TICKET_ADMIN), + requireRoles(RoleType.Admin, RoleType.TicketAdmin), removeTicket, (_req, res) => res.status(204).send('A hibajegy sikeresen törölve') ) diff --git a/src/components/tickets/ticket.service.ts b/src/components/tickets/ticket.service.ts index ccba89b0..cb20017a 100644 --- a/src/components/tickets/ticket.service.ts +++ b/src/components/tickets/ticket.service.ts @@ -1,17 +1,17 @@ -import { StatusType, Ticket } from './ticket' import { Request, Response, NextFunction } from 'express' import { formatMdToSafeHTML } from '../../util/convertMarkdown' import { asyncWrapper } from '../../util/asyncWrapper' import { sendEmail } from '../../util/sendEmail' -import { RoleType } from '../users/user' - -import { User } from '../users/user' +import { prisma } from '../../prisma' +import { RoleType, StatusType } from '@prisma/client' export const getOtherTickets = asyncWrapper( async (req: Request, _res: Response, next: NextFunction) => { - req.otherTickets = (await Ticket.query().where('userId', '!=', req.user.id) - .orderBy('createdAt', 'ASC')).map(ticket => { + req.otherTickets = (await prisma.ticket.findMany({ + where: { NOT: { ownerId: req.user.id } }, + orderBy: { createdAt: 'asc' } + })).map(ticket => { ticket.description = formatMdToSafeHTML(ticket.description) return ticket }) @@ -20,8 +20,10 @@ export const getOtherTickets = asyncWrapper( export const getMyTickets = asyncWrapper( async (req: Request, _res: Response, next: NextFunction) => { - req.myTickets = (await Ticket.query().where('userId', '=', req.user.id) - .orderBy('createdAt', 'ASC')).map(ticket => { + req.myTickets = (await prisma.ticket.findMany({ + where: { ownerId: req.user.id }, + orderBy: { createdAt: 'asc' } + })).map(ticket => { ticket.description = formatMdToSafeHTML(ticket.description) return ticket }) @@ -30,24 +32,21 @@ export const getMyTickets = asyncWrapper( export const createTicket = asyncWrapper( async (req: Request, _res: Response, next: NextFunction) => { - await Ticket.transaction(async trx => { - return await Ticket.query(trx) - .insert( - { - roomNumber: +req.body.roomNumber, - description: req.body.description, - userId: req.user.id, - } - ) + await prisma.ticket.create({ + data: { + roomNumber: +req.body.roomNumber, + description: req.body.description, + ownerId: req.user.id, + } }) next() }) export const sendEmailToTicketAdmins = asyncWrapper( - async (req: Request, res: Response, next: NextFunction) => { + async (req: Request, res: Response, next: NextFunction) => { const ticket = req.body - const emailRecepients = await User.query().where({ role: RoleType.TICKET_ADMIN }) + const emailRecepients = await prisma.user.findMany({ where: { role: RoleType.TicketAdmin } }) sendEmail(emailRecepients, { subject: `Új hibajegyet vettek fel a ${ticket.roomNumber}. emeleti tanulószobába!`, body: `Új hibajegyet vettek fel a ${ticket.roomNumber}. emeleti tanulószobába! @@ -61,7 +60,10 @@ export const sendEmailToTicketAdmins = asyncWrapper( export const moveTicket = asyncWrapper( async (req: Request, _res: Response, next: NextFunction) => { const id = parseInt(req.params.id) - await Ticket.query().findById(id).patch({ status: req.body.status || StatusType.SENT }) + await prisma.ticket.update({ + where: { id }, + data: { status: req.body.status || StatusType.Sent } + }) next() }) @@ -69,9 +71,9 @@ export const removeTicket = asyncWrapper( async (req: Request, res: Response, next: NextFunction) => { const id = parseInt(req.params.id) if (!isNaN(id)) { - const deletedCount = await Ticket.query().deleteById(id) + const deletedTicket = await prisma.ticket.delete({ where: { id }, select: { id: true } }) - if (deletedCount === 0) { + if (!deletedTicket) { res.status(404).send({ message: 'Nem található hibajegy a megadott ID-val' }) } else { next() @@ -83,8 +85,8 @@ export const removeTicket = asyncWrapper( export const checkTicketOwner = asyncWrapper( async (req: Request, res: Response, next: NextFunction) => { - const ticket = await Ticket.query().findOne({ id: parseInt(req.params.id) }) - if (ticket.userId == req.user.id) { + const ticket = await prisma.ticket.findFirst({ where: { id: parseInt(req.params.id) } }) + if (ticket.ownerId == req.user.id) { next() } else { return res.sendStatus(403) diff --git a/src/components/tickets/ticket.ts b/src/components/tickets/ticket.ts deleted file mode 100644 index 2385055d..00000000 --- a/src/components/tickets/ticket.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Model } from 'objection' - -export enum StatusType { - SENT = 'SENT', - IN_PROGRESS = 'IN_PROGRESS', - DONE = 'DONE', - ARCHIVED ='ARCHIVED' -} - -export class Ticket extends Model { - id!: number - description: string - roomNumber: number - status: StatusType - createdAt: Date - userId: number - - $beforeInsert(): void { - this.createdAt = new Date() - } - - static get tableName(): string { - return 'tickets' - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - static get jsonSchema(): Record { - return { - type: 'object', - required: ['roomNumber', 'description', 'userId'], - - properties: { - id: { type: 'integer' }, - description: { type: 'string' , maxLenght: 500 }, - roomNumber: { type: 'integer' }, - status: { type: 'string' }, - userId: { type: 'integer' }, - } - } - } -} diff --git a/src/components/users/user.routes.ts b/src/components/users/user.routes.ts index 488c5e42..5233bb6c 100644 --- a/src/components/users/user.routes.ts +++ b/src/components/users/user.routes.ts @@ -4,7 +4,7 @@ import { ROLES } from '../../util/constants' import { requireRoles, isAuthenticated } from '../../config/passport' import { handleValidationError, checkIdParam } from '../../util/validators' -import { RoleType } from './user' +import { RoleType } from '@prisma/client' import { isSameUser } from './user.middlewares' import { getUser, updateRole, updateUser } from './user.service' @@ -13,12 +13,13 @@ const router = Router() router.get('/:id', isAuthenticated, checkIdParam, getUser, (req, res) => res.render('user/show', { userToShow: req.userToShow, - ROLES: ROLES + ROLES: ROLES, + RoleType }) ) router.patch('/:id/role', - requireRoles(RoleType.ADMIN), + requireRoles(RoleType.Admin), check('role') .isString() .custom((input) => { diff --git a/src/components/users/user.service.ts b/src/components/users/user.service.ts index 0d00740a..689db200 100644 --- a/src/components/users/user.service.ts +++ b/src/components/users/user.service.ts @@ -1,7 +1,8 @@ import { Request, Response, NextFunction } from 'express' -import { RoleType, User } from './user' import { asyncWrapper } from '../../util/asyncWrapper' +import { prisma } from '../../prisma' +import { Prisma, RoleType, User } from '@prisma/client' interface OAuthUser { displayName: string @@ -10,17 +11,13 @@ interface OAuthUser { } export const getUser = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - const user = await User.query() - .findOne({ id: parseInt(req.params.id) }) - .withGraphFetched('groups(orderByEndDate)') - .modifiers({ - orderByEndDate(builder) { - builder.orderBy('endDate', 'DESC') - } - }) + const user = await prisma.user.findFirst({ + where: { id: parseInt(req.params.id) }, + include: { groups: { include: { group: true } } }, + }) if (!user) { - res.render('error/not-found') + res.status(404).render('error/not-found') } else { req.userToShow = user next() @@ -28,36 +25,40 @@ export const getUser = asyncWrapper(async (req: Request, res: Response, next: Ne }) export const updateRole = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { - const user = await User.query().findOne({ id: parseInt(req.params.id) }) - - if (!user) { - res.redirect('/not-found') - } else { - await User.query() - .patch({ role: req.body.role }) - .where({ id: user.id }) + try { + await prisma.user.update({ + where: { id: parseInt(req.params.id) }, + data: { role: req.body.role }, + select: { id: true }, + }) next() + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === '2025') { + res.status(404).redirect('/not-found') + } else { + throw err + } } }) export const updateUser = asyncWrapper(async (req: Request, res: Response, next: NextFunction) => { const id = req.user.id const { floor, wantEmail } = req.body - req.user = await User.query().patchAndFetchById(id, { floor, wantEmail }) + req.user = await prisma.user.update({ + where: { id }, + data: { floor, wantEmail } + }) next() }) export const createUser = async (user: OAuthUser): Promise => { - return await User.transaction(async trx => { - return await User.query(trx) - .insert( - { - name: user.displayName, - email: user.mail, - authSchId: user.internal_id, - role: RoleType.USER - } - ) + return prisma.user.create({ + data: { + name: user.displayName, + email: user.mail, + authSchId: user.internal_id, + role: RoleType.User + } }) } diff --git a/src/components/users/user.ts b/src/components/users/user.ts deleted file mode 100644 index d0fd1cbc..00000000 --- a/src/components/users/user.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Model } from 'objection' - -import { Group } from '../groups/group' - -export enum RoleType { - ADMIN = 'ADMIN', - TICKET_ADMIN = 'TICKET_ADMIN', - USER = 'USER', -} - - -export class User extends Model { - id!: number - name: string - email: string - authSchId: string - role: RoleType - floor: number - wantEmail: boolean - groups: Group[] - static get tableName(): string { - return 'users' - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - static get relationMappings(): Record { - return { - groups: { - relation: Model.ManyToManyRelation, - modelClass: Group, - - join: { - from: 'users.id', - through: { - from: 'users_groups.userId', - to: 'users_groups.groupId' - }, - to: 'groups.id' - } - } - } - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - static get jsonSchema(): Record { - return { - type: 'object', - required: ['name', 'email', 'authSchId'], - - properties: { - id: { type: 'integer' }, - name: { type: 'string', minLength: 1, maxLength: 255 }, - authSchId: { type: 'string' }, - floor: { type: ['integer', 'null'] }, - wantEmail: { type: 'boolean'} - } - } - } -} diff --git a/src/config/passport.ts b/src/config/passport.ts index ff2abbc4..3103e51e 100644 --- a/src/config/passport.ts +++ b/src/config/passport.ts @@ -1,10 +1,11 @@ +import { RoleType, User } from '@prisma/client' import { NextFunction, Request, Response } from 'express' import fetch from 'node-fetch' import passport from 'passport' import { Strategy } from 'passport-oauth2' -import { RoleType, User } from '../components/users/user' import { createUser } from '../components/users/user.service' +import { prisma } from '../prisma' const AUTH_SCH_URL = 'https://auth.sch.bme.hu' @@ -31,7 +32,7 @@ passport.use( `${AUTH_SCH_URL}/api/profile?access_token=${accessToken}` ).then(res => res.json()) - const user = await User.query().findOne({ authSchId: responseUser.internal_id }) + const user = await prisma.user.findFirst({ where: { authSchId: responseUser.internal_id } }) if (user) { done(null, user) @@ -48,7 +49,7 @@ passport.serializeUser((user: User, done) => { }) passport.deserializeUser(async (id: number, done) => { - const user = await User.query().findOne({ id }) + const user = await prisma.user.findFirst({ where: { id } }) done(null, user) }) @@ -56,22 +57,22 @@ passport.deserializeUser(async (id: number, done) => { * Login Required middleware. */ export const isAuthenticated = -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -(req: Request, res: Response, next: NextFunction): Response> => { - const contentType = req.headers['content-type'] + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (req: Request, res: Response, next: NextFunction): Response> => { + const contentType = req.headers['content-type'] - if (req.isAuthenticated()) { - next() - } else { - if ((contentType && - (contentType.indexOf('application/json') !== 0 || - contentType.indexOf('multipart/form-data') !== 0)) || - req.method !== 'GET') { - return res.sendStatus(401) + if (req.isAuthenticated()) { + next() + } else { + if ((contentType && + (contentType.indexOf('application/json') !== 0 || + contentType.indexOf('multipart/form-data') !== 0)) || + req.method !== 'GET') { + return res.sendStatus(401) + } + res.render('error/not-authenticated') } - res.render('error/not-authenticated') } -} /** * Authorization Required middleware. diff --git a/src/prisma.ts b/src/prisma.ts new file mode 100644 index 00000000..65fa0560 --- /dev/null +++ b/src/prisma.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from '@prisma/client' + +export const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'production' ? + ['info', 'warn', 'error'] : + ['query', 'info', 'warn', 'error'] +}) diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 3e81997f..1e05100f 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,7 +1,5 @@ -import { Group } from '../components/groups/group' -import { Ticket } from '../components/tickets/ticket' -import { User as LocalUser } from '../components/users/user' import { PaginationOptions } from '../components/groups/paginationOptions' +import { Prisma, PrismaClient, User as LocalUser, Group, Ticket, Membership } from '@prisma/client'; declare global { namespace Express { @@ -10,11 +8,13 @@ declare global { myTickets: Ticket[] groups: Group[] paginationOptions: PaginationOptions - group: Group + group: Group & { + users: Membership[]; + } userToShow: LocalUser } - interface User extends LocalUser {} + interface User extends LocalUser { } } } diff --git a/src/util/constants.ts b/src/util/constants.ts index 8d63bbaa..b6368aad 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -1,16 +1,18 @@ +import { StatusType, RoleType } from '@prisma/client' + export const ROOMS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] as const export const DAYS_OF_WEEK = ['Va', 'Hé', 'Ke', 'Sze', 'Csü', 'Pé', 'Szo'] as const export const DATE_FORMAT = 'yyyy-MM-dd HH:mm' -export const STATUSES = new Map([ - ['SENT', 'Elküldve'], - ['IN_PROGRESS', 'Folyamatban'], - ['DONE', 'Kész'], - ['ARCHIVED', 'Archiválva'] +export const STATUSES = new Map([ + [StatusType.Sent, 'Elküldve'], + [StatusType.InProgress, 'Folyamatban'], + [StatusType.Done, 'Kész'], + [StatusType.Archived, 'Archiválva'] ]) -export const ROLES = new Map([ - ['ADMIN', 'Admin'], - ['TICKET_ADMIN', 'Hibajegy admin'], - ['USER', 'Felhasználó'], +export const ROLES = new Map([ + [RoleType.Admin, 'Admin'], + [RoleType.TicketAdmin, 'Hibajegy admin'], + [RoleType.User, 'Felhasználó'], ]) diff --git a/src/util/emailTemplate.ts b/src/util/emailTemplate.ts index b718de8b..3c2ba132 100644 --- a/src/util/emailTemplate.ts +++ b/src/util/emailTemplate.ts @@ -1,5 +1,5 @@ +import { User } from '@prisma/client' import { Email } from './sendEmail' -import { User } from '../components/users/user' const styles = { body: `"min-height: 50vh; diff --git a/src/util/sendEmail.ts b/src/util/sendEmail.ts index c136417a..2df44123 100644 --- a/src/util/sendEmail.ts +++ b/src/util/sendEmail.ts @@ -1,5 +1,5 @@ +import { User } from '@prisma/client' import transporter from '../config/email' -import { User } from '../components/users/user' import { generateEmailHTML } from './emailTemplate' export interface Email { diff --git a/src/util/validators.ts b/src/util/validators.ts index cab0d33b..3554ec0c 100644 --- a/src/util/validators.ts +++ b/src/util/validators.ts @@ -20,7 +20,7 @@ export function handleValidationError(statusCode: number): ExpressMiddleware { export function checkIdParam(req: Request, res: Response, next: NextFunction): void { if (isNaN(parseInt(req.params.id))) { - res.render('error/not-found') + res.status(404).render('error/not-found') } else { next() } diff --git a/views/group/show.pug b/views/group/show.pug index 5b8731bf..0045b85d 100644 --- a/views/group/show.pug +++ b/views/group/show.pug @@ -105,7 +105,7 @@ block content svg(xmlns='http://www.w3.org/2000/svg' alt="Más hely" aria-label="Más hely" fill='none' viewbox='0 0 24 24' stroke='currentColor' class='w-6 h-6') path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z') span(class='text-lg sm:text-xl')= group.place - + if startDate == endDate li(class='flex flex-row items-center space-x-2') //- Heroicon name: calendar @@ -152,7 +152,7 @@ block content div(class='mt-4 mr-10 lg:pl-10 lg:border-l lg:mt-0') h2(class='mb-3 text-3xl uppercase') Résztvevők div(class='flex flex-col items-start space-y-2 text-xl') - each user in group.users + each user in group.users.map(x => x.user) div(class='w-full flex flex-row justify-between items-center ml-6 space-x-2') a(href=`/users/${user.id}` class='lg:whitespace-no-wrap hover:text-blue-500')= user.name if user.id === group.ownerId diff --git a/views/ticket/ticketTemplate.pug b/views/ticket/ticketTemplate.pug index 0bb3338a..eb01a6ca 100644 --- a/views/ticket/ticketTemplate.pug +++ b/views/ticket/ticketTemplate.pug @@ -9,18 +9,18 @@ mixin ticketTemplate(ticket, format, DATE_FORMAT, STATUSES, own) div(class='flex items-center space-x-4') h2(class='text-2xl sm:xt-3xl') #{ticket.roomNumber}. emelet span(id=`ticket-label-${ticket.id}` class='px-2 py-1 text-sm font-bold text-gray-600 border-2 border-gray-500 rounded-lg dark:text-gray-400') #{STATUSES.get(ticket.status) ? STATUSES.get(ticket.status): 'Ismeretlen'} - if (user.role === "ADMIN" || user.role === "TICKET_ADMIN" || user.id == ticket.userId) + if (user.role === RoleType.Admin || user.role === RoleType.TicketAdmin || user.id == ticket.ownerId) a(type="button", onclick=`toggleModal('deleteTicket(${ticket.id}, ${own})')`) svg(xmlns='http://www.w3.org/2000/svg' alt="Törlés" aria-label="Törlés" fill='none' viewbox='0 0 24 24' stroke='currentColor' class='w-6 h-6 text-red-500 cursor-pointer') path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16') div(class='ml-8 text-xl break-words') !{ticket.description} - div(class='flex flex-row flex-wrap items-center mt-3' :class="{ 'justify-between': user.role === 'ADMIN', 'justify-around': user.role !== 'ADMIN' }") + div(class='flex flex-row flex-wrap items-center mt-3' :class=`{ 'justify-between': user.role === '${RoleType.Admin}', 'justify-around': user.role !== '${RoleType.Admin}' }`) div(class='flex flex-row items-center mb-2 space-x-2') //- Heroicon name: paper-airplane svg(xmlns='http://www.w3.org/2000/svg' alt="Küldve" aria-label="Küldve" fill='none' viewbox='0 0 24 24' stroke='currentColor' class='w-6 h-6') path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 19l9 2-9-18-9 18 9-2zm0 0v-8') time= format(ticket.createdAt, DATE_FORMAT) - if (user.role === "ADMIN" || user.role === "TICKET_ADMIN") + if (user.role === RoleType.Admin || user.role === RoleType.TicketAdmin) form(id=`ticket-form-${ticket.id}`) select(name="status", id="status", selected=ticket.status, class='rounded form-element', onChange =`moveTicket(${ticket.id})`) each key in [...STATUSES.keys()] diff --git a/views/user/show.pug b/views/user/show.pug index 444ea92e..8c7a95b7 100644 --- a/views/user/show.pug +++ b/views/user/show.pug @@ -14,7 +14,7 @@ block content input(type="checkbox" class="rounded" checked=user.wantEmail id="emailCheckbox" name="emailCheckbox") label(for="emailCheckbox") Email értesítések button(onclick=`updateUser(${user.id})` class='btn btn-primary animate-hover') Mentés - if user.role == 'ADMIN' + if user.role === RoleType.Admin div h2(class='mb-2') Felhasználó jogainak beállítása form(action=`/users/${userToShow.id}/role`, method="post") @@ -27,10 +27,11 @@ block content - const isOld = (element) => element.endDate <= Date.now() div(class='space-y-2') - if userToShow.groups.some(isUpcoming) + - groups = userToShow.groups.map(x => x.group) + if groups.some(isUpcoming) h3(class='mb-1 text-lg') Közelgő csoportesemények div(class='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4') - each group in userToShow.groups.filter(isUpcoming) + each group in groups.filter(isUpcoming) a(href=`/groups/${group.id}` class='transition duration-300 ease-in-out transform hover:-translate-y-2') div(class='flex items-center justify-center p-6 border rounded-md dark:border-gray-500 hover:border-gray-400 animate-hover dark:hover:border-gray-300') div(class='flex flex-row items-center') @@ -40,10 +41,10 @@ block content svg(xmlns="http://www.w3.org/2000/svg" alt="Saját esemény" aria-label="Saját esemény" viewbox="0 0 20 20" fill="currentColor" class="w-4 h-4 ml-1") path(fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd") - if userToShow.groups.some(isOld) + if groups.some(isOld) h3(class='mb-2 text-lg') Elmúlt csoportesemények div(class='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4') - each group in userToShow.groups.filter(isOld) + each group in groups.filter(isOld) a(href=`/groups/${group.id}` class='transition duration-300 ease-in-out transform hover:-translate-y-2') div(class='flex items-center justify-center p-6 border rounded-md dark:border-gray-500 hover:border-gray-400 animate-hover dark:hover:border-gray-300') div(class='flex flex-row items-center') diff --git a/yarn.lock b/yarn.lock index c571898a..ea3e437b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -168,6 +168,18 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@prisma/client@^2.30.0": + version "2.30.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.30.0.tgz#b0ed9db67405f619e428577f2d45843104142e00" + integrity sha512-tjJNHVfgyNOwS2F+AkjMMCJGPnXzHuUCrOnAMJyidAu4aNzxbJ8jWwjt96rRMpyrg9Hwen3xqqQ2oA+ikK7nhQ== + dependencies: + "@prisma/engines-version" "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb" + +"@prisma/engines-version@2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb": + version "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb.tgz#1360113dc19e1d43d4442e3b638ccfa0e1711943" + integrity sha512-oThNpx7HtJ0eEmnvrWARYcNCs6dqFdAK3Smt2bJVDD6Go4HLuuhjx028osP+rHaFrGOTx7OslLZYtvvFlAXRDA== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"