diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..b83508c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,23 @@ +name: Fehlerbericht +description: | + Nutze dieses Formular, um einen Fehler zu melden. + Du kannst es auch benutzen, wenn die ein Verhalten komisch oder ungewollt vorkommt. +type: Bug +title: "[Bug] {kurzbeschreibung}" +labels: + - "state:open" +body: + - type: input + id: kurzbeschreibung + attributes: + label: Kurzbeschreibung + description: | + Eine kurze Beschreibung des Fehlers. + Halte es so kurz wie möglich. + - type: textarea + id: beschreibung + attributes: + label: Beschreibung + description: | + Eine detaillierte Beschreibung des Fehlers. + Erkläre, was du gemacht hast, was du erwartet hast und was tatsächlich passiert ist. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 234ece0e..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Bug report -about: Melde einen Fehler und helfe uns diesen zu beheben -title: '' -labels: bug -assignees: '' ---- - -\*\*Beschreibung des Fehlers -Klare und prägnante Beschreibung des Fehlers. - -\*\*Reproduzieren -Schritte, um das Verhalten zu reproduzieren: - -1. Gehen Sie zu '...'. -2. Klicken Sie auf '....'. -3. Scrollen Sie nach unten zu '....'. -4. Siehe Fehler - -\*\*Erwartetes Verhalten -Eine klare und prägnante Beschreibung dessen, was Sie erwartet haben. - -\*\*Screenshots -Fügen Sie Screenshots bei, um das Problem zu veranschaulichen. - -**Desktop (bitte füllen Sie die folgenden Informationen aus):**. - -- Betriebssystem: [z.B. iOS]. -- Browser [z.B. Chrome, Safari]. -- Version [z.B. 22] - -**Smartphone (bitte füllen Sie die folgenden Felder aus):**. - -- Gerät: [z.B. iPhone6]. -- Betriebssystem: [z.B. iOS8.1]. -- Browser: [z.B. Standardbrowser, Safari] -- Version [z.B. 22] - -\*\*Zusätzlicher Kontext -Fügen Sie hier weitere Informationen zum Problem hinzu. diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 00000000..c78dbebc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,24 @@ +name: Feature Request +description: | + Nutze dieses Formular, um eine neue Funktion zu beantragen. + Du kannst es auch benutzen, wenn du eine Idee hast, + wie wir das System verbessern können. +type: Feature +title: "[Feature] {kurzbeschreibung}" +labels: + - "state:open" +body: + - type: input + id: kurzbeschreibung + attributes: + label: Kurzbeschreibung + description: | + Eine kurze Beschreibung der Funktion. + Halte es so kurz wie möglich. + - type: textarea + id: beschreibung + attributes: + label: Beschreibung + description: | + Eine detaillierte Beschreibung der Funktion. + Erkläre, was du dir wünschst und warum es wichtig ist. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 14084708..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: feature-request -assignees: '' ---- - -**Bezieht sich Ihre Funktionsanfrage auf ein Problem? Bitte beschreiben Sie es.** -Eine klare und präzise Beschreibung des Problems. Beispiel. Ich bin immer frustriert, wenn [...] - -**Beschreiben Sie die Lösung, die Sie sich wünschen** -Eine klare und prägnante Beschreibung dessen, was Sie wollen. - -**Beschreiben Sie die Alternativen, die Sie in Betracht gezogen haben** -Eine klare und prägnante Beschreibung aller alternativen Lösungen oder Funktionen, die Sie in Betracht gezogen haben. - -**Zusätzlicher Kontext** -Fügen Sie hier weitere Informationen oder Screenshots zu Ihrer Anfrage hinzu. diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..d6bdf782 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Frontend", + "request": "launch", + "type": "chrome", + "url": "https://localhost:8080", + "webRoot": "${workspaceFolder}/apps/frontend/src", + "pathMapping": { + "/@fs/workspaces/brahmsee.digital": "${workspaceFolder}" + }, + "runtimeArgs": [ + "--ignore-certificate-errors", + "--auto-open-devtools-for-tabs" + ] + }, + { + "type": "node", + "request": "attach", + "name": "Attach to API", + "restart": true, + "address": "localhost", + "port": 9229, + "localRoot": "${workspaceFolder}/apps/api/src", + }, + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 68d940fb..7839222e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,7 @@ "problemMatcher": [], "presentation": { "reveal": "always", - "panel": "new", + "panel": "shared", "group": "start", "close": false }, @@ -25,7 +25,7 @@ "problemMatcher": [], "presentation": { "reveal": "always", - "panel": "new", + "panel": "shared", "group": "start", "close": false }, @@ -42,7 +42,7 @@ "problemMatcher": [], "presentation": { "reveal": "silent", - "panel": "new", + "panel": "shared", "group": "start", "close": false }, @@ -59,7 +59,7 @@ "problemMatcher": [], "presentation": { "reveal": "always", - "panel": "new", + "panel": "shared", "group": "start", "close": false }, @@ -76,7 +76,7 @@ "problemMatcher": [], "presentation": { "reveal": "always", - "panel": "new", + "panel": "shared", "group": "start", "close": false }, @@ -119,7 +119,7 @@ "presentation": { "reveal": "silent", "revealProblems": "onProblem", - "panel": "new", + "panel": "shared", "close": true } }, @@ -134,7 +134,7 @@ "presentation": { "reveal": "always", "revealProblems": "onProblem", - "panel": "new", + "panel": "shared", "close": false } }, @@ -151,7 +151,7 @@ }, "presentation": { "reveal": "always", - "panel": "new", + "panel": "shared", "group": "start", "close": false } @@ -169,7 +169,7 @@ }, "presentation": { "reveal": "always", - "panel": "new", + "panel": "shared", "group": "start", "close": false } @@ -187,7 +187,7 @@ "presentation": { "reveal": "always", "revealProblems": "onProblem", - "panel": "new", + "panel": "shared", "close": true } }, @@ -205,7 +205,7 @@ "presentation": { "reveal": "always", "revealProblems": "onProblem", - "panel": "new", + "panel": "shared", "close": true } } diff --git a/README.md b/README.md index 31ab564c..84a13b14 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Am besten wird das Projekt in einem Devcontainer gestartet. Dazu wird nur Docker Über den Eintrag "Clone Repository in Container Volume" kann das Projekt heruntergeladen und geöffnet werden. Das Projekt wird dann automatisch eingerichtet und kann wenn das Abgeschlossen ist auch gestartet werden. - ## Starten Find all Tasks in CMD/CTRL-SHIFT-P -> `Tasks: Run Tasks` diff --git a/apps/api/config/custom-environment-variables.json b/apps/api/config/custom-environment-variables.json index eab89594..bee9cdf3 100644 --- a/apps/api/config/custom-environment-variables.json +++ b/apps/api/config/custom-environment-variables.json @@ -43,5 +43,9 @@ "imprint": "PUBLIC_LEGAL_IMPRINT", "privacy": "PUBLIC_LEGAL_PRIVACY" } + }, + + "export": { + "sheetPassword": "EXPORT_SHEET_PASSWORD" } } diff --git a/apps/api/config/default.json b/apps/api/config/default.json index 3e516148..44b94cf4 100644 --- a/apps/api/config/default.json +++ b/apps/api/config/default.json @@ -51,5 +51,9 @@ "imprint": "https://localhost:8080", "privacy": "https://localhost:8080" } + }, + + "export": { + "sheetPassword": "brahmsee.digital" } } diff --git a/apps/api/package.json b/apps/api/package.json index 1ec487d3..b78471ca 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "npx prisma migrate deploy && tsx src/server.ts", - "dev": "tsx watch --clear-screen=false --env-file .env src/server.ts", + "dev": "tsx watch --inspect --clear-screen=false --env-file .env src/server.ts", "createAccount": "tsx src/scripts/createAccount.ts", "initMeilisearch": "tsx src/scripts/initMeilisearch.ts", "cli": "tsx src/cli/index.ts", @@ -40,6 +40,7 @@ "@prisma/extension-accelerate": "^1.1.0", "@sendgrid/mail": "^8.1.0", "@trpc/server": "catalog:", + "archiver": "^7.0.1", "axios": "^1.7.7", "config": "^3.3.9", "dayjs": "^1.11.10", @@ -48,15 +49,14 @@ "grant": "^5.4.22", "handlebars": "^4.7.8", "jsonwebtoken": "^9.0.2", - "koa": "^2.14.2", + "koa": "^2.16.0", "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", - "lodash-es": "^4.17.21", "meilisearch": "^0.37.0", - "mime-types": "^2.1.35", + "mime": "^4.0.6", "mjml": "^4.15.3", "prom-client": "^15.0.0", "superjson": "catalog:", @@ -69,9 +69,10 @@ "@codeanker/eslint-config": "workspace:*", "@codeanker/typescript-config": "workspace:*", "@inquirer/prompts": "^7.1.0", + "@types/archiver": "^6.0.3", "@types/config": "^3.3.3", "@types/http-status-codes": "^1.2.0", - "@types/jsonwebtoken": "^9.0.5", + "@types/jsonwebtoken": "^9.0.8", "@types/koa": "^2.14.0", "@types/koa-bodyparser": "^4.3.12", "@types/koa-router": "^7.4.8", diff --git a/apps/api/prisma/migrations/20250322120939_gliederung_unterveranstaltung_unique/migration.sql b/apps/api/prisma/migrations/20250322120939_gliederung_unterveranstaltung_unique/migration.sql new file mode 100644 index 00000000..1bbc2485 --- /dev/null +++ b/apps/api/prisma/migrations/20250322120939_gliederung_unterveranstaltung_unique/migration.sql @@ -0,0 +1,8 @@ +/* + 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/20250323104704_drop_unique_index/migration.sql b/apps/api/prisma/migrations/20250323104704_drop_unique_index/migration.sql new file mode 100644 index 00000000..0a5abab2 --- /dev/null +++ b/apps/api/prisma/migrations/20250323104704_drop_unique_index/migration.sql @@ -0,0 +1,2 @@ +-- 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 new file mode 100644 index 00000000..a7b96501 --- /dev/null +++ b/apps/api/prisma/migrations/20250412132545_anmeldelink/migration.sql @@ -0,0 +1,45 @@ +/* + 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 new file mode 100644 index 00000000..e569de04 --- /dev/null +++ b/apps/api/prisma/migrations/20250412153210_anmeldelink_usage/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "AnmeldungLink" ADD COLUMN "usedAt" TIMESTAMP(3); diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml index 648c57fd..fbffa92c 100644 --- a/apps/api/prisma/migrations/migration_lock.toml +++ b/apps/api/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) +# It should be added in your version-control system (i.e. Git) provider = "postgresql" \ No newline at end of file diff --git a/apps/api/prisma/schema/Account.prisma b/apps/api/prisma/schema/Account.prisma index f407ce8c..cdc9fdb6 100644 --- a/apps/api/prisma/schema/Account.prisma +++ b/apps/api/prisma/schema/Account.prisma @@ -25,4 +25,5 @@ model Account { passwordResetToken String? @unique activities Activity[] Anmeldung Anmeldung[] + AnmeldungLink AnmeldungLink[] } diff --git a/apps/api/prisma/schema/Anmeldung.prisma b/apps/api/prisma/schema/Anmeldung.prisma index 8cf54640..8ab2ce16 100644 --- a/apps/api/prisma/schema/Anmeldung.prisma +++ b/apps/api/prisma/schema/Anmeldung.prisma @@ -26,4 +26,23 @@ model Anmeldung { mahlzeiten Mahlzeit[] uebernachtungsTage DateTime[] @db.Date customFieldValues CustomFieldValue[] + + link AnmeldungLink? +} + +model AnmeldungLink { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + usedAt DateTime? + comment String? + accessToken String @db.Uuid + + unterveranstaltungId Int + unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) + + createdById Int + createdBy Account @relation(fields: [createdById], references: [id]) + + anmeldungId Int? @unique + anmeldung Anmeldung? @relation(fields: [anmeldungId], references: [id]) } diff --git a/apps/api/prisma/schema/Unterveranstaltung.prisma b/apps/api/prisma/schema/Unterveranstaltung.prisma index 63c4b192..64ec01d4 100644 --- a/apps/api/prisma/schema/Unterveranstaltung.prisma +++ b/apps/api/prisma/schema/Unterveranstaltung.prisma @@ -25,6 +25,7 @@ model Unterveranstaltung { landingSettings UnterveranstaltungLandingSettings? @relation(fields: [landingSettingsId], references: [unterveranstaltungId], onDelete: Cascade) landingSettingsId Int? @unique + AnmeldungLink AnmeldungLink[] } model UnterveranstaltungDocument { diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index f2db9a09..e60c24c1 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -1,6 +1,7 @@ import path from 'node:path' import { fileURLToPath } from 'url' +import type { StringValue } from '@codeanker/authentication' import { FileProvider } from '@prisma/client' import config from 'config' import { z } from 'zod' @@ -11,6 +12,15 @@ const __dirname = path.dirname(__filename) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const baseConfig = config.util.loadFileConfigs(path.join(__dirname, '..', 'config')) +const zMsUnit = z + .string() + .regex( + /^\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({ clientUrl: z.string(), @@ -25,7 +35,7 @@ export const configSchema = z.strictObject({ authentication: z.strictObject({ secret: z.string(), - expiresIn: z.string(), + expiresIn: zMsUnit, dlrg: z.strictObject({ client_id: z.string(), }), @@ -67,6 +77,10 @@ export const configSchema = z.strictObject({ privacy: z.string().url(), }), }), + + export: z.strictObject({ + sheetPassword: z.string().optional(), + }), }) export default configSchema.parse(baseConfig) diff --git a/apps/api/src/routes/exports/archives/photos.ts b/apps/api/src/routes/exports/archives/photos.ts new file mode 100644 index 00000000..be34566c --- /dev/null +++ b/apps/api/src/routes/exports/archives/photos.ts @@ -0,0 +1,103 @@ +import archiver from 'archiver' +import type { Context } from 'koa' +import mime from 'mime' +import { randomUUID } from 'node:crypto' +import prisma from '../../../prisma.js' +import { openFileStream } from '../../../services/file/helpers/getFileUrl.js' +import { sheetAuthorize } from '../sheets/sheets.schema.js' + +export async function veranstaltungPhotoArchive(ctx: Context) { + const authorization = await sheetAuthorize(ctx) + if (!authorization) { + return + } + + const { query, gliederung } = authorization + + const anmeldungen = await prisma.anmeldung.findMany({ + where: { + OR: [ + { + unterveranstaltungId: query.unterveranstaltungId, + }, + { + unterveranstaltung: { + veranstaltungId: query.veranstaltungId, + }, + }, + ], + unterveranstaltung: { + gliederungId: gliederung?.id, + }, + status: 'BESTAETIGT', + person: { + photoId: { + not: null, + }, + }, + }, + select: { + id: true, + unterveranstaltung: { + select: { + veranstaltung: { + select: { + name: true, + }, + }, + gliederung: { + select: { + name: true, + }, + }, + }, + }, + person: { + select: { + firstname: true, + lastname: true, + photo: true, + }, + }, + }, + }) + + const zip = archiver('zip') + + zip.on('warning', function (err) { + if (err.code === 'ENOENT') { + console.warn(err) + } else { + throw err + } + }) + + // 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="photos-${randomUUID()}.zip"`) + zip.pipe(ctx.res) + + for (const { person, unterveranstaltung } of anmeldungen) { + if (!person.photo) { + return + } + + const stream = await openFileStream(person.photo) + + const directory = `Fotos Teilnehmende ${unterveranstaltung.veranstaltung.name}/${unterveranstaltung.gliederung.name}` + const name = `${person.firstname} ${person.lastname}.${mime.getExtension(person.photo.mimetype ?? 'text/plain')}` + zip.append(stream, { + name: `${directory}/${name}`, + date: person.photo.createdAt, + }) + } + + await zip.finalize() + + ctx.res.end() +} diff --git a/apps/api/src/routes/exports/helpers/getSecurityWorksheet.ts b/apps/api/src/routes/exports/helpers/getSecurityWorksheet.ts index 415ade1c..b3d563d0 100644 --- a/apps/api/src/routes/exports/helpers/getSecurityWorksheet.ts +++ b/apps/api/src/routes/exports/helpers/getSecurityWorksheet.ts @@ -1,5 +1,6 @@ import dayjs from 'dayjs' import XLSX from '@e965/xlsx' +import config from '../../../config.js' interface exportAccount { person: { @@ -21,6 +22,7 @@ export function getSecurityWorksheet(account: exportAccount, countDataEntries: n ]) securityWorksheet['!protect'] = { + password: config.export.sheetPassword, selectLockedCells: true, selectUnlockedCells: true, formatCells: true, diff --git a/apps/api/src/routes/exports/sheets/teilnehmendenliste.ts b/apps/api/src/routes/exports/sheets/teilnehmendenliste.ts index 933efb05..cc57e593 100644 --- a/apps/api/src/routes/exports/sheets/teilnehmendenliste.ts +++ b/apps/api/src/routes/exports/sheets/teilnehmendenliste.ts @@ -1,6 +1,6 @@ +import XLSX from '@e965/xlsx' import dayjs from 'dayjs' import type { Context } from 'koa' -import XLSX from '@e965/xlsx' import { AnmeldungStatusMapping, GenderMapping } from '../../../client.js' import prisma from '../../../prisma.js' import { getSecurityWorksheet } from '../helpers/getSecurityWorksheet.js' @@ -41,13 +41,8 @@ export async function veranstaltungTeilnehmendenliste(ctx: Context) { birthday: true, email: true, telefon: true, + photoId: true, essgewohnheit: true, - gliederung: { - select: { - id: true, - name: true, - }, - }, address: { select: { zip: true, @@ -69,8 +64,16 @@ export async function veranstaltungTeilnehmendenliste(ctx: Context) { status: true, unterveranstaltung: { select: { + beschreibung: true, + type: true, + gliederung: { + select: { + name: true, + }, + }, veranstaltung: { select: { + name: true, meldeschluss: true, beginn: true, }, @@ -103,26 +106,35 @@ export async function veranstaltungTeilnehmendenliste(ctx: Context) { }) .reduce((acc, cur) => ({ ...acc, ...cur }), {}) return { - ['id']: anmeldung.id, - ['Status']: AnmeldungStatusMapping[anmeldung.status].human, - ['Gender']: anmeldung.person.gender ? GenderMapping[anmeldung.person.gender].human : '', - ['Vorname']: anmeldung.person.firstname, - ['Nachname']: anmeldung.person.lastname, - ['Geburtstag']: anmeldung.person.birthday, - ['Alter zu Beginn']: dayjs(anmeldung.unterveranstaltung.veranstaltung.beginn).diff( + '#': anmeldung.id, + + Veranstaltung: anmeldung.unterveranstaltung.veranstaltung.name, + Ausschreibung: + anmeldung.unterveranstaltung.beschreibung?.substring(0, 30) || anmeldung.unterveranstaltung.gliederung.name, + 'Art der Ausschreibung': anmeldung.unterveranstaltung.type, + + Status: AnmeldungStatusMapping[anmeldung.status].human, + Anmeldedatum: anmeldung.createdAt, + Foto: anmeldung.person.photoId ? 'Ja' : 'Nein', + + Geschlecht: anmeldung.person.gender ? GenderMapping[anmeldung.person.gender].human : '', + Vorname: anmeldung.person.firstname, + Nachname: anmeldung.person.lastname, + Geburtstag: anmeldung.person.birthday, + 'Alter zu Beginn': dayjs(anmeldung.unterveranstaltung.veranstaltung.beginn).diff( anmeldung.person.birthday, 'years' ), - ['Gliederung']: anmeldung.person.gliederung?.name, - ['Email']: anmeldung.person.email, - ['Telefon']: anmeldung.person.telefon, - ['Essgewohnheit']: anmeldung.person.essgewohnheit, - ['Anmeldedatum']: anmeldung.createdAt, + Email: anmeldung.person.email, + Telefon: anmeldung.person.telefon, + Essgewohnheit: anmeldung.person.essgewohnheit, + + PLZ: anmeldung.person.address?.zip ?? '', + Stadt: anmeldung.person.address?.city ?? '', + Straße: anmeldung.person.address?.street ?? '', + ...customFields, - ['PLZ']: anmeldung.person.address?.zip, - ['Stadt']: anmeldung.person.address?.city, - ['Straße']: anmeldung.person.address?.street, ...anmeldung.person.notfallkontakte .map((kontakt, index) => ({ [`NF ${index + 1} Vorname`]: kontakt.firstname, diff --git a/apps/api/src/routes/files/downloadFileLocal.ts b/apps/api/src/routes/files/downloadFileLocal.ts index 73139213..0d517e22 100644 --- a/apps/api/src/routes/files/downloadFileLocal.ts +++ b/apps/api/src/routes/files/downloadFileLocal.ts @@ -1,11 +1,10 @@ import * as fs from 'fs' -import * as path from 'path' import type { Middleware } from 'koa' -import mime from 'mime-types' +import mime from 'mime' -import config from '../../config.js' 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) { @@ -14,6 +13,7 @@ export const downloadFileLocal: Middleware = async function (ctx, next) { const file = await prisma.file.findFirst({ where: { id: fileId, + provider: 'LOCAL', }, }) if (file === null) { @@ -26,10 +26,8 @@ export const downloadFileLocal: Middleware = async function (ctx, next) { return } - const uploadDir = path.join(process.cwd(), config.fileProviders.LOCAL.path) const mimetype = file.mimetype ?? 'application/octet-stream' - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const filename = file.filename ?? `${file.id}.${mime.extension(mimetype)}` + 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) diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts index 75f3b380..8b5edf42 100644 --- a/apps/api/src/routes/index.ts +++ b/apps/api/src/routes/index.ts @@ -1,6 +1,7 @@ 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' @@ -13,6 +14,7 @@ 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) diff --git a/apps/api/src/services/account/account.router.ts b/apps/api/src/services/account/account.router.ts index 55be6a9a..8b97b234 100644 --- a/apps/api/src/services/account/account.router.ts +++ b/apps/api/src/services/account/account.router.ts @@ -7,6 +7,7 @@ import { accountEmailConfirmProcedure } from './accountEmailConfirm.js' import { accountEmailConfirmRequestProcedure } from './accountEmailConfirmRequest.js' import { accountGliederungAdminCreateProcedure } from './accountGliederungAdminCreate.js' import { accountPasswordResetProcedure } from './accountPasswordReset.js' +import { accountTeilnehmerCreateProcedure } from './accountTeilnehmerCreate.js' import { accountVerwaltungCreateProcedure } from './accountVerwaltungCreate.js' import { accountVerwaltungGetProcedure } from './accountVerwaltungGet.js' import { accountVerwaltungCountProcedure, accountVerwaltungListProcedure } from './accountVerwaltungList.js' @@ -15,17 +16,18 @@ import { accountVerwaltungRemoveProcedure } from './accountVerwaltungRemove.js' // Import Routes here - do not delete this line export const accountRouter = mergeRouters( - accountActivateProcedure.router, - accountChangePasswordProcedure.router, - accountGliederungAdminCreateProcedure.router, - accountVerwaltungCreateProcedure.router, - accountVerwaltungGetProcedure.router, - accountVerwaltungListProcedure.router, - accountVerwaltungCountProcedure.router, - accountVerwaltungPatchProcedure.router, - accountEmailConfirmRequestProcedure.router, - accountEmailConfirmProcedure.router, - accountPasswordResetProcedure.router, - accountVerwaltungRemoveProcedure.router + accountActivateProcedure, + accountChangePasswordProcedure, + accountGliederungAdminCreateProcedure, + accountVerwaltungCreateProcedure, + accountVerwaltungGetProcedure, + accountVerwaltungListProcedure, + accountVerwaltungCountProcedure, + accountVerwaltungPatchProcedure, + accountEmailConfirmRequestProcedure, + accountEmailConfirmProcedure, + accountPasswordResetProcedure, + accountVerwaltungRemoveProcedure, + accountTeilnehmerCreateProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/account/accountEmailConfirm.ts b/apps/api/src/services/account/accountEmailConfirm.ts index c5bff564..74ded3bb 100644 --- a/apps/api/src/services/account/accountEmailConfirm.ts +++ b/apps/api/src/services/account/accountEmailConfirm.ts @@ -15,6 +15,7 @@ export const accountEmailConfirmProcedure = definePublicMutateProcedure({ }, select: { id: true, + role: true, activatedAt: true, }, }) @@ -30,6 +31,7 @@ export const accountEmailConfirmProcedure = definePublicMutateProcedure({ id: account.id, }, data: { + status: account.role === 'USER' ? 'AKTIV' : 'OFFEN', activatedAt: new Date(), activationToken: null, }, diff --git a/apps/api/src/services/account/accountTeilnehmerCreate.ts b/apps/api/src/services/account/accountTeilnehmerCreate.ts new file mode 100644 index 00000000..c9015f80 --- /dev/null +++ b/apps/api/src/services/account/accountTeilnehmerCreate.ts @@ -0,0 +1,74 @@ +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({ + 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 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) { + 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: 'USER', + isActiv: false, + gliederungId: 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/activity/activity.routes.ts b/apps/api/src/services/activity/activity.routes.ts index 92b1fe3f..41990d1a 100644 --- a/apps/api/src/services/activity/activity.routes.ts +++ b/apps/api/src/services/activity/activity.routes.ts @@ -5,7 +5,7 @@ import { activityListProcedure, activityCountProcedure } from './activityList.js // Import Routes here - do not delete this line export const activityRouter = mergeRouters( - activityListProcedure.router, - activityCountProcedure.router + activityListProcedure, + activityCountProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/address/address.router.ts b/apps/api/src/services/address/address.router.ts index 9a9ec4a2..cdd7b6e1 100644 --- a/apps/api/src/services/address/address.router.ts +++ b/apps/api/src/services/address/address.router.ts @@ -5,6 +5,6 @@ import { addressFindActionProcedure } from './addressFindAddress.js' // Import Routes here - do not delete this line export const addressRouter = mergeRouters( - addressFindActionProcedure.router + addressFindActionProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/anmeldung/anmeldung.router.ts b/apps/api/src/services/anmeldung/anmeldung.router.ts index f68c80d1..6e5a9249 100644 --- a/apps/api/src/services/anmeldung/anmeldung.router.ts +++ b/apps/api/src/services/anmeldung/anmeldung.router.ts @@ -10,26 +10,24 @@ import { anmeldungPublicCreateProcedure } from './anmeldungPublicCreate.js' import { anmeldungTeilnehmerStornoProcedure } from './anmeldungTeilnehmerStorno.js' import { anmeldungVerwaltungAblehnenProcedure } from './anmeldungVerwaltungAblehnen.js' import { anmeldungVerwaltungAnnehmenProcedure } from './anmeldungVerwaltungAnnehmen.js' -import { anmeldungVerwaltungCreateProcedure } from './anmeldungVerwaltungCreate.js' import { anmeldungVerwaltungPatchProcedure } from './anmeldungVerwaltungPatch.js' import { anmeldungVerwaltungStornoProcedure } from './anmeldungVerwaltungStorno.js' import { anmeldungZuordnenProcedure } from './anmeldungZuordnen.js' // Import Routes here - do not delete this line export const anmeldungRouter = mergeRouters( - anmeldungPublicCreateProcedure.router, - anmeldungTeilnehmerStornoProcedure.router, - anmeldungVerwaltungAblehnenProcedure.router, - anmeldungVerwaltungAnnehmenProcedure.router, - anmeldungVerwaltungStornoProcedure.router, - anmeldungVerwaltungCreateProcedure.router, - anmeldungVerwaltungPatchProcedure.router, - anmeldungGliederungPatchProcedure.router, - anmeldungCountProcedure.router, - anmeldungListProcedure.router, - anmeldungGetProcedure.router, - anmeldungZuordnenProcedure.router, - anmeldungAccessTokenValidateProcedure.router, - anmeldungFotoUploadProcedure.router + anmeldungPublicCreateProcedure, + anmeldungTeilnehmerStornoProcedure, + anmeldungVerwaltungAblehnenProcedure, + anmeldungVerwaltungAnnehmenProcedure, + anmeldungVerwaltungStornoProcedure, + anmeldungVerwaltungPatchProcedure, + anmeldungGliederungPatchProcedure, + anmeldungCountProcedure, + anmeldungListProcedure, + anmeldungGetProcedure, + anmeldungZuordnenProcedure, + anmeldungAccessTokenValidateProcedure, + anmeldungFotoUploadProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts b/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts index 8ec020ed..23a0e1aa 100644 --- a/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts +++ b/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts @@ -2,16 +2,16 @@ import { TRPCError } from '@trpc/server' import dayjs from 'dayjs' import { z } from 'zod' +import { randomUUID } from 'node:crypto' import prisma from '../../prisma.js' import { customFieldValuesCreateMany, defineCustomFieldValues } from '../../types/defineCustomFieldValues.js' import { definePublicMutateProcedure } from '../../types/defineProcedure.js' import logActivity from '../../util/activity.js' import { sendMail } from '../../util/mail.js' import { getPersonCreateData, personSchema } from '../person/schema/person.schema.js' -import type { Context } from '../../context.js' -import { randomUUID } from 'node:crypto' export const inputSchema = z.strictObject({ + token: z.string().optional(), data: personSchema.extend({ unterveranstaltungId: z.number().int(), mahlzeitenIds: z.array(z.number().int()).optional(), @@ -23,116 +23,171 @@ export const inputSchema = z.strictObject({ customFieldValues: defineCustomFieldValues(), }) -type HandleProps = { - ctx: Context - input: z.infer - isPublic: boolean -} - -export async function handle({ ctx, input, isPublic }: HandleProps) { - const unterveranstaltung = await prisma.unterveranstaltung.findUniqueOrThrow({ - where: { - id: input.data.unterveranstaltungId, - }, - select: { - id: true, - meldeschluss: true, - veranstaltung: { - select: { - id: true, - name: true, - hostname: { - select: { - hostname: true, +export const anmeldungPublicCreateProcedure = definePublicMutateProcedure({ + key: 'publicCreate', + inputSchema, + handler: async ({ ctx, input }) => { + const unterveranstaltung = await prisma.unterveranstaltung.findUniqueOrThrow({ + where: { + id: input.data.unterveranstaltungId, + }, + select: { + id: true, + meldeschluss: true, + maxTeilnehmende: true, + veranstaltung: { + select: { + id: true, + name: true, + hostname: { + select: { + hostname: true, + }, + }, + maxTeilnehmende: true, + unterveranstaltungen: { + select: { + _count: { + select: { + Anmeldung: { + where: { + status: 'BESTAETIGT', + }, + }, + }, + }, + }, }, }, }, - }, - gliederung: { - select: { - name: true, + gliederung: { + select: { + name: true, + }, + }, + _count: { + select: { + Anmeldung: { + where: { + status: 'BESTAETIGT', + }, + }, + }, }, }, - }, - }) + }) - if (isPublic && dayjs().isAfter(unterveranstaltung.meldeschluss)) { - throw new TRPCError({ - code: 'FORBIDDEN', - message: 'Meldeschluss erreicht', + const anmeldeLink = await prisma.anmeldungLink.findFirst({ + where: { + accessToken: input.token, + unterveranstaltungId: unterveranstaltung.id, + usedAt: null, + }, + select: { + id: true, + }, }) - } - const personData = await getPersonCreateData(input.data) - const person = await prisma.person.create({ - data: personData, - select: { - id: true, - }, - }) + // skip validations if a valid bypass link is being used + if (anmeldeLink === null) { + if (dayjs().isAfter(unterveranstaltung.meldeschluss)) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Meldeschluss erreicht', + }) + } + + const activeAnmeldungenGlobal = unterveranstaltung.veranstaltung.unterveranstaltungen.reduce( + (prev, curr) => prev + curr._count.Anmeldung, + 0 + ) + if ( + activeAnmeldungenGlobal >= unterveranstaltung.veranstaltung.maxTeilnehmende || + unterveranstaltung._count.Anmeldung >= unterveranstaltung.veranstaltung.maxTeilnehmende || + unterveranstaltung._count.Anmeldung >= unterveranstaltung.maxTeilnehmende + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Maximale Anzahl an Anmeldungen ist bereits erreicht!', + }) + } + } - const accessToken = randomUUID() - const assignmentCode = ctx.authenticated ? null : randomUUID() - const anmeldung = await prisma.anmeldung.create({ - data: { - unterveranstaltungId: unterveranstaltung.id, - accountId: ctx.accountId, - personId: person.id, - mahlzeiten: input.data.mahlzeitenIds - ? { - connect: input.data.mahlzeitenIds.map((id) => ({ - id, - })), - } - : undefined, - uebernachtungsTage: input.data.uebernachtungsTage, - comment: input.data.comment, - createdAt: new Date(), - customFieldValues: { - createMany: customFieldValuesCreateMany(input.customFieldValues), + const personData = await getPersonCreateData(input.data) + const person = await prisma.person.create({ + data: personData, + select: { + id: true, }, - accessToken, - assignmentCode, - }, - }) + }) - 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, - }), - ]) + const accessToken = randomUUID() + const assignmentCode = ctx.authenticated ? null : randomUUID() + const anmeldung = await prisma.anmeldung.create({ + data: { + unterveranstaltungId: unterveranstaltung.id, + accountId: ctx.accountId, + personId: person.id, + mahlzeiten: input.data.mahlzeitenIds + ? { + connect: input.data.mahlzeitenIds.map((id) => ({ + id, + })), + } + : undefined, + uebernachtungsTage: input.data.uebernachtungsTage, + comment: input.data.comment, + createdAt: new Date(), + customFieldValues: { + createMany: customFieldValuesCreateMany(input.customFieldValues), + }, + accessToken, + assignmentCode, + }, + }) - await sendMail({ - to: input.data.email, - subject: `${unterveranstaltung?.veranstaltung?.hostname?.hostname} Anmeldung erfolgreich`, - categories: ['anmeldung', 'create'], - template: 'registration-successful', - variables: { - name: `${personData.firstname} ${personData.lastname}`, - gliederung: unterveranstaltung.gliederung.name, - veranstaltung: unterveranstaltung.veranstaltung.name, - hostname: unterveranstaltung.veranstaltung.hostname!.hostname, - unterveranstaltungId: unterveranstaltung.id, - anmeldungId: anmeldung.id, - assignmentCode, - accessToken, - }, - }) -} + if (anmeldeLink !== null) { + await prisma.anmeldungLink.update({ + where: { + id: anmeldeLink.id, + }, + data: { + anmeldungId: anmeldung.id, + usedAt: new Date(), + }, + }) + } -export const anmeldungPublicCreateProcedure = definePublicMutateProcedure({ - key: 'publicCreate', - inputSchema: inputSchema, - async handler({ ctx, input }) { - await handle({ ctx, input, isPublic: true }) + 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 sendMail({ + to: input.data.email, + subject: `${unterveranstaltung?.veranstaltung?.hostname?.hostname} Anmeldung erfolgreich`, + categories: ['anmeldung', 'create'], + template: 'registration-successful', + variables: { + name: `${personData.firstname} ${personData.lastname}`, + gliederung: unterveranstaltung.gliederung.name, + veranstaltung: unterveranstaltung.veranstaltung.name, + hostname: unterveranstaltung.veranstaltung.hostname!.hostname, + unterveranstaltungId: unterveranstaltung.id, + anmeldungId: anmeldung.id, + assignmentCode, + accessToken, + }, + }) }, }) diff --git a/apps/api/src/services/anmeldung/anmeldungVerwaltungCreate.ts b/apps/api/src/services/anmeldung/anmeldungVerwaltungCreate.ts deleted file mode 100644 index 716cce93..00000000 --- a/apps/api/src/services/anmeldung/anmeldungVerwaltungCreate.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Role } from '@prisma/client' - -import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' -import { handle, inputSchema } from './anmeldungPublicCreate.js' - -export const anmeldungVerwaltungCreateProcedure = defineProtectedMutateProcedure({ - key: 'verwaltungCreate', - inputSchema: inputSchema, - async handler({ ctx, input }) { - await handle({ ctx, input, isPublic: false }) - }, - roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], -}) diff --git a/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts b/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts new file mode 100644 index 00000000..e3f7b88b --- /dev/null +++ b/apps/api/src/services/anmeldungLink/anmeldeLink.create.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'node:crypto' +import { z } from 'zod' +import client from '../../prisma.js' +import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' + +export const anmeldungLinkCreateProcedure = defineProtectedMutateProcedure({ + key: 'create', + roleIds: ['ADMIN'], + inputSchema: z.strictObject({ + unterveranstaltungId: z.number().int(), + comment: z.string().optional(), + }), + handler: async ({ ctx, input: { unterveranstaltungId, comment } }) => { + const result = await client.anmeldungLink.create({ + data: { + unterveranstaltungId, + comment, + accessToken: randomUUID(), + createdById: ctx.accountId, + }, + select: { + accessToken: true, + }, + }) + + return result.accessToken + }, +}) diff --git a/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts b/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts new file mode 100644 index 00000000..7f21b4c1 --- /dev/null +++ b/apps/api/src/services/anmeldungLink/anmeldeLink.list.ts @@ -0,0 +1,61 @@ +import { z } from 'zod' +import { definePublicQueryProcedure } from '../../types/defineProcedure.js' +import client from '../../prisma.js' + +export const anmeldungLinkListProcedure = definePublicQueryProcedure({ + key: 'list', + inputSchema: z.strictObject({ + unterveranstaltungId: z.number().int(), + }), + handler: async ({ input: { unterveranstaltungId } }) => { + const result = await client.anmeldungLink.findMany({ + where: { + unterveranstaltungId, + }, + orderBy: { + createdAt: 'desc', + }, + select: { + id: true, + createdAt: true, + usedAt: true, + comment: true, + createdBy: { + select: { + person: { + select: { + firstname: true, + lastname: true, + }, + }, + }, + }, + anmeldung: { + select: { + person: { + select: { + firstname: true, + lastname: true, + }, + }, + }, + }, + }, + }) + + return result + }, +}) + +export const anmeldungLinkCountProcedure = definePublicQueryProcedure({ + key: 'count', + inputSchema: z.strictObject({ + unterveranstaltungId: z.number().int(), + }), + handler: ({ input: { unterveranstaltungId } }) => + client.anmeldungLink.count({ + where: { + unterveranstaltungId, + }, + }), +}) diff --git a/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts b/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts new file mode 100644 index 00000000..eae5dc5d --- /dev/null +++ b/apps/api/src/services/anmeldungLink/anmeldeLink.validate.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' +import { definePublicQueryProcedure } from '../../types/defineProcedure.js' +import client from '../../prisma.js' + +export const anmeldungLinkAuthorizeProcedure = definePublicQueryProcedure({ + key: 'authorize', + inputSchema: z.strictObject({ + unterveranstaltungId: z.number().int(), + accessToken: z.string().uuid(), + }), + handler: async ({ input: { unterveranstaltungId, accessToken } }) => { + const result = await client.anmeldungLink.findFirst({ + where: { + unterveranstaltungId, + accessToken, + usedAt: null, + }, + }) + + return result !== null + }, +}) diff --git a/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts b/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts new file mode 100644 index 00000000..c6730892 --- /dev/null +++ b/apps/api/src/services/anmeldungLink/anmeldungLink.router.ts @@ -0,0 +1,16 @@ +// 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 { anmeldungLinkAuthorizeProcedure } from './anmeldeLink.validate.js' + +// Import Routes here - do not delete this line + +export const anmeldungLinkRouter = mergeRouters( + anmeldungLinkCountProcedure, + anmeldungLinkListProcedure, + anmeldungLinkAuthorizeProcedure, + anmeldungLinkCreateProcedure +) + +// Add Routes here - do not delete this line diff --git a/apps/api/src/services/authentication/authentication.router.ts b/apps/api/src/services/authentication/authentication.router.ts index cecf9ed5..b5103e3f 100644 --- a/apps/api/src/services/authentication/authentication.router.ts +++ b/apps/api/src/services/authentication/authentication.router.ts @@ -5,6 +5,6 @@ import { authenticationLoginProcedure } from './authenticationLogin.js' // Import Routes here - do not delete this line export const authenticationRouter = mergeRouters( - authenticationLoginProcedure.router + authenticationLoginProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/customFields/customFields.router.ts b/apps/api/src/services/customFields/customFields.router.ts index 926ee5d4..96e23167 100644 --- a/apps/api/src/services/customFields/customFields.router.ts +++ b/apps/api/src/services/customFields/customFields.router.ts @@ -12,14 +12,14 @@ import { customFieldValuesUpdate } from './customFieldValuesUpdate.js' // Import Routes here - do not delete this line export const customFieldsRouter = mergeRouters( - customFieldsList.router, - customFieldsGet.router, - customFieldsVeranstaltungCreate.router, - customFieldsUpdate.router, - customFieldsVeranstaltungDelete.router, - customFieldsUnterveranstaltungDelete.router, - customFieldsUnterveranstaltungCreate.router, - customFieldValuesUpdate.router, - customFieldsTemplates.router + customFieldsList, + customFieldsGet, + customFieldsVeranstaltungCreate, + customFieldsUpdate, + customFieldsVeranstaltungDelete, + customFieldsUnterveranstaltungDelete, + customFieldsUnterveranstaltungCreate, + customFieldValuesUpdate, + customFieldsTemplates // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/faqs/faqs.router.ts b/apps/api/src/services/faqs/faqs.router.ts index 29e6dd1e..9d137081 100644 --- a/apps/api/src/services/faqs/faqs.router.ts +++ b/apps/api/src/services/faqs/faqs.router.ts @@ -8,10 +8,10 @@ import { faqUpdateProcedure } from './faqUpdateProcedure.js' // Import Routes here - do not delete this line export const faqsRouter = mergeRouters( - faqListProcedure.router, - faqCategorySearchProcedure.router, - faqCreateProcedure.router, - faqUpdateProcedure.router, - faqDeleteProcedure.router + faqListProcedure, + faqCategorySearchProcedure, + faqCreateProcedure, + faqUpdateProcedure, + faqDeleteProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/file/file.router.ts b/apps/api/src/services/file/file.router.ts index b91e3d63..3c19e624 100644 --- a/apps/api/src/services/file/file.router.ts +++ b/apps/api/src/services/file/file.router.ts @@ -7,8 +7,8 @@ import { fileGetUrlActionProcedure } from './fileGetUrl.js' // Import Routes here - do not delete this line export const fileRouter = mergeRouters( - fileCreateProcedure.router, - fileGetUrlActionProcedure.router, - anmeldungPublicFotoUploadProcedure.router + fileCreateProcedure, + fileGetUrlActionProcedure, + anmeldungPublicFotoUploadProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/file/fileGetUrl.ts b/apps/api/src/services/file/fileGetUrl.ts index 1ae7dac4..d41de868 100644 --- a/apps/api/src/services/file/fileGetUrl.ts +++ b/apps/api/src/services/file/fileGetUrl.ts @@ -28,12 +28,6 @@ export const fileGetUrlActionProcedure = definePublicQueryProcedure({ const file = await prisma.file.findUnique({ where, - select: { - id: true, - provider: true, - uploaded: true, - key: true, - }, }) if (file === null) { diff --git a/apps/api/src/services/file/helpers/getFileUrl.ts b/apps/api/src/services/file/helpers/getFileUrl.ts index 3d4dc647..12841ac1 100644 --- a/apps/api/src/services/file/helpers/getFileUrl.ts +++ b/apps/api/src/services/file/helpers/getFileUrl.ts @@ -1,22 +1,22 @@ -import config from '../../../config.js' -import { azureStorage } from '../../../azureStorage.js' -import dayjs from 'dayjs' import { BlobSASPermissions } from '@azure/storage-blob' - -export type File = { - id: string - provider: 'LOCAL' | 'AZURE' - uploaded: boolean - key: string -} +import type { File } from '@prisma/client' +import dayjs from 'dayjs' +import { createReadStream, ReadStream } from 'node:fs' +import { join } from 'node:path' +import { cwd } from 'node:process' +import { azureStorage } from '../../../azureStorage.js' +import config from '../../../config.js' const downloadUrlLifespan = 60 * 60 // 1 hour +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 } + if (file.provider === 'AZURE' && azureStorage !== null) { const containerClient = azureStorage.getContainerClient(config.fileProviders.AZURE.container) const blockBlobClient = containerClient.getBlockBlobClient(file.key) @@ -26,5 +26,24 @@ export async function getFileUrl(file: File) { permissions: BlobSASPermissions.from({ read: true }), }) } + + throw new Error('Unknown file provider') +} + +export async function openFileStream(file: File) { + if (file.provider === 'LOCAL') { + return createReadStream(uploadDir + '/' + file.key) + } + + if (file.provider === 'AZURE' && azureStorage !== null) { + const containerClient = azureStorage.getContainerClient(config.fileProviders.AZURE.container) + const blockBlobClient = containerClient.getBlockBlobClient(file.key) + const response = await blockBlobClient.download() + if (!response.readableStreamBody) { + throw new Error('failed downloading file') + } + return ReadStream.from(response.readableStreamBody) + } + throw new Error('Unknown file provider') } diff --git a/apps/api/src/services/gliederung/gliederung.router.ts b/apps/api/src/services/gliederung/gliederung.router.ts index 69c78f02..054b7925 100644 --- a/apps/api/src/services/gliederung/gliederung.router.ts +++ b/apps/api/src/services/gliederung/gliederung.router.ts @@ -10,12 +10,12 @@ import { gliederungVerwaltungPatchProcedure } from './gliederungVerwaltungPatch. // Import Routes here - do not delete this line export const gliederungRouter = mergeRouters( - gliederungPublicGetProcedure.router, - gliederungPublicListProcedure.router, - gliederungVerwaltungCreateProcedure.router, - gliederungVerwaltungGetProcedure.router, - gliederungListProcedure.router, - gliederungCountProcedure.router, - gliederungVerwaltungPatchProcedure.router + gliederungPublicGetProcedure, + gliederungPublicListProcedure, + gliederungVerwaltungCreateProcedure, + gliederungVerwaltungGetProcedure, + gliederungListProcedure, + gliederungCountProcedure, + gliederungVerwaltungPatchProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/index.ts b/apps/api/src/services/index.ts index b8184e22..7b565473 100644 --- a/apps/api/src/services/index.ts +++ b/apps/api/src/services/index.ts @@ -4,6 +4,7 @@ import { accountRouter } from './account/account.router.js' import { activityRouter } from './activity/activity.routes.js' import { addressRouter } from './address/address.router.js' import { anmeldungRouter } from './anmeldung/anmeldung.router.js' +import { anmeldungLinkRouter } from './anmeldungLink/anmeldungLink.router.js' import { authenticationRouter } from './authentication/authentication.router.js' import { customFieldsRouter } from './customFields/customFields.router.js' import { faqsRouter } from './faqs/faqs.router.js' @@ -33,5 +34,6 @@ export const serviceRouter = router({ file: fileRouter, address: addressRouter, faq: faqsRouter, + anmeldungLink: anmeldungLinkRouter, // Add Routers here - do not delete this line }) diff --git a/apps/api/src/services/ort/ort.router.ts b/apps/api/src/services/ort/ort.router.ts index 713b6aa9..86bd8402 100644 --- a/apps/api/src/services/ort/ort.router.ts +++ b/apps/api/src/services/ort/ort.router.ts @@ -9,11 +9,11 @@ import { ortVerwaltungRemoveProcedure } from './ortVerwaltungRemove.js' // Import Routes here - do not delete this line export const ortRouter = mergeRouters( - ortVerwaltungCreateProcedure.router, - ortVerwaltungGetProcedure.router, - ortListProcedure.router, - ortCountProcedure.router, - ortVerwaltungPatchProcedure.router, - ortVerwaltungRemoveProcedure.router + ortVerwaltungCreateProcedure, + ortVerwaltungGetProcedure, + ortListProcedure, + ortCountProcedure, + ortVerwaltungPatchProcedure, + ortVerwaltungRemoveProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/person/person.router.ts b/apps/api/src/services/person/person.router.ts index 6ae0554a..97344110 100644 --- a/apps/api/src/services/person/person.router.ts +++ b/apps/api/src/services/person/person.router.ts @@ -9,12 +9,12 @@ import { personVerwaltungRemoveProcedure } from './personVerwaltungRemove.js' // Import Routes here - do not delete this line export const personRouter = mergeRouters( - personAuthenticatedGetProcedure.router, - personVerwaltungCreateProcedure.router, - personListProcedure.router, - personCountProcedure.router, - personGetProcedure.router, - personVerwaltungPatchProcedure.router, - personVerwaltungRemoveProcedure.router + personAuthenticatedGetProcedure, + personVerwaltungCreateProcedure, + personListProcedure, + personCountProcedure, + personGetProcedure, + personVerwaltungPatchProcedure, + personVerwaltungRemoveProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/person/personList.ts b/apps/api/src/services/person/personList.ts index c577ef38..ab4fcde4 100644 --- a/apps/api/src/services/person/personList.ts +++ b/apps/api/src/services/person/personList.ts @@ -87,7 +87,7 @@ export function getPersonProtectionFilter({ if (account.role === Role.USER) { where.anmeldungen = { - every: { + some: { accountId: account.id, }, } diff --git a/apps/api/src/services/search/search.router.ts b/apps/api/src/services/search/search.router.ts index aa87154f..5bb37103 100644 --- a/apps/api/src/services/search/search.router.ts +++ b/apps/api/src/services/search/search.router.ts @@ -6,6 +6,6 @@ import { searchProcedure } from './search.js' // Import Routes here - do not delete this line export const searchRouter = mergeRouters( - searchProcedure.router + searchProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/system/system.router.ts b/apps/api/src/services/system/system.router.ts index 21ace667..b1038bbb 100644 --- a/apps/api/src/services/system/system.router.ts +++ b/apps/api/src/services/system/system.router.ts @@ -6,7 +6,6 @@ import { systemHostnamesGetProcedure } from './systemHostnamesGet.js' // Import Routes here - do not delete this line export const systemRouter = mergeRouters( - systemHostnamesGetProcedure.router - + systemHostnamesGetProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts index acb0b9f8..abb2685a 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltung.router.ts @@ -13,15 +13,14 @@ import { unterveranstaltungVerwaltungPatchProcedure } from './unterveranstaltung // Import Routes here - do not delete this line export const unterveranstaltungRouter = mergeRouters( - unterveranstaltungGliederungCreateProcedure.router, - unterveranstaltungGliederungPatchProcedure.router, - unterveranstaltungGliederungGetProcedure.router, - unterveranstaltungPublicGetProcedure.router, - unterveranstaltungVerwaltungCreateProcedure.router, - unterveranstaltungVerwaltungPatchProcedure.router, - unterveranstaltungVerwaltungGetProcedure.router, - unterveranstaltungListProcedure.router, - unterveranstaltungCountProcedure.router - + unterveranstaltungGliederungCreateProcedure, + unterveranstaltungGliederungPatchProcedure, + unterveranstaltungGliederungGetProcedure, + unterveranstaltungPublicGetProcedure, + unterveranstaltungVerwaltungCreateProcedure, + unterveranstaltungVerwaltungPatchProcedure, + unterveranstaltungVerwaltungGetProcedure, + unterveranstaltungListProcedure, + unterveranstaltungCountProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungCreate.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungCreate.ts index 212d9ed0..1c5492d7 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungCreate.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungGliederungCreate.ts @@ -16,6 +16,20 @@ export const unterveranstaltungGliederungCreateProcedure = defineProtectedMutate async handler({ ctx, input }) { // check logged in user is admin of gliederung const gliederung = await getGliederungRequireAdmin(ctx.accountId) + + const existing = await prisma.unterveranstaltung.findFirst({ + where: { + veranstaltungId: input.data.veranstaltungId, + gliederungId: gliederung.id, + }, + }) + if (existing !== null) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Für die angegebene Veranstaltung`, + }) + } + const veranstaltung = await prisma.veranstaltung.findUniqueOrThrow({ where: { id: input.data.veranstaltungId, diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts index c7753c99..fad69c3c 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungList.ts @@ -43,6 +43,7 @@ export const unterveranstaltungListProcedure = defineProtectedQueryProcedure({ select: { id: true, type: true, + beschreibung: true, gliederung: { select: { id: true, diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts index 47c11d5b..7d59e45e 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts @@ -54,14 +54,7 @@ export const unterveranstaltungPublicGetProcedure = definePublicQueryProcedure({ select: { fileId: true, name: true, - file: { - select: { - id: true, - provider: true, - uploaded: true, - key: true, - }, - }, + file: true, }, }, eventDetailsTitle: true, diff --git a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts index 2ff3405a..09f0c244 100644 --- a/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts +++ b/apps/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungCreate.ts @@ -4,6 +4,7 @@ import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' import { unterveranstaltungCreateSchema } from './schema/unterveranstaltung.schema.js' +import { TRPCError } from '@trpc/server' const unterveranstaltungVerwaltungCreateSchema = unterveranstaltungCreateSchema.extend({ gliederungId: z.number().int(), @@ -17,6 +18,21 @@ export const unterveranstaltungVerwaltungCreateProcedure = defineProtectedMutate data: unterveranstaltungVerwaltungCreateSchema, }), async handler({ input }) { + if (input.data.type !== 'CREW') { + const existing = await prisma.unterveranstaltung.findFirst({ + where: { + veranstaltungId: input.data.veranstaltungId, + gliederungId: input.data.gliederungId, + }, + }) + if (existing !== null) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Für die angegebene Veranstaltung`, + }) + } + } + const unterveranstaltung = await prisma.unterveranstaltung.create({ data: { veranstaltung: { diff --git a/apps/api/src/services/veranstaltung/veranstaltung.router.ts b/apps/api/src/services/veranstaltung/veranstaltung.router.ts index 7c0861a5..536a6c8e 100644 --- a/apps/api/src/services/veranstaltung/veranstaltung.router.ts +++ b/apps/api/src/services/veranstaltung/veranstaltung.router.ts @@ -12,11 +12,11 @@ import { veranstaltungVerwaltungPatchProcedure } from './veranstaltungVerwaltung // Import Routes here - do not delete this line export const veranstaltungRouter = mergeRouters( - veranstaltungVerwaltungCreateProcedure.router, - veranstaltungVerwaltungGetProcedure.router, - veranstaltungVerwaltungListProcedure.router, - veranstaltungVerwaltungCountProcedure.router, - veranstaltungVerwaltungPatchProcedure.router, - veranstaltungGliederungListProcedure.router + veranstaltungVerwaltungCreateProcedure, + veranstaltungVerwaltungGetProcedure, + veranstaltungVerwaltungListProcedure, + veranstaltungVerwaltungCountProcedure, + veranstaltungVerwaltungPatchProcedure, + veranstaltungGliederungListProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts index 3847c878..3996f429 100644 --- a/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts +++ b/apps/api/src/services/veranstaltung/veranstaltungVerwaltungGet.ts @@ -41,6 +41,22 @@ export const veranstaltungVerwaltungGetProcedure = defineProtectedQueryProcedure hostname: true, }, }, + unterveranstaltungen: { + select: { + id: true, + _count: { + select: { + Anmeldung: { + where: { + status: { + equals: 'BESTAETIGT', + }, + }, + }, + }, + }, + }, + }, }, }) return veranstaltungWithunterveranstaltungen diff --git a/apps/api/src/types/defineProcedure.ts b/apps/api/src/types/defineProcedure.ts index 2d0a2a45..dfb2d542 100644 --- a/apps/api/src/types/defineProcedure.ts +++ b/apps/api/src/types/defineProcedure.ts @@ -23,11 +23,9 @@ export function defineProtectedQueryProcedure< const procedure = protectedProcedure(config.roleIds) .input(config.inputSchema) .query((opts) => config.handler(opts)) - return { - router: router({ - [config.key]: procedure, - } as { [k in TProcedureKey]: typeof procedure }), - } + return router({ + [config.key]: procedure, + } as { [k in TProcedureKey]: typeof procedure }) } export function defineProtectedMutateProcedure< @@ -47,11 +45,9 @@ export function defineProtectedMutateProcedure< const procedure = protectedProcedure(config.roleIds) .input(config.inputSchema) .mutation((opts) => config.handler(opts)) - return { - router: router({ - [config.key]: procedure, - } as { [k in TProcedureKey]: typeof procedure }), - } + return router({ + [config.key]: procedure, + } as { [k in TProcedureKey]: typeof procedure }) } export function definePublicQueryProcedure< @@ -67,11 +63,9 @@ export function definePublicQueryProcedure< handler: (options: { ctx: Context; input: z.infer }) => MaybePromise }) { const procedure = publicProcedure.input(config.inputSchema).query((opts) => config.handler(opts)) - return { - router: router({ - [config.key]: procedure, - } as { [k in TProcedureKey]: typeof procedure }), - } + return router({ + [config.key]: procedure, + } as { [k in TProcedureKey]: typeof procedure }) } export function definePublicMutateProcedure< @@ -87,9 +81,7 @@ export function definePublicMutateProcedure< handler: (options: { ctx: Context; input: z.infer }) => MaybePromise }) { const procedure = publicProcedure.input(config.inputSchema).mutation((opts) => config.handler(opts)) - return { - router: router({ - [config.key]: procedure, - } as { [k in TProcedureKey]: typeof procedure }), - } + return router({ + [config.key]: procedure, + } as { [k in TProcedureKey]: typeof procedure }) } diff --git a/apps/api/src/types/defineQuery.ts b/apps/api/src/types/defineQuery.ts index 264baaa7..4229263c 100644 --- a/apps/api/src/types/defineQuery.ts +++ b/apps/api/src/types/defineQuery.ts @@ -1,5 +1,5 @@ +import { setProperty } from 'dot-prop' import z from 'zod' -import { set } from 'lodash-es' export const ZPaginationSchema = z.strictObject({ skip: z.number().optional(), @@ -34,13 +34,11 @@ export interface TQuery { filter: Record } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getOrderBy(orderBy: Array<[T, 'asc' | 'desc']>): Record { +export function getOrderBy(orderBy: Array<[T, 'asc' | 'desc']>) { return orderBy.map(([field, order]) => { const result = {} as { [key in T]: 'asc' | 'desc' } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - set(result, field, order) + setProperty(result, field, order) return result }) } diff --git a/apps/api/src/util/mail.ts b/apps/api/src/util/mail.ts index 81671ed5..756e9052 100644 --- a/apps/api/src/util/mail.ts +++ b/apps/api/src/util/mail.ts @@ -122,7 +122,7 @@ export async function sendMail(mailParams: EMailParams) { } const mailToSend: EMail = { - from: 'brahmsee.digital', + from: `${mailParams.variables.hostname}`, to: parseMaybeArray(mailParams.to), attachments: formatAttachments(mailParams.attachments), subject, diff --git a/apps/frontend/index.html b/apps/frontend/index.html index ecad54d7..ef1f1580 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -4,11 +4,11 @@ brahmsee.digital diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 9fda9fa3..ccfe5677 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -41,12 +41,15 @@ "intl-tel-input": "^24.4.0", "primevue": "^4.2.5", "radix-vue": "^1.9.5", + "reka-ui": "^2.1.1", "remixicon": "^3.5.0", "simple-syntax-highlighter": "^3.1.1", "superjson": "catalog:", "tailwind-merge": "^2.6.0", + "vaul-vue": "^0.4.1", "vue": "catalog:", - "vue-router": "^4.2.5" + "vue-router": "^4.2.5", + "vue-sonner": "^1.3.0" }, "devDependencies": { "@codeanker/eslint-config": "workspace:*", diff --git a/apps/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico deleted file mode 100644 index a21c259b..00000000 Binary files a/apps/frontend/public/favicon.ico and /dev/null differ diff --git a/apps/frontend/public/favicon.webp b/apps/frontend/public/favicon.webp new file mode 100644 index 00000000..552d84a2 Binary files /dev/null and b/apps/frontend/public/favicon.webp differ diff --git a/apps/frontend/src/App.vue b/apps/frontend/src/App.vue index 98240aef..8c5caebb 100644 --- a/apps/frontend/src/App.vue +++ b/apps/frontend/src/App.vue @@ -1,3 +1,14 @@ + + diff --git a/apps/frontend/src/assets/images/brahmsee.digital.webm b/apps/frontend/src/assets/images/brahmsee.digital.webm new file mode 100644 index 00000000..2c48a6d7 Binary files /dev/null and b/apps/frontend/src/assets/images/brahmsee.digital.webm differ diff --git a/apps/frontend/src/assets/images/dilly_logo.webp b/apps/frontend/src/assets/images/dilly_logo.webp new file mode 100644 index 00000000..552d84a2 Binary files /dev/null and b/apps/frontend/src/assets/images/dilly_logo.webp differ diff --git a/apps/frontend/src/assets/images/dilly_logo_sm.webp b/apps/frontend/src/assets/images/dilly_logo_sm.webp new file mode 100644 index 00000000..8b725e63 Binary files /dev/null and b/apps/frontend/src/assets/images/dilly_logo_sm.webp differ diff --git a/apps/frontend/src/assets/images/publicBg.webp b/apps/frontend/src/assets/images/publicBg.webp index a5a88da6..d0330a2e 100644 Binary files a/apps/frontend/src/assets/images/publicBg.webp and b/apps/frontend/src/assets/images/publicBg.webp differ diff --git a/apps/frontend/src/assets/images/svg/isc.svg b/apps/frontend/src/assets/images/svg/isc.svg new file mode 100644 index 00000000..98760ffe --- /dev/null +++ b/apps/frontend/src/assets/images/svg/isc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/assets/main.scss b/apps/frontend/src/assets/main.scss index 9ac61135..a6a4d082 100644 --- a/apps/frontend/src/assets/main.scss +++ b/apps/frontend/src/assets/main.scss @@ -12,6 +12,7 @@ html { html, body, #app { + height: 100vh; width: 100vw; overflow: hidden; overflow-y: overlay; @@ -21,7 +22,7 @@ body, } body { - @apply transition-colors duration-200; + @apply transition-colors duration-200 dark:text-gray-200; } abbr { diff --git a/apps/frontend/src/components/BasicInputs/BasicInput.vue b/apps/frontend/src/components/BasicInputs/BasicInput.vue index 82c7cb5a..d8d4ef6a 100644 --- a/apps/frontend/src/components/BasicInputs/BasicInput.vue +++ b/apps/frontend/src/components/BasicInputs/BasicInput.vue @@ -43,6 +43,7 @@ const { model, errorMessage } = props.disableValidation :label="label" :required="required" :error-message="errorMessage" + class="dark:text-gray-200" >
({ - show(ctx) { + show() { if (isDeleting) { return } - modal.value?.show(ctx) + modal.value?.show() }, - hide(ctx) { + hide() { if (isDeleting) { return } - modal.value?.hide(ctx) + modal.value?.hide() }, }) diff --git a/apps/frontend/src/components/CustomFields/CustomFieldTemplateModal.vue b/apps/frontend/src/components/CustomFields/CustomFieldTemplateModal.vue index 97b34e47..d3d5cd58 100644 --- a/apps/frontend/src/components/CustomFields/CustomFieldTemplateModal.vue +++ b/apps/frontend/src/components/CustomFields/CustomFieldTemplateModal.vue @@ -42,11 +42,11 @@ function onSelect() { } defineExpose({ - show(ctx) { - modal.value?.show(ctx) + show() { + modal.value?.show() }, - hide(ctx) { - modal.value?.hide(ctx) + hide() { + modal.value?.hide() }, }) diff --git a/apps/frontend/src/components/FilesExport.vue b/apps/frontend/src/components/FilesExport.vue index 5f2e381d..31bdc66e 100644 --- a/apps/frontend/src/components/FilesExport.vue +++ b/apps/frontend/src/components/FilesExport.vue @@ -2,7 +2,7 @@ import { ArrowDownTrayIcon } from '@heroicons/vue/24/outline' import type { FunctionalComponent } from 'vue' -interface fileType { +export interface ExportedFileType { name: string initial?: string icon?: FunctionalComponent @@ -13,7 +13,7 @@ interface fileType { } defineProps<{ - files: fileType[] + files: ExportedFileType[] }>() diff --git a/apps/frontend/src/components/IscBadge.vue b/apps/frontend/src/components/IscBadge.vue new file mode 100644 index 00000000..349743bb --- /dev/null +++ b/apps/frontend/src/components/IscBadge.vue @@ -0,0 +1,9 @@ + diff --git a/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue b/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue index 6d202c5d..42e67a80 100644 --- a/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue +++ b/apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue @@ -19,8 +19,10 @@ import SidebarItems, { type DividerItem, type SidebarItem } from './SidebarItems import UserLogo from '@/components/UIComponents/UserLogo.vue' import { loggedInAccount, logout } from '@/composables/useAuthentication' +import { useAssets } from '@/composables/useAssets' const route = useRoute() +const { logoSmall } = useAssets() const veranstaltungId = computed(() => { if (route.params.veranstaltungId !== undefined && typeof route.params.veranstaltungId === 'string') { @@ -162,13 +164,6 @@ const navigation = computed>(() => [ icon: CalendarDaysIcon, visible: hasPermissionToView(['ADMIN']), }, - { - type: 'SidebarItem', - name: 'Ausschreibungen', - route: { name: 'UnterveranstaltungList', params: { veranstaltungId: veranstaltungId.value } }, - icon: MegaphoneIcon, - visible: hasPermissionToView(['ADMIN', 'GLIEDERUNG_ADMIN']), - }, { type: 'SidebarItem', name: 'Gliederungen', @@ -213,8 +208,13 @@ const navigation = computed>(() => [
+ Dilly brahmsee.digital
diff --git a/apps/frontend/src/components/Modal/IscRedirectModal.vue b/apps/frontend/src/components/Modal/IscRedirectModal.vue new file mode 100644 index 00000000..3ba1b4cf --- /dev/null +++ b/apps/frontend/src/components/Modal/IscRedirectModal.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/frontend/src/components/Modal/RegisterModal.vue b/apps/frontend/src/components/Modal/RegisterModal.vue new file mode 100644 index 00000000..cc5eeb44 --- /dev/null +++ b/apps/frontend/src/components/Modal/RegisterModal.vue @@ -0,0 +1,119 @@ + + + diff --git a/apps/frontend/src/components/UIComponents/Alert.vue b/apps/frontend/src/components/UIComponents/Alert.vue new file mode 100644 index 00000000..b808dc56 --- /dev/null +++ b/apps/frontend/src/components/UIComponents/Alert.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue b/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue new file mode 100644 index 00000000..1b02bbdb --- /dev/null +++ b/apps/frontend/src/components/UIComponents/AnmeldeLinkCreateModal.vue @@ -0,0 +1,162 @@ + + + diff --git a/apps/frontend/src/components/UIComponents/Breadcrumbs.vue b/apps/frontend/src/components/UIComponents/Breadcrumbs.vue index 3ab4713d..c24c3831 100644 --- a/apps/frontend/src/components/UIComponents/Breadcrumbs.vue +++ b/apps/frontend/src/components/UIComponents/Breadcrumbs.vue @@ -1,44 +1,50 @@