diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 163416f9..fe06a86e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,9 +1,13 @@ FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:22-bullseye +RUN apt-get update && \ + apt-get install -y \ + socat + # pnpm ENV PNPM_HOME=/workspaces/pnpm ENV PATH="$PNPM_HOME:$PATH" -RUN su node -c "npm install -g pnpm@9.15.2" +RUN su node -c "npm install -g pnpm@10.24.0" RUN corepack enable ENV TZ=Europe/Berlin diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 59221fec..5d01109d 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -10,9 +10,3 @@ services: # Overrides default command so things don't shut down after the process ends. command: sleep infinity - - # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. - network_mode: service:postgres - - # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7839222e..0a17c958 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -32,6 +32,9 @@ "options": { "statusbar": { "hide": true + }, + "env": { + "NODE_ENV": "development" } } }, @@ -86,20 +89,34 @@ } } }, + { + "type": "shell", + "command": "socat TCP-LISTEN:6060,fork TCP:keycloak:6060", + "label": "proxy:keycloak", + "problemMatcher": [], + "presentation": { + "reveal": "silent" + }, + "options": { + "statusbar": { + "hide": true + } + } + }, { "label": "start:apps", "dependsOn": [ "start:api", - "start:frontend" + "start:frontend", + "proxy:keycloak" ], "problemMatcher": [] }, { "label": "start:apps and services", "dependsOn": [ - "start:api", - "start:frontend", - "start:services" + "start:apps", + "start:services", ], "problemMatcher": [], "options": { diff --git a/Dockerfile b/Dockerfile index 0c10f120..2251e120 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.14.2-alpine3.16 AS workspace-base +FROM node:22.20.0-alpine3.22 AS workspace-base RUN apk add --no-cache bash curl jq diff --git a/apps/api/config/.gitignore b/apps/api/config/.gitignore new file mode 100644 index 00000000..5826a3ae --- /dev/null +++ b/apps/api/config/.gitignore @@ -0,0 +1 @@ +local.json diff --git a/apps/api/config/custom-environment-variables.json b/apps/api/config/custom-environment-variables.json index bee9cdf3..d6cf2ef1 100644 --- a/apps/api/config/custom-environment-variables.json +++ b/apps/api/config/custom-environment-variables.json @@ -1,28 +1,38 @@ { + "clientUrl": "CLIENT_URL", + "server": { "host": "SERVER_HOST", "port": "SERVER_PORT" }, - "loggingLevel": "LOGGING_LEVEL", - - "clientUrl": "CLIENT_URL", - "db": { "url": "DATABASE_URL" }, "authentication": { - "secret": "AUTHENTICATION_SECRET" + "secret": "AUTHENTICATION_SECRET", + "expiresIn": "AUTHENTICATION_EXPIRES_IN", + "dlrg": { + "issuer": "AUTHENTICATION_DLRG_ISSUER", + "clientId": "AUTHENTICATION_DLRG_CLIENTID", + "clientSecret": "AUTHENTICATION_DLRG_CLIENTSECRET", + "allowInsecure": "AUTHENTICATION_DLRG_ALLOWINSECURE" + } }, + "mail": { "sendMails": "MAIL_SENDMAILS", "sendgridApiKey": "MAIL_SENDGRID_APIKEY" }, + + "loggingLevel": "LOGGING_LEVEL", + "meilisearch": { "host": "MEILISEARCH_HOST", "apiKey": "MEILISEARCH_KEY" }, + "fileDefaultProvider": "FILE_DEFAULT_PROVIDER", "fileProviders": { "LOCAL": { @@ -35,9 +45,11 @@ "folder": "FILE_PROVIDER_AZURE_FOLDER" } }, + "tomtom": { "apiKey": "TOMTOM_APIKEY" }, + "public": { "legal": { "imprint": "PUBLIC_LEGAL_IMPRINT", diff --git a/apps/api/config/default.json b/apps/api/config/default.json index 44b94cf4..ca84a59c 100644 --- a/apps/api/config/default.json +++ b/apps/api/config/default.json @@ -10,7 +10,7 @@ "secret": "secret", "expiresIn": "1d", "dlrg": { - "client_id": "1200000-0" + "allowInsecure": false } }, @@ -48,8 +48,8 @@ }, "public": { "legal": { - "imprint": "https://localhost:8080", - "privacy": "https://localhost:8080" + "imprint": "https://sh.dlrg.de/index.php?id=457933&L=0", + "privacy": "https://sh.dlrg.de/impressum-und-datenschutz/" } }, diff --git a/apps/api/config/development.json b/apps/api/config/development.json new file mode 100644 index 00000000..023aa85c --- /dev/null +++ b/apps/api/config/development.json @@ -0,0 +1,12 @@ +{ + "loggingLevel": "debug", + + "authentication": { + "dlrg": { + "issuer": "http://localhost:6060/realms/dlrg", + "clientId": "brahmsee.digital", + "clientSecret": "", + "allowInsecure": true + } + } +} diff --git a/apps/api/email/account-password-reset-oauth.mjml b/apps/api/email/account-password-reset-oauth.mjml new file mode 100644 index 00000000..1cffc8a3 --- /dev/null +++ b/apps/api/email/account-password-reset-oauth.mjml @@ -0,0 +1,17 @@ +{{#> layout }} + + + Wir haben eine Anfrage erhalten, das Passwort für deinen Account bei {{ hostname }} zurückzusetzen. + + + + Dein Account ist mit dem ISC verbunden. Nutze auf der Login-Seite den "DLRG|ISC" Link zum anmelden. + + +{{/layout}} diff --git a/apps/api/package.json b/apps/api/package.json index d3e6cfdd..d4815d83 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,43 +27,36 @@ "seed": "tsx prisma/seeders/index.ts" }, "dependencies": { - "@azure/storage-blob": "^12.17.0", + "@azure/storage-blob": "catalog:", "@codeanker/authentication": "workspace:*", - "@codeanker/datagrid": "file:../../vendor/codeanker-datagrid-2.7.1-trimmed.tgz", "@codeanker/helpers": "workspace:*", "@codeanker/service-sms": "file:../../vendor/codeanker-service-sms-0.0.2.tar.gz", "@e965/xlsx": "^0.20.3", - "@faker-js/faker": "^9.4.0", - "@koa/cors": "^5.0.0", - "@koa/router": "^12.0.1", + "@faker-js/faker": "catalog:", + "@hono/node-server": "^1.19.6", + "@hono/trpc-server": "^0.4.0", "@prisma/client": "^5.19.1", "@prisma/extension-accelerate": "^1.1.0", "@sendgrid/mail": "^8.1.0", "@trpc/server": "catalog:", "archiver": "^7.0.1", - "axios": "^1.9.0", "config": "^3.3.9", "dayjs": "^1.11.10", "dot-prop": "^9.0.0", "fast-csv": "^5.0.1", - "grant": "^5.4.22", "handlebars": "^4.7.8", + "hono": "^4.10.7", + "http-errors": "^2.0.1", "jsonwebtoken": "^9.0.2", - "koa": "^2.16.1", - "koa-body": "^6.0.1", - "koa-helmet": "^7.0.2", - "koa-router": "^13.0.1", - "koa-session": "^6.4.0", - "koa-static": "^5.0.0", "meilisearch": "^0.37.0", "mime": "^4.0.6", "mjml": "^4.15.3", + "oauth4webapi": "^3.8.3", "prom-client": "^15.0.0", "superjson": "catalog:", - "trpc-koa-adapter": "^1.1.3", "uuid": "^11.0.5", "winston": "^3.11.0", - "zod": "^3.22.4" + "zod": "catalog:" }, "devDependencies": { "@codeanker/eslint-config": "workspace:*", @@ -71,11 +64,9 @@ "@inquirer/prompts": "^7.1.0", "@types/archiver": "^6.0.3", "@types/config": "^3.3.3", + "@types/http-errors": "^2.0.5", "@types/http-status-codes": "^1.2.0", "@types/jsonwebtoken": "^9.0.8", - "@types/koa": "^2.14.0", - "@types/koa-bodyparser": "^4.3.12", - "@types/koa-router": "^7.4.8", "@types/mjml": "^4.7.4", "@types/node": "catalog:", "commander": "^13.0.0", diff --git a/apps/api/prisma/migrations/20240907155549_rename_address_schema/migration.sql b/apps/api/prisma/migrations/20240907155549_rename_address_schema/migration.sql deleted file mode 100644 index 4b7b39ed..00000000 --- a/apps/api/prisma/migrations/20240907155549_rename_address_schema/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `number` on the `Address` table. All the data in the column will be lost. - - Added the required column `streetNumber` to the `Address` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Address" DROP COLUMN "number", -ADD COLUMN "lat" TEXT, -ADD COLUMN "lon" TEXT, -ADD COLUMN "streetNumber" TEXT NOT NULL; diff --git a/apps/api/prisma/migrations/20240907172928_add_field_to_address/migration.sql b/apps/api/prisma/migrations/20240907172928_add_field_to_address/migration.sql deleted file mode 100644 index f0246974..00000000 --- a/apps/api/prisma/migrations/20240907172928_add_field_to_address/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - Added the required column `country` to the `Address` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Address" ADD COLUMN "country" TEXT NOT NULL, -ADD COLUMN "valid" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/api/prisma/migrations/20240907174812_edit_fields_address/migration.sql b/apps/api/prisma/migrations/20240907174812_edit_fields_address/migration.sql deleted file mode 100644 index b7fa33f7..00000000 --- a/apps/api/prisma/migrations/20240907174812_edit_fields_address/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - The `lat` column on the `Address` table would be dropped and recreated. This will lead to data loss if there is data in the column. - - The `lon` column on the `Address` table would be dropped and recreated. This will lead to data loss if there is data in the column. - -*/ --- AlterTable -ALTER TABLE "Address" DROP COLUMN "lat", -ADD COLUMN "lat" DOUBLE PRECISION, -DROP COLUMN "lon", -ADD COLUMN "lon" DOUBLE PRECISION; diff --git a/apps/api/prisma/migrations/20241118163314_init_file/migration.sql b/apps/api/prisma/migrations/20241118163314_init_file/migration.sql deleted file mode 100644 index 33e407ff..00000000 --- a/apps/api/prisma/migrations/20241118163314_init_file/migration.sql +++ /dev/null @@ -1,42 +0,0 @@ --- CreateEnum -CREATE TYPE "FileProvider" AS ENUM ('LOCAL', 'AZURE'); - --- CreateTable -CREATE TABLE "File" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "uploaded" BOOLEAN NOT NULL DEFAULT false, - "uploadedAt" TIMESTAMP(3), - "provider" "FileProvider" NOT NULL, - "key" TEXT NOT NULL, - "filename" TEXT, - "mimetype" TEXT, - - CONSTRAINT "File_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UnterveranstaltungDocument" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "unterveranstaltungId" INTEGER NOT NULL, - "fileId" TEXT NOT NULL, - - CONSTRAINT "UnterveranstaltungDocument_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "File_id_key" ON "File"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "File_key_key" ON "File"("key"); - --- CreateIndex -CREATE UNIQUE INDEX "UnterveranstaltungDocument_fileId_key" ON "UnterveranstaltungDocument"("fileId"); - --- AddForeignKey -ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20241229191039_person_photo/migration.sql b/apps/api/prisma/migrations/20241229191039_person_photo/migration.sql deleted file mode 100644 index e121de46..00000000 --- a/apps/api/prisma/migrations/20241229191039_person_photo/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "Person" ADD COLUMN "photoId" TEXT; - --- AddForeignKey -ALTER TABLE "Person" ADD CONSTRAINT "Person_photoId_fkey" FOREIGN KEY ("photoId") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20250115215644_change_subject_id_type/migration.sql b/apps/api/prisma/migrations/20250115215644_change_subject_id_type/migration.sql deleted file mode 100644 index e1f6cd1f..00000000 --- a/apps/api/prisma/migrations/20250115215644_change_subject_id_type/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Activity" ALTER COLUMN "subjectId" SET DATA TYPE TEXT; diff --git a/apps/api/prisma/migrations/20250201150234_multiple_addresses_to_schema/migration.sql b/apps/api/prisma/migrations/20250201150234_multiple_addresses_to_schema/migration.sql deleted file mode 100644 index 6f02445b..00000000 --- a/apps/api/prisma/migrations/20250201150234_multiple_addresses_to_schema/migration.sql +++ /dev/null @@ -1,53 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `tshirtBestellt` on the `Anmeldung` table. All the data in the column will be lost. - - You are about to drop the column `konfektionsgroesse` on the `Person` table. All the data in the column will be lost. - - You are about to drop the column `qualifikationenErsteHilfe` on the `Person` table. All the data in the column will be lost. - - You are about to drop the column `qualifikationenFahrerlaubnis` on the `Person` table. All the data in the column will be lost. - - You are about to drop the column `qualifikationenFunk` on the `Person` table. All the data in the column will be lost. - - You are about to drop the column `qualifikationenSanitaeter` on the `Person` table. All the data in the column will be lost. - - You are about to drop the column `qualifikationenSchwimmer` on the `Person` table. All the data in the column will be lost. - -*/ --- AlterEnum -ALTER TYPE "Role" ADD VALUE 'USER'; - --- AlterTable -ALTER TABLE "Anmeldung" DROP COLUMN "tshirtBestellt", -ADD COLUMN "accountId" INTEGER; - --- AlterTable -ALTER TABLE "Person" DROP COLUMN "konfektionsgroesse", -DROP COLUMN "qualifikationenErsteHilfe", -DROP COLUMN "qualifikationenFahrerlaubnis", -DROP COLUMN "qualifikationenFunk", -DROP COLUMN "qualifikationenSanitaeter", -DROP COLUMN "qualifikationenSchwimmer"; - --- AlterTable -ALTER TABLE "_AnmeldungToMahlzeit" ADD CONSTRAINT "_AnmeldungToMahlzeit_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "_AnmeldungToMahlzeit_AB_unique"; - --- DropEnum -DROP TYPE "Konfektionsgroesse"; - --- DropEnum -DROP TYPE "QualificationErsteHilfe"; - --- DropEnum -DROP TYPE "QualificationFahrerlaubnis"; - --- DropEnum -DROP TYPE "QualificationFunk"; - --- DropEnum -DROP TYPE "QualificationSanitaeter"; - --- DropEnum -DROP TYPE "QualificationSchwimmer"; - --- AddForeignKey -ALTER TABLE "Anmeldung" ADD CONSTRAINT "Anmeldung_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20250201154538_anmeldung_linkage/migration.sql b/apps/api/prisma/migrations/20250201154538_anmeldung_linkage/migration.sql deleted file mode 100644 index efe2b687..00000000 --- a/apps/api/prisma/migrations/20250201154538_anmeldung_linkage/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Anmeldung" ADD COLUMN "assignmentCode" UUID; diff --git a/apps/api/prisma/migrations/20250203181204_public_landing/migration.sql b/apps/api/prisma/migrations/20250203181204_public_landing/migration.sql deleted file mode 100644 index 02012517..00000000 --- a/apps/api/prisma/migrations/20250203181204_public_landing/migration.sql +++ /dev/null @@ -1,104 +0,0 @@ --- DropForeignKey -ALTER TABLE "Person" DROP CONSTRAINT "Person_photoId_fkey"; - --- CreateTable -CREATE TABLE "faq_categories" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "unterveranstaltungId" INTEGER NOT NULL, - - CONSTRAINT "faq_categories_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "faqs" ( - "id" SERIAL NOT NULL, - "question" TEXT NOT NULL, - "answer" TEXT NOT NULL, - "categoryId" INTEGER NOT NULL, - - CONSTRAINT "faqs_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UnterveranstaltungLandingSettings" ( - "unterveranstaltungId" SERIAL NOT NULL, - "heroTitle" TEXT NOT NULL, - "heroSubtitle" TEXT NOT NULL, - "eventDetailsTitle" TEXT NOT NULL, - "eventDetailsContent" TEXT NOT NULL, - "miscellaneousVisible" BOOLEAN, - "miscellaneousTitle" TEXT, - "faqVisible" BOOLEAN, - "faqEmail" TEXT, - "instagramVisible" BOOLEAN, - "instagramUrl" TEXT, - "facebookVisible" BOOLEAN, - "facebookUrl" TEXT, - - CONSTRAINT "UnterveranstaltungLandingSettings_pkey" PRIMARY KEY ("unterveranstaltungId") -); - --- CreateTable -CREATE TABLE "UnterveranstaltungLandingImages" ( - "id" SERIAL NOT NULL, - "name" TEXT, - "unterveranstaltungLandingSettingsId" INTEGER, - "fileId" TEXT NOT NULL, - - CONSTRAINT "UnterveranstaltungLandingImages_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UnterveranstaltungLandingMiscellaneous" ( - "id" SERIAL NOT NULL, - "title" TEXT NOT NULL, - "content" TEXT NOT NULL, - "unterveranstaltungLandingSettingsId" INTEGER, - - CONSTRAINT "UnterveranstaltungLandingMiscellaneous_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "_FaqToUnterveranstaltung" ( - "A" INTEGER NOT NULL, - "B" INTEGER NOT NULL, - - CONSTRAINT "_FaqToUnterveranstaltung_AB_pkey" PRIMARY KEY ("A","B") -); - --- CreateIndex -CREATE UNIQUE INDEX "faq_categories_name_unterveranstaltungId_key" ON "faq_categories"("name", "unterveranstaltungId"); - --- CreateIndex -CREATE UNIQUE INDEX "UnterveranstaltungLandingImages_fileId_key" ON "UnterveranstaltungLandingImages"("fileId"); - --- CreateIndex -CREATE INDEX "_FaqToUnterveranstaltung_B_index" ON "_FaqToUnterveranstaltung"("B"); - --- AddForeignKey -ALTER TABLE "faq_categories" ADD CONSTRAINT "faq_categories_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "faqs" ADD CONSTRAINT "faqs_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "faq_categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Person" ADD CONSTRAINT "Person_photoId_fkey" FOREIGN KEY ("photoId") REFERENCES "File"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UnterveranstaltungLandingSettings" ADD CONSTRAINT "UnterveranstaltungLandingSettings_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UnterveranstaltungLandingImages" ADD CONSTRAINT "UnterveranstaltungLandingImages_unterveranstaltungLandingS_fkey" FOREIGN KEY ("unterveranstaltungLandingSettingsId") REFERENCES "UnterveranstaltungLandingSettings"("unterveranstaltungId") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UnterveranstaltungLandingImages" ADD CONSTRAINT "UnterveranstaltungLandingImages_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UnterveranstaltungLandingMiscellaneous" ADD CONSTRAINT "UnterveranstaltungLandingMiscellaneous_unterveranstaltungL_fkey" FOREIGN KEY ("unterveranstaltungLandingSettingsId") REFERENCES "UnterveranstaltungLandingSettings"("unterveranstaltungId") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_FaqToUnterveranstaltung" ADD CONSTRAINT "_FaqToUnterveranstaltung_A_fkey" FOREIGN KEY ("A") REFERENCES "faqs"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_FaqToUnterveranstaltung" ADD CONSTRAINT "_FaqToUnterveranstaltung_B_fkey" FOREIGN KEY ("B") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20250204082400_edit_landing_relation/migration.sql b/apps/api/prisma/migrations/20250204082400_edit_landing_relation/migration.sql deleted file mode 100644 index 3f43a2b4..00000000 --- a/apps/api/prisma/migrations/20250204082400_edit_landing_relation/migration.sql +++ /dev/null @@ -1,34 +0,0 @@ -/* - Warnings: - - - The primary key for the `_AnmeldungToMahlzeit` table will be changed. If it partially fails, the table could be left without primary key constraint. - - The primary key for the `_FaqToUnterveranstaltung` table will be changed. If it partially fails, the table could be left without primary key constraint. - - A unique constraint covering the columns `[landingSettingsId]` on the table `Unterveranstaltung` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[A,B]` on the table `_AnmeldungToMahlzeit` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[A,B]` on the table `_FaqToUnterveranstaltung` will be added. If there are existing duplicate values, this will fail. - - Added the required column `landingSettingsId` to the `Unterveranstaltung` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "UnterveranstaltungLandingSettings" DROP CONSTRAINT "UnterveranstaltungLandingSettings_unterveranstaltungId_fkey"; - --- AlterTable -ALTER TABLE "Unterveranstaltung" ADD COLUMN "landingSettingsId" INTEGER NOT NULL; - --- AlterTable -ALTER TABLE "_AnmeldungToMahlzeit" DROP CONSTRAINT "_AnmeldungToMahlzeit_AB_pkey"; - --- AlterTable -ALTER TABLE "_FaqToUnterveranstaltung" DROP CONSTRAINT "_FaqToUnterveranstaltung_AB_pkey"; - --- CreateIndex -CREATE UNIQUE INDEX "Unterveranstaltung_landingSettingsId_key" ON "Unterveranstaltung"("landingSettingsId"); - --- CreateIndex -CREATE UNIQUE INDEX "_AnmeldungToMahlzeit_AB_unique" ON "_AnmeldungToMahlzeit"("A", "B"); - --- CreateIndex -CREATE UNIQUE INDEX "_FaqToUnterveranstaltung_AB_unique" ON "_FaqToUnterveranstaltung"("A", "B"); - --- AddForeignKey -ALTER TABLE "Unterveranstaltung" ADD CONSTRAINT "Unterveranstaltung_landingSettingsId_fkey" FOREIGN KEY ("landingSettingsId") REFERENCES "UnterveranstaltungLandingSettings"("unterveranstaltungId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20250204090657_add_subtitle/migration.sql b/apps/api/prisma/migrations/20250204090657_add_subtitle/migration.sql deleted file mode 100644 index 9f64f091..00000000 --- a/apps/api/prisma/migrations/20250204090657_add_subtitle/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "UnterveranstaltungLandingSettings" ADD COLUMN "miscellaneousSubtitle" TEXT; diff --git a/apps/api/prisma/migrations/20250209183036_make_landingsettingsid_optional/migration.sql b/apps/api/prisma/migrations/20250209183036_make_landingsettingsid_optional/migration.sql deleted file mode 100644 index 7965b4fb..00000000 --- a/apps/api/prisma/migrations/20250209183036_make_landingsettingsid_optional/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- AlterTable -ALTER TABLE "Unterveranstaltung" ALTER COLUMN "landingSettingsId" DROP NOT NULL; - --- AlterTable -ALTER TABLE "_AnmeldungToMahlzeit" ADD CONSTRAINT "_AnmeldungToMahlzeit_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "_AnmeldungToMahlzeit_AB_unique"; - --- AlterTable -ALTER TABLE "_FaqToUnterveranstaltung" ADD CONSTRAINT "_FaqToUnterveranstaltung_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "_FaqToUnterveranstaltung_AB_unique"; diff --git a/apps/api/prisma/migrations/20250210192033_anmeldung_access_token/migration.sql b/apps/api/prisma/migrations/20250210192033_anmeldung_access_token/migration.sql deleted file mode 100644 index bc485b8c..00000000 --- a/apps/api/prisma/migrations/20250210192033_anmeldung_access_token/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Anmeldung" ADD COLUMN "accessToken" UUID; diff --git a/apps/api/prisma/migrations/20250322120939_gliederung_unterveranstaltung_unique/migration.sql b/apps/api/prisma/migrations/20250322120939_gliederung_unterveranstaltung_unique/migration.sql deleted file mode 100644 index 1bbc2485..00000000 --- a/apps/api/prisma/migrations/20250322120939_gliederung_unterveranstaltung_unique/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[veranstaltungId,gliederungId]` on the table `Unterveranstaltung` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "Unterveranstaltung_veranstaltungId_gliederungId_key" ON "Unterveranstaltung"("veranstaltungId", "gliederungId"); diff --git a/apps/api/prisma/migrations/20250323092444_programm/migration.sql b/apps/api/prisma/migrations/20250323092444_programm/migration.sql deleted file mode 100644 index efa609ec..00000000 --- a/apps/api/prisma/migrations/20250323092444_programm/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ --- CreateTable -CREATE TABLE "ProgrammPunkt" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "startingAt" TIMESTAMP(3) NOT NULL, - "endingAt" TIMESTAMP(3) NOT NULL, - "location" TEXT NOT NULL, - "responsible" TEXT NOT NULL, - "veranstaltungId" INTEGER NOT NULL, - - CONSTRAINT "ProgrammPunkt_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "ProgrammPunkt" ADD CONSTRAINT "ProgrammPunkt_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20250323104704_drop_unique_index/migration.sql b/apps/api/prisma/migrations/20250323104704_drop_unique_index/migration.sql deleted file mode 100644 index 0a5abab2..00000000 --- a/apps/api/prisma/migrations/20250323104704_drop_unique_index/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- DropIndex -DROP INDEX "Unterveranstaltung_veranstaltungId_gliederungId_key"; diff --git a/apps/api/prisma/migrations/20250412132545_anmeldelink/migration.sql b/apps/api/prisma/migrations/20250412132545_anmeldelink/migration.sql deleted file mode 100644 index a7b96501..00000000 --- a/apps/api/prisma/migrations/20250412132545_anmeldelink/migration.sql +++ /dev/null @@ -1,45 +0,0 @@ -/* - Warnings: - - - The primary key for the `_AnmeldungToMahlzeit` table will be changed. If it partially fails, the table could be left without primary key constraint. - - The primary key for the `_FaqToUnterveranstaltung` table will be changed. If it partially fails, the table could be left without primary key constraint. - - A unique constraint covering the columns `[A,B]` on the table `_AnmeldungToMahlzeit` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[A,B]` on the table `_FaqToUnterveranstaltung` will be added. If there are existing duplicate values, this will fail. - -*/ --- AlterTable -ALTER TABLE "_AnmeldungToMahlzeit" DROP CONSTRAINT "_AnmeldungToMahlzeit_AB_pkey"; - --- AlterTable -ALTER TABLE "_FaqToUnterveranstaltung" DROP CONSTRAINT "_FaqToUnterveranstaltung_AB_pkey"; - --- CreateTable -CREATE TABLE "AnmeldungLink" ( - "id" SERIAL NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "comment" TEXT, - "accessToken" UUID NOT NULL, - "unterveranstaltungId" INTEGER NOT NULL, - "createdById" INTEGER NOT NULL, - "anmeldungId" INTEGER, - - CONSTRAINT "AnmeldungLink_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "AnmeldungLink_anmeldungId_key" ON "AnmeldungLink"("anmeldungId"); - --- CreateIndex -CREATE UNIQUE INDEX "_AnmeldungToMahlzeit_AB_unique" ON "_AnmeldungToMahlzeit"("A", "B"); - --- CreateIndex -CREATE UNIQUE INDEX "_FaqToUnterveranstaltung_AB_unique" ON "_FaqToUnterveranstaltung"("A", "B"); - --- AddForeignKey -ALTER TABLE "AnmeldungLink" ADD CONSTRAINT "AnmeldungLink_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AnmeldungLink" ADD CONSTRAINT "AnmeldungLink_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AnmeldungLink" ADD CONSTRAINT "AnmeldungLink_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "Anmeldung"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20250412153210_anmeldelink_usage/migration.sql b/apps/api/prisma/migrations/20250412153210_anmeldelink_usage/migration.sql deleted file mode 100644 index e569de04..00000000 --- a/apps/api/prisma/migrations/20250412153210_anmeldelink_usage/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "AnmeldungLink" ADD COLUMN "usedAt" TIMESTAMP(3); diff --git a/apps/api/prisma/migrations/20250510143106_veranstaltung_public_read_token/migration.sql b/apps/api/prisma/migrations/20250510143106_veranstaltung_public_read_token/migration.sql deleted file mode 100644 index 55b087f9..00000000 --- a/apps/api/prisma/migrations/20250510143106_veranstaltung_public_read_token/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[publicReadToken]` on the table `Veranstaltung` will be added. If there are existing duplicate values, this will fail. - -*/ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- AlterTable -ALTER TABLE "Veranstaltung" ADD COLUMN "publicReadToken" TEXT DEFAULT uuid_generate_v4(); - --- CreateIndex -CREATE UNIQUE INDEX "Veranstaltung_publicReadToken_key" ON "Veranstaltung"("publicReadToken"); diff --git a/apps/api/prisma/migrations/20250510202534_add_credit_institution/migration.sql b/apps/api/prisma/migrations/20250510202534_add_credit_institution/migration.sql deleted file mode 100644 index 6920be56..00000000 --- a/apps/api/prisma/migrations/20250510202534_add_credit_institution/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- AlterTable -ALTER TABLE "Veranstaltung" ALTER COLUMN "publicReadToken" DROP DEFAULT; - --- CreateTable -CREATE TABLE "Kreditinstitute" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "shortName" TEXT NOT NULL, - "blz" INTEGER NOT NULL, - "bic" TEXT NOT NULL, - "pan" TEXT NOT NULL, - - CONSTRAINT "Kreditinstitute_pkey" PRIMARY KEY ("id") -); diff --git a/apps/api/prisma/migrations/20240906171118_init/migration.sql b/apps/api/prisma/migrations/20260201195324_init/migration.sql similarity index 52% rename from apps/api/prisma/migrations/20240906171118_init/migration.sql rename to apps/api/prisma/migrations/20260201195324_init/migration.sql index 60d7a9db..c4daf0a9 100644 --- a/apps/api/prisma/migrations/20240906171118_init/migration.sql +++ b/apps/api/prisma/migrations/20260201195324_init/migration.sql @@ -1,121 +1,189 @@ -- CreateEnum -CREATE TYPE "AccountStatus" AS ENUM ('OFFEN', 'AKTIV', 'DEAKTIVIERT'); +CREATE TYPE "Role" AS ENUM ('USER', 'GLIEDERUNG_ADMIN', 'ADMIN'); -- CreateEnum -CREATE TYPE "Role" AS ENUM ('GLIEDERUNG_ADMIN', 'ADMIN'); +CREATE TYPE "AccountStatus" AS ENUM ('OFFEN', 'AKTIV', 'DEAKTIVIERT'); -- CreateEnum -CREATE TYPE "Gender" AS ENUM ('MALE', 'FEMALE', 'UNSPECIFIED'); +CREATE TYPE "ActivityType" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'EMAIL', 'OTHER'); -- CreateEnum -CREATE TYPE "Essgewohnheit" AS ENUM ('OMNIVOR', 'VEGETARISCH', 'VEGAN'); +CREATE TYPE "AnmeldungStatus" AS ENUM ('OFFEN', 'BESTAETIGT', 'STORNIERT', 'ABGELEHNT'); -- CreateEnum -CREATE TYPE "NahrungsmittelIntoleranz" AS ENUM ('SCHWEIN', 'GLUTEN', 'LAKTOSE', 'FRUCTOSE'); +CREATE TYPE "CustomFieldType" AS ENUM ('BASIC_INPUT', 'BASIC_TEXT_AREA', 'BASIC_EDITOR', 'BASIC_SWITCH', 'BASIC_CHECKBOX', 'BASIC_INPUT_NUMBER', 'BASIC_RADIO', 'BASIC_SELECT', 'BASIC_DROPDOWN'); -- CreateEnum -CREATE TYPE "QualificationFahrerlaubnis" AS ENUM ('B', 'BE', 'C', 'CE', 'D1', 'D', 'D1E', 'DE', 'T', 'L'); +CREATE TYPE "CustomFieldPosition" AS ENUM ('PUBLIC_ANMELDUNG', 'INTERN_ANMELDUNG'); -- CreateEnum -CREATE TYPE "QualificationSchwimmer" AS ENUM ('BRONZE', 'SILBER', 'GOLD', 'JUNIORRETTER', 'RETTUNGSSCHWIMMER_BRONZE', 'RETTUNGSSCHWIMMER_SILBER', 'RETTUNGSSCHWIMMER_GOLD'); +CREATE TYPE "FileProvider" AS ENUM ('LOCAL', 'AZURE'); -- CreateEnum -CREATE TYPE "QualificationErsteHilfe" AS ENUM ('EINWEISER_EHSH', 'AUSBILDER_EHSH_MODUL_1_2', 'AUSBILDER_EHSH_MODUL_3', 'MODULE_1', 'MODULE_2', 'MODULE_3', 'AUSBILDUNG', 'KINDERNOTFAELLE', 'BILDUNGS_UND_BETREUUNGSEINRICHTUNGEN_KINDER', 'AUSBILDER'); +CREATE TYPE "GliederungAccountRole" AS ENUM ('DELEGATIONSLEITER', 'BETREUER', 'TEILNEHMER'); -- CreateEnum -CREATE TYPE "QualificationSanitaeter" AS ENUM ('SAN_A', 'SAN_B', 'FORTBILDUNG', 'AUSBILDER'); +CREATE TYPE "MahlzeitType" AS ENUM ('FRUEHSTUECK', 'MITTAGESSEN', 'ABENDESSEN'); -- CreateEnum -CREATE TYPE "QualificationFunk" AS ENUM ('DLRG_SPRECHFUNKER', 'BOS_SPRECHFUNKER_ANALOG', 'BOS_SPRECHFUNKER_DIGITAL', 'AUSBILDER_SPRECHFUNK', 'AUSBILDER_BOS_SPRECHFUNK', 'MULTIPLIKATOR_SPRECHFUNK', 'MULTIPLIKATOR_BOS_SPRECHFUNK', 'EINSATZFAEHIGKEIT'); +CREATE TYPE "Gender" AS ENUM ('MALE', 'FEMALE', 'UNSPECIFIED'); -- CreateEnum -CREATE TYPE "Konfektionsgroesse" AS ENUM ('JUNIOR_98_104', 'JUNIOR_110_116', 'JUNIOR_122_128', 'JUNIOR_134_140', 'JUNIOR_146_152', 'JUNIOR_158_164', 'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'); +CREATE TYPE "Essgewohnheit" AS ENUM ('OMNIVOR', 'VEGETARISCH', 'VEGAN'); -- CreateEnum -CREATE TYPE "GliederungAccountRole" AS ENUM ('DELEGATIONSLEITER', 'BETREUER', 'TEILNEHMER'); +CREATE TYPE "NahrungsmittelIntoleranz" AS ENUM ('SCHWEIN', 'GLUTEN', 'LAKTOSE', 'FRUCTOSE'); -- CreateEnum -CREATE TYPE "AnmeldungStatus" AS ENUM ('OFFEN', 'BESTAETIGT', 'STORNIERT', 'ABGELEHNT'); +CREATE TYPE "UnterveranstaltungType" AS ENUM ('CREW', 'GLIEDERUNG'); --- CreateEnum -CREATE TYPE "MahlzeitType" AS ENUM ('FRUEHSTUECK', 'MITTAGESSEN', 'ABENDESSEN'); +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "dlrgOauthId" TEXT, + "password" TEXT, + "role" "Role" NOT NULL, + "personId" TEXT NOT NULL, + "activatedAt" TIMESTAMP(3), + "activationToken" TEXT, + "status" "AccountStatus" NOT NULL DEFAULT 'OFFEN', + "passwordResetToken" TEXT, --- CreateEnum -CREATE TYPE "UnterveranstaltungType" AS ENUM ('CREW', 'GLIEDERUNG'); + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); --- CreateEnum -CREATE TYPE "ActivityType" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'EMAIL', 'OTHER'); +-- CreateTable +CREATE TABLE "Activity" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "type" "ActivityType" NOT NULL, + "description" TEXT, + "subjectType" TEXT NOT NULL, + "subjectId" TEXT, + "causerId" TEXT, + "metadata" JSONB NOT NULL DEFAULT '{}', --- CreateEnum -CREATE TYPE "CustomFieldType" AS ENUM ('BASIC_INPUT', 'BASIC_TEXT_AREA', 'BASIC_EDITOR', 'BASIC_SWITCH', 'BASIC_CHECKBOX', 'BASIC_INPUT_NUMBER', 'BASIC_RADIO', 'BASIC_SELECT', 'BASIC_DROPDOWN'); + CONSTRAINT "Activity_pkey" PRIMARY KEY ("id") +); --- CreateEnum -CREATE TYPE "CustomFieldPosition" AS ENUM ('PUBLIC_ANMELDUNG', 'INTERN_ANMELDUNG'); +-- CreateTable +CREATE TABLE "Address" ( + "id" TEXT NOT NULL, + "street" TEXT NOT NULL, + "streetNumber" TEXT NOT NULL, + "zip" TEXT NOT NULL, + "city" TEXT NOT NULL, + "country" TEXT NOT NULL, + "lat" DOUBLE PRECISION, + "lon" DOUBLE PRECISION, + "valid" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Address_pkey" PRIMARY KEY ("id") +); -- CreateTable -CREATE TABLE "Hostname" ( - "id" SERIAL NOT NULL, - "hostname" TEXT NOT NULL, +CREATE TABLE "Ort" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "addressId" TEXT, - CONSTRAINT "Hostname_pkey" PRIMARY KEY ("id") + CONSTRAINT "Ort_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Notfallkontakt" ( - "id" SERIAL NOT NULL, - "firstname" TEXT NOT NULL, - "lastname" TEXT NOT NULL, - "telefon" TEXT NOT NULL, - "istErziehungsberechtigt" BOOLEAN NOT NULL DEFAULT false, - "personId" INTEGER NOT NULL, +CREATE TABLE "Anmeldung" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "AnmeldungStatus" NOT NULL DEFAULT 'OFFEN', + "comment" TEXT, + "accessToken" UUID, + "assignmentCode" UUID, + "unterveranstaltungId" TEXT NOT NULL, + "personId" TEXT NOT NULL, + "accountId" TEXT, + "mahlzeitenIds" TEXT[], + "uebernachtungsTage" DATE[], - CONSTRAINT "Notfallkontakt_pkey" PRIMARY KEY ("id") + CONSTRAINT "Anmeldung_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Person" ( - "id" SERIAL NOT NULL, - "firstname" TEXT NOT NULL, - "lastname" TEXT NOT NULL, - "birthday" DATE, - "gender" "Gender", - "email" TEXT NOT NULL, - "telefon" TEXT NOT NULL, - "gliederungId" INTEGER, - "essgewohnheit" "Essgewohnheit", - "nahrungsmittelIntoleranzen" "NahrungsmittelIntoleranz"[], - "weitereIntoleranzen" TEXT[], - "qualifikationenFahrerlaubnis" "QualificationFahrerlaubnis"[], - "qualifikationenSchwimmer" "QualificationSchwimmer"[], - "qualifikationenErsteHilfe" "QualificationErsteHilfe"[], - "qualifikationenSanitaeter" "QualificationSanitaeter"[], - "qualifikationenFunk" "QualificationFunk"[], - "konfektionsgroesse" "Konfektionsgroesse", - "notfallkontaktIds" INTEGER[], - "addressId" INTEGER, +CREATE TABLE "AnmeldungLink" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "usedAt" TIMESTAMP(3), + "comment" TEXT, + "accessToken" UUID, + "unterveranstaltungId" TEXT NOT NULL, + "createdById" TEXT NOT NULL, + "anmeldungId" TEXT, - CONSTRAINT "Person_pkey" PRIMARY KEY ("id") + CONSTRAINT "AnmeldungLink_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Account" ( - "id" SERIAL NOT NULL, - "email" TEXT NOT NULL, - "dlrgOauthId" TEXT, - "password" TEXT, - "role" "Role" NOT NULL, - "personId" INTEGER NOT NULL, - "activatedAt" TIMESTAMP(3), - "activationToken" TEXT, - "status" "AccountStatus" NOT NULL DEFAULT 'OFFEN', - "passwordResetToken" TEXT, +CREATE TABLE "CustomField" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "type" "CustomFieldType" NOT NULL, + "required" BOOLEAN NOT NULL DEFAULT false, + "options" TEXT[], + "role" "Role"[], + "positions" "CustomFieldPosition"[], + "veranstaltungId" TEXT, + "unterveranstaltungId" TEXT, - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") + CONSTRAINT "CustomField_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CustomFieldValue" ( + "id" TEXT NOT NULL, + "value" JSONB NOT NULL DEFAULT '{}', + "fieldId" TEXT NOT NULL, + "anmeldungId" TEXT, + + CONSTRAINT "CustomFieldValue_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "faq_categories" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "unterveranstaltungId" TEXT NOT NULL, + + CONSTRAINT "faq_categories_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "faqs" ( + "id" TEXT NOT NULL, + "question" TEXT NOT NULL, + "answer" TEXT NOT NULL, + "categoryId" TEXT NOT NULL, + + CONSTRAINT "faqs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "File" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "uploaded" BOOLEAN NOT NULL DEFAULT false, + "uploadedAt" TIMESTAMP(3), + "provider" "FileProvider" NOT NULL, + "key" TEXT NOT NULL, + "filename" TEXT, + "mimetype" TEXT, + + CONSTRAINT "File_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Gliederung" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "name" TEXT NOT NULL, "edv" TEXT NOT NULL, @@ -125,88 +193,138 @@ CREATE TABLE "Gliederung" ( -- CreateTable CREATE TABLE "GliederungToAccount" ( "id" SERIAL NOT NULL, - "gliederungId" INTEGER NOT NULL, - "accountId" INTEGER NOT NULL, + "gliederungId" TEXT NOT NULL, + "accountId" TEXT NOT NULL, "role" "GliederungAccountRole" NOT NULL, CONSTRAINT "GliederungToAccount_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Anmeldung" ( - "id" SERIAL NOT NULL, - "unterveranstaltungId" INTEGER NOT NULL, - "personId" INTEGER NOT NULL, - "status" "AnmeldungStatus" NOT NULL DEFAULT 'OFFEN', - "mahlzeitenIds" INTEGER[], - "uebernachtungsTage" DATE[], - "tshirtBestellt" BOOLEAN NOT NULL DEFAULT false, - "comment" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +CREATE TABLE "Hostname" ( + "id" TEXT NOT NULL, + "hostname" TEXT NOT NULL, - CONSTRAINT "Anmeldung_pkey" PRIMARY KEY ("id") + CONSTRAINT "Hostname_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Address" ( +CREATE TABLE "Kreditinstitute" ( "id" SERIAL NOT NULL, - "street" TEXT NOT NULL, - "number" TEXT NOT NULL, - "zip" TEXT NOT NULL, - "city" TEXT NOT NULL, + "name" TEXT NOT NULL, + "shortName" TEXT NOT NULL, + "blz" INTEGER NOT NULL, + "bic" TEXT NOT NULL, + "pan" TEXT NOT NULL, - CONSTRAINT "Address_pkey" PRIMARY KEY ("id") + CONSTRAINT "Kreditinstitute_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Ort" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "addressId" INTEGER, +CREATE TABLE "Mahlzeit" ( + "id" TEXT NOT NULL, + "type" "MahlzeitType" NOT NULL, + "date" DATE NOT NULL, + "veranstaltungId" TEXT NOT NULL, - CONSTRAINT "Ort_pkey" PRIMARY KEY ("id") + CONSTRAINT "Mahlzeit_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Veranstaltung" ( - "id" SERIAL NOT NULL, +CREATE TABLE "Person" ( + "id" TEXT NOT NULL, + "firstname" TEXT NOT NULL, + "lastname" TEXT NOT NULL, + "birthday" DATE, + "gender" "Gender", + "email" TEXT NOT NULL, + "telefon" TEXT NOT NULL, + "gliederungId" TEXT, + "essgewohnheit" "Essgewohnheit", + "nahrungsmittelIntoleranzen" "NahrungsmittelIntoleranz"[], + "weitereIntoleranzen" TEXT[], + "notfallkontaktIds" INTEGER[], + "addressId" TEXT, + "photoId" TEXT, + + CONSTRAINT "Person_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Notfallkontakt" ( + "id" TEXT NOT NULL, + "firstname" TEXT NOT NULL, + "lastname" TEXT NOT NULL, + "telefon" TEXT NOT NULL, + "istErziehungsberechtigt" BOOLEAN NOT NULL DEFAULT false, + "personId" TEXT NOT NULL, + + CONSTRAINT "Notfallkontakt_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProgrammPunkt" ( + "id" TEXT NOT NULL, "name" TEXT NOT NULL, - "beginn" DATE NOT NULL, - "ende" DATE NOT NULL, - "meldebeginn" TIMESTAMP(3) NOT NULL, - "meldeschluss" TIMESTAMP(3) NOT NULL, - "ortId" INTEGER, - "maxTeilnehmende" INTEGER NOT NULL, - "teilnahmegebuehr" INTEGER NOT NULL, - "beschreibung" TEXT, - "datenschutz" TEXT, - "teilnahmeBedingungen" TEXT, - "teilnahmeBedingungenPublic" TEXT, - "zielgruppe" TEXT, - "hostnameId" INTEGER, + "description" TEXT, + "startingAt" TIMESTAMP(3) NOT NULL, + "endingAt" TIMESTAMP(3) NOT NULL, + "location" TEXT NOT NULL, + "responsible" TEXT NOT NULL, + "veranstaltungId" TEXT NOT NULL, - CONSTRAINT "Veranstaltung_pkey" PRIMARY KEY ("id") + CONSTRAINT "ProgrammPunkt_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Mahlzeit" ( - "id" SERIAL NOT NULL, - "type" "MahlzeitType" NOT NULL, - "date" DATE NOT NULL, - "veranstaltungId" INTEGER NOT NULL, +CREATE TABLE "UnterveranstaltungLandingSettings" ( + "unterveranstaltungId" TEXT NOT NULL, + "heroTitle" TEXT NOT NULL, + "heroSubtitle" TEXT NOT NULL, + "eventDetailsTitle" TEXT NOT NULL, + "eventDetailsContent" TEXT NOT NULL, + "miscellaneousVisible" BOOLEAN, + "miscellaneousTitle" TEXT, + "miscellaneousSubtitle" TEXT, + "faqVisible" BOOLEAN, + "faqEmail" TEXT, + "instagramVisible" BOOLEAN, + "instagramUrl" TEXT, + "facebookVisible" BOOLEAN, + "facebookUrl" TEXT, + + CONSTRAINT "UnterveranstaltungLandingSettings_pkey" PRIMARY KEY ("unterveranstaltungId") +); - CONSTRAINT "Mahlzeit_pkey" PRIMARY KEY ("id") +-- CreateTable +CREATE TABLE "UnterveranstaltungLandingImages" ( + "id" TEXT NOT NULL, + "name" TEXT, + "unterveranstaltungLandingSettingsId" TEXT, + "fileId" TEXT NOT NULL, + + CONSTRAINT "UnterveranstaltungLandingImages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UnterveranstaltungLandingMiscellaneous" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "unterveranstaltungLandingSettingsId" TEXT, + + CONSTRAINT "UnterveranstaltungLandingMiscellaneous_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Unterveranstaltung" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "maxTeilnehmende" INTEGER NOT NULL, "teilnahmegebuehr" INTEGER NOT NULL, "meldebeginn" TIMESTAMP(3) NOT NULL, "meldeschluss" TIMESTAMP(3) NOT NULL, - "veranstaltungId" INTEGER NOT NULL, - "gliederungId" INTEGER NOT NULL, + "veranstaltungId" TEXT NOT NULL, + "gliederungId" TEXT NOT NULL, "beschreibung" TEXT, "bedingungen" TEXT, "type" "UnterveranstaltungType" NOT NULL DEFAULT 'GLIEDERUNG', @@ -215,49 +333,48 @@ CREATE TABLE "Unterveranstaltung" ( ); -- CreateTable -CREATE TABLE "Activity" ( - "id" SERIAL NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "type" "ActivityType" NOT NULL, +CREATE TABLE "UnterveranstaltungDocument" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, "description" TEXT, - "subjectType" TEXT NOT NULL, - "subjectId" INTEGER, - "causerId" INTEGER, - "metadata" JSONB NOT NULL DEFAULT '{}', + "unterveranstaltungId" TEXT NOT NULL, + "fileId" TEXT NOT NULL, - CONSTRAINT "Activity_pkey" PRIMARY KEY ("id") + CONSTRAINT "UnterveranstaltungDocument_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "CustomField" ( - "id" SERIAL NOT NULL, +CREATE TABLE "Veranstaltung" ( + "id" TEXT NOT NULL, "name" TEXT NOT NULL, - "description" TEXT, - "type" "CustomFieldType" NOT NULL, - "required" BOOLEAN NOT NULL DEFAULT false, - "options" TEXT[], - "role" "Role"[], - "positions" "CustomFieldPosition"[], - "veranstaltungId" INTEGER, - "unterveranstaltungId" INTEGER, + "beginn" DATE NOT NULL, + "ende" DATE NOT NULL, + "meldebeginn" TIMESTAMP(3) NOT NULL, + "meldeschluss" TIMESTAMP(3) NOT NULL, + "ortId" TEXT, + "maxTeilnehmende" INTEGER NOT NULL, + "teilnahmegebuehr" INTEGER NOT NULL, + "beschreibung" TEXT, + "datenschutz" TEXT, + "teilnahmeBedingungen" TEXT, + "teilnahmeBedingungenPublic" TEXT, + "zielgruppe" TEXT, + "hostnameId" TEXT, + "publicReadToken" TEXT, - CONSTRAINT "CustomField_pkey" PRIMARY KEY ("id") + CONSTRAINT "Veranstaltung_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "CustomFieldValue" ( - "id" SERIAL NOT NULL, - "value" JSONB NOT NULL DEFAULT '{}', - "fieldId" INTEGER NOT NULL, - "anmeldungId" INTEGER, - - CONSTRAINT "CustomFieldValue_pkey" PRIMARY KEY ("id") +CREATE TABLE "_AnmeldungToMahlzeit" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL ); -- CreateTable -CREATE TABLE "_AnmeldungToMahlzeit" ( - "A" INTEGER NOT NULL, - "B" INTEGER NOT NULL +CREATE TABLE "_FaqToUnterveranstaltung" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL ); -- CreateIndex @@ -275,29 +392,86 @@ CREATE UNIQUE INDEX "Account_activationToken_key" ON "Account"("activationToken" -- CreateIndex CREATE UNIQUE INDEX "Account_passwordResetToken_key" ON "Account"("passwordResetToken"); +-- CreateIndex +CREATE UNIQUE INDEX "AnmeldungLink_anmeldungId_key" ON "AnmeldungLink"("anmeldungId"); + +-- CreateIndex +CREATE UNIQUE INDEX "faq_categories_name_unterveranstaltungId_key" ON "faq_categories"("name", "unterveranstaltungId"); + +-- CreateIndex +CREATE UNIQUE INDEX "File_key_key" ON "File"("key"); + -- CreateIndex CREATE UNIQUE INDEX "Gliederung_edv_key" ON "Gliederung"("edv"); -- CreateIndex CREATE UNIQUE INDEX "GliederungToAccount_gliederungId_accountId_key" ON "GliederungToAccount"("gliederungId", "accountId"); +-- CreateIndex +CREATE UNIQUE INDEX "UnterveranstaltungLandingImages_fileId_key" ON "UnterveranstaltungLandingImages"("fileId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UnterveranstaltungDocument_fileId_key" ON "UnterveranstaltungDocument"("fileId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Veranstaltung_publicReadToken_key" ON "Veranstaltung"("publicReadToken"); + -- CreateIndex CREATE UNIQUE INDEX "_AnmeldungToMahlzeit_AB_unique" ON "_AnmeldungToMahlzeit"("A", "B"); -- CreateIndex CREATE INDEX "_AnmeldungToMahlzeit_B_index" ON "_AnmeldungToMahlzeit"("B"); +-- CreateIndex +CREATE UNIQUE INDEX "_FaqToUnterveranstaltung_AB_unique" ON "_FaqToUnterveranstaltung"("A", "B"); + +-- CreateIndex +CREATE INDEX "_FaqToUnterveranstaltung_B_index" ON "_FaqToUnterveranstaltung"("B"); + -- AddForeignKey -ALTER TABLE "Notfallkontakt" ADD CONSTRAINT "Notfallkontakt_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Account" ADD CONSTRAINT "Account_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Person" ADD CONSTRAINT "Person_gliederungId_fkey" FOREIGN KEY ("gliederungId") REFERENCES "Gliederung"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_causerId_fkey" FOREIGN KEY ("causerId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Person" ADD CONSTRAINT "Person_addressId_fkey" FOREIGN KEY ("addressId") REFERENCES "Address"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Ort" ADD CONSTRAINT "Ort_addressId_fkey" FOREIGN KEY ("addressId") REFERENCES "Address"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Account" ADD CONSTRAINT "Account_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Anmeldung" ADD CONSTRAINT "Anmeldung_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Anmeldung" ADD CONSTRAINT "Anmeldung_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Anmeldung" ADD CONSTRAINT "Anmeldung_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnmeldungLink" ADD CONSTRAINT "AnmeldungLink_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnmeldungLink" ADD CONSTRAINT "AnmeldungLink_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnmeldungLink" ADD CONSTRAINT "AnmeldungLink_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "Anmeldung"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomField" ADD CONSTRAINT "CustomField_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomField" ADD CONSTRAINT "CustomField_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomFieldValue" ADD CONSTRAINT "CustomFieldValue_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "CustomField"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomFieldValue" ADD CONSTRAINT "CustomFieldValue_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "Anmeldung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "faq_categories" ADD CONSTRAINT "faq_categories_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "faqs" ADD CONSTRAINT "faqs_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "faq_categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "GliederungToAccount" ADD CONSTRAINT "GliederungToAccount_gliederungId_fkey" FOREIGN KEY ("gliederungId") REFERENCES "Gliederung"("id") ON DELETE CASCADE ON UPDATE CASCADE; @@ -306,46 +480,61 @@ ALTER TABLE "GliederungToAccount" ADD CONSTRAINT "GliederungToAccount_gliederung ALTER TABLE "GliederungToAccount" ADD CONSTRAINT "GliederungToAccount_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Anmeldung" ADD CONSTRAINT "Anmeldung_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Mahlzeit" ADD CONSTRAINT "Mahlzeit_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Anmeldung" ADD CONSTRAINT "Anmeldung_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Person" ADD CONSTRAINT "Person_gliederungId_fkey" FOREIGN KEY ("gliederungId") REFERENCES "Gliederung"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Ort" ADD CONSTRAINT "Ort_addressId_fkey" FOREIGN KEY ("addressId") REFERENCES "Address"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Person" ADD CONSTRAINT "Person_addressId_fkey" FOREIGN KEY ("addressId") REFERENCES "Address"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Veranstaltung" ADD CONSTRAINT "Veranstaltung_ortId_fkey" FOREIGN KEY ("ortId") REFERENCES "Ort"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Person" ADD CONSTRAINT "Person_photoId_fkey" FOREIGN KEY ("photoId") REFERENCES "File"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Veranstaltung" ADD CONSTRAINT "Veranstaltung_hostnameId_fkey" FOREIGN KEY ("hostnameId") REFERENCES "Hostname"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Notfallkontakt" ADD CONSTRAINT "Notfallkontakt_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Mahlzeit" ADD CONSTRAINT "Mahlzeit_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ProgrammPunkt" ADD CONSTRAINT "ProgrammPunkt_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Unterveranstaltung" ADD CONSTRAINT "Unterveranstaltung_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "UnterveranstaltungLandingSettings" ADD CONSTRAINT "UnterveranstaltungLandingSettings_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Unterveranstaltung" ADD CONSTRAINT "Unterveranstaltung_gliederungId_fkey" FOREIGN KEY ("gliederungId") REFERENCES "Gliederung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "UnterveranstaltungLandingImages" ADD CONSTRAINT "UnterveranstaltungLandingImages_unterveranstaltungLandingS_fkey" FOREIGN KEY ("unterveranstaltungLandingSettingsId") REFERENCES "UnterveranstaltungLandingSettings"("unterveranstaltungId") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Activity" ADD CONSTRAINT "Activity_causerId_fkey" FOREIGN KEY ("causerId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "UnterveranstaltungLandingImages" ADD CONSTRAINT "UnterveranstaltungLandingImages_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "CustomField" ADD CONSTRAINT "CustomField_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "UnterveranstaltungLandingMiscellaneous" ADD CONSTRAINT "UnterveranstaltungLandingMiscellaneous_unterveranstaltungL_fkey" FOREIGN KEY ("unterveranstaltungLandingSettingsId") REFERENCES "UnterveranstaltungLandingSettings"("unterveranstaltungId") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "CustomField" ADD CONSTRAINT "CustomField_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Unterveranstaltung" ADD CONSTRAINT "Unterveranstaltung_veranstaltungId_fkey" FOREIGN KEY ("veranstaltungId") REFERENCES "Veranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "CustomFieldValue" ADD CONSTRAINT "CustomFieldValue_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "CustomField"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Unterveranstaltung" ADD CONSTRAINT "Unterveranstaltung_gliederungId_fkey" FOREIGN KEY ("gliederungId") REFERENCES "Gliederung"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "CustomFieldValue" ADD CONSTRAINT "CustomFieldValue_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "Anmeldung"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Veranstaltung" ADD CONSTRAINT "Veranstaltung_ortId_fkey" FOREIGN KEY ("ortId") REFERENCES "Ort"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Veranstaltung" ADD CONSTRAINT "Veranstaltung_hostnameId_fkey" FOREIGN KEY ("hostnameId") REFERENCES "Hostname"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "_AnmeldungToMahlzeit" ADD CONSTRAINT "_AnmeldungToMahlzeit_A_fkey" FOREIGN KEY ("A") REFERENCES "Anmeldung"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "_AnmeldungToMahlzeit" ADD CONSTRAINT "_AnmeldungToMahlzeit_B_fkey" FOREIGN KEY ("B") REFERENCES "Mahlzeit"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_FaqToUnterveranstaltung" ADD CONSTRAINT "_FaqToUnterveranstaltung_A_fkey" FOREIGN KEY ("A") REFERENCES "faqs"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_FaqToUnterveranstaltung" ADD CONSTRAINT "_FaqToUnterveranstaltung_B_fkey" FOREIGN KEY ("B") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/schema/Account.prisma b/apps/api/prisma/schema/Account.prisma index cdc9fdb6..04669b87 100644 --- a/apps/api/prisma/schema/Account.prisma +++ b/apps/api/prisma/schema/Account.prisma @@ -11,12 +11,12 @@ enum AccountStatus { } model Account { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) email String @unique dlrgOauthId String? @unique password String? role Role - personId Int @unique + personId String @unique person Person @relation(fields: [personId], references: [id], onDelete: Cascade) activatedAt DateTime? GliederungToAccount GliederungToAccount[] diff --git a/apps/api/prisma/schema/Activity.prisma b/apps/api/prisma/schema/Activity.prisma index c9b949f5..531484f1 100644 --- a/apps/api/prisma/schema/Activity.prisma +++ b/apps/api/prisma/schema/Activity.prisma @@ -7,13 +7,13 @@ enum ActivityType { } model Activity { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) createdAt DateTime @default(now()) type ActivityType description String? subjectType String subjectId String? - causerId Int? + causerId String? causer Account? @relation(fields: [causerId], references: [id], onDelete: SetNull) metadata Json @default("{}") } diff --git a/apps/api/prisma/schema/Address.prisma b/apps/api/prisma/schema/Address.prisma index 98fb4c2d..4c1076e8 100644 --- a/apps/api/prisma/schema/Address.prisma +++ b/apps/api/prisma/schema/Address.prisma @@ -1,5 +1,5 @@ model Address { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) street String streetNumber String zip String @@ -13,9 +13,9 @@ model Address { } model Ort { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String - addressId Int? + addressId String? address Address? @relation(fields: [addressId], references: [id], onDelete: SetNull) Veranstaltung Veranstaltung[] } diff --git a/apps/api/prisma/schema/Anmeldung.prisma b/apps/api/prisma/schema/Anmeldung.prisma index 8ab2ce16..c4e251d7 100644 --- a/apps/api/prisma/schema/Anmeldung.prisma +++ b/apps/api/prisma/schema/Anmeldung.prisma @@ -6,23 +6,23 @@ enum AnmeldungStatus { } model Anmeldung { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) createdAt DateTime @default(now()) status AnmeldungStatus @default(OFFEN) comment String? accessToken String? @db.Uuid assignmentCode String? @db.Uuid - unterveranstaltungId Int + unterveranstaltungId String unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) - personId Int + personId String person Person @relation(fields: [personId], references: [id], onDelete: Cascade) - accountId Int? + accountId String? account Account? @relation(fields: [accountId], references: [id], onDelete: Cascade) - mahlzeitenIds Int[] + mahlzeitenIds String[] mahlzeiten Mahlzeit[] uebernachtungsTage DateTime[] @db.Date customFieldValues CustomFieldValue[] @@ -31,18 +31,18 @@ model Anmeldung { } model AnmeldungLink { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) createdAt DateTime @default(now()) usedAt DateTime? comment String? - accessToken String @db.Uuid + accessToken String? @db.Uuid - unterveranstaltungId Int + unterveranstaltungId String unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) - createdById Int + createdById String createdBy Account @relation(fields: [createdById], references: [id]) - anmeldungId Int? @unique + anmeldungId String? @unique anmeldung Anmeldung? @relation(fields: [anmeldungId], references: [id]) } diff --git a/apps/api/prisma/schema/CustomField.prisma b/apps/api/prisma/schema/CustomField.prisma index a06b4cd2..0a1415df 100644 --- a/apps/api/prisma/schema/CustomField.prisma +++ b/apps/api/prisma/schema/CustomField.prisma @@ -16,7 +16,7 @@ enum CustomFieldPosition { } model CustomField { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String description String? type CustomFieldType @@ -25,17 +25,17 @@ model CustomField { role Role[] values CustomFieldValue[] positions CustomFieldPosition[] - veranstaltungId Int? - unterveranstaltungId Int? + veranstaltungId String? + unterveranstaltungId String? veranstaltung Veranstaltung? @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade) unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) } model CustomFieldValue { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) value Json @default("{}") - fieldId Int + fieldId String field CustomField @relation(fields: [fieldId], references: [id], onDelete: Cascade) - anmeldungId Int? + anmeldungId String? anmeldung Anmeldung? @relation(fields: [anmeldungId], references: [id], onDelete: Cascade) } diff --git a/apps/api/prisma/schema/Faq.prisma b/apps/api/prisma/schema/Faq.prisma index 1968ba92..1db11286 100644 --- a/apps/api/prisma/schema/Faq.prisma +++ b/apps/api/prisma/schema/Faq.prisma @@ -1,8 +1,8 @@ model FaqCategory { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String - unterveranstaltungId Int + unterveranstaltungId String unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id]) faqs Faq[] @@ -11,11 +11,11 @@ model FaqCategory { } model Faq { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) question String answer String - categoryId Int + categoryId String category FaqCategory @relation(fields: [categoryId], references: [id]) unterveranstaltung Unterveranstaltung[] diff --git a/apps/api/prisma/schema/File.prisma b/apps/api/prisma/schema/File.prisma index 15f8768c..c46f7168 100644 --- a/apps/api/prisma/schema/File.prisma +++ b/apps/api/prisma/schema/File.prisma @@ -4,7 +4,7 @@ enum FileProvider { } model File { - id String @id @unique @default(uuid()) + id String @id @default(uuid(7)) createdAt DateTime @default(now()) uploaded Boolean @default(false) uploadedAt DateTime? diff --git a/apps/api/prisma/schema/Gliederung.prisma b/apps/api/prisma/schema/Gliederung.prisma index 7478a2c5..80f8dfaf 100644 --- a/apps/api/prisma/schema/Gliederung.prisma +++ b/apps/api/prisma/schema/Gliederung.prisma @@ -1,5 +1,5 @@ model Gliederung { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String edv String @unique unterveranstaltungen Unterveranstaltung[] diff --git a/apps/api/prisma/schema/GliederungToAccount.prisma b/apps/api/prisma/schema/GliederungToAccount.prisma index f91d51a9..b6fcc185 100644 --- a/apps/api/prisma/schema/GliederungToAccount.prisma +++ b/apps/api/prisma/schema/GliederungToAccount.prisma @@ -6,9 +6,9 @@ enum GliederungAccountRole { model GliederungToAccount { id Int @id @default(autoincrement()) - gliederungId Int + gliederungId String gliederung Gliederung @relation(fields: [gliederungId], references: [id], onDelete: Cascade) - accountId Int + accountId String account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) role GliederungAccountRole diff --git a/apps/api/prisma/schema/Hostname.prisma b/apps/api/prisma/schema/Hostname.prisma index 1ec2d477..0b799c5e 100644 --- a/apps/api/prisma/schema/Hostname.prisma +++ b/apps/api/prisma/schema/Hostname.prisma @@ -1,5 +1,5 @@ model Hostname { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) hostname String veranstaltung Veranstaltung[] } diff --git a/apps/api/prisma/schema/Mahlzeit.prisma b/apps/api/prisma/schema/Mahlzeit.prisma index 31f56189..5b7de3f7 100644 --- a/apps/api/prisma/schema/Mahlzeit.prisma +++ b/apps/api/prisma/schema/Mahlzeit.prisma @@ -5,10 +5,10 @@ enum MahlzeitType { } model Mahlzeit { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) type MahlzeitType date DateTime @db.Date - veranstaltungId Int + veranstaltungId String veranstaltung Veranstaltung @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade) anmeldung Anmeldung[] } diff --git a/apps/api/prisma/schema/Person.prisma b/apps/api/prisma/schema/Person.prisma index a35a6329..93cd90e3 100644 --- a/apps/api/prisma/schema/Person.prisma +++ b/apps/api/prisma/schema/Person.prisma @@ -22,7 +22,7 @@ enum NahrungsmittelIntoleranz { //endregion model Person { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) firstname String lastname String birthday DateTime? @db.Date @@ -30,7 +30,7 @@ model Person { email String telefon String anmeldungen Anmeldung[] - gliederungId Int? + gliederungId String? gliederung Gliederung? @relation(fields: [gliederungId], references: [id], onDelete: SetNull) account Account? essgewohnheit Essgewohnheit? @@ -38,18 +38,18 @@ model Person { weitereIntoleranzen String[] notfallkontaktIds Int[] notfallkontakte Notfallkontakt[] - addressId Int? + addressId String? address Address? @relation(fields: [addressId], references: [id], onDelete: Cascade) photoId String? photo File? @relation(fields: [photoId], references: [id]) } model Notfallkontakt { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) firstname String lastname String telefon String istErziehungsberechtigt Boolean @default(false) - personId Int + personId String person Person @relation(fields: [personId], references: [id], onDelete: Cascade) } diff --git a/apps/api/prisma/schema/ProgrammPunkt.prisma b/apps/api/prisma/schema/ProgrammPunkt.prisma index 279603c3..069111d7 100644 --- a/apps/api/prisma/schema/ProgrammPunkt.prisma +++ b/apps/api/prisma/schema/ProgrammPunkt.prisma @@ -1,5 +1,5 @@ model ProgrammPunkt { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String description String? startingAt DateTime @@ -7,6 +7,6 @@ model ProgrammPunkt { location String responsible String - veranstaltungId Int + veranstaltungId String veranstaltung Veranstaltung @relation(fields: [veranstaltungId], references: [id]) } diff --git a/apps/api/prisma/schema/PublicLanding.prisma b/apps/api/prisma/schema/PublicLanding.prisma index ed359ab9..f950e910 100644 --- a/apps/api/prisma/schema/PublicLanding.prisma +++ b/apps/api/prisma/schema/PublicLanding.prisma @@ -1,6 +1,6 @@ model UnterveranstaltungLandingSettings { - unterveranstaltungId Int @id @default(autoincrement()) - unterveranstaltung Unterveranstaltung? + unterveranstaltungId String @id + unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) heroTitle String heroSubtitle String @@ -25,22 +25,22 @@ model UnterveranstaltungLandingSettings { } model UnterveranstaltungLandingImages { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String? UnterveranstaltungLandingSettings UnterveranstaltungLandingSettings? @relation(fields: [unterveranstaltungLandingSettingsId], references: [unterveranstaltungId]) - unterveranstaltungLandingSettingsId Int? + unterveranstaltungLandingSettingsId String? file File @relation(fields: [fileId], references: [id]) fileId String @unique } model UnterveranstaltungLandingMiscellaneous { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) title String content String UnterveranstaltungLandingSettings UnterveranstaltungLandingSettings? @relation(fields: [unterveranstaltungLandingSettingsId], references: [unterveranstaltungId]) - unterveranstaltungLandingSettingsId Int? + unterveranstaltungLandingSettingsId String? } diff --git a/apps/api/prisma/schema/Unterveranstaltung.prisma b/apps/api/prisma/schema/Unterveranstaltung.prisma index 64ec01d4..097cd2ed 100644 --- a/apps/api/prisma/schema/Unterveranstaltung.prisma +++ b/apps/api/prisma/schema/Unterveranstaltung.prisma @@ -4,14 +4,14 @@ enum UnterveranstaltungType { } model Unterveranstaltung { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) maxTeilnehmende Int teilnahmegebuehr Int meldebeginn DateTime meldeschluss DateTime - veranstaltungId Int + veranstaltungId String veranstaltung Veranstaltung @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade) - gliederungId Int + gliederungId String gliederung Gliederung @relation(fields: [gliederungId], references: [id], onDelete: Cascade) Anmeldung Anmeldung[] beschreibung String? @@ -23,16 +23,15 @@ model Unterveranstaltung { faqs Faq[] faqCategories FaqCategory[] - landingSettings UnterveranstaltungLandingSettings? @relation(fields: [landingSettingsId], references: [unterveranstaltungId], onDelete: Cascade) - landingSettingsId Int? @unique - AnmeldungLink AnmeldungLink[] + landingSettings UnterveranstaltungLandingSettings? + AnmeldungLink AnmeldungLink[] } model UnterveranstaltungDocument { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String description String? - unterveranstaltungId Int + unterveranstaltungId String unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) file File @relation(fields: [fileId], references: [id]) fileId String @unique diff --git a/apps/api/prisma/schema/Veranstaltung.prisma b/apps/api/prisma/schema/Veranstaltung.prisma index 4fad20f5..9be1f65d 100644 --- a/apps/api/prisma/schema/Veranstaltung.prisma +++ b/apps/api/prisma/schema/Veranstaltung.prisma @@ -1,11 +1,11 @@ model Veranstaltung { - id Int @id @default(autoincrement()) + id String @id @default(uuid(7)) name String beginn DateTime @db.Date ende DateTime @db.Date meldebeginn DateTime meldeschluss DateTime - ortId Int? + ortId String? ort Ort? @relation(fields: [ortId], references: [id], onDelete: SetNull) maxTeilnehmende Int teilnahmegebuehr Int @@ -16,7 +16,7 @@ model Veranstaltung { teilnahmeBedingungen String? teilnahmeBedingungenPublic String? zielgruppe String? - hostnameId Int? + hostnameId String? hostname Hostname? @relation(fields: [hostnameId], references: [id]) customFields CustomField[] programmPunkte ProgrammPunkt[] diff --git a/apps/api/prisma/seeders/account.ts b/apps/api/prisma/seeders/account.ts index 1f090ee2..d50e5f7d 100644 --- a/apps/api/prisma/seeders/account.ts +++ b/apps/api/prisma/seeders/account.ts @@ -1,8 +1,7 @@ -import { PrismaClient, AccountStatus, Role } from '@prisma/client' +import { AccountStatus, PrismaClient, Role } from '@prisma/client' import { logger } from '../../src/logger.js' import logActivity from '../../src/util/activity.js' -import { isProduction } from '../../src/util/is-production.js' import type { Seeder } from './index.js' @@ -10,11 +9,6 @@ import { hashPassword } from '@codeanker/authentication' import { faker } from '@faker-js/faker' const createAccount: Seeder = async (prisma: PrismaClient) => { - // create default user in development - if (isProduction()) { - return - } - const email = 'admin@example.org' const password = 'admin' diff --git a/apps/api/prisma/seeders/hostname.ts b/apps/api/prisma/seeders/hostname.ts new file mode 100644 index 00000000..35c02c78 --- /dev/null +++ b/apps/api/prisma/seeders/hostname.ts @@ -0,0 +1,17 @@ +import type { PrismaClient } from '@prisma/client' +import type { Seeder } from './index.js' +import logActivity from '../../src/util/activity.js' + +const importHostnames: Seeder = async (prisma: PrismaClient) => { + await prisma.hostname.createMany({ + data: [{ hostname: 'brahmsee.digital' }, { hostname: 'landes.digital' }], + }) + + await logActivity({ + type: 'CREATE', + subjectType: 'hostname', + description: 'hostname list created via db seeder', + }) +} + +export { importHostnames } diff --git a/apps/api/prisma/seeders/index.ts b/apps/api/prisma/seeders/index.ts index 43cbe07e..925db47e 100644 --- a/apps/api/prisma/seeders/index.ts +++ b/apps/api/prisma/seeders/index.ts @@ -10,16 +10,21 @@ import importGliederungen from './gliederungen.js' import createVeranstaltung from './veranstaltung.js' import createProgramm from './programm.js' import importKreditinstitute from './kreditinstitute.js' +import { importHostnames } from './hostname.js' export type Seeder = (prisma: PrismaClient) => Promise const seeders: Seeder[] = (() => { // in produktion nur Gliederungen importieren if (isProduction() || process.env.DISABLE_SEEDER === '1') { - return [importGliederungen, importKreditinstitute] + logger.info('running production seeders') + return [importHostnames, importGliederungen, importKreditinstitute] } + logger.info('running development seeders') + return [ + importHostnames, importGliederungen, importKreditinstitute, createAccount, diff --git a/apps/api/src/cli/generator/generateProcedureDelete.ts b/apps/api/src/cli/generator/generateProcedureDelete.ts index 53d2ae2d..562558a9 100644 --- a/apps/api/src/cli/generator/generateProcedureDelete.ts +++ b/apps/api/src/cli/generator/generateProcedureDelete.ts @@ -49,7 +49,7 @@ export const ${procedureFileName}Procedure = ${procedureFunction}({ ${roleIds} protection: ${protectionContent}, inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { return prisma.${procedure.service}.delete({ diff --git a/apps/api/src/cli/generator/generateProcedureGet.ts b/apps/api/src/cli/generator/generateProcedureGet.ts index 83998180..6468bc97 100644 --- a/apps/api/src/cli/generator/generateProcedureGet.ts +++ b/apps/api/src/cli/generator/generateProcedureGet.ts @@ -49,7 +49,7 @@ export const ${procedureFileName}Procedure = ${procedureFunction}({ ${roleIds} protection: ${protectionContent}, inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { return prisma.${procedure.service}.findUniqueOrThrow({ diff --git a/apps/api/src/cli/generator/generateProcedurePatch.ts b/apps/api/src/cli/generator/generateProcedurePatch.ts index 168325ca..7159bb5c 100644 --- a/apps/api/src/cli/generator/generateProcedurePatch.ts +++ b/apps/api/src/cli/generator/generateProcedurePatch.ts @@ -50,7 +50,7 @@ export const ${procedureFileName}Procedure = ${procedureFunction}({ ${roleIds} protection: ${protectionContent}, inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: z.strictObject({}), }), async handler(options) { diff --git a/apps/api/src/client.ts b/apps/api/src/client.ts index 35646dcf..a1496fc4 100644 --- a/apps/api/src/client.ts +++ b/apps/api/src/client.ts @@ -2,7 +2,7 @@ import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server' import type { appRouter } from './index.js' -export * from '@prisma/client' +export type * from '@prisma/client' export type AppRouter = typeof appRouter export type { TKontaktSchema } from './services/kontakt/schema/kontakt.schema.js' @@ -13,3 +13,5 @@ export type RouterOutput = inferRouterOutputs export * from './types/enums/index.js' export * from './types/enums/mappings/index.js' + +export type * from './types/defineTableProcedure.js' diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index e60c24c1..d6b7558d 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -9,8 +9,7 @@ import { z } from 'zod' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -const baseConfig = config.util.loadFileConfigs(path.join(__dirname, '..', 'config')) +const baseConfig = config.util.loadFileConfigs(path.join(__dirname, '..', 'config')) as object const zMsUnit = z .string() @@ -18,7 +17,6 @@ const zMsUnit = z /^\d+\s?\w*$/g, 'Value must be a valid duration as specified by vercel/ms: https://github.com/vercel/ms?tab=readme-ov-file#examples' ) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return .transform((arg) => arg as StringValue) export const configSchema = z.strictObject({ @@ -37,7 +35,19 @@ export const configSchema = z.strictObject({ secret: z.string(), expiresIn: zMsUnit, dlrg: z.strictObject({ - client_id: z.string(), + issuer: z.string().url(), + clientId: z.string(), + clientSecret: z + .string() + .optional() + .transform((v) => { + const trimmed = v?.trim() + if (trimmed === undefined || trimmed.length === 0) { + return undefined + } + return trimmed + }), + allowInsecure: z.boolean(), }), }), diff --git a/apps/api/src/context.ts b/apps/api/src/context.ts index bce50cf7..507acc92 100644 --- a/apps/api/src/context.ts +++ b/apps/api/src/context.ts @@ -1,30 +1,24 @@ +import type { Account } from '@prisma/client' import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' -import type { CreateTrpcKoaContextOptions } from 'trpc-koa-adapter' - import { getEntityIdFromHeader } from './authentication.js' import { logger } from './logger.js' -import client from './prisma.js' -import type { Account } from '@prisma/client' +import prisma from './prisma.js' -function getAuthorizationHeader( - headers: CreateTrpcKoaContextOptions['req']['headers'] | FetchCreateContextFnOptions['req']['headers'] -) { +function getAuthorizationHeader(headers: FetchCreateContextFnOptions['req']['headers']) { if ('authorization' in headers && typeof headers['authorization'] === 'string') { return headers['authorization'] } else { - return (headers as FetchCreateContextFnOptions['req']['headers']).get('authorization') + return headers.get('authorization') } } -export async function createContext({ - req, -}: CreateTrpcKoaContextOptions | FetchCreateContextFnOptions): Promise { +export async function createContext({ req }: FetchCreateContextFnOptions): Promise { try { const authorization = getAuthorizationHeader(req.headers) if (authorization === null) throw new Error('No authorization header found.') - const accountIdFromHeader = getEntityIdFromHeader(authorization) - if (accountIdFromHeader === undefined) { + const accountId = getEntityIdFromHeader(authorization) + if (accountId === undefined) { return { authenticated: false, account: undefined, @@ -32,13 +26,20 @@ export async function createContext({ } } - const accountId = parseInt(accountIdFromHeader) - const account = await client.account.findFirstOrThrow({ + const account = await prisma.account.findFirst({ where: { id: accountId, }, }) + if (account === null) { + return { + authenticated: false, + accountId: undefined, + account: undefined, + } + } + return { authenticated: true, accountId, @@ -62,7 +63,7 @@ type AuthContext = } | { authenticated: true - accountId: number + accountId: string account: Account } diff --git a/apps/api/src/meilisearch/person.ts b/apps/api/src/meilisearch/person.ts index a9330584..3e1340af 100644 --- a/apps/api/src/meilisearch/person.ts +++ b/apps/api/src/meilisearch/person.ts @@ -5,13 +5,13 @@ import { meilisearchClient, updateSettings } from './index.js' const searchIndex = 'person' type MeiliPerson = { - id: number + id: string firstname: string lastname: string birthday: Date | null email: string gliederung: { - id: number + id: string name: string } | null } @@ -24,7 +24,7 @@ export async function updateMeiliPerson(person: MeiliPerson) { } } -export async function deleteMeiliPerson(id: number) { +export async function deleteMeiliPerson(id: string) { try { await meilisearchClient.index(searchIndex).deleteDocument(id) } catch (error) { @@ -36,11 +36,11 @@ export async function syncAllPersonsToMeili() { await meilisearchClient.index(searchIndex).updateSettings(updateSettings) await meilisearchClient.updateIndex(searchIndex, { primaryKey: 'id' }) - let cursorValue: number | undefined + let cursorValue: string | undefined const batchSize = 1000 do { const skip: number = cursorValue ? 1 : 0 - const cursor: { id: number } | undefined = cursorValue ? { id: cursorValue } : undefined + const cursor: { id: string } | undefined = cursorValue ? { id: cursorValue } : undefined const persons = await prisma.person.findMany({ take: batchSize, skip, diff --git a/apps/api/src/middleware/cache-control.ts b/apps/api/src/middleware/cache-control.ts deleted file mode 100644 index 55d8cfda..00000000 --- a/apps/api/src/middleware/cache-control.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Middleware } from 'koa' - -const cacheExclude = [/^\/api.*/gi, /^\/index.html/gi, /^\//gi] - -const cacheControl: Middleware = async (ctx, next) => { - let setCache = true - for (const exclusion of cacheExclude) { - if (exclusion.test(ctx.path)) { - setCache = false - break - } - } - - if (setCache) { - ctx.set('cache-control', 'max-age: 31536000, immutable') // 1 week - } - - await next() -} - -export default cacheControl diff --git a/apps/api/src/prisma.ts b/apps/api/src/prisma.ts index 375ea946..eb948ee8 100644 --- a/apps/api/src/prisma.ts +++ b/apps/api/src/prisma.ts @@ -10,7 +10,7 @@ import { logger } from './logger.js' const log = logger.child({ label: 'db' }) -const client = new PrismaClient({ +const prisma = new PrismaClient({ log: [ { emit: 'event', @@ -25,9 +25,9 @@ const client = new PrismaClient({ }) if (config.loggingLevel === 'debug') { - client.$on('query', (e) => { + prisma.$on('query', (e) => { log.debug(e.query) }) } -export default client +export default prisma diff --git a/apps/api/src/routes/connect.ts b/apps/api/src/routes/connect.ts deleted file mode 100644 index 99fa967a..00000000 --- a/apps/api/src/routes/connect.ts +++ /dev/null @@ -1,90 +0,0 @@ -import jwt from 'jsonwebtoken' -import type { Context } from 'koa' -import { z } from 'zod' - -import { sign } from '../authentication.js' -import config from '../config.js' -import prisma from '../prisma.js' - -const ZUserInfoReponse = z.object({ - access_token: z.string(), - refresh_token: z.string(), - profile: z.object({ - sub: z.string(), - email_verified: z.boolean(), - name: z.string(), - preferred_username: z.string(), - given_name: z.string(), - family_name: z.string(), - email: z.string().email(), - }), -}) - -const ZOauthMode = z.enum(['login', 'register']) - -export const ZOauthRegisterJwtPayloadSchema = z.object({ - sub: z.string(), - email: z.string(), - firstname: z.string(), - lastname: z.string(), -}) - -type TOauthRegisterJwtPayloadSchema = z.infer - -export default async function (ctx: Context) { - const session = ctx.session as { grant: { response: unknown; dynamic: { mode: unknown } } } - const userInfoResponse = ZUserInfoReponse.parse(session.grant.response) - const profile = userInfoResponse.profile - - const mode = ZOauthMode.parse(session.grant.dynamic.mode) - - if (mode === 'register') { - const payload: TOauthRegisterJwtPayloadSchema = { - sub: profile.sub, - email: profile.email, - firstname: profile.given_name, - lastname: profile.family_name, - } - const expiresIn = '1h' - // create a jwt with the sub and redirect to register page - const jwtOAuthToken = jwt.sign(payload, `${config.authentication.secret}-oauth`, { - expiresIn, - }) - ctx.redirect(`${config.clientUrl}/registrierung/gliederung/callback#jwtOAuthToken=${jwtOAuthToken}`) - } - - // if no mode selelected or mode is login, try to login - if (mode === 'login') { - await oauthLogin(ctx, profile) - } -} - -async function oauthLogin(ctx: Context, profile: (typeof ZUserInfoReponse._type)['profile']) { - // look for existing user - const existingUser = await prisma.account.findUnique({ - where: { - dlrgOauthId: profile.sub, - }, - select: { - id: true, - status: true, - }, - }) - - // if user exists, return jwt - if (existingUser) { - if (existingUser.status !== 'AKTIV') { - ctx.redirect( - `${config.clientUrl}/login#error=Account ist nicht aktiviert. Bitte wende dich an den Administrator.` - ) - } else { - const jwt = sign({ - sub: existingUser.id.toString(), - }) - // important to redirect with hash, so the jwt is not sent to the server - ctx.redirect(`${config.clientUrl}/login#jwt=${jwt}`) - } - } else { - ctx.redirect(`${config.clientUrl}/registrierung`) - } -} diff --git a/apps/api/src/routes/exports/index.ts b/apps/api/src/routes/exports/index.ts new file mode 100644 index 00000000..5ae64bed --- /dev/null +++ b/apps/api/src/routes/exports/index.ts @@ -0,0 +1,12 @@ +import { makeApp } from '../../util/make-app.js' +import { veranstaltungPhotoArchive } from './photos.archive.js' +import { veranstaltungTeilnehmendenliste } from './teilnehmendenliste.sheet.js' +import { veranstaltungVerpflegung } from './verpflegung.sheet.js' + +const exportRouter = makeApp() + +exportRouter.get('/sheet/teilnehmendenliste', veranstaltungTeilnehmendenliste) +exportRouter.get('/sheet/verpflegung', veranstaltungVerpflegung) +exportRouter.get('/archive/photos', veranstaltungPhotoArchive) + +export { exportRouter } diff --git a/apps/api/src/routes/exports/archives/photos.ts b/apps/api/src/routes/exports/photos.archive.ts similarity index 76% rename from apps/api/src/routes/exports/archives/photos.ts rename to apps/api/src/routes/exports/photos.archive.ts index 4d08ac7a..a0a7bc99 100644 --- a/apps/api/src/routes/exports/archives/photos.ts +++ b/apps/api/src/routes/exports/photos.archive.ts @@ -3,13 +3,15 @@ import XLSX from '@e965/xlsx' import type { Gliederung } from '@prisma/client' import { TRPCError } from '@trpc/server' import archiver from 'archiver' -import type { Context } from 'koa' +import { stream } from 'hono/streaming' import mime from 'mime' +import { Readable } from 'node:stream' import { z } from 'zod' -import prisma from '../../../prisma.js' -import { openFileStream } from '../../../services/file/helpers/getFileUrl.js' -import { getSecurityWorksheet } from '../helpers/getSecurityWorksheet.js' -import { sheetAuthorize, type SheetQuery } from '../sheets/sheets.schema.js' +import prisma from '../../prisma.js' +import { openFileStream } from '../../services/file/helpers/getFileUrl.js' +import type { AppContext } from '../../util/make-app.js' +import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js' +import { sheetAuthorize, type SheetQuery } from './sheets.schema.js' const querySchema = z.object({ mode: z.enum(['group', 'flat']), @@ -100,14 +102,16 @@ function buildSheet( return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer } -export async function veranstaltungPhotoArchive(ctx: Context) { +const baseDirectory = 'Fotos' + +export async function veranstaltungPhotoArchive(ctx: AppContext) { const authorization = await sheetAuthorize(ctx) if (!authorization) { return } const { query, gliederung, account } = authorization - const { mode } = querySchema.parse(ctx.query) + const { mode } = querySchema.parse(ctx.req.query()) if (mode === 'flat' && account.role !== 'ADMIN') { throw new TRPCError({ @@ -127,32 +131,32 @@ export async function veranstaltungPhotoArchive(ctx: Context) { } }) - // good practice to catch this error explicitly zip.on('error', function (err) { throw err }) - ctx.res.statusCode = 201 - ctx.res.setHeader('Content-Type', 'application/zip') - ctx.res.setHeader( - 'Content-Disposition', - `attachment; filename="${mode === 'flat' ? 'FotosForAutomation' : 'Fotos'}.zip"` - ) - zip.pipe(ctx.res) + ctx.status(201) + ctx.header('Content-Type', 'application/zip') + ctx.header('Content-Disposition', `attachment; filename="${mode === 'flat' ? 'FotosForAutomation' : 'Fotos'}.zip"`) + + zip.append(`Gesamtzahl Fotos: ${anmeldungen.length}`, { name: `${baseDirectory}/README.txt` }) for (const { person, unterveranstaltung } of anmeldungen) { if (!person.photo) { - return + continue } const stream = await openFileStream(person.photo) - const directory = `Fotos Teilnehmende ${unterveranstaltung.veranstaltung.name}/${unterveranstaltung.gliederung.name}` + const directory = `${unterveranstaltung.veranstaltung.name}/${unterveranstaltung.gliederung.name}` const basename = mode === 'group' ? `${person.firstname} ${person.lastname}` : person.id const extension = mime.getExtension(person.photo.mimetype ?? 'text/plain') zip.append(stream, { - name: mode === 'group' ? `${directory}/${basename}.${extension}` : `Fotos/${person.photo.id}.${extension}`, + name: + mode === 'group' + ? `${baseDirectory}/${directory}/${basename}.${extension}` + : `Fotos/${person.photo.id}.${extension}`, date: person.photo.createdAt, }) } @@ -166,5 +170,7 @@ export async function veranstaltungPhotoArchive(ctx: Context) { await zip.finalize() - ctx.res.end() + return stream(ctx, async (s) => { + await s.pipe(Readable.toWeb(zip)) + }) } diff --git a/apps/api/src/routes/exports/sheets/sheets.schema.ts b/apps/api/src/routes/exports/sheets.schema.ts similarity index 61% rename from apps/api/src/routes/exports/sheets/sheets.schema.ts rename to apps/api/src/routes/exports/sheets.schema.ts index 429c90a0..c3b4ff3d 100644 --- a/apps/api/src/routes/exports/sheets/sheets.schema.ts +++ b/apps/api/src/routes/exports/sheets.schema.ts @@ -1,16 +1,16 @@ import { Role, type Gliederung } from '@prisma/client' -import type { Context } from 'koa' +import type { Context } from 'hono' import { z } from 'zod' -import { getEntityIdFromHeader } from '../../../authentication.js' -import prisma from '../../../prisma.js' -import { getGliederungRequireAdmin } from '../../../util/getGliederungRequireAdmin.js' -import { zodSafe } from '../../../util/zod.js' +import { getEntityIdFromHeader } from '../../authentication.js' +import prisma from '../../prisma.js' +import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' +import { zodSafe } from '../../util/zod.js' export const sheetQuerySchema = z .object({ jwt: z.string(), - veranstaltungId: z.coerce.number().min(0).optional(), - unterveranstaltungId: z.coerce.number().min(0).optional(), + veranstaltungId: z.string().uuid().optional(), + unterveranstaltungId: z.string().uuid().optional(), }) .refine((data) => !!data.veranstaltungId || !!data.unterveranstaltungId, { message: 'Exactly one of veranstaltungId or unterveranstaltungId must be provided', @@ -19,23 +19,21 @@ export const sheetQuerySchema = z export type SheetQuery = z.infer export async function sheetAuthorize(ctx: Context) { - const [success, query] = await zodSafe(sheetQuerySchema, ctx.query) + const [success, query] = await zodSafe(sheetQuerySchema, ctx.req.query()) if (!success) { - ctx.response.status = 400 + ctx.status(400) return false } const accountId = getEntityIdFromHeader(`Bearer ${query.jwt}`) if (typeof accountId !== 'string') { - ctx.response.status = 401 + ctx.status(401) return false } - const accountIdNumber = parseInt(accountId) - const account = await prisma.account.findUnique({ where: { - id: accountIdNumber, + id: accountId, }, select: { role: true, @@ -49,17 +47,17 @@ export async function sheetAuthorize(ctx: Context) { }) if (account == null) { - ctx.res.statusCode = 401 + ctx.status(401) return false } let gliederung: Gliederung | undefined = undefined if (account.role == Role.GLIEDERUNG_ADMIN) { try { - gliederung = await getGliederungRequireAdmin(accountIdNumber) + gliederung = await getGliederungRequireAdmin(accountId) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { - ctx.res.statusCode = 401 + ctx.status(401) return false } } diff --git a/apps/api/src/routes/exports/sheets/teilnehmendenliste.ts b/apps/api/src/routes/exports/teilnehmendenliste.sheet.ts similarity index 91% rename from apps/api/src/routes/exports/sheets/teilnehmendenliste.ts rename to apps/api/src/routes/exports/teilnehmendenliste.sheet.ts index 48469d84..517b5e6f 100644 --- a/apps/api/src/routes/exports/sheets/teilnehmendenliste.ts +++ b/apps/api/src/routes/exports/teilnehmendenliste.sheet.ts @@ -1,10 +1,10 @@ import XLSX from '@e965/xlsx' import dayjs from 'dayjs' -import type { Context } from 'koa' -import { AnmeldungStatusMapping, GenderMapping } from '../../../client.js' -import prisma from '../../../prisma.js' -import { getSecurityWorksheet } from '../helpers/getSecurityWorksheet.js' +import { AnmeldungStatusMapping, GenderMapping } from '../../client.js' +import prisma from '../../prisma.js' +import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js' import { sheetAuthorize } from './sheets.schema.js' +import type { Context } from 'hono' export async function veranstaltungTeilnehmendenliste(ctx: Context) { const authorization = await sheetAuthorize(ctx) @@ -162,10 +162,9 @@ export async function veranstaltungTeilnehmendenliste(ctx: Context) { XLSX.utils.book_append_sheet(workbook, securityWorksheet, securityWorksheetName) const filename = `${dayjs().format('YYYYMMDD-hhmm')}-Teilnehmendenliste.xlsx` - const buf = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer + const buf = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as ArrayBuffer - ctx.res.statusCode = 201 - ctx.res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) - ctx.res.setHeader('Content-Type', 'application/vnd.ms-excel') - ctx.res.end(buf) + ctx.header('Content-Disposition', `attachment; filename="${filename}"`) + ctx.header('Content-Type', 'application/vnd.ms-excel') + return ctx.body(buf, 201) } diff --git a/apps/api/src/routes/exports/sheets/verpflegung.ts b/apps/api/src/routes/exports/verpflegung.sheet.ts similarity index 88% rename from apps/api/src/routes/exports/sheets/verpflegung.ts rename to apps/api/src/routes/exports/verpflegung.sheet.ts index da6e2ea8..ed8a2589 100644 --- a/apps/api/src/routes/exports/sheets/verpflegung.ts +++ b/apps/api/src/routes/exports/verpflegung.sheet.ts @@ -1,10 +1,10 @@ import { AnmeldungStatus, Essgewohnheit, NahrungsmittelIntoleranz } from '@prisma/client' import dayjs from 'dayjs' -import type { Context } from 'koa' +import type { Context } from 'hono' import XLSX from '@e965/xlsx' -import prisma from '../../../prisma.js' -import { getSecurityWorksheet } from '../helpers/getSecurityWorksheet.js' -import { getWorkbookDefaultProps } from '../helpers/getWorkbookDefaultProps.js' +import prisma from '../../prisma.js' +import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js' +import { getWorkbookDefaultProps } from './helpers/getWorkbookDefaultProps.js' import { sheetAuthorize } from './sheets.schema.js' export async function veranstaltungVerpflegung(ctx: Context) { @@ -109,10 +109,9 @@ export async function veranstaltungVerpflegung(ctx: Context) { XLSX.utils.book_append_sheet(workbook, securityWorksheet, securityWorksheetName) const filename = `${dayjs().format('YYYYMMDD-hhmm')}-Verpflegung.xlsx` - const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer + const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as ArrayBuffer - ctx.res.statusCode = 201 - ctx.res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) - ctx.res.setHeader('Content-Type', 'application/vnd.ms-excel') - ctx.res.end(buffer) + ctx.header('Content-Disposition', `attachment; filename="${filename}"`) + ctx.header('Content-Type', 'application/vnd.ms-excel') + return ctx.body(buffer, 201) } diff --git a/apps/api/src/routes/files/downloadFileLocal.ts b/apps/api/src/routes/files/downloadFileLocal.ts index 0d517e22..d2f709a3 100644 --- a/apps/api/src/routes/files/downloadFileLocal.ts +++ b/apps/api/src/routes/files/downloadFileLocal.ts @@ -1,15 +1,9 @@ -import * as fs from 'fs' - -import type { Middleware } from 'koa' import mime from 'mime' - +import { createReadStream } from 'node:fs' import prisma from '../../prisma.js' import { uploadDir } from '../../services/file/helpers/getFileUrl.js' -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const downloadFileLocal: Middleware = async function (ctx, next) { - const params = ctx.params as { id: string } - const fileId = params.id +export async function downloadFileLocal(fileId: string) { const file = await prisma.file.findFirst({ where: { id: fileId, @@ -17,18 +11,19 @@ export const downloadFileLocal: Middleware = async function (ctx, next) { }, }) if (file === null) { - ctx.response.status = 404 - return + return null } if (file.provider !== 'LOCAL') { - ctx.response.status = 404 - return + return null } const mimetype = file.mimetype ?? 'application/octet-stream' const filename = file.filename ?? `${file.id}.${mime.getExtension(mimetype)}` - ctx.set('Content-disposition', `attachment; filename=${filename.replace(/[^a-zA-Z0-9._-]/g, '_')}`) - ctx.set('Content-type', mimetype) - ctx.response.body = fs.createReadStream(uploadDir + '/' + file.key) + + return { + mimetype, + filename, + stream: createReadStream(uploadDir + '/' + file.key), + } } diff --git a/apps/api/src/routes/files/importAnmeldungen.ts b/apps/api/src/routes/files/importAnmeldungen.ts deleted file mode 100644 index 06322bc9..00000000 --- a/apps/api/src/routes/files/importAnmeldungen.ts +++ /dev/null @@ -1,227 +0,0 @@ -import * as fs from 'fs' -import * as path from 'path' - -import { Role } from '@prisma/client' -import * as csv from 'fast-csv' -import type { Middleware } from 'koa' - -import { getEntityIdFromHeader } from '../../authentication.js' -import prisma from '../../prisma.js' -import { inputSchema as anmeldungCreateSchema } from '../../services/anmeldung/anmeldungPublicCreate.js' -import { getPersonCreateData } from '../../services/person/schema/person.schema.js' -import { customFieldValuesCreateMany } from '../../types/defineCustomFieldValues.js' - -import { dayjs } from '@codeanker/helpers' - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const importAnmeldungen: Middleware = async function (ctx, next) { - let accountId: string | undefined - try { - accountId = getEntityIdFromHeader(ctx.request.header.authorization) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - ctx.response.status = 401 - ctx.response.message = 'Token not valid' - return - } - const ctxRequestBody = ctx.request.body as { unterveranstaltungId: string } - if (accountId == null || !ctx.request.files || !ctxRequestBody.unterveranstaltungId) { - ctx.response.status = 400 - ctx.response.message = 'Es wurden keine Dateien oder Unterveranstaltung übergeben' - return - } - - const account = await prisma.account.findUnique({ - where: { - id: parseInt(accountId), - }, - select: { - role: true, - person: { - select: { - firstname: true, - lastname: true, - }, - }, - }, - }) - - if (account == null) { - ctx.res.statusCode = 401 - ctx.res.end() - return - } - - if (account.role !== Role.ADMIN) { - ctx.response.status = 401 - ctx.response.message = 'Account not authorized' - return - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let files: any[] = [] - if (Array.isArray(ctx.request.files.files)) { - files = ctx.request.files.files - } else { - files.push(ctx.request.files.files) - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - files.filter((file: any) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (file.mimetype !== 'text/csv') { - ctx.response.status = 406 - ctx.response.message = 'Datei muss vom Typ CSV sein' - return - } - }) - - const unterveranstaltung = await findUnterveranstaltung(parseInt(ctxRequestBody.unterveranstaltungId)) - if (!unterveranstaltung) { - ctx.response.status = 400 - ctx.response.message = 'Unterveranstaltung nicht gefunden' - return - } - - files.forEach((file) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access - fs.createReadStream(path.resolve(file.filepath)) - .pipe(csv.parse({ headers: true, delimiter: ';', ignoreEmpty: true })) - .on('error', (error) => console.error(error)) - // eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/no-unsafe-argument - .on('data', (row) => createAnmeldung(row, unterveranstaltung)) - - .on('end', (rowCount: number) => console.log(`Parsed ${rowCount} rows`)) - - ctx.response.status = 200 - ctx.response.body = { - ok: true, - } - }) -} - -/** - * Erstelle Anmeldung in der Datenbank - * @param row - * @param unterveranstaltungId - */ -async function createAnmeldung( - row: { - vorname: string - nachname: string - geschlecht: string - email: string - telefon: string - strasse: string - geburtstag: string - hausnummer: string - plz: string - ort: string - essgewohnheit: string - nahrungsmittelIntoleranzen: string - weitereIntoleranzen: string - notfallkontaktVorname: string - notfallkontaktNachname: string - notfallkontaktTelefon: string - istErziehungsberechtigt: string - comment: string - }, - unterveranstaltung: { id: number; gliederungId: number } -) { - try { - const mappedRow = { - data: { - gliederungId: unterveranstaltung.gliederungId, - unterveranstaltungId: unterveranstaltung.id, - //Person Schema - firstname: row.vorname, - lastname: row.nachname, - birthday: dayjs(row.geburtstag, 'DD.MM.YYYY').toDate(), - gender: row.geschlecht, - email: row.email, - telefon: row.telefon, - address: { - street: row.strasse, - streetNumber: row.hausnummer, - zip: row.plz, - city: row.ort, - country: 'DE', - valid: false, - }, - essgewohnheit: row.essgewohnheit, - nahrungsmittelIntoleranzen: formatNahrungsmittelIntoleranzen(row.nahrungsmittelIntoleranzen), - weitereIntoleranzen: formatNahrungsmittelIntoleranzen(row.weitereIntoleranzen), - notfallkontaktPersonen: [ - { - firstname: row.notfallkontaktVorname, - lastname: row.notfallkontaktNachname, - telefon: row.notfallkontaktTelefon, - istErziehungsberechtigt: row.istErziehungsberechtigt === 'Ja' ? true : false, - }, - ], - }, - customFieldValues: mapCustomFields(row), - } - - const validatedData = anmeldungCreateSchema.parse(mappedRow) - const personData = await getPersonCreateData(validatedData.data) - await prisma.person.create({ - data: { - ...personData, - anmeldungen: { - create: { - unterveranstaltungId: unterveranstaltung.id, - comment: validatedData.data.comment, - createdAt: new Date(), - customFieldValues: { - createMany: customFieldValuesCreateMany(validatedData.customFieldValues), - }, - }, - }, - }, - }) - } catch (e) { - console.error('Anmeldung konnte nicht erstellt werden', row, e) - } -} -/** - * Suche nach der passenden Unterveranstaltung anhand der ID - * @param unterveranstaltungId - * @returns - */ -async function findUnterveranstaltung(unterveranstaltungId: number) { - return await prisma.unterveranstaltung.findUnique({ - where: { - id: unterveranstaltungId, - }, - select: { - id: true, - gliederungId: true, - }, - }) -} - -function formatNahrungsmittelIntoleranzen(nahrungsmittelIntoleranzen: string) { - if (!nahrungsmittelIntoleranzen) return [] - return nahrungsmittelIntoleranzen.split(',').map((item) => item.trim()) -} - -function mapCustomFields(obj: Record) { - const customFields: { fieldId: number; value: string | boolean }[] = [] - for (const key in obj) { - if (key.startsWith('customFieldId_')) { - if (!obj[key]) continue - customFields.push({ - fieldId: parseInt(key.replace('customFieldId_', '')), - value: parseValue(obj[key]), - }) - } - } - return customFields -} - -function parseValue(value: string) { - if (value === 'Ja') return true - if (value === 'Nein') return false - return value -} diff --git a/apps/api/src/routes/files/index.ts b/apps/api/src/routes/files/index.ts new file mode 100644 index 00000000..189b749f --- /dev/null +++ b/apps/api/src/routes/files/index.ts @@ -0,0 +1,68 @@ +import type { File as Entity } from '@prisma/client' +import { createMiddleware } from 'hono/factory' +import { stream } from 'hono/streaming' +import { Readable } from 'node:stream' +import prisma from '../../prisma.js' +import { makeApp } from '../../util/make-app.js' +import { downloadFileLocal } from '../files/downloadFileLocal.js' +import { uploadFileLocal } from '../files/uploadFileLocal.js' + +const fileRouter = makeApp() + +fileRouter.get('/download/LOCAL/:id', async (ctx) => { + const fileId = ctx.req.param('id') + const result = await downloadFileLocal(fileId) + if (result === null) { + return ctx.status(404) + } else { + ctx.header('Content-Disposition', `attachment; filename=${result.filename.replace(/[^a-zA-Z0-9._-]/g, '_')}`) + ctx.header('Content-Type', result.mimetype) + return stream(ctx, async (s) => { + await s.pipe(Readable.toWeb(result.stream)) + }) + } +}) + +const entity = createMiddleware<{ + Bindings: { entity: Entity } +}>(async (ctx, next) => { + const fileId = ctx.req.param('id') + + const file = await prisma.file.findFirst({ + where: { + id: fileId, + }, + }) + + if (file === null || file.provider !== 'LOCAL') { + return ctx.json({ error: 'file not found' }, 404) + } + if (file.uploaded) { + return ctx.json({ error: 'file already present' }, 403) + } + + ctx.env.entity = file + + return await next() +}) + +const multipart = createMiddleware<{ + Bindings: { file: File } +}>(async (ctx, next) => { + const body = await ctx.req.parseBody() + const file = body['file'] + if (!(file instanceof File)) { + return ctx.json({ error: 'Upload is not a file' }, 400) + } + + ctx.env.file = file + + return await next() +}) + +fileRouter.post('/upload/LOCAL/:id', entity, multipart, async (ctx) => { + await uploadFileLocal(ctx.env.entity, ctx.env.file) + return ctx.json({ ok: true }, 201) +}) + +export { fileRouter } diff --git a/apps/api/src/routes/files/uploadFileLocal.ts b/apps/api/src/routes/files/uploadFileLocal.ts index a81fd44f..f9a9f18b 100644 --- a/apps/api/src/routes/files/uploadFileLocal.ts +++ b/apps/api/src/routes/files/uploadFileLocal.ts @@ -1,91 +1,39 @@ -import * as fs from 'fs/promises' -import * as path from 'path' - -import type { Middleware } from 'koa' - -import config from '../../config.js' +import type { File as Entity } from '@prisma/client' +import { createWriteStream } from 'node:fs' +import { mkdir, stat } from 'node:fs/promises' +import { Readable } from 'node:stream' import prisma from '../../prisma.js' +import { uploadDir } from '../../services/file/helpers/getFileUrl.js' -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const uploadFileLocal: Middleware = async function (ctx, next) { - const params = ctx.params as { id: string } - const fileId = params.id - const file = await prisma.file.findFirst({ - where: { - id: fileId, - }, - }) - if (file === null) { - ctx.response.status = 400 - ctx.response.body = { error: `File with id '${fileId}' not found` } - return - } - - if (file.uploaded) { - ctx.response.status = 400 - ctx.response.body = { error: `File with id '${fileId}' already uploaded` } - return - } - - if (file.provider !== 'LOCAL') { - ctx.response.status = 400 - ctx.response.body = { error: `File provider is '${file.provider}'. This endpoint is for LOCAL` } - return - } - - const uploadDir = path.join(process.cwd(), config.fileProviders.LOCAL.path) +export async function uploadFileLocal(entity: Entity, multipart: File) { try { - await checkLocalUploadFolder(uploadDir) - } catch (e) { - console.error('Error while creating upload-directory\n', e) - ctx.response.status = 500 - ctx.response.body = { - error: 'Something went wrong during creation of upload-directory', + const stats = await stat(uploadDir) + if (!stats.isDirectory()) { + await mkdir(uploadDir, { recursive: true }) } - return + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + await mkdir(uploadDir, { recursive: true }) } - const fileData = ctx.request.files?.file - if (!fileData || Array.isArray(fileData)) { - ctx.response.status = 400 - ctx.response.body = { - error: 'No or Invalid File provided', - } - return - } + const ws = createWriteStream(`${uploadDir}/${entity.key}`) + const rs = Readable.fromWeb(multipart.stream()) - try { - await fs.copyFile(fileData.filepath, uploadDir + '/' + file.key) - } catch (e) { - console.error('Error while copy to upload-directory\n', e) - ctx.response.status = 500 - ctx.response.body = { - error: 'Something went wrong during copy to upload-directory', - } - return - } + rs.pipe(ws) + + await new Promise((resolve, reject) => { + rs.once('close', resolve) + rs.once('error', reject) + }) await prisma.file.update({ - where: { id: fileId }, + where: { id: entity.id }, data: { - mimetype: fileData.mimetype ?? 'application/octet-stream', - filename: fileData.originalFilename ?? undefined, + mimetype: multipart.type ?? 'application/octet-stream', + filename: multipart.name, uploaded: true, uploadedAt: new Date(), + provider: 'LOCAL', }, }) - - ctx.response.status = 201 - ctx.response.body = { uploaded: true } -} - -async function checkLocalUploadFolder(uploadDir: string) { - try { - await fs.stat(uploadDir) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (e.code === 'ENOENT') await fs.mkdir(uploadDir, { recursive: true }) - else throw e - } } diff --git a/apps/api/src/routes/imports/anmeldungen.ts b/apps/api/src/routes/imports/anmeldungen.ts new file mode 100644 index 00000000..5c4ba6f3 --- /dev/null +++ b/apps/api/src/routes/imports/anmeldungen.ts @@ -0,0 +1,116 @@ +import { dayjs } from '@codeanker/helpers' +import prisma from '../../prisma.js' +import { inputSchema as anmeldungCreateSchema } from '../../services/anmeldung/anmeldungPublicCreate.js' +import { getPersonCreateData } from '../../services/person/schema/person.schema.js' +import { customFieldValuesCreateMany } from '../../types/defineCustomFieldValues.js' +import { z } from 'zod' + +const rowSchema = z.object({ + vorname: z.string(), + nachname: z.string(), + geschlecht: z.string(), + email: z.string(), + telefon: z.string(), + strasse: z.string(), + geburtstag: z.string(), + hausnummer: z.string(), + plz: z.string(), + ort: z.string(), + essgewohnheit: z.string(), + nahrungsmittelIntoleranzen: z.string(), + weitereIntoleranzen: z.string(), + notfallkontaktVorname: z.string(), + notfallkontaktNachname: z.string(), + notfallkontaktTelefon: z.string(), + istErziehungsberechtigt: z.string(), + comment: z.string(), +}) + +/** + * Erstelle Anmeldung in der Datenbank + * @param row + * @param unterveranstaltungId + */ +export async function createAnmeldung(payload: unknown, unterveranstaltung: { id: string; gliederungId: string }) { + try { + const row = rowSchema.parse(payload) + const mappedRow = { + data: { + gliederungId: unterveranstaltung.gliederungId, + unterveranstaltungId: unterveranstaltung.id, + //Person Schema + firstname: row.vorname, + lastname: row.nachname, + birthday: dayjs(row.geburtstag, 'DD.MM.YYYY').toDate(), + gender: row.geschlecht, + email: row.email, + telefon: row.telefon, + address: { + street: row.strasse, + streetNumber: row.hausnummer, + zip: row.plz, + city: row.ort, + country: 'DE', + valid: false, + }, + essgewohnheit: row.essgewohnheit, + nahrungsmittelIntoleranzen: formatNahrungsmittelIntoleranzen(row.nahrungsmittelIntoleranzen), + weitereIntoleranzen: formatNahrungsmittelIntoleranzen(row.weitereIntoleranzen), + notfallkontaktPersonen: [ + { + firstname: row.notfallkontaktVorname, + lastname: row.notfallkontaktNachname, + telefon: row.notfallkontaktTelefon, + istErziehungsberechtigt: row.istErziehungsberechtigt === 'Ja' ? true : false, + }, + ], + }, + customFieldValues: mapCustomFields(row), + } + + const validatedData = anmeldungCreateSchema.parse(mappedRow) + const personData = await getPersonCreateData(validatedData.data) + await prisma.person.create({ + data: { + ...personData, + anmeldungen: { + create: { + unterveranstaltungId: unterveranstaltung.id, + comment: validatedData.data.comment, + createdAt: new Date(), + customFieldValues: { + createMany: customFieldValuesCreateMany(validatedData.customFieldValues), + }, + }, + }, + }, + }) + } catch (e) { + console.error('Anmeldung konnte nicht erstellt werden', payload, e) + } +} + +function formatNahrungsmittelIntoleranzen(nahrungsmittelIntoleranzen: string) { + if (!nahrungsmittelIntoleranzen) return [] + return nahrungsmittelIntoleranzen.split(',').map((item) => item.trim()) +} + +function mapCustomFields(obj: Record) { + const customFields: { fieldId: string; value: string | boolean }[] = [] + for (const key in obj) { + if (key.startsWith('customFieldId_')) { + if (!obj[key]) continue + customFields.push({ + fieldId: key.replace('customFieldId_', ''), + value: parseValue(obj[key]), + }) + } + } + return customFields +} + +function parseValue(value: string) { + if (value === 'Ja') return true + if (value === 'Nein') return false + return value +} diff --git a/apps/api/src/routes/imports/index.ts b/apps/api/src/routes/imports/index.ts new file mode 100644 index 00000000..e1c25234 --- /dev/null +++ b/apps/api/src/routes/imports/index.ts @@ -0,0 +1,91 @@ +import * as csv from 'fast-csv' +import { Role } from '@prisma/client' +import { getEntityIdFromHeader } from '../../authentication.js' +import prisma from '../../prisma.js' +import { makeApp } from '../../util/make-app.js' +import { ReadStream } from 'node:fs' +import { createAnmeldung } from './anmeldungen.js' + +const importRouter = makeApp() + +importRouter.post('/anmeldungen/:unterveranstaltungId', async (ctx) => { + const authorization = ctx.req.header('Authorization') + if (!authorization) { + ctx.status(401) + return + } + + const accountId = getEntityIdFromHeader(authorization) + if (!accountId) { + ctx.status(401) + return + } + + const account = await prisma.account.findUnique({ + where: { + id: accountId, + }, + select: { + role: true, + person: { + select: { + firstname: true, + lastname: true, + }, + }, + }, + }) + if (!account || account.role !== Role.ADMIN) { + ctx.status(401) + return + } + + const body = await ctx.req.parseBody({ + all: true, + }) + + const files = Object.values(body).filter((v) => v instanceof File) + for (const file of files) { + if (file.type !== 'text/csv') { + return ctx.json({ message: 'Datei muss vom Typ CSV sein' }, 400) + } + } + + const unterveranstaltung = await findUnterveranstaltung(ctx.req.param('unterveranstaltungId')) + if (!unterveranstaltung) { + return ctx.json({ error: 'Unterveranstaltung not found' }, 404) + } + + for (const file of files) { + const parser = csv.parse({ headers: true, delimiter: ';', ignoreEmpty: true }) + await new Promise((resolve, reject) => { + ReadStream.fromWeb(file.stream()) + .pipe(parser) + .on('error', reject) + .on('data', (row: unknown) => { + // TODO: push rows to a cache and flush it afterwards (redis for example) + void createAnmeldung(row, unterveranstaltung) + }) + .on('end', resolve) + }) + } +}) + +export { importRouter } + +/** + * Suche nach der passenden Unterveranstaltung anhand der ID + * @param unterveranstaltungId + * @returns + */ +async function findUnterveranstaltung(unterveranstaltungId: string) { + return await prisma.unterveranstaltung.findUnique({ + where: { + id: unterveranstaltungId, + }, + select: { + id: true, + gliederungId: true, + }, + }) +} diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts index 8b5edf42..28ccb977 100644 --- a/apps/api/src/routes/index.ts +++ b/apps/api/src/routes/index.ts @@ -1,24 +1,4 @@ -import Router from 'koa-router' - -import connect from './connect.js' -import { veranstaltungPhotoArchive } from './exports/archives/photos.js' -import { veranstaltungTeilnehmendenliste } from './exports/sheets/teilnehmendenliste.js' -import { veranstaltungVerpflegung } from './exports/sheets/verpflegung.js' -import { downloadFileLocal } from './files/downloadFileLocal.js' -import { importAnmeldungen } from './files/importAnmeldungen.js' -import { uploadFileLocal } from './files/uploadFileLocal.js' - -const koaRouter = new Router() - -koaRouter.get('/connect/dlrg/callback', connect) - -koaRouter.get('/export/sheet/teilnehmendenliste', veranstaltungTeilnehmendenliste) -koaRouter.get('/export/sheet/verpflegung', veranstaltungVerpflegung) -koaRouter.get('/export/archive/photos', veranstaltungPhotoArchive) - -koaRouter.get('/download/file/LOCAL/:id', downloadFileLocal) -koaRouter.post('/upload/file/LOCAL/:id', uploadFileLocal) - -koaRouter.post('/upload/anmeldungen', importAnmeldungen) - -export default koaRouter +export * from './exports/index.js' +export * from './files/index.js' +export * from './imports/index.js' +export * from './oidc/index.js' diff --git a/apps/api/src/routes/oidc/index.ts b/apps/api/src/routes/oidc/index.ts new file mode 100644 index 00000000..eed98eaa --- /dev/null +++ b/apps/api/src/routes/oidc/index.ts @@ -0,0 +1,173 @@ +import { deleteCookie, getCookie, setCookie } from 'hono/cookie' +import { HTTPException } from 'hono/http-exception' +import * as oauth from 'oauth4webapi' +import { z } from 'zod' +import { sign } from '../../authentication.js' +import config from '../../config.js' +import prisma from '../../prisma.js' +import { makeApp } from '../../util/make-app.js' +import type { Account } from '@prisma/client' + +export const oidcRouter = makeApp() + +const algorithm: oauth.DiscoveryRequestOptions['algorithm'] = 'oidc' +const ZProfile = z.object({ + sub: z.string(), + preferred_username: z.string(), + email: z.string().email(), + given_name: z.string(), + family_name: z.string(), +}) + +oidcRouter.get('/dlrg/callback', async (c) => { + const issuer = new URL(config.authentication.dlrg.issuer) + const discoveryRequestResponse = await oauth.discoveryRequest(issuer, { + algorithm, + [oauth.allowInsecureRequests]: config.authentication.dlrg.allowInsecure, + }) + const as = await oauth.processDiscoveryResponse(issuer, discoveryRequestResponse) + const client: oauth.Client = { client_id: config.authentication.dlrg.clientId } + const clientAuth = + config.authentication.dlrg.clientSecret === undefined + ? oauth.None() + : oauth.ClientSecretPost(config.authentication.dlrg.clientSecret) + const redirect_uri = `${config.clientUrl}/api/oidc/dlrg/callback` + const currentUrl: URL = new URL(c.req.url, config.clientUrl) + const params = oauth.validateAuthResponse(as, client, currentUrl) + + const code_verifier = getCookie(c, 'code_verifier') + if (!code_verifier) { + deleteCookie(c, 'code_verifier') + throw new HTTPException(400) + } + const nonce = getCookie(c, 'nonce') + if (!nonce) { + deleteCookie(c, 'nonce') + throw new HTTPException(400) + } + + const response = await oauth.authorizationCodeGrantRequest( + as, + client, + clientAuth, + params, + redirect_uri, + code_verifier, + { + [oauth.allowInsecureRequests]: config.authentication.dlrg.allowInsecure, + } + ) + + const result = await oauth.processAuthorizationCodeResponse(as, client, response, { + expectedNonce: nonce, + requireIdToken: true, + }) + // const claims = oauth.getValidatedIdTokenClaims(result) + const userInfoResponse = await fetch(as.userinfo_endpoint!, { + headers: { + Authorization: `Bearer ${result.access_token}`, + }, + }) + const profileRaw = await userInfoResponse.json() + const profile = ZProfile.parse(profileRaw) + const existingUser = await prisma.account.findUnique({ + where: { + dlrgOauthId: profile.sub, + }, + }) + + let registerAsGliederung = false + const registerAs = c.req.query('as')?.trim() + if (registerAs !== undefined && registerAs?.length > 0) { + registerAsGliederung = true + } + + let account: Account + + // if user exists, return jwt + if (existingUser) { + account = await prisma.account.update({ + where: { + id: existingUser.id, + }, + data: { + person: { + update: { + firstname: profile.given_name, + lastname: profile.family_name, + }, + }, + }, + }) + } else { + account = await prisma.account.create({ + data: { + dlrgOauthId: profile.sub, + email: profile.email, + password: '', + role: registerAsGliederung ? 'GLIEDERUNG_ADMIN' : 'USER', + status: registerAsGliederung ? 'OFFEN' : 'AKTIV', + activatedAt: new Date(), + person: { + create: { + firstname: profile.given_name, + lastname: profile.family_name, + email: profile.email, + telefon: '', + }, + }, + }, + }) + } + + // TODO: Implement onboarding + const redirectUri = new URL(registerAsGliederung ? '/onboarding' : '/login', config.clientUrl) + + const jwt = sign({ + sub: account.id.toString(), + }) + redirectUri.searchParams.set('jwt', jwt) + + return c.redirect(redirectUri) +}) + +oidcRouter.get('/dlrg/login', async (c) => { + const issuer = new URL(config.authentication.dlrg.issuer) + const code_challenge_method = 'S256' + const code_verifier = oauth.generateRandomCodeVerifier() + const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) + + const discoveryRequestResponse = await oauth.discoveryRequest(issuer, { + algorithm, + [oauth.allowInsecureRequests]: config.authentication.dlrg.allowInsecure, + }) + const as = await oauth.processDiscoveryResponse(issuer, discoveryRequestResponse) + const authorizationUrl = new URL(as.authorization_endpoint!) + + const redirectUri = new URL('/api/oidc/dlrg/callback', config.clientUrl) + const registerAs = c.req.query('as')?.trim() + if (registerAs !== undefined && registerAs?.length > 0) { + redirectUri.searchParams.set('as', registerAs) + } + + authorizationUrl.searchParams.set('client_id', config.authentication.dlrg.clientId) + authorizationUrl.searchParams.set('redirect_uri', redirectUri.toString()) + authorizationUrl.searchParams.set('response_type', 'code') + authorizationUrl.searchParams.set('scope', 'openid profile email') + authorizationUrl.searchParams.set('code_challenge', code_challenge) + authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) + + setCookie(c, 'code_verifier', code_verifier, { + httpOnly: true, + secure: false, + }) + + const nonce = oauth.generateRandomNonce() + authorizationUrl.searchParams.set('nonce', nonce) + setCookie(c, 'nonce', nonce, { + httpOnly: true, + secure: false, + }) + + return c.redirect(authorizationUrl) +}) diff --git a/apps/api/src/scripts/createAccount.ts b/apps/api/src/scripts/createAccount.ts index 3e6cd917..3d79f170 100644 --- a/apps/api/src/scripts/createAccount.ts +++ b/apps/api/src/scripts/createAccount.ts @@ -21,7 +21,7 @@ async function createUser() { }), }) - async function selectGliederung(): Promise { + async function selectGliederung(): Promise { return await search({ message: 'Deine Gliederung', source: async (term) => { diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index b0306897..3cce184b 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -1,88 +1,81 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import cors from '@koa/cors' -import grant from 'grant' -import Koa from 'koa' -import { koaBody } from 'koa-body' -import helmet from 'koa-helmet' -import session from 'koa-session' -import serve from 'koa-static' -import { createKoaMiddleware } from 'trpc-koa-adapter' - +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { trpcServer } from '@hono/trpc-server' +import { cors } from 'hono/cors' +import { logger } from 'hono/logger' +import { requestId } from 'hono/request-id' +import { secureHeaders } from 'hono/secure-headers' +import { resolve } from 'node:path' import config from './config.js' import { createContext } from './context.js' -import { logger } from './logger.js' -import cacheControl from './middleware/cache-control.js' -import router from './routes/index.js' - import { appRouter } from './index.js' +import { logger as appLogger } from './logger.js' +import * as routes from './routes/index.js' +import { makeApp } from './util/make-app.js' -export const app = new Koa() - -app.use( - helmet({ - contentSecurityPolicy: { - directives: { - 'img-src': [ +const app = makeApp() + .use(async (c, next) => { + // generic error handler + await next() + if (c.error) { + console.error(c.error) + } + }) + .use(logger(appLogger.debug)) + .use(requestId()) + .use( + secureHeaders({ + contentSecurityPolicy: { + connectSrc: ["'self'", 'dlrgbrahmseedigitalprod.blob.core.windows.net'], + imgSrc: [ "'self'", '*.githubusercontent.com', 'blob:', 'data:', 'dlrgbrahmseedigitalprod.blob.core.windows.net', ], - 'connect-src': ["'self'", 'dlrgbrahmseedigitalprod.blob.core.windows.net'], }, - }, - }) -) -app.use(cors({ origin: '*' })) -app.use(serve('./static', { defer: false })) -app.use(cacheControl) - -// koa-session is required by grant -app.keys = ['grant'] -app.use(session({}, app)) + }) + ) + .use(cors({ origin: '*' })) + .use( + '/api/trpc/*', + trpcServer({ + router: appRouter, + endpoint: '/api/trpc', + createContext, + }) + ) + .use( + serveStatic({ + root: resolve('./static'), + rewriteRequestPath: (p) => p.replace(/^\/static/, '/'), + }) + ) + .route('/export', routes.exportRouter) + .route('/import', routes.importRouter) + .route('/file', routes.fileRouter) + .route('/oidc', routes.oidcRouter) -// grant is used for oauth -app.use( - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (grant as any).koa()({ - defaults: { - origin: `${config.clientUrl}/api`, - transport: 'session', - }, - dlrg: { - dynamic: ['mode', 'origin'], - transport: 'session', - oauth: 2, - response: ['token', 'profile'], - authorize_url: 'https://iam.dlrg.net/auth/realms/master/protocol/openid-connect/auth', - access_url: 'https://iam.dlrg.net/auth/realms/master/protocol/openid-connect/token', - key: config.authentication.dlrg.client_id, - scope: ['profile'], - profile_url: 'https://iam.dlrg.net/auth/realms/master/protocol/openid-connect/userinfo', - pkce: true, - }, - }) -) -// initialize trpc middleware -app.use( - createKoaMiddleware({ - prefix: '/api/trpc', - router: appRouter, - createContext, - }) -) +const server = serve({ + fetch: app.fetch, + hostname: config.server.host, + port: config.server.port, +}) -app.use(koaBody({ multipart: true })) -app.use(router.routes()) -app.use(router.allowedMethods()) +appLogger.info(`app listening on http://${config.server.host}:${config.server.port}`) -app.use(async (ctx, next) => { - // serve index.html as catch all - ctx.url = '/' - await serve('./static')(ctx, next) +// graceful shutdown +process.on('SIGINT', () => { + server.close() + process.exit(0) +}) +process.on('SIGTERM', () => { + server.close((err) => { + if (err) { + console.error(err) + process.exit(1) + } + process.exit(0) + }) }) - -app.listen(config.server.port, config.server.host) -logger.info(`app listening on http://${config.server.host}:${config.server.port}`) diff --git a/apps/api/src/services/account/account.router.ts b/apps/api/src/services/account/account.router.ts index 8b97b234..7cd5f0c8 100644 --- a/apps/api/src/services/account/account.router.ts +++ b/apps/api/src/services/account/account.router.ts @@ -5,12 +5,13 @@ import { accountActivateProcedure } from './accountActivate.js' import { accountChangePasswordProcedure } from './accountChangePassword.js' import { accountEmailConfirmProcedure } from './accountEmailConfirm.js' import { accountEmailConfirmRequestProcedure } from './accountEmailConfirmRequest.js' -import { accountGliederungAdminCreateProcedure } from './accountGliederungAdminCreate.js' import { accountPasswordResetProcedure } from './accountPasswordReset.js' +import { accountPasswordResetRequestProcedure } from './accountPasswordResetRequest.js' +import { accountPasswordResetValidateProcedure } from './accountPasswordResetToken.js' import { accountTeilnehmerCreateProcedure } from './accountTeilnehmerCreate.js' import { accountVerwaltungCreateProcedure } from './accountVerwaltungCreate.js' import { accountVerwaltungGetProcedure } from './accountVerwaltungGet.js' -import { accountVerwaltungCountProcedure, accountVerwaltungListProcedure } from './accountVerwaltungList.js' +import { accountVerwaltungListProcedure } from './accountVerwaltungList.js' import { accountVerwaltungPatchProcedure } from './accountVerwaltungPatch.js' import { accountVerwaltungRemoveProcedure } from './accountVerwaltungRemove.js' // Import Routes here - do not delete this line @@ -18,14 +19,14 @@ import { accountVerwaltungRemoveProcedure } from './accountVerwaltungRemove.js' export const accountRouter = mergeRouters( accountActivateProcedure, accountChangePasswordProcedure, - accountGliederungAdminCreateProcedure, accountVerwaltungCreateProcedure, accountVerwaltungGetProcedure, accountVerwaltungListProcedure, - accountVerwaltungCountProcedure, accountVerwaltungPatchProcedure, accountEmailConfirmRequestProcedure, accountEmailConfirmProcedure, + accountPasswordResetRequestProcedure, + accountPasswordResetValidateProcedure, accountPasswordResetProcedure, accountVerwaltungRemoveProcedure, accountTeilnehmerCreateProcedure diff --git a/apps/api/src/services/account/accountActivate.ts b/apps/api/src/services/account/accountActivate.ts index 2becb8f1..6dba77fa 100644 --- a/apps/api/src/services/account/accountActivate.ts +++ b/apps/api/src/services/account/accountActivate.ts @@ -10,7 +10,7 @@ export const accountActivateProcedure = defineProtectedMutateProcedure({ key: 'activate', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - accountId: z.number().int(), + accountId: z.string().uuid(), }), async handler(options) { const account = await prisma.account.update({ diff --git a/apps/api/src/services/account/accountChangePassword.ts b/apps/api/src/services/account/accountChangePassword.ts index 8c4d5e48..9055e5dc 100644 --- a/apps/api/src/services/account/accountChangePassword.ts +++ b/apps/api/src/services/account/accountChangePassword.ts @@ -13,7 +13,7 @@ export const accountChangePasswordProcedure = defineProtectedMutateProcedure({ key: 'changePassword', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), password_old: z.string(), password: z.string(), password_confirm: z.string(), diff --git a/apps/api/src/services/account/accountEmailConfirmRequest.ts b/apps/api/src/services/account/accountEmailConfirmRequest.ts index 42c89446..cc325d6b 100644 --- a/apps/api/src/services/account/accountEmailConfirmRequest.ts +++ b/apps/api/src/services/account/accountEmailConfirmRequest.ts @@ -11,7 +11,7 @@ export const accountEmailConfirmRequestProcedure = defineProtectedQueryProcedure key: 'emailConfirmRequest', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - accountId: z.number().int(), + accountId: z.string().uuid(), }), async handler(options) { const account = await prisma.account.findUnique({ diff --git a/apps/api/src/services/account/accountGliederungAdminCreate.ts b/apps/api/src/services/account/accountGliederungAdminCreate.ts deleted file mode 100644 index adf06b8a..00000000 --- a/apps/api/src/services/account/accountGliederungAdminCreate.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Gender } from '@prisma/client' -import { TRPCError } from '@trpc/server' -import jwt from 'jsonwebtoken' -import z from 'zod' - -import config from '../../config.js' -import prisma from '../../prisma.js' -import { ZOauthRegisterJwtPayloadSchema } from '../../routes/connect.js' -import { definePublicMutateProcedure } from '../../types/defineProcedure.js' - -import { sendMailConfirmEmailRequest } from './helpers/sendMailConfirmEmailRequest.js' -import { getAccountCreateData } from './schema/account.schema.js' - -const ZAccountGliederungAdminCreateInput = z.strictObject({ - data: z.strictObject({ - firstname: z.string(), - lastname: z.string(), - gender: z.nativeEnum(Gender), - birthday: z.date(), - email: z.string().email().optional(), // email is required, because oauth login does not have an email - password: z.string().optional(), // optional, because oauth login does not have a password - gliederungId: z.number().int(), - jwtOAuthToken: z.string().optional(), // optional, becaus normal registration does not have a jwtOAuthToken - }), -}) - -export const accountGliederungAdminCreateProcedure = definePublicMutateProcedure({ - key: 'gliederungAdminCreate', - inputSchema: ZAccountGliederungAdminCreateInput, - async handler(options) { - let dlrgOauthId: undefined | string = undefined - // check if jwtOAuthToken set and if so, check if it is valid - - if (options.input.data.jwtOAuthToken) { - const jwtOAuthTokenPayload = ZOauthRegisterJwtPayloadSchema.parse( - jwt.verify(options.input.data.jwtOAuthToken, `${config.authentication.secret}-oauth`) - ) - options.input.data.email = jwtOAuthTokenPayload.email - dlrgOauthId = jwtOAuthTokenPayload.sub - } - - if (!options.input.data.email) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Email muss angegeben werden', - }) - } - const accountData = await getAccountCreateData({ - email: options.input.data.email, - firstname: options.input.data.firstname, - lastname: options.input.data.lastname, - password: options.input.data.password, - birthday: options.input.data.birthday, - gender: options.input.data.gender, - roleId: 'GLIEDERUNG_ADMIN', - isActiv: false, - gliederungId: options.input.data.gliederungId, - adminInGliederungId: options.input.data.gliederungId, - }) - const res = await prisma.account.create({ - data: { - ...accountData, - dlrgOauthId, - }, - select: { - id: true, - }, - }) - - if (!accountData.activationToken) throw new Error('No activation token generated') - await sendMailConfirmEmailRequest(accountData.email, accountData.activationToken) - - return res - }, -}) diff --git a/apps/api/src/services/account/accountPasswordReset.ts b/apps/api/src/services/account/accountPasswordReset.ts index 80cafc15..832da3eb 100644 --- a/apps/api/src/services/account/accountPasswordReset.ts +++ b/apps/api/src/services/account/accountPasswordReset.ts @@ -1,121 +1,40 @@ -import { v4 as uuidv4 } from 'uuid' import z from 'zod' -import config from '../../config.js' import prisma from '../../prisma.js' import { definePublicMutateProcedure } from '../../types/defineProcedure.js' -import { sendMail } from '../../util/mail.js' import { hashPassword } from '@codeanker/authentication' export const accountPasswordResetProcedure = definePublicMutateProcedure({ key: 'resetPassword', inputSchema: z.strictObject({ - email: z.string().optional(), - passwordResetToken: z.string().optional(), - password: z.string().optional(), + token: z.string(), + password: z.string(), }), - async handler(options) { - // Create Reset Token and send per Mail - if (options.input.email != null && options.input.passwordResetToken == null && options.input.password == null) { - const findRes = await prisma.account.findUnique({ - where: { - email: options.input.email, - }, - select: { - email: true, - passwordResetToken: true, - person: { - select: { - firstname: true, - lastname: true, - gliederung: { - select: { - name: true, - }, - }, - }, - }, - }, - }) - if (findRes === null) { - return { - status: true, - } - } - - let resetToken: string | null = null - const email = findRes.email - - if (findRes.passwordResetToken != null) { - resetToken = findRes.passwordResetToken - } else { - const res = await prisma.account.update({ - where: { - email: options.input.email, - }, - data: { - passwordResetToken: uuidv4(), - }, - }) - resetToken = res.passwordResetToken - } - - if (resetToken !== null) { - const resetUrl = `${config.clientUrl}/password-reset/${resetToken}` - await sendMail({ - to: email, - subject: 'Passwort zurücksetzen', - categories: ['account', 'passwordReset'], - template: 'account-password-reset', - variables: { - gliederung: findRes.person.gliederung!.name, - name: `${findRes.person.firstname} ${findRes.person.lastname}`, - hostname: 'brahmsee.digital', - veranstaltung: 'brahmsee.digital', - resetUrl, - }, - }) - } + async handler({ input }) { + const findRes = await prisma.account.findUnique({ + where: { + passwordResetToken: input.token, + }, + select: { + id: true, + passwordResetToken: true, + }, + }) + + if (findRes == null) return { status: true, - process: 'sendResetToken', } - } else if ( - options.input.email == null && - options.input.passwordResetToken != null && - options.input.password != null - ) { - // Reset Password with Token - const findRes = await prisma.account.findUnique({ - where: { - passwordResetToken: options.input.passwordResetToken, - }, - select: { - id: true, - passwordResetToken: true, - }, - }) - if (findRes == null) - return { - status: true, - } - - await prisma.account.update({ - where: { - id: findRes.id, - }, - data: { - password: await hashPassword(options.input.password), - passwordResetToken: null, - }, - }) - - return { - status: true, - process: 'setNewPassword', - } - } + await prisma.account.update({ + where: { + id: findRes.id, + }, + data: { + password: await hashPassword(input.password), + passwordResetToken: null, + }, + }) }, }) diff --git a/apps/api/src/services/account/accountPasswordResetRequest.ts b/apps/api/src/services/account/accountPasswordResetRequest.ts new file mode 100644 index 00000000..7a81888c --- /dev/null +++ b/apps/api/src/services/account/accountPasswordResetRequest.ts @@ -0,0 +1,85 @@ +import * as uuid from 'uuid' +import z from 'zod' + +import config from '../../config.js' +import prisma from '../../prisma.js' +import { definePublicMutateProcedure } from '../../types/defineProcedure.js' +import { sendMail } from '../../util/mail.js' + +export const accountPasswordResetRequestProcedure = definePublicMutateProcedure({ + key: 'requestPasswordReset', + inputSchema: z.strictObject({ + email: z.string(), + }), + async handler({ input }) { + const account = await prisma.account.findUnique({ + where: { + email: input.email, + }, + select: { + email: true, + dlrgOauthId: true, + passwordResetToken: true, + person: { + select: { + firstname: true, + lastname: true, + }, + }, + }, + }) + if (account === null) { + return + } + + // send informational mail + if (account.dlrgOauthId !== null) { + await sendMail({ + to: account.email, + subject: 'Passwort zurücksetzen', + categories: ['account', 'passwordReset'], + template: 'account-password-reset-oauth', + variables: { + gliederung: 'DLRG', + name: `${account.person.firstname} ${account.person.lastname}`, + hostname: 'brahmsee.digital', + veranstaltung: 'brahmsee.digital', + }, + }) + return + } + + let resetToken: string | null = null + + if (account.passwordResetToken != null) { + resetToken = account.passwordResetToken + } else { + const res = await prisma.account.update({ + where: { + email: input.email, + }, + data: { + passwordResetToken: uuid.v4(), + }, + }) + resetToken = res.passwordResetToken + } + + if (resetToken !== null) { + const resetUrl = `${config.clientUrl}/password-reset/${resetToken}` + await sendMail({ + to: account.email, + subject: 'Passwort zurücksetzen', + categories: ['account', 'passwordReset'], + template: 'account-password-reset', + variables: { + gliederung: 'DLRG', + name: `${account.person.firstname} ${account.person.lastname}`, + hostname: 'brahmsee.digital', + veranstaltung: 'brahmsee.digital', + resetUrl, + }, + }) + } + }, +}) diff --git a/apps/api/src/services/account/accountPasswordResetToken.ts b/apps/api/src/services/account/accountPasswordResetToken.ts new file mode 100644 index 00000000..28f5fae3 --- /dev/null +++ b/apps/api/src/services/account/accountPasswordResetToken.ts @@ -0,0 +1,23 @@ +import z from 'zod' + +import prisma from '../../prisma.js' +import { definePublicQueryProcedure } from '../../types/defineProcedure.js' + +export const accountPasswordResetValidateProcedure = definePublicQueryProcedure({ + key: 'validatePasswordResetToken', + inputSchema: z.strictObject({ + token: z.string(), + }), + async handler({ input }) { + const findRes = await prisma.account.findUnique({ + where: { + passwordResetToken: input.token, + }, + select: { + id: true, + }, + }) + + return findRes !== null + }, +}) diff --git a/apps/api/src/services/account/accountTeilnehmerCreate.ts b/apps/api/src/services/account/accountTeilnehmerCreate.ts index c9015f80..d0dc0f5c 100644 --- a/apps/api/src/services/account/accountTeilnehmerCreate.ts +++ b/apps/api/src/services/account/accountTeilnehmerCreate.ts @@ -1,66 +1,50 @@ import { Gender } from '@prisma/client' import { TRPCError } from '@trpc/server' -import jwt from 'jsonwebtoken' import z from 'zod' - -import config from '../../config.js' import prisma from '../../prisma.js' -import { ZOauthRegisterJwtPayloadSchema } from '../../routes/connect.js' import { definePublicMutateProcedure } from '../../types/defineProcedure.js' - import { sendMailConfirmEmailRequest } from './helpers/sendMailConfirmEmailRequest.js' import { getAccountCreateData } from './schema/account.schema.js' -const ZAccountTeilnehmerCreateInput = z.strictObject({ - data: z.strictObject({ +export const accountTeilnehmerCreateProcedure = definePublicMutateProcedure({ + key: 'teilnehmerCreate', + inputSchema: z.strictObject({ firstname: z.string(), lastname: z.string(), gender: z.nativeEnum(Gender), birthday: z.date(), - email: z.string().email().optional(), // email is required, because oauth login does not have an email - password: z.string().optional(), // optional, because oauth login does not have a password - gliederungId: z.number().int(), - jwtOAuthToken: z.string().optional(), // optional, becaus normal registration does not have a jwtOAuthToken + email: z.string().email(), + password: z.string(), }), -}) - -export const accountTeilnehmerCreateProcedure = definePublicMutateProcedure({ - key: 'teilnehmerCreate', - inputSchema: ZAccountTeilnehmerCreateInput, - async handler(options) { - let dlrgOauthId: undefined | string = undefined - // check if jwtOAuthToken set and if so, check if it is valid - - if (options.input.data.jwtOAuthToken) { - const jwtOAuthTokenPayload = ZOauthRegisterJwtPayloadSchema.parse( - jwt.verify(options.input.data.jwtOAuthToken, `${config.authentication.secret}-oauth`) - ) - options.input.data.email = jwtOAuthTokenPayload.email - dlrgOauthId = jwtOAuthTokenPayload.sub - } - - if (!options.input.data.email) { + handler: async ({ input }) => { + const existing = await prisma.account.findFirst({ + where: { + email: input.email, + }, + select: { + id: true, + }, + }) + if (existing !== null) { throw new TRPCError({ code: 'BAD_REQUEST', - message: 'Email muss angegeben werden', + message: 'Email is already registered', }) } + const accountData = await getAccountCreateData({ - email: options.input.data.email, - firstname: options.input.data.firstname, - lastname: options.input.data.lastname, - password: options.input.data.password, - birthday: options.input.data.birthday, - gender: options.input.data.gender, + email: input.email, + firstname: input.firstname, + lastname: input.lastname, + password: input.password, + birthday: input.birthday, + gender: input.gender, roleId: 'USER', isActiv: false, - gliederungId: options.input.data.gliederungId, }) + const res = await prisma.account.create({ - data: { - ...accountData, - dlrgOauthId, - }, + data: accountData, select: { id: true, }, diff --git a/apps/api/src/services/account/accountVerwaltungGet.ts b/apps/api/src/services/account/accountVerwaltungGet.ts index c64bf308..4cd9654a 100644 --- a/apps/api/src/services/account/accountVerwaltungGet.ts +++ b/apps/api/src/services/account/accountVerwaltungGet.ts @@ -8,7 +8,7 @@ export const accountVerwaltungGetProcedure = defineProtectedQueryProcedure({ key: 'verwaltungGet', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { const res = await prisma.account.findUniqueOrThrow({ diff --git a/apps/api/src/services/account/accountVerwaltungList.ts b/apps/api/src/services/account/accountVerwaltungList.ts index 89856490..adc60db1 100644 --- a/apps/api/src/services/account/accountVerwaltungList.ts +++ b/apps/api/src/services/account/accountVerwaltungList.ts @@ -1,36 +1,48 @@ -import { Prisma, Role } from '@prisma/client' +import { AccountStatus, Prisma, Role } from '@prisma/client' import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - personName: z.string().optional(), - email: z.string().optional(), - status: z.enum(['AKTIV', 'DEAKTIVIERT', 'OFFEN']).optional(), - }), - - orderBy: z.array( - z.tuple([ - z.union([z.literal('person.firstname'), z.literal('email'), z.literal('role'), z.literal('status')]), - z.union([z.literal('asc'), z.literal('desc')]), - ]) - ), -}) +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' export const accountVerwaltungListProcedure = defineProtectedQueryProcedure({ key: 'verwaltungList', roleIds: [Role.ADMIN], - inputSchema, - async handler(options) { - const { skip, take } = options.input.pagination - const list = await prisma.account.findMany({ - skip, - take, - where: await getWhere(options.input.filter, options.ctx.account), - orderBy: getOrderBy(options.input.orderBy), + inputSchema: defineTableInput({ + filter: { + person: z.string(), + email: z.string(), + status: z.nativeEnum(AccountStatus), + role: z.nativeEnum(Role), + }, + orderBy: ['email', 'activatedAt'], + }), + handler: async ({ input: { filter, pagination, orderBy } }) => { + const where: Prisma.AccountWhereInput = { + email: filter?.email + ? { + contains: filter.email, + mode: 'insensitive', + } + : undefined, + status: filter?.status, + role: filter?.role, + OR: filter?.person + ? [ + { person: { firstname: { contains: filter.person, mode: 'insensitive' } } }, + { person: { lastname: { contains: filter.person, mode: 'insensitive' } } }, + ] + : undefined, + } + + const total = await prisma.account.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + + const accounts = await prisma.account.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + where, + orderBy, select: { id: true, email: true, @@ -57,38 +69,7 @@ export const accountVerwaltungListProcedure = defineProtectedQueryProcedure({ }, }, }) - return list - }, -}) -export const accountVerwaltungCountProcedure = defineProtectedQueryProcedure({ - key: 'verwaltungCount', - roleIds: [Role.ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - const list = await prisma.account.count({ - where: await getWhere(options.input.filter, options.ctx.account), - }) - return list + return defineQueryResponse({ data: accounts, total, pagination: { pageIndex, pageSize, pages } }) }, }) - -// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars -async function getWhere(filter: z.infer['filter'], _account: { id: number; role: Role }) { - const where: Prisma.AccountWhereInput = {} - - if (filter.email != null && filter.email != '') - where.email = { - contains: filter.email, - } - - if (filter.personName != null && filter.personName != '') - where.OR = [ - { person: { firstname: { contains: filter.personName } } }, - { person: { lastname: { contains: filter.personName } } }, - ] - - if (filter.status != null) where.status = filter.status - - return where -} diff --git a/apps/api/src/services/account/accountVerwaltungPatch.ts b/apps/api/src/services/account/accountVerwaltungPatch.ts index 5d714fdd..fd2e16cb 100644 --- a/apps/api/src/services/account/accountVerwaltungPatch.ts +++ b/apps/api/src/services/account/accountVerwaltungPatch.ts @@ -11,7 +11,7 @@ export const accountVerwaltungPatchProcedure = defineProtectedMutateProcedure({ key: 'verwaltungPatch', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: z.strictObject({ email: z.string().email(), role: z.nativeEnum(Role), diff --git a/apps/api/src/services/account/accountVerwaltungRemove.ts b/apps/api/src/services/account/accountVerwaltungRemove.ts index c7cebae2..00652f60 100644 --- a/apps/api/src/services/account/accountVerwaltungRemove.ts +++ b/apps/api/src/services/account/accountVerwaltungRemove.ts @@ -9,7 +9,7 @@ export const accountVerwaltungRemoveProcedure = defineProtectedMutateProcedure({ key: 'verwaltungRemove', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { return await prisma.account diff --git a/apps/api/src/services/account/helpers/sendMailConfirmEmailRequest.ts b/apps/api/src/services/account/helpers/sendMailConfirmEmailRequest.ts index b95f672c..4526e03b 100644 --- a/apps/api/src/services/account/helpers/sendMailConfirmEmailRequest.ts +++ b/apps/api/src/services/account/helpers/sendMailConfirmEmailRequest.ts @@ -1,11 +1,11 @@ import config from '../../../config.js' -import client from '../../../prisma.js' +import prisma from '../../../prisma.js' import { sendMail } from '../../../util/mail.js' export async function sendMailConfirmEmailRequest(email: string, activationToken: string) { const activationUrl = `${config.clientUrl}/registrierung/confirm/${activationToken}` - const account = await client.account.findUniqueOrThrow({ + const account = await prisma.account.findUniqueOrThrow({ where: { email, }, @@ -31,7 +31,7 @@ export async function sendMailConfirmEmailRequest(email: string, activationToken template: 'account-email-confirm', variables: { name: `${account.person.firstname} ${account.person.lastname}`, - gliederung: account.person.gliederung!.name, + gliederung: account.person.gliederung?.name, hostname: 'brahmsee.digital', veranstaltung: 'brahmsee.digital', activationUrl, diff --git a/apps/api/src/services/account/schema/account.schema.ts b/apps/api/src/services/account/schema/account.schema.ts index 93808a4c..f42b03bc 100644 --- a/apps/api/src/services/account/schema/account.schema.ts +++ b/apps/api/src/services/account/schema/account.schema.ts @@ -14,8 +14,8 @@ export const accountSchema = z.strictObject({ roleId: z.nativeEnum(Role), isActiv: z.boolean().optional(), status: z.nativeEnum(AccountStatus).optional(), - gliederungId: z.number().int(), - adminInGliederungId: z.number().int().optional(), + gliederungId: z.string().uuid().optional(), + adminInGliederungId: z.string().uuid().optional(), activationToken: z.string().optional(), passwordResetToken: z.string().optional(), }) @@ -42,11 +42,14 @@ export async function getAccountCreateData(data: TGetAccountCreateDataSchema): P birthday: data.birthday, email: data.email, telefon: '', - gliederung: { - connect: { - id: data.gliederungId, - }, - }, + gliederung: + data.gliederungId === undefined + ? undefined + : { + connect: { + id: data.gliederungId, + }, + }, }, }, activatedAt: data.isActiv ? new Date() : null, diff --git a/apps/api/src/services/activity/activity.routes.ts b/apps/api/src/services/activity/activity.routes.ts index 41990d1a..1200b600 100644 --- a/apps/api/src/services/activity/activity.routes.ts +++ b/apps/api/src/services/activity/activity.routes.ts @@ -1,11 +1,11 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' -import { activityListProcedure, activityCountProcedure } from './activityList.js' +import { activityCompleteSubjectsProcedure, activityListProcedure } from './activityList.js' // Import Routes here - do not delete this line export const activityRouter = mergeRouters( activityListProcedure, - activityCountProcedure + activityCompleteSubjectsProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/activity/activityList.ts b/apps/api/src/services/activity/activityList.ts index d2070dcd..d31429db 100644 --- a/apps/api/src/services/activity/activityList.ts +++ b/apps/api/src/services/activity/activityList.ts @@ -1,34 +1,45 @@ -import { type Prisma, Role } from '@prisma/client' +import { ActivityType, Prisma, Role } from '@prisma/client' import z from 'zod' +import dayjs from 'dayjs' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - veranstaltungId: z.string().optional(), - }), - orderBy: z.array( - z.tuple([z.union([z.literal('id'), z.literal('createdAt')]), z.union([z.literal('asc'), z.literal('desc')])]) - ), -}) - -type Input = z.infer +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' export const activityListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN], - inputSchema, - async handler({ input, ctx }) { - const { skip, take } = input.pagination + inputSchema: defineTableInput({ + filter: { + createdAt: z + .tuple([z.date(), z.date()]) + .transform(([from, to]) => [dayjs(from).startOf('day').toDate(), dayjs(to).endOf('day').toDate()]), + type: z.nativeEnum(ActivityType), + subjectType: z.string(), + }, + orderBy: ['createdAt'], + }), + handler: async ({ input: { pagination, filter, orderBy } }) => { + const where: Prisma.ActivityWhereInput = { + createdAt: filter?.createdAt + ? { + gte: filter.createdAt[0], + lte: filter.createdAt[1], + } + : undefined, + type: filter?.type, + subjectType: filter?.subjectType, + } + + const total = await prisma.activity.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) const activities = await prisma.activity.findMany({ - skip, - take, - orderBy: getOrderBy(input.orderBy), - where: await getWhere(input.filter, ctx.account), - include: { + take: pageSize, + skip: pageSize * pageIndex, + where, + orderBy, + select: { causer: { select: { person: { @@ -39,35 +50,30 @@ export const activityListProcedure = defineProtectedQueryProcedure({ }, }, }, + subjectId: true, + subjectType: true, + createdAt: true, + type: true, + description: true, }, }) - return activities + return defineQueryResponse({ data: activities, total, pagination: { pageIndex, pageSize, pages } }) }, }) -export const activityCountProcedure = defineProtectedQueryProcedure({ - key: 'count', +export const activityCompleteSubjectsProcedure = defineProtectedQueryProcedure({ + key: 'listSubjectTypes', + inputSchema: z.void(), roleIds: [Role.ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler({ input, ctx }) { - const activities = await prisma.activity.count({ - where: await getWhere(input.filter, ctx.account), + handler: async () => { + const result = await prisma.activity.findMany({ + distinct: ['subjectType'], + select: { + subjectType: true, + }, }) - return activities + return result.map((r) => r.subjectType) }, }) - -// eslint-disable-next-line @typescript-eslint/require-await -async function getWhere( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - filter: Input['filter'], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - account: { - id: number - role: Role - } -): Promise { - return {} -} diff --git a/apps/api/src/services/address/addressFindAddress.ts b/apps/api/src/services/address/addressFindAddress.ts index 29794add..a72374e0 100644 --- a/apps/api/src/services/address/addressFindAddress.ts +++ b/apps/api/src/services/address/addressFindAddress.ts @@ -1,9 +1,29 @@ -import axios from 'axios' import z from 'zod' import config from '../../config.js' import { definePublicQueryProcedure } from '../../types/defineProcedure.js' +const ZTomTomSearchResponse = z.object({ + summary: z.object({ + numResults: z.number(), + }), + results: z.array( + z.object({ + address: z.object({ + streetName: z.string(), + streetNumber: z.string(), + postalCode: z.string(), + municipality: z.string(), + countryCode: z.string(), + }), + position: z.object({ + lat: z.number(), + lon: z.number(), + }), + }) + ), +}) + export const addressFindActionProcedure = definePublicQueryProcedure({ key: 'findAddress', inputSchema: z.object({ @@ -14,7 +34,6 @@ export const addressFindActionProcedure = definePublicQueryProcedure({ streetNumber: z.string().optional(), country: z.string().optional(), }), - async handler(options) { let searchText = '' if (options.input.query != null) { @@ -42,50 +61,41 @@ export const addressFindActionProcedure = definePublicQueryProcedure({ } const country = options?.input?.country != null ? options.input.country.toUpperCase() : 'DE' - try { - const query = await axios.get( - `https://api.tomtom.com/search/2/search/${encodeURIComponent( - searchText - )}.json?typeahead=true&limit=5&countrySet=${country}&language=${language}&idxSet=PAD&minFuzzyLevel=1&maxFuzzyLevel=2&view=Unified&key=${token}` - ) + const url = new URL(`https://api.tomtom.com/search/2/search/${encodeURIComponent(searchText)}.json`) + url.searchParams.append('typeahead', 'true') + url.searchParams.append('limit', '5') + url.searchParams.append('countrySet', country) + url.searchParams.append('language', language) + url.searchParams.append('idxSet', 'PAD') + url.searchParams.append('minFuzzyLevel', '1') + url.searchParams.append('maxFuzzyLevel', '2') + url.searchParams.append('view', 'Unified') + url.searchParams.append('key', token) - const ZTomTomSearchResponse = z.object({ - summary: z.object({ - numResults: z.number(), - }), - results: z.array( - z.object({ - address: z.object({ - streetName: z.string(), - streetNumber: z.string(), - postalCode: z.string(), - municipality: z.string(), - countryCode: z.string(), - }), - position: z.object({ - lat: z.number(), - lon: z.number(), - }), - }) - ), - }) + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }) - const parsedSearchResponse = ZTomTomSearchResponse.parse(query.data) - - if (parsedSearchResponse.summary.numResults < 1) return [] - return parsedSearchResponse.results.map((result) => { - return { - street: result.address.streetName, - streetNumber: result.address.streetNumber, - zip: result.address.postalCode, - city: result.address.municipality, - country: result.address.countryCode, - position: result.position, - } - }) - } catch (e) { - console.error(e) + if (response.status !== 200) { return [] } + + const body = await response.json() + const parsedSearchResponse = ZTomTomSearchResponse.parse(body) + + if (parsedSearchResponse.summary.numResults < 1) return [] + return parsedSearchResponse.results.map((result) => { + return { + street: result.address.streetName, + streetNumber: result.address.streetNumber, + zip: result.address.postalCode, + city: result.address.municipality, + country: result.address.countryCode, + position: result.position, + } + }) }, }) diff --git a/apps/api/src/services/address/schema/address.schema.ts b/apps/api/src/services/address/schema/address.schema.ts index 475c4f17..f5708f3a 100644 --- a/apps/api/src/services/address/schema/address.schema.ts +++ b/apps/api/src/services/address/schema/address.schema.ts @@ -29,7 +29,7 @@ export async function findAddress(input: z.infer) { }) } -export async function createOrUpdateAddress(input: z.infer): Promise { +export async function createOrUpdateAddress(input: z.infer): Promise { if (isAllUndefined(input)) { return undefined } diff --git a/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts b/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts index 78b032da..85a98ea7 100644 --- a/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts +++ b/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts @@ -6,8 +6,8 @@ import { definePublicQueryProcedure } from '../../types/defineProcedure.js' export const anmeldungAccessTokenValidateProcedure = definePublicQueryProcedure({ key: 'accessTokenValidate', inputSchema: z.strictObject({ - unterveranstaltungId: z.number().int(), - anmeldungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), + anmeldungId: z.string().uuid(), accessToken: z.string().uuid(), }), handler: ({ input: { unterveranstaltungId, anmeldungId, accessToken } }) => { diff --git a/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts b/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts index cbf7cdf5..e01e9cc5 100644 --- a/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts +++ b/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts @@ -8,8 +8,8 @@ import { definePublicMutateProcedure } from '../../types/defineProcedure.js' export const anmeldungFotoUploadProcedure = definePublicMutateProcedure({ key: 'anmeldungFotoUpload', inputSchema: z.strictObject({ - unterveranstaltungId: z.number().int(), - anmeldungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), + anmeldungId: z.string().uuid(), accessToken: z.string().uuid(), fileId: z.string(), }), diff --git a/apps/api/src/services/anmeldung/anmeldungGet.ts b/apps/api/src/services/anmeldung/anmeldungGet.ts index d5d3d140..7e640fb4 100644 --- a/apps/api/src/services/anmeldung/anmeldungGet.ts +++ b/apps/api/src/services/anmeldung/anmeldungGet.ts @@ -66,8 +66,8 @@ const select = { } satisfies Prisma.AnmeldungSelect const inputSchema = z.strictObject({ - anmeldungId: z.number().optional(), - personId: z.number().optional(), + anmeldungId: z.string().uuid().optional(), + personId: z.string().uuid().optional(), }) type InputSchema = z.infer diff --git a/apps/api/src/services/anmeldung/anmeldungGliederungList.ts b/apps/api/src/services/anmeldung/anmeldungGliederungList.ts index 5f03949f..9d83f739 100644 --- a/apps/api/src/services/anmeldung/anmeldungGliederungList.ts +++ b/apps/api/src/services/anmeldung/anmeldungGliederungList.ts @@ -7,11 +7,11 @@ import { ZPaginationSchema } from '../../types/defineQuery.js' import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' const filter = z.strictObject({ - veranstaltungId: z.number().optional(), - unterveranstaltungId: z.number().optional(), + veranstaltungId: z.string().uuid().optional(), + unterveranstaltungId: z.string().uuid().optional(), }) -const where = (filter: { gliederungId: number; unterveranstaltungId?: number; veranstaltungId?: number }) => { +const where = (filter: { gliederungId: string; unterveranstaltungId?: string; veranstaltungId?: string }) => { return { OR: [ { diff --git a/apps/api/src/services/anmeldung/anmeldungGliederungPatch.ts b/apps/api/src/services/anmeldung/anmeldungGliederungPatch.ts index ccd87ea2..e01c7a4e 100644 --- a/apps/api/src/services/anmeldung/anmeldungGliederungPatch.ts +++ b/apps/api/src/services/anmeldung/anmeldungGliederungPatch.ts @@ -8,7 +8,7 @@ export const anmeldungGliederungPatchProcedure = defineProtectedMutateProcedure( key: 'gliederungPatch', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: z.strictObject({ comment: z.string().optional(), }), diff --git a/apps/api/src/services/anmeldung/anmeldungList.ts b/apps/api/src/services/anmeldung/anmeldungList.ts index 4ca5e8ac..cb169227 100644 --- a/apps/api/src/services/anmeldung/anmeldungList.ts +++ b/apps/api/src/services/anmeldung/anmeldungList.ts @@ -1,37 +1,35 @@ import { AnmeldungStatus, Prisma, Role } from '@prisma/client' import z from 'zod' +import type { Context } from '../../context.js' import prisma from '../../prisma.js' -import { defineOrderBy } from '../../types/defineOrderBy.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { ZPaginationSchema } from '../../types/defineQuery.js' -import type { Context } from '../../context.js' +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' -const filterSchema = z.discriminatedUnion('type', [ +const scopeSchema = z.discriminatedUnion('type', [ z.strictObject({ type: z.literal('veranstaltung'), - veranstaltungId: z.number(), + veranstaltungId: z.string().uuid(), }), z.strictObject({ type: z.literal('unterveranstaltung'), - unterveranstaltungId: z.number(), + unterveranstaltungId: z.string().uuid(), }), z.strictObject({ type: z.literal('own'), - }) + }), ]) -type FilterSchema = z.infer +type ScopeSchema = z.infer -function getWhere(ctx: Context, filter: FilterSchema): Prisma.AnmeldungWhereInput { +function getScope(ctx: Context, filter: ScopeSchema): Prisma.AnmeldungWhereInput { if (filter.type === 'veranstaltung') { return { unterveranstaltung: { veranstaltungId: filter.veranstaltungId, }, } - } - else if (filter.type === 'unterveranstaltung') { + } else if (filter.type === 'unterveranstaltung') { return { unterveranstaltungId: filter.unterveranstaltungId, } @@ -45,19 +43,63 @@ function getWhere(ctx: Context, filter: FilterSchema): Prisma.AnmeldungWhereInpu export const anmeldungListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN, Role.USER], - inputSchema: z.strictObject({ - pagination: ZPaginationSchema, - filter: filterSchema, - orderBy: defineOrderBy(['status', 'createdBy']), + inputSchema: defineTableInput({ + filter: { + scope: scopeSchema, + person: z.string(), + gliederung: z.string(), + status: z.nativeEnum(AnmeldungStatus), + withoutPhoto: z.boolean(), + }, }), - async handler({ ctx, input }) { - const { skip, take } = input.pagination + async handler({ ctx, input: { filter, pagination } }) { + const where: Prisma.AnmeldungWhereInput = { + ...getScope(ctx, filter?.scope ?? { type: 'own' }), + person: { + OR: filter?.person + ? [ + { + firstname: { + contains: filter?.person, + mode: 'insensitive', + }, + }, + { + lastname: { + contains: filter?.person, + mode: 'insensitive', + }, + }, + ] + : undefined, + photoId: filter?.withoutPhoto + ? { + equals: null, + } + : undefined, + gliederung: { + name: { + contains: filter?.gliederung, + mode: 'insensitive', + }, + }, + }, + status: filter?.status, + } + + const total = await prisma.anmeldung.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + const anmeldungen = await prisma.anmeldung.findMany({ - skip, - take, - where: getWhere(ctx, input.filter), + take: pageSize, + skip: pageSize * pageIndex, + orderBy: { + createdAt: 'desc', + }, + where, select: { id: true, + createdAt: true, person: { select: { id: true, @@ -84,12 +126,9 @@ export const anmeldungListProcedure = defineProtectedQueryProcedure({ }, }, }, - orderBy: { - createdAt: 'desc', - }, }) - return anmeldungen + return defineQueryResponse({ data: anmeldungen, total, pagination: { pageIndex, pageSize, pages } }) }, }) @@ -97,7 +136,7 @@ export const anmeldungCountProcedure = defineProtectedQueryProcedure({ key: 'count', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN, Role.USER], inputSchema: z.strictObject({ - filter: filterSchema, + filter: scopeSchema, }), async handler({ ctx, input }) { const countEntries = await Promise.all( @@ -106,7 +145,7 @@ export const anmeldungCountProcedure = defineProtectedQueryProcedure({ status, await prisma.anmeldung.count({ where: { - ...getWhere(ctx, input.filter), + ...getScope(ctx, input.filter), status: status, }, }), diff --git a/apps/api/src/services/anmeldung/anmeldungProtectedGet.ts b/apps/api/src/services/anmeldung/anmeldungProtectedGet.ts index d6435572..dd8557fc 100644 --- a/apps/api/src/services/anmeldung/anmeldungProtectedGet.ts +++ b/apps/api/src/services/anmeldung/anmeldungProtectedGet.ts @@ -5,8 +5,8 @@ import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' const inputSchema = z.strictObject({ - anmeldungId: z.number().optional(), - personId: z.number().optional(), + anmeldungId: z.string().uuid().optional(), + personId: z.string().uuid().optional(), }) export type AnmeldungProtectedGetSchema = z.infer diff --git a/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts b/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts index d3aebf77..857e69a7 100644 --- a/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts +++ b/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts @@ -14,8 +14,8 @@ import { updateMeiliPerson } from '../../meilisearch/person.js' export const inputSchema = z.strictObject({ token: z.string().optional(), data: personSchema.extend({ - unterveranstaltungId: z.number().int(), - mahlzeitenIds: z.array(z.number().int()).optional(), + unterveranstaltungId: z.string().uuid(), + mahlzeitenIds: z.array(z.string().uuid()).optional(), uebernachtungsTage: z.array(z.date()).optional(), tshirtBestellt: z.boolean().optional(), email: z.string().email(), @@ -178,20 +178,27 @@ export const anmeldungPublicCreateProcedure = definePublicMutateProcedure({ }) } - await Promise.all([ - logActivity({ - type: 'CREATE', - description: 'person created via public registration', - subjectType: 'person', - subjectId: person.id, - }), - logActivity({ - type: 'CREATE', - description: 'new public registration', - subjectType: 'anmeldung', - subjectId: anmeldung.id, - }), - ]) + await prisma.anmeldungLink.updateMany({ + where: { + accessToken: input.token, + unterveranstaltungId: unterveranstaltung.id, + }, + data: { + accessToken: null, + }, + }) + await logActivity({ + type: 'CREATE', + description: 'person created via public registration', + subjectType: 'person', + subjectId: person.id, + }) + await logActivity({ + type: 'CREATE', + description: 'new public registration', + subjectType: 'anmeldung', + subjectId: anmeldung.id, + }) await sendMail({ to: input.data.email, diff --git a/apps/api/src/services/anmeldung/anmeldungTeilnehmerStorno.ts b/apps/api/src/services/anmeldung/anmeldungTeilnehmerStorno.ts index d305365c..ab6ff28c 100644 --- a/apps/api/src/services/anmeldung/anmeldungTeilnehmerStorno.ts +++ b/apps/api/src/services/anmeldung/anmeldungTeilnehmerStorno.ts @@ -10,7 +10,7 @@ export const anmeldungTeilnehmerStornoProcedure = defineProtectedMutateProcedure roleIds: [Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ data: z.strictObject({ - anmeldungId: z.number().int(), + anmeldungId: z.string().uuid(), }), }), async handler(options) { diff --git a/apps/api/src/services/anmeldung/anmeldungVerwaltungAblehnen.ts b/apps/api/src/services/anmeldung/anmeldungVerwaltungAblehnen.ts index eef0de0f..36713c9f 100644 --- a/apps/api/src/services/anmeldung/anmeldungVerwaltungAblehnen.ts +++ b/apps/api/src/services/anmeldung/anmeldungVerwaltungAblehnen.ts @@ -11,7 +11,7 @@ export const anmeldungVerwaltungAblehnenProcedure = defineProtectedMutateProcedu key: 'verwaltungAblehnen', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - anmeldungId: z.number().int(), + anmeldungId: z.string().uuid(), }), async handler(options) { type AnmeldungWhereUniqueInput = Parameters[0]['where'] diff --git a/apps/api/src/services/anmeldung/anmeldungVerwaltungAnnehmen.ts b/apps/api/src/services/anmeldung/anmeldungVerwaltungAnnehmen.ts index b8991cb6..c2babfc3 100644 --- a/apps/api/src/services/anmeldung/anmeldungVerwaltungAnnehmen.ts +++ b/apps/api/src/services/anmeldung/anmeldungVerwaltungAnnehmen.ts @@ -11,7 +11,7 @@ export const anmeldungVerwaltungAnnehmenProcedure = defineProtectedMutateProcedu key: 'verwaltungAnnehmen', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - anmeldungId: z.number().int(), + anmeldungId: z.string().uuid(), }), async handler(options) { type AnmeldungWhereUniqueInput = Parameters[0]['where'] diff --git a/apps/api/src/services/anmeldung/anmeldungVerwaltungPatch.ts b/apps/api/src/services/anmeldung/anmeldungVerwaltungPatch.ts index 559fd362..c69a1acb 100644 --- a/apps/api/src/services/anmeldung/anmeldungVerwaltungPatch.ts +++ b/apps/api/src/services/anmeldung/anmeldungVerwaltungPatch.ts @@ -8,7 +8,7 @@ export const anmeldungVerwaltungPatchProcedure = defineProtectedMutateProcedure( key: 'verwaltungPatch', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: z.strictObject({ tshirtBestellt: z.boolean().optional(), comment: z.string().optional(), diff --git a/apps/api/src/services/anmeldung/anmeldungVerwaltungStorno.ts b/apps/api/src/services/anmeldung/anmeldungVerwaltungStorno.ts index ce10626a..1a46234c 100644 --- a/apps/api/src/services/anmeldung/anmeldungVerwaltungStorno.ts +++ b/apps/api/src/services/anmeldung/anmeldungVerwaltungStorno.ts @@ -11,7 +11,7 @@ export const anmeldungVerwaltungStornoProcedure = defineProtectedMutateProcedure key: 'verwaltungStorno', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - anmeldungId: z.number().int(), + anmeldungId: z.string().uuid(), }), async handler(options) { type AnmeldungWhereUniqueInput = Parameters[0]['where'] diff --git a/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts b/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts index e3f7b88b..a8a038d4 100644 --- a/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts +++ b/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts @@ -1,17 +1,18 @@ import { randomUUID } from 'node:crypto' import { z } from 'zod' -import client from '../../prisma.js' +import prisma from '../../prisma.js' import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' +import { TRPCError } from '@trpc/server' export const anmeldungLinkCreateProcedure = defineProtectedMutateProcedure({ key: 'create', roleIds: ['ADMIN'], inputSchema: z.strictObject({ - unterveranstaltungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), comment: z.string().optional(), }), handler: async ({ ctx, input: { unterveranstaltungId, comment } }) => { - const result = await client.anmeldungLink.create({ + const { accessToken } = await prisma.anmeldungLink.create({ data: { unterveranstaltungId, comment, @@ -23,6 +24,13 @@ export const anmeldungLinkCreateProcedure = defineProtectedMutateProcedure({ }, }) - return result.accessToken + if (accessToken === null) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'failed to generate access token', + }) + } + + return accessToken }, }) diff --git a/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts b/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts index fbae0ac6..651739f1 100644 --- a/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts +++ b/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts @@ -1,19 +1,18 @@ +import { Prisma, Role } from '@prisma/client' import { z } from 'zod' -import { defineOrderBy } from '../../types/defineOrderBy.js' -import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import client from '../../prisma.js' -import { Role, Prisma } from '@prisma/client' -import { ZPaginationSchema } from '../../types/defineQuery.js' import type { Context } from '../../context.js' +import prisma from '../../prisma.js' +import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' const filterSchema = z.discriminatedUnion('type', [ z.strictObject({ type: z.literal('veranstaltung'), - veranstaltungId: z.number(), + veranstaltungId: z.string().uuid(), }), z.strictObject({ type: z.literal('unterveranstaltung'), - unterveranstaltungId: z.number(), + unterveranstaltungId: z.string().uuid(), }), ]) @@ -38,19 +37,30 @@ export const anmeldungLinkListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN, Role.USER], inputSchema: z.strictObject({ - pagination: ZPaginationSchema, - filter: filterSchema, - orderBy: defineOrderBy(['usedAt', 'createdBy', 'comment']), + section: filterSchema, + table: defineTableInput({ + filter: { + status: z.enum(['used', 'unused']), + }, + }), }), async handler({ ctx, input }) { - const { skip, take } = input.pagination - const result = await client.anmeldungLink.findMany({ - skip, - take, - where: getWhere(ctx, input.filter), - orderBy: { - createdAt: 'desc', - }, + const where: Prisma.AnmeldungLinkWhereInput = { + ...getWhere(ctx, input.section), + } + if (input.table.filter?.status === 'used') { + where.NOT = [{ usedAt: null }] + } else if (input.table.filter?.status === 'unused') { + where.usedAt = null + } + + const total = await prisma.anmeldungLink.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, input.table.pagination) + + const result = await prisma.anmeldungLink.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + where, select: { id: true, createdAt: true, @@ -92,19 +102,6 @@ export const anmeldungLinkListProcedure = defineProtectedQueryProcedure({ }, }) - return result - }, -}) - -export const anmeldungLinkCountProcedure = defineProtectedQueryProcedure({ - key: 'count', - roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN, Role.USER], - inputSchema: z.strictObject({ - filter: filterSchema, - }), - handler: ({ ctx, input }) => { - return client.anmeldungLink.count({ - where: getWhere(ctx, input.filter), - }) + return defineQueryResponse({ data: result, total, pagination: { pageIndex, pageSize, pages } }) }, }) diff --git a/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts b/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts index eae5dc5d..78d28a51 100644 --- a/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts +++ b/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts @@ -1,15 +1,15 @@ import { z } from 'zod' import { definePublicQueryProcedure } from '../../types/defineProcedure.js' -import client from '../../prisma.js' +import prisma from '../../prisma.js' export const anmeldungLinkAuthorizeProcedure = definePublicQueryProcedure({ key: 'authorize', inputSchema: z.strictObject({ - unterveranstaltungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), accessToken: z.string().uuid(), }), handler: async ({ input: { unterveranstaltungId, accessToken } }) => { - const result = await client.anmeldungLink.findFirst({ + const result = await prisma.anmeldungLink.findFirst({ where: { unterveranstaltungId, accessToken, diff --git a/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts b/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts index c6730892..aa97978c 100644 --- a/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts +++ b/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts @@ -1,13 +1,12 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' import { anmeldungLinkCreateProcedure } from './anmeldeLink.create.js' -import { anmeldungLinkCountProcedure, anmeldungLinkListProcedure } from './anmeldeLink.list.js' +import { anmeldungLinkListProcedure } from './anmeldeLink.list.js' import { anmeldungLinkAuthorizeProcedure } from './anmeldeLink.validate.js' // Import Routes here - do not delete this line export const anmeldungLinkRouter = mergeRouters( - anmeldungLinkCountProcedure, anmeldungLinkListProcedure, anmeldungLinkAuthorizeProcedure, anmeldungLinkCreateProcedure diff --git a/apps/api/src/services/customFields/customFieldValuesUpdate.ts b/apps/api/src/services/customFields/customFieldValuesUpdate.ts index 7ef4dd84..23ee9d28 100644 --- a/apps/api/src/services/customFields/customFieldValuesUpdate.ts +++ b/apps/api/src/services/customFields/customFieldValuesUpdate.ts @@ -12,11 +12,11 @@ export const customFieldValuesUpdate = defineProtectedMutateProcedure({ inputSchema: z.strictObject({ data: z.array( z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), value: z.union([z.string(), z.boolean(), z.undefined()]), }) ), - anmeldungId: z.number().int(), + anmeldungId: z.string().uuid(), }), async handler({ ctx: { account, accountId }, input }) { type AnmeldungWhereUniqueInput = Parameters[0]['where'] diff --git a/apps/api/src/services/customFields/customFields.router.ts b/apps/api/src/services/customFields/customFields.router.ts index 96e23167..3e38d8cc 100644 --- a/apps/api/src/services/customFields/customFields.router.ts +++ b/apps/api/src/services/customFields/customFields.router.ts @@ -2,7 +2,7 @@ import { mergeRouters } from '../../trpc.js' import { customFieldsGet } from './customFieldsGet.js' -import { customFieldsList } from './customFieldsList.js' +import { customFieldsList, customFieldsTable } from './customFieldsList.js' import { customFieldsTemplates } from './customFieldsTemplates.js' import { customFieldsUnterveranstaltungCreate } from './customFieldsUnterveranstaltungCreate.js' import { customFieldsVeranstaltungCreate } from './customFieldsVeranstaltungCreate.js' @@ -13,6 +13,7 @@ import { customFieldValuesUpdate } from './customFieldValuesUpdate.js' export const customFieldsRouter = mergeRouters( customFieldsList, + customFieldsTable, customFieldsGet, customFieldsVeranstaltungCreate, customFieldsUpdate, diff --git a/apps/api/src/services/customFields/customFieldsDelete.ts b/apps/api/src/services/customFields/customFieldsDelete.ts index 46ea0f48..35743725 100644 --- a/apps/api/src/services/customFields/customFieldsDelete.ts +++ b/apps/api/src/services/customFields/customFieldsDelete.ts @@ -9,8 +9,8 @@ export const customFieldsVeranstaltungDelete = defineProtectedMutateProcedure({ key: 'veranstaltungDelete', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - veranstaltungId: z.number(), - fieldId: z.number(), + veranstaltungId: z.string().uuid(), + fieldId: z.string().uuid(), }), async handler({ input }) { await prisma.customField.delete({ @@ -26,8 +26,8 @@ export const customFieldsUnterveranstaltungDelete = defineProtectedMutateProcedu key: 'unterveranstaltungDelete', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - unterveranstaltungId: z.number(), - fieldId: z.number(), + unterveranstaltungId: z.string().uuid(), + fieldId: z.string().uuid(), }), async handler({ ctx, input }) { if (ctx.account.role === Role.GLIEDERUNG_ADMIN) { diff --git a/apps/api/src/services/customFields/customFieldsGet.ts b/apps/api/src/services/customFields/customFieldsGet.ts index fa2b90f7..e52a63f1 100644 --- a/apps/api/src/services/customFields/customFieldsGet.ts +++ b/apps/api/src/services/customFields/customFieldsGet.ts @@ -9,7 +9,7 @@ export const customFieldsGet = defineProtectedQueryProcedure({ key: 'get', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number(), + id: z.string().uuid(), }), async handler({ input }) { const field = await prisma.customField.findUnique({ diff --git a/apps/api/src/services/customFields/customFieldsList.ts b/apps/api/src/services/customFields/customFieldsList.ts index 01c81d2a..f7d9a09c 100644 --- a/apps/api/src/services/customFields/customFieldsList.ts +++ b/apps/api/src/services/customFields/customFieldsList.ts @@ -1,57 +1,159 @@ import { z } from 'zod' +import { CustomFieldPosition, CustomFieldType, Prisma } from '@prisma/client' import prisma from '../../prisma.js' import { definePublicQueryProcedure } from '../../types/defineProcedure.js' -import { CustomFieldPosition, Prisma } from '@prisma/client' +import { + calculatePagination, + defineEmptyQueryResponse, + defineQueryResponse, + defineTableInput, +} from '../../types/defineTableProcedure.js' +import { boolish } from '../../util/zod.js' -export const customFieldsList = definePublicQueryProcedure({ +const baseFilter = z.strictObject({ + entity: z.enum(['veranstaltung', 'unterveranstaltung']), + entityId: z.string().uuid(), + position: z.nativeEnum(CustomFieldPosition).optional(), +}) + +export const customFieldsTable = definePublicQueryProcedure({ key: 'list', - inputSchema: z.strictObject({ - entity: z.enum(['veranstaltung', 'unterveranstaltung']), - entityId: z.number(), - position: z.nativeEnum(CustomFieldPosition).optional(), + inputSchema: baseFilter, + async handler({ input: { entity, entityId, position } }) { + if (entity === 'veranstaltung') { + return await prisma.customField.findMany({ + where: { + veranstaltungId: entityId, + positions: + position === undefined + ? undefined + : { + has: position, + }, + }, + }) + } else if (entity === 'unterveranstaltung') { + return await prisma.customField.findMany({ + where: { + positions: + position === undefined + ? undefined + : { + has: position, + }, + OR: [ + { + unterveranstaltungId: entityId, + }, + { + veranstaltung: { + unterveranstaltungen: { + some: { + id: entityId, + }, + }, + }, + }, + ], + }, + }) + } + + return [] + }, +}) + +export const customFieldsList = definePublicQueryProcedure({ + key: 'table', + inputSchema: baseFilter.extend({ + table: defineTableInput({ + filter: { + name: z.string(), + type: z.nativeEnum(CustomFieldType), + required: boolish, + position: z.nativeEnum(CustomFieldPosition), + }, + orderBy: ['name'], + }), }), async handler({ input }) { - const customFieldsFilter: Prisma.CustomFieldWhereInput = {} - if (input.position !== undefined) { - customFieldsFilter.positions = { - hasSome: [input.position] - } + const where: Prisma.CustomFieldWhereInput = { + name: { + contains: input.table?.filter?.name, + mode: 'insensitive', + }, + type: input.table?.filter?.type, + required: input.table?.filter?.required, + positions: + input.table?.filter?.position === undefined + ? undefined + : { + hasSome: [input.table.filter.position], + }, } + const total = await prisma.customField.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, input.table?.pagination) + if (input.entity === 'veranstaltung') { - const veranstaltung = await prisma.veranstaltung.findUniqueOrThrow({ + const customFields = await prisma.customField.findMany({ + take: pageSize, + skip: pageSize * pageIndex, where: { - id: input.entityId, + ...where, + veranstaltungId: input.entityId, }, - include: { - customFields: { - where: customFieldsFilter, - }, + orderBy: input.table?.orderBy, + select: { + id: true, + name: true, + description: true, + type: true, + positions: true, + required: true, + veranstaltungId: true, + unterveranstaltungId: true, }, }) - return veranstaltung.customFields + return defineQueryResponse({ data: customFields, total, pagination: { pageIndex, pageSize, pages } }) } else if (input.entity === 'unterveranstaltung') { - const ausschreibung = await prisma.unterveranstaltung.findUniqueOrThrow({ + const customFields = await prisma.customField.findMany({ + take: pageSize, + skip: pageSize * pageIndex, where: { - id: input.entityId, - }, - include: { - customFields: true, - veranstaltung: { - include: { - customFields: { - where: customFieldsFilter, + ...where, + OR: [ + { + unterveranstaltungId: input.entityId, + }, + { + veranstaltung: { + unterveranstaltungen: { + some: { + id: input.entityId, + }, + }, }, }, - }, + ], + }, + select: { + id: true, + name: true, + description: true, + type: true, + positions: true, + required: true, + veranstaltungId: true, + unterveranstaltungId: true, }, }) - return [...ausschreibung.veranstaltung.customFields, ...ausschreibung.customFields] + return defineQueryResponse({ data: customFields, total, pagination: { pageIndex, pageSize, pages } }) } - return [] + return defineEmptyQueryResponse() }, }) diff --git a/apps/api/src/services/customFields/customFieldsUnterveranstaltungCreate.ts b/apps/api/src/services/customFields/customFieldsUnterveranstaltungCreate.ts index 9b25f1db..4c594b94 100644 --- a/apps/api/src/services/customFields/customFieldsUnterveranstaltungCreate.ts +++ b/apps/api/src/services/customFields/customFieldsUnterveranstaltungCreate.ts @@ -10,7 +10,7 @@ export const customFieldsUnterveranstaltungCreate = defineProtectedMutateProcedu key: 'unterveranstaltungCreate', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - unterveranstaltungId: z.number(), + unterveranstaltungId: z.string().uuid(), data: customFieldSchema, }), handler: ({ input }) => diff --git a/apps/api/src/services/customFields/customFieldsUpdate.ts b/apps/api/src/services/customFields/customFieldsUpdate.ts index 4ef8c3ea..619b1be5 100644 --- a/apps/api/src/services/customFields/customFieldsUpdate.ts +++ b/apps/api/src/services/customFields/customFieldsUpdate.ts @@ -11,7 +11,7 @@ export const customFieldsUpdate = defineProtectedMutateProcedure({ key: 'update', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - fieldId: z.number(), + fieldId: z.string().uuid(), data: customFieldSchema, }), async handler({ ctx, input }) { diff --git a/apps/api/src/services/customFields/customFieldsVeranstaltungCreate.ts b/apps/api/src/services/customFields/customFieldsVeranstaltungCreate.ts index 5e1370f3..073da4bc 100644 --- a/apps/api/src/services/customFields/customFieldsVeranstaltungCreate.ts +++ b/apps/api/src/services/customFields/customFieldsVeranstaltungCreate.ts @@ -14,12 +14,12 @@ export const customFieldsVeranstaltungCreate = defineProtectedMutateProcedure({ inputSchema: z.discriminatedUnion('type', [ z.strictObject({ type: z.literal('new'), - veranstaltungId: z.number(), + veranstaltungId: z.string().uuid(), data: customFieldSchema, }), z.strictObject({ type: z.literal('fromTemplate'), - veranstaltungId: z.number(), + veranstaltungId: z.string().uuid(), template: z.string(), }), ]), diff --git a/apps/api/src/services/faqs/faqCreateProcedure.ts b/apps/api/src/services/faqs/faqCreateProcedure.ts index 2f682493..f9df6a43 100644 --- a/apps/api/src/services/faqs/faqCreateProcedure.ts +++ b/apps/api/src/services/faqs/faqCreateProcedure.ts @@ -8,7 +8,7 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({ key: 'create', roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'], inputSchema: z.strictObject({ - unterveranstaltungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), faq: faqSchema, }), handler: async ({ input: { faq, unterveranstaltungId } }) => { diff --git a/apps/api/src/services/faqs/faqDeleteProcecure.ts b/apps/api/src/services/faqs/faqDeleteProcecure.ts index 87052556..60258f55 100644 --- a/apps/api/src/services/faqs/faqDeleteProcecure.ts +++ b/apps/api/src/services/faqs/faqDeleteProcecure.ts @@ -6,7 +6,7 @@ import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' export const faqDeleteProcedure = defineProtectedMutateProcedure({ key: 'delete', roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'], - inputSchema: z.number().int(), + inputSchema: z.string().uuid(), handler: async ({ input }) => { await prisma.faq.delete({ where: { diff --git a/apps/api/src/services/faqs/faqListProcedure.ts b/apps/api/src/services/faqs/faqListProcedure.ts index 0008574a..14c9b5ff 100644 --- a/apps/api/src/services/faqs/faqListProcedure.ts +++ b/apps/api/src/services/faqs/faqListProcedure.ts @@ -4,7 +4,7 @@ import { groupBy } from '@codeanker/helpers' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -export async function listFaqs(unterveranstaltungId: number) { +export async function listFaqs(unterveranstaltungId: string) { const list = await prisma.faq.findMany({ where: { unterveranstaltung: { @@ -37,7 +37,7 @@ export const faqListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'], inputSchema: z.strictObject({ - unterveranstaltungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), }), handler: ({ input }) => listFaqs(input.unterveranstaltungId), }) diff --git a/apps/api/src/services/faqs/faqUpdateProcedure.ts b/apps/api/src/services/faqs/faqUpdateProcedure.ts index 9b57f73e..b7c47882 100644 --- a/apps/api/src/services/faqs/faqUpdateProcedure.ts +++ b/apps/api/src/services/faqs/faqUpdateProcedure.ts @@ -8,8 +8,8 @@ export const faqUpdateProcedure = defineProtectedMutateProcedure({ key: 'update', roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'], inputSchema: z.strictObject({ - id: z.number().int(), - unterveranstaltungId: z.number().int(), + id: z.string().uuid(), + unterveranstaltungId: z.string().uuid(), faq: faqSchema, }), handler: async ({ input: { id, unterveranstaltungId, faq } }) => { diff --git a/apps/api/src/services/file/anmeldungPublicFotoUpload.ts b/apps/api/src/services/file/anmeldungPublicFotoUpload.ts index e2c385eb..3bc22e9f 100644 --- a/apps/api/src/services/file/anmeldungPublicFotoUpload.ts +++ b/apps/api/src/services/file/anmeldungPublicFotoUpload.ts @@ -1,18 +1,18 @@ import { z } from 'zod' import { definePublicMutateProcedure } from '../../types/defineProcedure.js' import { fileCreateSchema, handleFileUpload } from './fileCreate.js' -import client from '../../prisma.js' +import prisma from '../../prisma.js' import { TRPCError } from '@trpc/server' export const anmeldungPublicFotoUploadProcedure = definePublicMutateProcedure({ key: 'anmeldungPublicFotoUpload', inputSchema: fileCreateSchema.extend({ - unterveranstaltungId: z.number().int(), - anmeldungId: z.number().int(), + unterveranstaltungId: z.string().uuid(), + anmeldungId: z.string().uuid(), accessToken: z.string().uuid(), }), handler: async ({ input: { unterveranstaltungId, anmeldungId, accessToken, mimetype } }) => { - const anmeldung = await client.anmeldung.findFirst({ + const anmeldung = await prisma.anmeldung.findFirst({ where: { unterveranstaltungId, id: anmeldungId, diff --git a/apps/api/src/services/file/fileGetUrl.ts b/apps/api/src/services/file/fileGetUrl.ts index d41de868..4a62f6b0 100644 --- a/apps/api/src/services/file/fileGetUrl.ts +++ b/apps/api/src/services/file/fileGetUrl.ts @@ -8,7 +8,7 @@ export const fileGetUrlActionProcedure = definePublicQueryProcedure({ key: 'fileGetUrl', inputSchema: z.strictObject({ id: z.string().uuid().nullable(), - personId: z.number().int().optional(), + personId: z.string().uuid().optional(), }), async handler({ input }) { if (typeof input.id !== 'string') { diff --git a/apps/api/src/services/file/helpers/getFileUrl.ts b/apps/api/src/services/file/helpers/getFileUrl.ts index 12841ac1..24cf0c82 100644 --- a/apps/api/src/services/file/helpers/getFileUrl.ts +++ b/apps/api/src/services/file/helpers/getFileUrl.ts @@ -14,7 +14,7 @@ export const uploadDir = join(cwd(), config.fileProviders.LOCAL.path) export async function getFileUrl(file: File) { if (file.provider === 'LOCAL') { if (!file.uploaded) throw new Error('File is not uploaded') - return new URL(`/api/download/file/${file.provider}/${file.id}`, config.clientUrl).href + return new URL(`/api/file/download/${file.provider}/${file.id}`, config.clientUrl).href } if (file.provider === 'AZURE' && azureStorage !== null) { diff --git a/apps/api/src/services/gliederung/gliederung.router.ts b/apps/api/src/services/gliederung/gliederung.router.ts index 054b7925..86e9ab19 100644 --- a/apps/api/src/services/gliederung/gliederung.router.ts +++ b/apps/api/src/services/gliederung/gliederung.router.ts @@ -1,7 +1,7 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' -import { gliederungListProcedure, gliederungCountProcedure } from './gliederungList.js' +import { gliederungListProcedure } from './gliederungList.js' import { gliederungPublicGetProcedure } from './gliederungPublicGet.js' import { gliederungPublicListProcedure } from './gliederungPublicList.js' import { gliederungVerwaltungCreateProcedure } from './gliederungVerwaltungCreate.js' @@ -15,7 +15,6 @@ export const gliederungRouter = mergeRouters( gliederungVerwaltungCreateProcedure, gliederungVerwaltungGetProcedure, gliederungListProcedure, - gliederungCountProcedure, gliederungVerwaltungPatchProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/gliederung/gliederungList.ts b/apps/api/src/services/gliederung/gliederungList.ts index 66efb86c..7e09bc0d 100644 --- a/apps/api/src/services/gliederung/gliederungList.ts +++ b/apps/api/src/services/gliederung/gliederungList.ts @@ -3,76 +3,44 @@ import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - edv: z.string().optional(), - name: z.string().optional(), - }), - orderBy: z.array( - z.tuple([z.union([z.literal('edv'), z.literal('name')]), z.union([z.literal('asc'), z.literal('desc')])]) - ), -}) -type TInput = z.infer +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' export const gliederungListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN], - inputSchema, - async handler(options) { - const { skip, take } = options.input.pagination - const list = await prisma.gliederung.findMany({ - skip, - take, - orderBy: getOrderBy(options.input.orderBy), - where: await getWhere(options.input.filter, options.ctx.account), + inputSchema: defineTableInput({ + filter: { + name: z.string().optional(), + edv: z.string().optional(), + }, + orderBy: ['name', 'edv'], + }), + async handler({ input: { pagination, filter, orderBy } }) { + const where: Prisma.GliederungWhereInput = { + name: { + contains: filter?.name, + mode: 'insensitive', + }, + edv: { + contains: filter?.edv, + }, + } + + const total = await prisma.gliederung.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + + const orte = await prisma.gliederung.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + where, + orderBy, select: { - id: true, name: true, edv: true, + id: true, }, }) - return list + return defineQueryResponse({ data: orte, total, pagination: { pageIndex, pageSize, pages } }) }, }) - -export const gliederungCountProcedure = defineProtectedQueryProcedure({ - key: 'count', - roleIds: [Role.ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - const list = await prisma.gliederung.count({ - where: await getWhere(options.input.filter, options.ctx.account), - }) - - return list - }, -}) - -// eslint-disable-next-line @typescript-eslint/require-await -async function getWhere( - filter: TInput['filter'], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - account: { - id: number - role: Role - } -): Promise { - const where: Prisma.GliederungWhereInput = {} - - if (filter.name != undefined && filter.name !== '') - where.name = { - contains: filter.name, - mode: 'insensitive', - } - - if (filter.edv != undefined && filter.edv !== '') - where.edv = { - contains: filter.edv, - mode: 'insensitive', - } - - return where -} diff --git a/apps/api/src/services/gliederung/gliederungPublicGet.ts b/apps/api/src/services/gliederung/gliederungPublicGet.ts index 86c834e8..480a48f3 100644 --- a/apps/api/src/services/gliederung/gliederungPublicGet.ts +++ b/apps/api/src/services/gliederung/gliederungPublicGet.ts @@ -6,7 +6,7 @@ import { definePublicQueryProcedure } from '../../types/defineProcedure.js' export const gliederungPublicGetProcedure = definePublicQueryProcedure({ key: 'publicGet', inputSchema: z.strictObject({ - gliederungId: z.number().int(), + gliederungId: z.string().uuid(), }), async handler(options) { return prisma.gliederung.findUniqueOrThrow({ diff --git a/apps/api/src/services/gliederung/gliederungVerwaltungGet.ts b/apps/api/src/services/gliederung/gliederungVerwaltungGet.ts index 7155731e..c5c42340 100644 --- a/apps/api/src/services/gliederung/gliederungVerwaltungGet.ts +++ b/apps/api/src/services/gliederung/gliederungVerwaltungGet.ts @@ -8,7 +8,7 @@ export const gliederungVerwaltungGetProcedure = defineProtectedQueryProcedure({ key: 'verwaltungGet', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number(), + id: z.string().uuid(), }), async handler(options) { return prisma.gliederung.findUniqueOrThrow({ diff --git a/apps/api/src/services/gliederung/gliederungVerwaltungPatch.ts b/apps/api/src/services/gliederung/gliederungVerwaltungPatch.ts index f2e6572f..7188279e 100644 --- a/apps/api/src/services/gliederung/gliederungVerwaltungPatch.ts +++ b/apps/api/src/services/gliederung/gliederungVerwaltungPatch.ts @@ -8,7 +8,7 @@ export const gliederungVerwaltungPatchProcedure = defineProtectedMutateProcedure key: 'verwaltungPatch', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: z.strictObject({ name: z.string(), edv: z.string(), diff --git a/apps/api/src/services/ort/ort.router.ts b/apps/api/src/services/ort/ort.router.ts index 86bd8402..6e13520d 100644 --- a/apps/api/src/services/ort/ort.router.ts +++ b/apps/api/src/services/ort/ort.router.ts @@ -1,7 +1,7 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' -import { ortListProcedure, ortCountProcedure } from './ortList.js' +import { ortListProcedure, ortTableProcedure } from './ortList.js' import { ortVerwaltungCreateProcedure } from './ortVerwaltungCreate.js' import { ortVerwaltungGetProcedure } from './ortVerwaltungGet.js' import { ortVerwaltungPatchProcedure } from './ortVerwaltungPatch.js' @@ -9,10 +9,10 @@ import { ortVerwaltungRemoveProcedure } from './ortVerwaltungRemove.js' // Import Routes here - do not delete this line export const ortRouter = mergeRouters( + ortListProcedure, + ortTableProcedure, ortVerwaltungCreateProcedure, ortVerwaltungGetProcedure, - ortListProcedure, - ortCountProcedure, ortVerwaltungPatchProcedure, ortVerwaltungRemoveProcedure // Add Routes here - do not delete this line diff --git a/apps/api/src/services/ort/ortList.ts b/apps/api/src/services/ort/ortList.ts index 3fde11f6..11eaf388 100644 --- a/apps/api/src/services/ort/ortList.ts +++ b/apps/api/src/services/ort/ortList.ts @@ -3,80 +3,61 @@ import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery } from '../../types/defineQuery.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - name: z.string().optional(), - city: z.string().optional(), - }), - orderBy: z.array( - z.tuple([ - z.union([z.literal('id'), z.literal('name'), z.literal('address.city')]), - z.union([z.literal('asc'), z.literal('desc')]), - ]) - ), -}) -type TInput = z.infer - -// eslint-disable-next-line @typescript-eslint/require-await -async function getWhere( - filter: TInput['filter'], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _account: { - id: number - role: Role - } -): Promise { - const where: Prisma.OrtWhereInput = {} - - // Input filter - if (filter.name != undefined && filter.name != '') { - where.name = { - contains: filter.name, - } - } - if (filter.city != undefined && filter.city != '') { - where.address = { - city: { - contains: filter.city, - }, - } - } - - // Role filter - // -- - - return where +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' + +const select: Prisma.OrtSelect = { + id: true, + name: true, + address: { + select: { + zip: true, + country: true, + city: true, + street: true, + streetNumber: true, + }, + }, } export const ortListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN], - inputSchema, - async handler(options) { - const { skip, take } = options.input.pagination - - return await prisma.ort.findMany({ - skip, - take, - where: await getWhere(options.input.filter, options.ctx.account), - select: { - id: true, - name: true, - address: true, - }, + inputSchema: z.void(), + handler() { + return prisma.ort.findMany({ + select, }) }, }) -export const ortCountProcedure = defineProtectedQueryProcedure({ - key: 'count', +export const ortTableProcedure = defineProtectedQueryProcedure({ + key: 'table', roleIds: [Role.ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - return await prisma.ort.count({ - where: await getWhere(options.input.filter, options.ctx.account), + inputSchema: defineTableInput({ + filter: { + name: z.string().optional(), + }, + orderBy: ['name'], + }), + async handler({ input: { pagination, filter, orderBy } }) { + const where: Prisma.OrtWhereInput = { + name: { + contains: filter?.name, + mode: 'insensitive', + }, + } + + const total = await prisma.ort.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + + const orte = await prisma.ort.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + where, + orderBy, + select, }) + + return defineQueryResponse({ data: orte, total, pagination: { pageIndex, pageSize, pages } }) }, }) diff --git a/apps/api/src/services/ort/ortVerwaltungGet.ts b/apps/api/src/services/ort/ortVerwaltungGet.ts index 6b7cec8e..5cbd235c 100644 --- a/apps/api/src/services/ort/ortVerwaltungGet.ts +++ b/apps/api/src/services/ort/ortVerwaltungGet.ts @@ -8,7 +8,7 @@ export const ortVerwaltungGetProcedure = defineProtectedQueryProcedure({ key: 'verwaltungGet', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { return prisma.ort.findUniqueOrThrow({ diff --git a/apps/api/src/services/ort/ortVerwaltungPatch.ts b/apps/api/src/services/ort/ortVerwaltungPatch.ts index 20ff6e71..775a5f88 100644 --- a/apps/api/src/services/ort/ortVerwaltungPatch.ts +++ b/apps/api/src/services/ort/ortVerwaltungPatch.ts @@ -9,7 +9,7 @@ export const ortVerwaltungPatchProcedure = defineProtectedMutateProcedure({ key: 'verwaltungPatch', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: z.strictObject({ name: z.string(), address: addressSchema, diff --git a/apps/api/src/services/ort/ortVerwaltungRemove.ts b/apps/api/src/services/ort/ortVerwaltungRemove.ts index 96394df0..e76d994d 100644 --- a/apps/api/src/services/ort/ortVerwaltungRemove.ts +++ b/apps/api/src/services/ort/ortVerwaltungRemove.ts @@ -8,7 +8,7 @@ export const ortVerwaltungRemoveProcedure = defineProtectedMutateProcedure({ key: 'verwaltungRemove', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler({ input }) { return prisma.ort.delete({ diff --git a/apps/api/src/services/person/person.router.ts b/apps/api/src/services/person/person.router.ts index 97344110..b9050313 100644 --- a/apps/api/src/services/person/person.router.ts +++ b/apps/api/src/services/person/person.router.ts @@ -2,9 +2,9 @@ import { mergeRouters } from '../../trpc.js' import { personAuthenticatedGetProcedure } from './personAuthenticatedGet.js' import { personGetProcedure } from './personGet.js' -import { personCountProcedure, personListProcedure } from './personList.js' -import { personVerwaltungCreateProcedure } from './personVerwaltungCreate.js' +import { personListProcedure } from './personList.js' import { personVerwaltungPatchProcedure } from './personPatch.js' +import { personVerwaltungCreateProcedure } from './personVerwaltungCreate.js' import { personVerwaltungRemoveProcedure } from './personVerwaltungRemove.js' // Import Routes here - do not delete this line @@ -12,7 +12,6 @@ export const personRouter = mergeRouters( personAuthenticatedGetProcedure, personVerwaltungCreateProcedure, personListProcedure, - personCountProcedure, personGetProcedure, personVerwaltungPatchProcedure, personVerwaltungRemoveProcedure diff --git a/apps/api/src/services/person/personGet.ts b/apps/api/src/services/person/personGet.ts index a4f77bb0..0af7b150 100644 --- a/apps/api/src/services/person/personGet.ts +++ b/apps/api/src/services/person/personGet.ts @@ -9,15 +9,16 @@ export const personGetProcedure = defineProtectedQueryProcedure({ key: 'get', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN, Role.USER], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), - handler: ({ ctx, input }) => { + handler: async ({ ctx, input }) => { + const protection = await getPersonProtectionFilter(ctx) const where: Prisma.PersonWhereUniqueInput = { - ...getPersonProtectionFilter(ctx), + ...protection, id: input.id, } - return prisma.person.findUniqueOrThrow({ + return await prisma.person.findUniqueOrThrow({ where, select: { id: true, diff --git a/apps/api/src/services/person/personList.ts b/apps/api/src/services/person/personList.ts index ab4fcde4..ae78473b 100644 --- a/apps/api/src/services/person/personList.ts +++ b/apps/api/src/services/person/personList.ts @@ -2,37 +2,69 @@ import { Prisma, Role } from '@prisma/client' import z from 'zod' import prisma from '../../prisma.js' -import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' import type { AuthenticatedContext } from '../../trpc.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - name: z.string().optional(), - gliederungName: z.string().optional(), - }), - orderBy: z.array( - z.tuple([ - z.union([z.literal('firstname'), z.literal('birthday'), z.literal('gliederung.name')]), - z.union([z.literal('asc'), z.literal('desc')]), - ]) - ), -}) - -type TInput = z.infer +import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' +import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' export const personListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN, Role.USER], - inputSchema, - handler: ({ ctx, input }) => { - const { skip, take } = input.pagination + inputSchema: defineTableInput({ + filter: { + name: z.string(), + birthday: z.array(z.date()).min(2).max(2), + gliederung_name: z.string(), + // gliederungId: z.string().uuid(), + // veranstaltungId: z.string().uuid(), + }, + orderBy: ['birthday', 'anmeldungen__count'], + }), + handler: async ({ ctx, input: { filter, orderBy, pagination } }) => { + const protection = await getPersonProtectionFilter(ctx) + const where: Prisma.PersonWhereInput = { + OR: filter?.name + ? [ + { + firstname: { + contains: filter.name, + mode: 'insensitive', + }, + }, + { + lastname: { + contains: filter.name, + mode: 'insensitive', + }, + }, + ] + : undefined, + gliederung: { + name: { + contains: filter?.gliederung_name, + mode: 'insensitive', + }, + }, + birthday: + filter?.birthday === undefined + ? undefined + : { + gte: filter.birthday[0], + lte: filter.birthday[1], + }, + ...protection, + } - return prisma.person.findMany({ - skip, - take, - where: getWhere(input.filter, ctx), - orderBy: getOrderBy(input.orderBy), + const total = await prisma.person.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + + console.log(orderBy) + + const persons = await prisma.person.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + where, + orderBy, select: { id: true, firstname: true, @@ -41,20 +73,24 @@ export const personListProcedure = defineProtectedQueryProcedure({ photoId: true, gliederung: { select: { - id: true, name: true, }, }, account: { select: { id: true, - activatedAt: true, - role: true, - status: true, - GliederungToAccount: { + }, + }, + _count: { + select: { + anmeldungen: true, + }, + }, + anmeldungen: { + select: { + unterveranstaltung: { select: { - role: true, - gliederung: { + veranstaltung: { select: { name: true, }, @@ -65,24 +101,14 @@ export const personListProcedure = defineProtectedQueryProcedure({ }, }, }) - }, -}) -export const personCountProcedure = defineProtectedQueryProcedure({ - key: 'count', - roleIds: [Role.ADMIN, Role.USER], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - const total = await prisma.person.count({ - where: getWhere(options.input.filter, options.ctx), - }) - return total + return defineQueryResponse({ data: persons, total, pagination: { pageIndex, pageSize, pages } }) }, }) -export function getPersonProtectionFilter({ +export async function getPersonProtectionFilter({ account, -}: AuthenticatedContext): Prisma.PersonWhereInput | Prisma.PersonWhereUniqueInput { +}: AuthenticatedContext): Promise { const where: Prisma.PersonWhereInput = {} if (account.role === Role.USER) { @@ -91,27 +117,10 @@ export function getPersonProtectionFilter({ accountId: account.id, }, } + } else if (account.role === Role.GLIEDERUNG_ADMIN) { + const gliederung = await getGliederungRequireAdmin(account.id) + where.gliederungId = gliederung.id } return where } - -function getWhere(filter: TInput['filter'], ctx: AuthenticatedContext): Prisma.PersonWhereInput { - const where: Prisma.PersonWhereInput = {} - - if (filter.name != null && filter.name != '') { - where.OR = [{ firstname: { contains: filter.name } }, { lastname: { contains: filter.name } }] - } - if (filter.gliederungName != null && filter.gliederungName != '') { - where.gliederung = { - name: { - contains: filter.gliederungName, - }, - } - } - - return { - ...where, - ...getPersonProtectionFilter(ctx), - } -} diff --git a/apps/api/src/services/person/personPatch.ts b/apps/api/src/services/person/personPatch.ts index 92205e1f..7afd20d9 100644 --- a/apps/api/src/services/person/personPatch.ts +++ b/apps/api/src/services/person/personPatch.ts @@ -10,9 +10,9 @@ import { updateMeiliPerson } from '../../meilisearch/person.js' export const personVerwaltungPatchProcedure = defineProtectedMutateProcedure({ key: 'patch', - roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], + roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN, Role.USER], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: personSchemaOptional, }), async handler({ ctx, input }) { @@ -41,6 +41,24 @@ export const personVerwaltungPatchProcedure = defineProtectedMutateProcedure({ code: 'NOT_FOUND', }) } + } else if (ctx.account.role === 'USER') { + const ownIds = await prisma.person.findMany({ + where: { + anmeldungen: { + some: { + accountId: ctx.accountId, + }, + }, + }, + select: { + id: true, + }, + }) + if (ownIds.findIndex((v) => v.id === input.id) === -1) { + throw new TRPCError({ + code: 'NOT_FOUND', + }) + } } await prisma.notfallkontakt.deleteMany({ diff --git a/apps/api/src/services/person/personVerwaltungRemove.ts b/apps/api/src/services/person/personVerwaltungRemove.ts index 8734de04..dc25ee03 100644 --- a/apps/api/src/services/person/personVerwaltungRemove.ts +++ b/apps/api/src/services/person/personVerwaltungRemove.ts @@ -9,7 +9,7 @@ export const personVerwaltungRemoveProcedure = defineProtectedMutateProcedure({ key: 'verwaltungRemove', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { await prisma.person.delete({ diff --git a/apps/api/src/services/person/schema/person.schema.ts b/apps/api/src/services/person/schema/person.schema.ts index 1e061fe0..0ce822e2 100644 --- a/apps/api/src/services/person/schema/person.schema.ts +++ b/apps/api/src/services/person/schema/person.schema.ts @@ -10,7 +10,7 @@ export const personSchema = z.strictObject({ lastname: z.string(), birthday: z.date(), gender: z.nativeEnum(Gender), - gliederungId: z.number(), + gliederungId: z.string().uuid(), email: z.string(), telefon: z.string(), address: addressSchema, @@ -27,7 +27,7 @@ export const personSchemaOptional = personSchema.partial() export async function getPersonCreateData( input: z.infer ): Promise { - let addressId: number | undefined = undefined + let addressId: string | undefined = undefined if (input.address) { addressId = await createOrUpdateAddress(input.address) } diff --git a/apps/api/src/services/program/program.create.ts b/apps/api/src/services/program/program.create.ts index c83c30d4..0a1c173e 100644 --- a/apps/api/src/services/program/program.create.ts +++ b/apps/api/src/services/program/program.create.ts @@ -1,14 +1,14 @@ import { TRPCError } from '@trpc/server' import dayjs from 'dayjs' import { z } from 'zod' -import client from '../../prisma.js' +import prisma from '../../prisma.js' import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' export const programCreateProcedure = defineProtectedMutateProcedure({ key: 'create', roleIds: ['ADMIN'], inputSchema: z.strictObject({ - veranstaltungId: z.number().int(), + veranstaltungId: z.string().uuid(), name: z.string(), description: z.string(), location: z.string(), @@ -24,7 +24,7 @@ export const programCreateProcedure = defineProtectedMutateProcedure({ }) } - await client.programmPunkt.create({ + await prisma.programmPunkt.create({ data: input, }) }, diff --git a/apps/api/src/services/program/program.list.ts b/apps/api/src/services/program/program.list.ts index 0a3dc5fc..ff7a0722 100644 --- a/apps/api/src/services/program/program.list.ts +++ b/apps/api/src/services/program/program.list.ts @@ -6,7 +6,7 @@ export const programListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: ['ADMIN'], inputSchema: z.strictObject({ - veranstaltungId: z.number().int(), + veranstaltungId: z.string().uuid(), }), handler: ({ input }) => prisma.programmPunkt.findMany({ diff --git a/apps/api/src/services/unterveranstaltung/schema/crudFiles.ts b/apps/api/src/services/unterveranstaltung/schema/crudFiles.ts index 3292a169..51fa61e9 100644 --- a/apps/api/src/services/unterveranstaltung/schema/crudFiles.ts +++ b/apps/api/src/services/unterveranstaltung/schema/crudFiles.ts @@ -6,14 +6,14 @@ */ export function crudFiles( addFiles?: { name: string; fileId: string }[], - updateFiles?: { id: number; name: string }[], - deleteFilesIds?: number[] + updateFiles?: { id: string; name: string }[], + deleteFilesIds?: string[] ) { // Documents create, update, delete const files: { createMany?: { data: { name: string; fileId: string }[] } - updateMany?: { where: { id: number }; data: { name: string } }[] - deleteMany?: { id: number }[] + updateMany?: { where: { id: string }; data: { name: string } }[] + deleteMany?: { id: string }[] } = {} if (addFiles) { files.createMany = { diff --git a/apps/api/src/services/unterveranstaltung/schema/crudMiscellaneousItems.ts b/apps/api/src/services/unterveranstaltung/schema/crudMiscellaneousItems.ts index a12575e7..05bb2236 100644 --- a/apps/api/src/services/unterveranstaltung/schema/crudMiscellaneousItems.ts +++ b/apps/api/src/services/unterveranstaltung/schema/crudMiscellaneousItems.ts @@ -1,13 +1,13 @@ export function crudMiscellaneousItems( addMiscellaneousItems?: { title: string; content: string }[], - updateMiscellaneousItems?: { id: number; title: string; content: string }[], - deleteMiscellaneousItemIds?: number[] + updateMiscellaneousItems?: { id: string; title: string; content: string }[], + deleteMiscellaneousItemIds?: string[] ) { // Miscellaneous create, update, delete const miscellaneousItems: { createMany?: { data: { title: string; content: string }[] } - updateMany?: { where: { id: number }; data: { title: string; content: string } }[] - deleteMany?: { id: number }[] + updateMany?: { where: { id: string }; data: { title: string; content: string } }[] + deleteMany?: { id: string }[] } = {} if (addMiscellaneousItems) { miscellaneousItems.createMany = { diff --git a/apps/api/src/services/unterveranstaltung/schema/unterveranstaltung.schema.ts b/apps/api/src/services/unterveranstaltung/schema/unterveranstaltung.schema.ts index b40a6bde..bc25b86c 100644 --- a/apps/api/src/services/unterveranstaltung/schema/unterveranstaltung.schema.ts +++ b/apps/api/src/services/unterveranstaltung/schema/unterveranstaltung.schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod' export const unterveranstaltungCreateSchema = z.strictObject({ - veranstaltungId: z.number().int(), + veranstaltungId: z.string().uuid(), maxTeilnehmende: z.number().int(), teilnahmegebuehr: z.number({ description: 'In Cent' }).int(), meldebeginn: z.date(), @@ -23,8 +23,8 @@ export const unterveranstaltungLandingSchema = z.strictObject({ }) ) .optional(), - updateHeroImages: z.array(z.strictObject({ id: z.number().int(), name: z.string() })).optional(), - deleteHeroImageIds: z.array(z.number().int()).optional(), + updateHeroImages: z.array(z.strictObject({ id: z.string().uuid(), name: z.string() })).optional(), + deleteHeroImageIds: z.array(z.string().uuid()).optional(), eventDetailsTitle: z.string(), eventDetailsContent: z.string(), @@ -42,9 +42,9 @@ export const unterveranstaltungLandingSchema = z.strictObject({ ) .optional(), updateMiscellaneousItems: z - .array(z.strictObject({ id: z.number().int(), title: z.string(), content: z.string() })) + .array(z.strictObject({ id: z.string().uuid(), title: z.string(), content: z.string() })) .optional(), - deleteMiscellaneousItemIds: z.array(z.number().int()).optional(), + deleteMiscellaneousItemIds: z.array(z.string().uuid()).optional(), faqVisible: z.boolean().optional(), faqEmail: z.string().optional(), @@ -65,8 +65,8 @@ export const unterveranstaltungUpdateSchema = unterveranstaltungCreateSchema.omi }) ) .optional(), - updateDocuments: z.array(z.strictObject({ id: z.number().int(), name: z.string() })).optional(), - deleteDocumentIds: z.array(z.number().int()).optional(), + updateDocuments: z.array(z.strictObject({ id: z.string().uuid(), name: z.string() })).optional(), + deleteDocumentIds: z.array(z.string().uuid()).optional(), }) export type TUnterveranstaltungLandingSchema = z.infer diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts index abb2685a..c60a62bd 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts @@ -4,7 +4,7 @@ import { mergeRouters } from '../../trpc.js' import { unterveranstaltungGliederungCreateProcedure } from './unterveranstaltungGliederungCreate.js' import { unterveranstaltungGliederungGetProcedure } from './unterveranstaltungGliederungGet.js' import { unterveranstaltungGliederungPatchProcedure } from './unterveranstaltungGliederungPatch.js' -import { unterveranstaltungListProcedure, unterveranstaltungCountProcedure } from './unterveranstaltungList.js' +import { unterveranstaltungListProcedure } from './unterveranstaltungList.js' import { unterveranstaltungPublicGetProcedure } from './unterveranstaltungPublicGet.js' import { unterveranstaltungVerwaltungCreateProcedure } from './unterveranstaltungVerwaltungCreate.js' import { unterveranstaltungVerwaltungGetProcedure } from './unterveranstaltungVerwaltungGet.js' @@ -20,7 +20,6 @@ export const unterveranstaltungRouter = mergeRouters( unterveranstaltungVerwaltungCreateProcedure, unterveranstaltungVerwaltungPatchProcedure, unterveranstaltungVerwaltungGetProcedure, - unterveranstaltungListProcedure, - unterveranstaltungCountProcedure + unterveranstaltungListProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts index bd196074..9e37ba6d 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts @@ -8,7 +8,7 @@ export const unterveranstaltungGliederungGetProcedure = defineProtectedQueryProc key: 'gliederungGet', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), handler: (options) => prisma.unterveranstaltung.findUniqueOrThrow({ diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts index 39b5aa99..e3022940 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts @@ -12,7 +12,7 @@ export const unterveranstaltungGliederungPatchProcedure = defineProtectedMutateP key: 'gliederungPatch', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: unterveranstaltungUpdateSchema.partial().optional(), landingSettings: unterveranstaltungLandingSchema.partial().optional(), }), diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts index 97072df2..0382c9be 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts @@ -1,45 +1,76 @@ -import { Role, type Prisma } from '@prisma/client' +import { Role, UnterveranstaltungType, type Prisma } from '@prisma/client' import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - veranstaltungId: z.number().optional(), - gliederungId: z.number().optional(), - gliederungName: z.string().optional(), - }), - orderBy: z.array( - z.tuple([ - z.union([ - z.literal('id'), - z.literal('veranstaltung.name'), - z.literal('gliederung.name'), - z.literal('meldeschluss'), - z.literal('teilnahmegebuehr'), - z.literal('type'), - ]), - z.union([z.literal('asc'), z.literal('desc')]), - ]) - ), -}) - -type Input = z.infer +import { dayjs } from '@codeanker/helpers' export const unterveranstaltungListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], - inputSchema: inputSchema, - async handler(options) { - const { skip, take } = options.input.pagination + inputSchema: z.strictObject({ + veranstaltungId: z.string().uuid().optional(), + table: defineTableInput({ + filter: { + gliederungName: z.string().optional(), + type: z.nativeEnum(UnterveranstaltungType).optional(), + meldeschluss: z.tuple([z.date(), z.date()]), + }, + orderBy: ['meldeschluss', 'teilnahmegebuehr'], + }), + }), + async handler({ + ctx: { account }, + input: { + veranstaltungId, + table: { filter, pagination }, + }, + }) { + const where: Prisma.UnterveranstaltungWhereInput = { + veranstaltungId, + gliederung: { + name: { + contains: filter?.gliederungName, + mode: 'insensitive', + }, + }, + type: filter?.type, + meldeschluss: + filter?.meldeschluss === undefined + ? undefined + : { + gte: dayjs(filter.meldeschluss[0]).startOf('day').toDate(), + lte: dayjs(filter.meldeschluss[1]).endOf('day').toDate(), + }, + } + + // Role-based Filter + if (account.role !== Role.ADMIN) { + const gliederung = await getGliederungRequireAdmin(account.id) + where.gliederungId = gliederung.id + } + + const total = await prisma.unterveranstaltung.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + const veranstaltungen = await prisma.unterveranstaltung.findMany({ - skip, - take, - where: await getWhere(options.input.filter, options.ctx.account), - orderBy: getOrderBy(options.input.orderBy), + take: pageSize, + skip: pageSize * pageIndex, + orderBy: [ + { + veranstaltung: { + name: 'asc', + }, + }, + { + gliederung: { + name: 'asc', + }, + }, + ], + where, select: { id: true, type: true, @@ -73,47 +104,6 @@ export const unterveranstaltungListProcedure = defineProtectedQueryProcedure({ }, }) - return veranstaltungen + return defineQueryResponse({ data: veranstaltungen, total, pagination: { pageIndex, pageSize, pages } }) }, }) - -export const unterveranstaltungCountProcedure = defineProtectedQueryProcedure({ - key: 'count', - roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - const unterveranstaltungenCount = await prisma.unterveranstaltung.count({ - where: await getWhere(options.input.filter, options.ctx.account), - }) - - return unterveranstaltungenCount - }, -}) - -async function getWhere( - filter: Input['filter'], - account: { - id: number - role: Role - } -): Promise { - const where: Prisma.UnterveranstaltungWhereInput = {} - - // Input-Filter - if (filter.gliederungId !== undefined) where.gliederungId = filter.gliederungId - if (filter.veranstaltungId !== undefined) where.veranstaltungId = filter.veranstaltungId - if (filter.gliederungName !== undefined) - where.gliederung = { - name: { - contains: filter.gliederungName, - }, - } - - // Role-based Filter - if (account.role !== Role.ADMIN) { - const gliederung = await getGliederungRequireAdmin(account.id) - where.gliederungId = gliederung.id - } - - return where -} diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts index 7d59e45e..9a933cd9 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts @@ -8,7 +8,7 @@ import { getFileUrl } from '../file/helpers/getFileUrl.js' export const unterveranstaltungPublicGetProcedure = definePublicQueryProcedure({ key: 'publicGet', inputSchema: z.strictObject({ - id: z.number(), + id: z.string().uuid(), }), async handler({ input }) { const unterveranstaltung = await prisma.unterveranstaltung.findUniqueOrThrow({ diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts index 09f0c244..a834c009 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts @@ -7,7 +7,7 @@ import { unterveranstaltungCreateSchema } from './schema/unterveranstaltung.sche import { TRPCError } from '@trpc/server' const unterveranstaltungVerwaltungCreateSchema = unterveranstaltungCreateSchema.extend({ - gliederungId: z.number().int(), + gliederungId: z.string().uuid(), type: z.nativeEnum(UnterveranstaltungType), }) diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts index 09f1b0ad..a51616b2 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts @@ -8,7 +8,7 @@ export const unterveranstaltungVerwaltungGetProcedure = defineProtectedQueryProc key: 'verwaltungGet', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), }), async handler(options) { return await prisma.unterveranstaltung.findUniqueOrThrow({ diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts index 6787b198..2b773ac5 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts @@ -11,7 +11,7 @@ export const unterveranstaltungVerwaltungPatchProcedure = defineProtectedMutateP key: 'verwaltungPatch', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number().int(), + id: z.string().uuid(), data: unterveranstaltungUpdateSchema.partial().optional(), landingSettings: unterveranstaltungLandingSchema.partial().optional(), }), diff --git a/apps/api/src/services/veranstaltung/veranstaltung.list.ts b/apps/api/src/services/veranstaltung/veranstaltung.list.ts new file mode 100644 index 00000000..fc63da28 --- /dev/null +++ b/apps/api/src/services/veranstaltung/veranstaltung.list.ts @@ -0,0 +1,79 @@ +import { Prisma, Role } from '@prisma/client' +import { z } from 'zod' +import prisma from '../../prisma.js' +import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' + +export const veranstaltungSelect: Prisma.VeranstaltungSelect = { + id: true, + name: true, + beginn: true, + ende: true, + ort: { + select: { + name: true, + id: true, + }, + }, + meldebeginn: true, + meldeschluss: true, + maxTeilnehmende: true, + teilnahmegebuehr: true, + hostname: { + select: { + id: true, + hostname: true, + }, + }, +} + +export const unterveranstaltungSelect: Prisma.UnterveranstaltungSelect = { + id: true, + maxTeilnehmende: true, + teilnahmegebuehr: true, + meldebeginn: true, + meldeschluss: true, + gliederungId: true, + _count: { + select: { + Anmeldung: { + where: { + status: { + equals: 'BESTAETIGT', + }, + }, + }, + }, + }, +} + +export const veranstaltungListProcedure = defineProtectedQueryProcedure({ + key: 'list', + roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], + inputSchema: z.void(), + async handler({ ctx }) { + const gliederung = + ctx.account.role === Role.GLIEDERUNG_ADMIN ? await getGliederungRequireAdmin(ctx.accountId) : undefined + + const data = await prisma.veranstaltung.findMany({ + select: { + ...veranstaltungSelect, + unterveranstaltungen: { + where: { + gliederungId: gliederung?.id, + }, + select: unterveranstaltungSelect, + }, + }, + }) + + const mapped = data.map((v) => { + return { + ...v, + anzahlAnmeldungen: v.unterveranstaltungen.reduce((a, b) => a + b._count.Anmeldung, 0), + } + }) + + return mapped + }, +}) diff --git a/apps/api/src/services/veranstaltung/veranstaltung.router.ts b/apps/api/src/services/veranstaltung/veranstaltung.router.ts index 37c9e2b9..616e584a 100644 --- a/apps/api/src/services/veranstaltung/veranstaltung.router.ts +++ b/apps/api/src/services/veranstaltung/veranstaltung.router.ts @@ -1,24 +1,20 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' +import { veranstaltungListProcedure } from './veranstaltung.list.js' +import { veranstaltungTableProcedure } from './veranstaltung.table.js' -import { veranstaltungGliederungListProcedure } from './veranstaltungGliederungList.js' import { veranstaltungPublicGetProcedure } from './veranstaltungPublicGet.js' import { veranstaltungVerwaltungCreateProcedure } from './veranstaltungVerwaltungCreate.js' import { veranstaltungVerwaltungGetProcedure } from './veranstaltungVerwaltungGet.js' -import { - veranstaltungVerwaltungListProcedure, - veranstaltungVerwaltungCountProcedure, -} from './veranstaltungVerwaltungList.js' import { veranstaltungVerwaltungPatchProcedure } from './veranstaltungVerwaltungPatch.js' // Import Routes here - do not delete this line export const veranstaltungRouter = mergeRouters( + veranstaltungListProcedure, + veranstaltungTableProcedure, veranstaltungVerwaltungCreateProcedure, veranstaltungVerwaltungGetProcedure, - veranstaltungVerwaltungListProcedure, - veranstaltungVerwaltungCountProcedure, veranstaltungVerwaltungPatchProcedure, - veranstaltungGliederungListProcedure, veranstaltungPublicGetProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/veranstaltung/veranstaltung.table.ts b/apps/api/src/services/veranstaltung/veranstaltung.table.ts new file mode 100644 index 00000000..7c45dfa5 --- /dev/null +++ b/apps/api/src/services/veranstaltung/veranstaltung.table.ts @@ -0,0 +1,98 @@ +import { dayjs } from '@codeanker/helpers' +import { Prisma, Role } from '@prisma/client' +import { z } from 'zod' +import prisma from '../../prisma.js' +import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' + +export const veranstaltungTableProcedure = defineProtectedQueryProcedure({ + key: 'table', + roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], + inputSchema: defineTableInput({ + filter: { + name: z.string(), + zeitraum: z.tuple([z.date(), z.date()]), + meldeschluss: z.tuple([z.date(), z.date()]), + }, + orderBy: ['name', 'teilnahmegebuehr'], + }), + async handler({ input: { pagination, filter, orderBy } }) { + const where: Prisma.VeranstaltungWhereInput = { + name: { + contains: filter?.name, + mode: 'insensitive', + }, + meldeschluss: filter?.meldeschluss + ? { + gte: dayjs(filter.meldeschluss[0]).startOf('day').toDate(), + lte: dayjs(filter.meldeschluss[1]).endOf('day').toDate(), + } + : undefined, + OR: + filter?.zeitraum === undefined + ? undefined + : [ + { + beginn: { + gte: filter.zeitraum[0], + lte: filter.zeitraum[1], + }, + }, + { + ende: { + gte: filter.zeitraum[0], + lte: filter.zeitraum[1], + }, + }, + ], + } + + const total = await prisma.veranstaltung.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + + const data = await prisma.veranstaltung.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + where, + orderBy, + select: { + id: true, + name: true, + beginn: true, + ende: true, + ort: { + select: { + name: true, + }, + }, + meldebeginn: true, + meldeschluss: true, + maxTeilnehmende: true, + teilnahmegebuehr: true, + unterveranstaltungen: { + select: { + _count: { + select: { + Anmeldung: { + where: { + status: 'BESTAETIGT', + }, + }, + }, + }, + }, + }, + }, + }) + + const mapped = data.map((v) => { + return { + ...v, + unterveranstaltungen: undefined, + anzahlAnmeldungen: v.unterveranstaltungen.reduce((a, b) => a + b._count.Anmeldung, 0), + } + }) + + return defineQueryResponse({ data: mapped, total, pagination: { pageIndex, pageSize, pages } }) + }, +}) diff --git a/apps/api/src/services/veranstaltung/veranstaltungGliederungList.ts b/apps/api/src/services/veranstaltung/veranstaltungGliederungList.ts deleted file mode 100644 index 2969d8cb..00000000 --- a/apps/api/src/services/veranstaltung/veranstaltungGliederungList.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Role } from '@prisma/client' -import z from 'zod' - -import prisma from '../../prisma.js' -import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery } from '../../types/defineQuery.js' -import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' - -export const veranstaltungGliederungListProcedure = defineProtectedQueryProcedure({ - key: 'gliederungList', - roleIds: [Role.GLIEDERUNG_ADMIN], - inputSchema: defineQuery({ - filter: z.strictObject({}), - orderBy: z.array( - z.tuple([z.union([z.literal('id'), z.literal('name')]), z.union([z.literal('asc'), z.literal('desc')])]) - ), - }), - async handler(options) { - const { skip, take } = options.input.pagination - const gliederung = await getGliederungRequireAdmin(options.ctx.accountId) - - const data = await prisma.veranstaltung.findMany({ - skip, - take, - where: { - OR: [ - { - unterveranstaltungen: { - some: { - gliederungId: gliederung.id, - }, - }, - }, - { - meldebeginn: { - lte: new Date(), - }, - meldeschluss: { - gte: new Date(), - }, - }, - ], - }, - select: { - id: true, - name: true, - beginn: true, - ende: true, - ort: { - select: { - name: true, - id: true, - }, - }, - meldebeginn: true, - meldeschluss: true, - maxTeilnehmende: true, - teilnahmegebuehr: true, - unterveranstaltungen: { - where: { - gliederungId: gliederung.id, - }, - select: { - id: true, - _count: { - select: { - Anmeldung: { - where: { - status: { - equals: 'BESTAETIGT', - }, - }, - }, - }, - }, - }, - }, - hostname: { - select: { - id: true, - hostname: true, - }, - }, - }, - }) - - return data - }, -}) diff --git a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungCreate.ts b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungCreate.ts index d1e2149c..6d116ff5 100644 --- a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungCreate.ts +++ b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungCreate.ts @@ -12,7 +12,7 @@ export const veranstaltungVerwaltungCreateProcedure = defineProtectedMutateProce name: z.string(), beginn: z.date(), ende: z.date(), - ortId: z.number().int(), + ortId: z.string().uuid(), meldebeginn: z.date(), meldeschluss: z.date(), maxTeilnehmende: z.number().int(), @@ -22,7 +22,7 @@ export const veranstaltungVerwaltungCreateProcedure = defineProtectedMutateProce teilnahmeBedingungen: z.string().optional(), teilnahmeBedingungenPublic: z.string().optional(), zielgruppe: z.string().optional(), - hostnameId: z.number().int().optional(), + hostnameId: z.string().uuid().optional(), }), }), async handler(options) { diff --git a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts index fa8604c1..3b785252 100644 --- a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts +++ b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts @@ -3,63 +3,35 @@ import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import { unterveranstaltungSelect, veranstaltungSelect } from './veranstaltung.list.js' export const veranstaltungVerwaltungGetProcedure = defineProtectedQueryProcedure({ key: 'verwaltungGet', roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], inputSchema: z.strictObject({ - id: z.number(), + id: z.string().uuid(), }), - async handler(options) { - const veranstaltungWithunterveranstaltungen = await prisma.veranstaltung.findUniqueOrThrow({ + async handler({ input }) { + const v = await prisma.veranstaltung.findUniqueOrThrow({ where: { - id: options.input.id, + id: input.id, }, select: { - id: true, - name: true, - beginn: true, - ende: true, - ort: { - select: { - name: true, - id: true, - }, - }, - meldebeginn: true, - meldeschluss: true, - maxTeilnehmende: true, - teilnahmegebuehr: true, - beschreibung: true, - datenschutz: true, - teilnahmeBedingungen: true, - teilnahmeBedingungenPublic: true, - zielgruppe: true, - hostname: { - select: { - id: true, - hostname: true, - }, - }, + ...veranstaltungSelect, publicReadToken: true, unterveranstaltungen: { select: { - id: true, - _count: { - select: { - Anmeldung: { - where: { - status: { - equals: 'BESTAETIGT', - }, - }, - }, - }, - }, + ...unterveranstaltungSelect, }, }, }, }) - return veranstaltungWithunterveranstaltungen + + const mapped = { + ...v, + anzahlAnmeldungen: v.unterveranstaltungen.reduce((a, b) => a + b._count.Anmeldung, 0), + } + + return mapped }, }) diff --git a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungList.ts b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungList.ts deleted file mode 100644 index 88b4ecc0..00000000 --- a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungList.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Prisma, Role } from '@prisma/client' -import z from 'zod' - -import prisma from '../../prisma.js' -import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - name: z.string().optional(), - }), - orderBy: z.array( - z.tuple([ - z.union([ - z.literal('id'), - z.literal('name'), - z.literal('maxTeilnehmende'), - z.literal('teilnahmegebuehr'), - z.literal('beginn'), - z.literal('meldeschluss'), - ]), - z.union([z.literal('asc'), z.literal('desc')]), - ]) - ), -}) - -type TInput = z.infer - -export const veranstaltungVerwaltungListProcedure = defineProtectedQueryProcedure({ - key: 'verwaltungList', - roleIds: [Role.ADMIN], - inputSchema, - async handler(options) { - const { skip, take } = options.input.pagination - const veranstaltungen = await prisma.veranstaltung.findMany({ - skip, - take, - where: await getWhere(options.input.filter, options.ctx.account), - orderBy: getOrderBy(options.input.orderBy), - select: { - id: true, - name: true, - beginn: true, - ende: true, - ort: { - select: { - name: true, - id: true, - }, - }, - meldebeginn: true, - meldeschluss: true, - maxTeilnehmende: true, - teilnahmegebuehr: true, - unterveranstaltungen: { - select: { - id: true, - maxTeilnehmende: true, - teilnahmegebuehr: true, - meldebeginn: true, - meldeschluss: true, - gliederungId: true, - _count: { - select: { - Anmeldung: { - where: { - status: { - equals: 'BESTAETIGT', - }, - }, - }, - }, - }, - }, - }, - hostname: { - select: { - id: true, - hostname: true, - }, - }, - }, - }) - return veranstaltungen.map((veranstaltung) => { - const count = veranstaltung.unterveranstaltungen.reduce((acc, unterveranstaltung) => { - if (unterveranstaltung._count.Anmeldung) { - acc += unterveranstaltung._count.Anmeldung - } - return acc - }, 0) - return { - ...veranstaltung, - anzahlAnmeldungen: count, - } - }) - }, -}) - -export const veranstaltungVerwaltungCountProcedure = defineProtectedQueryProcedure({ - key: 'verwaltungCount', - roleIds: [Role.ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - return await prisma.veranstaltung.count({ - where: await getWhere(options.input.filter, options.ctx.account), - }) - }, -}) - -// eslint-disable-next-line @typescript-eslint/require-await -async function getWhere( - filter: TInput['filter'], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _account: { - id: number - role: Role - } -): Promise { - const where: Prisma.VeranstaltungWhereInput = {} - - if (filter.name != undefined && filter.name != '') { - where.name = { - contains: filter.name, - } - } - - return where -} diff --git a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungPatch.ts b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungPatch.ts index df14b5b0..e2e8111d 100644 --- a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungPatch.ts +++ b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungPatch.ts @@ -8,12 +8,12 @@ export const veranstaltungVerwaltungPatchProcedure = defineProtectedMutateProced key: 'verwaltungPatch', roleIds: [Role.ADMIN], inputSchema: z.strictObject({ - id: z.number(), + id: z.string().uuid(), data: z.strictObject({ name: z.string().optional(), beginn: z.date().optional(), ende: z.date().optional(), - ortId: z.number().int().optional(), + ortId: z.string().uuid().optional(), meldebeginn: z.date().optional(), meldeschluss: z.date().optional(), maxTeilnehmende: z.number().int().optional(), @@ -23,7 +23,7 @@ export const veranstaltungVerwaltungPatchProcedure = defineProtectedMutateProced teilnahmeBedingungen: z.string().optional(), teilnahmeBedingungenPublic: z.string().optional(), zielgruppe: z.string().optional(), - hostnameId: z.number().int().optional(), + hostnameId: z.string().uuid().optional(), }), }), async handler(options) { diff --git a/apps/api/src/trpc.ts b/apps/api/src/trpc.ts index 6d493551..c196beb5 100644 --- a/apps/api/src/trpc.ts +++ b/apps/api/src/trpc.ts @@ -56,9 +56,9 @@ const logActivityMiddleware = middleware(async (opts) => { if (type !== undefined) { // const rawInput = opts.getRawInput() as { id: number } - const resultData = result.data as { id?: number } + const resultData = result.data as { id?: string } logger.verbose(`Recording activity ${opts.path} of type ${type}`) - const subjectId = type === 'CREATE' ? resultData?.id : 9999 + const subjectId = type === 'CREATE' ? resultData?.id : '9999' if (!subjectId) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'No subjectId found' }) await logActivity({ subjectId, diff --git a/apps/api/src/types/defineCustomFieldValues.ts b/apps/api/src/types/defineCustomFieldValues.ts index f681b768..031e06fb 100644 --- a/apps/api/src/types/defineCustomFieldValues.ts +++ b/apps/api/src/types/defineCustomFieldValues.ts @@ -4,14 +4,14 @@ import { z } from 'zod' export function defineCustomFieldValues() { return z.array( z.strictObject({ - fieldId: z.number().int(), + fieldId: z.string().uuid(), value: z.union([z.string(), z.boolean(), z.undefined(), z.number()]), }) ) } export function customFieldValuesCreateMany( - data: { fieldId: number; value?: string | number | boolean | undefined }[] + data: { fieldId: string; value?: string | number | boolean | undefined }[] ) { const customField: Prisma.CustomFieldValueCreateManyArgs = { data: data.map((field) => { diff --git a/apps/api/src/types/defineTableProcedure.ts b/apps/api/src/types/defineTableProcedure.ts new file mode 100644 index 00000000..604653c6 --- /dev/null +++ b/apps/api/src/types/defineTableProcedure.ts @@ -0,0 +1,142 @@ +import type { Prisma } from '@prisma/client' +import { z } from 'zod' + +export type QueryResponse = { + data: T[] + total: number + pagination: { + page: number + pages: number + hasNextPage: boolean + hasPreviousPage: boolean + } +} + +const zPagination = z + .strictObject({ + pageIndex: z.number().min(0), + pageSize: z.number().min(1).max(50), + }) + .optional() + +export type Pagination = z.infer + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface OrderBy extends Record {} + +/** + * Calculates pagination data based on total items and requested pagination. + * + * @param total The total amount of items available. + * @param pagination The user-requested pagination. + * @returns Pagination options to be used with the database query. + */ +export function calculatePagination(total: number, pagination: Pagination) { + const pageIndex = pagination?.pageIndex ?? 0 + const pageSize = pagination?.pageSize ?? 50 + const pages = Math.ceil(total / pageSize) + + return { pageIndex, pageSize, pages } +} + +/** + * Defines a common tRPC input schema structure for procedures used by the `DataTable` component. + * + * @returns A zod object depicting the target input schema. + */ +export function defineTableInput({ + filter, + orderBy, +}: { + /** + * An object representing all the supported values which can be filtered by. + */ + filter: TFilter + + /** + * A list of attributes which can be ordered by. + */ + orderBy?: TOrderBy +}) { + return z.strictObject({ + pagination: zPagination, + filter: z.strictObject(filter).partial().optional(), + orderBy: + orderBy === undefined + ? z.undefined().optional() + : z + .array( + z.strictObject({ + id: z.enum(orderBy), + desc: z.boolean(), + }) + ) + .optional() + .transform((list, ctx) => { + if (list === undefined) { + return [] + } + + return list.map(({ id, desc }, index) => { + if (id === undefined) { + ctx.addIssue({ + code: 'custom', + path: [...ctx.path, index], + fatal: true, + message: `encountered undefined id for orderBy operation`, + }) + return {} + } + + // the regex makes sure queries like 'anmeldungen__count' are not split into + // 'anmeldungen', '', 'count' + // but + // 'anmeldungen', '_count' + const keys = id.split(/_(?=_)/g) + const direction: Prisma.SortOrder = desc ? 'desc' : 'asc' + + return keys.reduceRight((acc, key) => ({ [key]: acc }), direction as unknown as OrderBy) + }) + }), + }) +} + +export type TableInput = z.infer> + +/** + * Defines a generic query response for consumption with the `DataTable` component. + * + * @returns An object which depicts the result of a table procedure. + */ +export function defineQueryResponse({ + data, + total, + pagination: { pageIndex, pages }, +}: { + data: T[] + total: number + pagination: ReturnType +}) { + return { + data, + total, + pagination: { + page: pageIndex, + pages, + hasNextPage: pageIndex < pages - 1, + hasPreviousPage: pageIndex > 0, + }, + } +} + +export function defineEmptyQueryResponse() { + return defineQueryResponse({ + data: [], + total: 0, + pagination: { + pageIndex: 0, + pages: 0, + pageSize: 0, + }, + }) +} diff --git a/apps/api/src/util/activity.ts b/apps/api/src/util/activity.ts index ebf827cb..1080bad2 100644 --- a/apps/api/src/util/activity.ts +++ b/apps/api/src/util/activity.ts @@ -6,10 +6,10 @@ import prisma from '../prisma.js' interface Opts { type: ActivityType description?: string - causerId?: number + causerId?: string metadata?: unknown subjectType: string - subjectId?: number | string + subjectId?: string } export default async function logActivity(opts: Opts) { @@ -17,7 +17,7 @@ export default async function logActivity(opts: Opts) { await prisma.activity.create({ data: { ...opts, - subjectId: opts.subjectId?.toString(), + subjectId: opts.subjectId, metadata: opts.metadata ?? {}, }, }) diff --git a/apps/api/src/util/getGliederungRequireAdmin.ts b/apps/api/src/util/getGliederungRequireAdmin.ts index 239bfe4f..d81f5586 100644 --- a/apps/api/src/util/getGliederungRequireAdmin.ts +++ b/apps/api/src/util/getGliederungRequireAdmin.ts @@ -8,7 +8,7 @@ import prisma from '../prisma.js' * @returns the gliederung or * @throws exception if the user identified by `accountId` is not an admin */ -export async function getGliederungRequireAdmin(accountId: number) { +export async function getGliederungRequireAdmin(accountId: string) { return prisma.gliederung.findFirstOrThrow({ where: { GliederungToAccount: { diff --git a/apps/api/src/util/mail.ts b/apps/api/src/util/mail.ts index 756e9052..d9565769 100644 --- a/apps/api/src/util/mail.ts +++ b/apps/api/src/util/mail.ts @@ -15,7 +15,7 @@ sgMail.setApiKey(config.mail.sendgridApiKey) type Variables = Record & { name: string - gliederung: string + gliederung?: string veranstaltung: string hostname: string } diff --git a/apps/api/src/util/make-app.ts b/apps/api/src/util/make-app.ts new file mode 100644 index 00000000..446642bc --- /dev/null +++ b/apps/api/src/util/make-app.ts @@ -0,0 +1,12 @@ +import type { HttpBindings } from '@hono/node-server' +import { Hono, type Context } from 'hono' + +type Bindings = HttpBindings & { + /* ... */ +} + +export type AppContext = Context<{ Bindings: Bindings }> + +export function makeApp() { + return new Hono<{ Bindings: Bindings }>() +} diff --git a/apps/api/src/util/zod.ts b/apps/api/src/util/zod.ts index e0258cdc..57a2e920 100644 --- a/apps/api/src/util/zod.ts +++ b/apps/api/src/util/zod.ts @@ -1,4 +1,4 @@ -import type { ZodError, ZodSchema } from 'zod' +import { z, type ZodError, type ZodSchema } from 'zod' type ZodSafeResult = [true, O] | [false, ZodError] @@ -18,3 +18,5 @@ export async function zodSafe(schema: ZodSchema, payload: I): Promise v === 'true') diff --git a/apps/frontend/package.json b/apps/frontend/package.json index a55fbf4f..c2fc50b6 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -11,18 +11,20 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@azure/storage-blob": "^12.17.0", + "@azure/storage-blob": "catalog:", "@codeanker/api": "workspace:*", "@codeanker/cookies": "workspace:*", - "@codeanker/datagrid": "file:../../vendor/codeanker-datagrid-2.7.1-trimmed.tgz", "@codeanker/helpers": "workspace:*", "@codeanker/interfaces": "workspace:*", "@codeanker/validation": "workspace:*", - "@faker-js/faker": "^9.4.0", - "@headlessui/vue": "^1.7.16", - "@heroicons/vue": "^2.0.18", + "@faker-js/faker": "catalog:", + "@headlessui/vue": "catalog:", + "@heroicons/vue": "catalog:", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.16", + "@tanstack/vue-query": "^5.72.0", + "@tanstack/vue-query-devtools": "^5.72.2", + "@tanstack/vue-table": "^8.21.2", "@tiptap/extension-document": "catalog:", "@tiptap/extension-highlight": "catalog:", "@tiptap/extension-link": "catalog:", @@ -36,7 +38,6 @@ "@vueuse/core": "^12.5.0", "@vueuse/router": "^12.3.0", "clsx": "^2.1.1", - "http2-proxy": "^5.0.53", "human-filetypes": "^1.1.3", "intl-tel-input": "^24.4.0", "primevue": "^4.2.5", @@ -55,15 +56,17 @@ "@codeanker/eslint-config": "workspace:*", "@codeanker/typescript-config": "workspace:*", "@types/node": "catalog:", - "@vitejs/plugin-basic-ssl": "^1.2.0", - "@vitejs/plugin-vue": "^5.2.1", + "@vitejs/plugin-basic-ssl": "^2.1.0", + "@vitejs/plugin-vue": "^6.0.2", "autoprefixer": "^10.4.16", "eslint": "catalog:", + "http2-proxy": "^5.0.53", "postcss": "^8.4.31", "sass": "^1.69.5", "tailwindcss": "^3.3.5", "typescript": "catalog:", - "vite": "^6.2.4", + "vite": "^7.2.4", + "vite-plugin-vue-devtools": "^8.0.5", "vue-tsc": "catalog:" } } diff --git a/apps/frontend/src/App.vue b/apps/frontend/src/App.vue index 8c5caebb..b1dd2766 100644 --- a/apps/frontend/src/App.vue +++ b/apps/frontend/src/App.vue @@ -1,6 +1,7 @@ - @@ -11,4 +12,6 @@ const isMobile = useMediaQuery('(max-width: 640px)') rich-colors /> + + diff --git a/apps/frontend/src/assets/fonts.scss b/apps/frontend/src/assets/fonts.scss index a68a6187..b618fdff 100644 --- a/apps/frontend/src/assets/fonts.scss +++ b/apps/frontend/src/assets/fonts.scss @@ -10,3 +10,9 @@ font-display: block; src: url(./fonts/InterVariable-Italic.woff2) format('woff2'); } +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-display: block; + src: url('./fonts/Inconsolata-VariableFont_wdth\,wght.ttf') format('ttf'); +} diff --git a/apps/frontend/src/assets/fonts/Inconsolata-VariableFont_wdth,wght.ttf b/apps/frontend/src/assets/fonts/Inconsolata-VariableFont_wdth,wght.ttf new file mode 100644 index 00000000..2739432d Binary files /dev/null and b/apps/frontend/src/assets/fonts/Inconsolata-VariableFont_wdth,wght.ttf differ diff --git a/apps/frontend/src/assets/illustration/undraw_empty_4zx0.svg b/apps/frontend/src/assets/illustration/undraw_empty_4zx0.svg new file mode 100644 index 00000000..2a4d2712 --- /dev/null +++ b/apps/frontend/src/assets/illustration/undraw_empty_4zx0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/components/AnmeldungStatusSelect.vue b/apps/frontend/src/components/AnmeldungStatusSelect.vue index f3163b4e..0d7b8222 100644 --- a/apps/frontend/src/components/AnmeldungStatusSelect.vue +++ b/apps/frontend/src/components/AnmeldungStatusSelect.vue @@ -12,7 +12,7 @@ import { type AnmeldungStatus, AnmeldungStatusMapping, getEnumOptions } from '@c const props = withDefaults( defineProps<{ status: AnmeldungStatus - id: number + id: string meldeschluss: Date }>(), {} diff --git a/apps/frontend/src/components/BasicInputs/BasicSelect.vue b/apps/frontend/src/components/BasicInputs/BasicSelect.vue index 30e38424..ad72eddb 100644 --- a/apps/frontend/src/components/BasicInputs/BasicSelect.vue +++ b/apps/frontend/src/components/BasicInputs/BasicSelect.vue @@ -61,7 +61,7 @@ const { model, errorMessage } = useValidationModel(props, emit) leave-to-class="opacity-0" > () const deleteModal = useTemplateRef('deleteModal') diff --git a/apps/frontend/src/components/CustomFields/CustomFieldsFormUser.vue b/apps/frontend/src/components/CustomFields/CustomFieldsFormUser.vue index f863d3c6..b0e802a0 100644 --- a/apps/frontend/src/components/CustomFields/CustomFieldsFormUser.vue +++ b/apps/frontend/src/components/CustomFields/CustomFieldsFormUser.vue @@ -10,13 +10,13 @@ import Button from '@/components/UIComponents/Button.vue' import type { RouterOutput } from '@codeanker/api' import { ValidateForm } from '@codeanker/validation' -type Field = Awaited[number] +type Field = RouterOutput['customFields']['list'][number] type Value = Awaited[number]['customFieldValues'][number] const props = defineProps<{ customFields: Array customFieldValues: Array - entryId: number + entryId: string }>() const emit = defineEmits<{ @@ -40,10 +40,10 @@ const { execute: submit, isLoading: isLoading } = useAsyncState(async () => { isLoading.value = true await apiClient.customFields.valuesUpdate.mutate({ data: Object.entries(bindings.value).map(([id, value]) => ({ - id: parseInt(id) as number, + id: id, value: String(value), })), - anmeldungId: props.entryId as number, + anmeldungId: props.entryId, }) emit('update:success') } diff --git a/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue b/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue index 674aa71e..f9bcf945 100644 --- a/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue +++ b/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue @@ -1,39 +1,143 @@ diff --git a/apps/frontend/src/components/DataGrid/DataGridHeader.vue b/apps/frontend/src/components/DataGrid/DataGridHeader.vue deleted file mode 100644 index a31f0429..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridHeader.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/apps/frontend/src/components/DataGrid/DataGridHeaderCell.vue b/apps/frontend/src/components/DataGrid/DataGridHeaderCell.vue deleted file mode 100644 index 7e4a1d2e..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridHeaderCell.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - diff --git a/apps/frontend/src/components/DataGrid/DataGridRow.vue b/apps/frontend/src/components/DataGrid/DataGridRow.vue deleted file mode 100644 index 4bb4630c..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridRow.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/apps/frontend/src/components/DataGrid/DataGridRowCell.vue b/apps/frontend/src/components/DataGrid/DataGridRowCell.vue deleted file mode 100644 index 370fb3da..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridRowCell.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/apps/frontend/src/components/DataGrid/DataGridRowPending.vue b/apps/frontend/src/components/DataGrid/DataGridRowPending.vue deleted file mode 100644 index bed9207a..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridRowPending.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/apps/frontend/src/components/DataGrid/DataGridRowSubheader.vue b/apps/frontend/src/components/DataGrid/DataGridRowSubheader.vue deleted file mode 100644 index 3b1ae72f..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridRowSubheader.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/DataGrid/DataGridVirtualList.vue b/apps/frontend/src/components/DataGrid/DataGridVirtualList.vue deleted file mode 100644 index 7d48d8dd..00000000 --- a/apps/frontend/src/components/DataGrid/DataGridVirtualList.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - diff --git a/apps/frontend/src/components/FilesListAndUpload.vue b/apps/frontend/src/components/FilesListAndUpload.vue index 476d33fe..612cea83 100644 --- a/apps/frontend/src/components/FilesListAndUpload.vue +++ b/apps/frontend/src/components/FilesListAndUpload.vue @@ -17,25 +17,25 @@ type Document = { name: string fileId: string added: boolean - id?: number + id?: string mimetype: string | null createdAt?: Date } type UpdateDocument = { name: string - id: number + id: string } type EntityType = 'unterveranstaltung' const props = defineProps<{ - entityId: number + entityId: string entityType: EntityType }>() const documents = ref([]) -const deletedDocumentIds = ref([]) +const deletedDocumentIds = ref([]) const fileInput = ref(null) const showNotification = ref(false) diff --git a/apps/frontend/src/components/GenericDataGrid.vue b/apps/frontend/src/components/GenericDataGrid.vue deleted file mode 100644 index 11512864..00000000 --- a/apps/frontend/src/components/GenericDataGrid.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - diff --git a/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue b/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue index 5fdfeb8b..b35dfa6e 100644 --- a/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue +++ b/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue @@ -26,11 +26,11 @@ const { logoSmall } = useAssets() const veranstaltungId = computed(() => { if (route.params.veranstaltungId !== undefined && typeof route.params.veranstaltungId === 'string') { - return parseInt(route.params.veranstaltungId) + return route.params.veranstaltungId } const letzteVeranstaltung = localStorage.getItem('letzteVeranstaltung') if (letzteVeranstaltung !== null) { - return parseInt(letzteVeranstaltung) + return letzteVeranstaltung } return undefined }) diff --git a/apps/frontend/src/components/LayoutComponents/Sidebar/SidebarVeranstaltungSwitcher.vue b/apps/frontend/src/components/LayoutComponents/Sidebar/SidebarVeranstaltungSwitcher.vue deleted file mode 100644 index a9918616..00000000 --- a/apps/frontend/src/components/LayoutComponents/Sidebar/SidebarVeranstaltungSwitcher.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - diff --git a/apps/frontend/src/components/LoadingBar.vue b/apps/frontend/src/components/LoadingBar.vue new file mode 100644 index 00000000..32ffdddd --- /dev/null +++ b/apps/frontend/src/components/LoadingBar.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/frontend/src/components/Modal/IscRedirectModal.vue b/apps/frontend/src/components/Modal/IscRedirectModal.vue deleted file mode 100644 index 3ba1b4cf..00000000 --- a/apps/frontend/src/components/Modal/IscRedirectModal.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/apps/frontend/src/components/Modal/RegisterModal.vue b/apps/frontend/src/components/Modal/RegisterModal.vue deleted file mode 100644 index cc5eeb44..00000000 --- a/apps/frontend/src/components/Modal/RegisterModal.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - diff --git a/apps/frontend/src/components/Table/DataTable.vue b/apps/frontend/src/components/Table/DataTable.vue new file mode 100644 index 00000000..b9cbd283 --- /dev/null +++ b/apps/frontend/src/components/Table/DataTable.vue @@ -0,0 +1,378 @@ + + + diff --git a/apps/frontend/src/components/Table/Filter.vue b/apps/frontend/src/components/Table/Filter.vue new file mode 100644 index 00000000..be7ff0f5 --- /dev/null +++ b/apps/frontend/src/components/Table/Filter.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/frontend/src/components/Table/initialData.ts b/apps/frontend/src/components/Table/initialData.ts new file mode 100644 index 00000000..40c7772c --- /dev/null +++ b/apps/frontend/src/components/Table/initialData.ts @@ -0,0 +1,18 @@ +import type { QueryResponse } from '@codeanker/api' + +const initialData = { + data: [], + total: 0, + pagination: { + page: 0, + pages: 0, + hasNextPage: false, + hasPreviousPage: false, + }, +} + +export default initialData + +export function defineInitialData(): QueryResponse { + return initialData +} diff --git a/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue b/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue index 1b02bbdb..05c402c4 100644 --- a/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue +++ b/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue @@ -6,10 +6,11 @@ import Modal from './Modal.vue' import Button from './Button.vue' import { apiClient } from '@/api' import { DocumentDuplicateIcon, RocketLaunchIcon } from '@heroicons/vue/24/outline' +import { useQueryClient } from '@tanstack/vue-query' const props = defineProps<{ veranstaltung: string - unterveranstaltungId: number + unterveranstaltungId: string url: string }>() @@ -42,6 +43,8 @@ const comment = ref('') const token = ref('') const locked = ref(true) +const queryClient = useQueryClient() + async function create() { if (state.value !== 'idle') { return @@ -54,6 +57,8 @@ async function create() { }) state.value = 'success' + + queryClient.invalidateQueries({ queryKey: ['anmeldeLink'] }) emit('success') } catch (e: unknown) { state.value = 'error' @@ -84,8 +89,6 @@ defineExpose({ open, close }) diff --git a/apps/frontend/src/components/UIComponents/VeranstaltungCard.vue b/apps/frontend/src/components/UIComponents/VeranstaltungCard.vue index 2faaa60f..5ceb4893 100644 --- a/apps/frontend/src/components/UIComponents/VeranstaltungCard.vue +++ b/apps/frontend/src/components/UIComponents/VeranstaltungCard.vue @@ -9,23 +9,18 @@ import Button from './Button.vue' import type { RouterOutput } from '@codeanker/api' import { formatDateWith } from '@codeanker/helpers' -const { veranstaltung } = defineProps() - -type Veranstaltung = Awaited< - RouterOutput['veranstaltung']['verwaltungList'] | RouterOutput['veranstaltung']['gliederungList'] ->[number] +type Veranstaltung = RouterOutput['veranstaltung']['list'][number] interface Props { veranstaltung: Veranstaltung hasUnterveranstaltungen?: number } -const totalAnmeldungen = computed(() => - veranstaltung.unterveranstaltungen.map((u) => u._count.Anmeldung).reduce((a, b) => a + b, 0) -) +const { veranstaltung } = defineProps() + const keyInfoDateFormat = 'dddd, DD. MMMM YYYY' -const percent = computed(() => (totalAnmeldungen.value / veranstaltung.maxTeilnehmende) * 100) +const percent = computed(() => (veranstaltung.anzahlAnmeldungen / veranstaltung.maxTeilnehmende) * 100)