diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile index cf076d4..a7f2073 100644 --- a/docker/web.Dockerfile +++ b/docker/web.Dockerfile @@ -59,7 +59,9 @@ ENV NODE_ENV=production \ PORT=3000 \ HOSTNAME=0.0.0.0 -RUN apk add --no-cache wget openssl && \ +# qpdf säubert hochgeladene PDF-Dateien, siehe docs/07-dateien-und-sicherheit.md. +# Fehlt es, bleiben PDF-Uploads in der Quarantäne statt ungeprüft durchzugehen. +RUN apk add --no-cache wget openssl qpdf && \ addgroup -g 1001 -S afis && \ adduser -u 1001 -S afis -G afis diff --git a/packages/db/prisma/migrations/20260902220000_dateien/migration.sql b/packages/db/prisma/migrations/20260902220000_dateien/migration.sql new file mode 100644 index 0000000..8cebc07 --- /dev/null +++ b/packages/db/prisma/migrations/20260902220000_dateien/migration.sql @@ -0,0 +1,27 @@ +-- Phase 5: Dateien. +-- +-- Anhänge können jetzt auch an einem Dokument hängen (Lichtbild im +-- Aktenblatt), tragen die Maße des Bildes und merken sich, wer eine Datei +-- freigegeben hat, deren Prüfung nicht abgeschlossen werden konnte. + +ALTER TABLE "Attachment" + ADD COLUMN "documentId" TEXT, + ADD COLUMN "width" INTEGER, + ADD COLUMN "height" INTEGER, + ADD COLUMN "releasedById" TEXT, + ADD COLUMN "releasedAt" TIMESTAMP(3); + +CREATE INDEX "Attachment_documentId_idx" ON "Attachment"("documentId"); + +ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_documentId_fkey" + FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Ein Anhang gehört zu höchstens einem Ort. Dieselbe Begründung wie beim +-- Dokument: zwei Besitzer wären eine Aussage über die Sichtbarkeit, die +-- niemand prüfen könnte. +ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_one_owner" + CHECK ( + (CASE WHEN "entryId" IS NULL THEN 0 ELSE 1 END) + + (CASE WHEN "caseId" IS NULL THEN 0 ELSE 1 END) + + (CASE WHEN "documentId" IS NULL THEN 0 ELSE 1 END) <= 1 + ); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 70a16b1..10960af 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -583,6 +583,7 @@ model Document { case Case? @relation(fields: [caseId], references: [id], onDelete: Restrict) personnelFile PersonnelFile? @relation(fields: [personnelFileId], references: [id], onDelete: Restrict) sections DocumentSection[] + attachments Attachment[] @@index([caseId, createdAt(sort: Desc)]) @@index([personnelFileId, createdAt(sort: Desc)]) @@ -639,29 +640,45 @@ model DocumentSectionVersion { // Dateien // --------------------------------------------------------------------------- +/// Eine hochgeladene Datei. Sie ist erst dann etwas wert, wenn sie die +/// Pruefkette aus docs/07-dateien-und-sicherheit.md ganz durchlaufen hat: +/// Typpruefung ueber Magic Bytes, ClamAV, Neukodierung. Bis dahin liegt sie in +/// quarantine/ und wird nicht ausgeliefert. model Attachment { id String @id @default(uuid(7)) entryId String? caseId String? + documentId String? originalName String + /// Der **erkannte** Typ, nicht der gemeldete. Siehe lib/files/magic.ts. mimeType String sizeBytes Int sha256 String scanStatus ScanStatus @default(PENDING) scanMessage String? - storageKey String // UUID-Pfad in store/ + /// Pfad unterhalb von FILES_DIR, inklusive quarantine/ oder store/. + storageKey String thumbKey String? blurhash String? + width Int? + height Int? uploadedById String createdAt DateTime @default(now()) - entry CaseEntry? @relation(fields: [entryId], references: [id], onDelete: Cascade) - case Case? @relation(fields: [caseId], references: [id], onDelete: Cascade) + /// Freigabe einer Datei, die die Pruefung nicht abschliessen konnte. + /// Nur mit Recht 'admin', immer protokolliert. + releasedById String? + releasedAt DateTime? + + entry CaseEntry? @relation(fields: [entryId], references: [id], onDelete: Cascade) + case Case? @relation(fields: [caseId], references: [id], onDelete: Cascade) + document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) @@index([sha256]) @@index([scanStatus]) @@index([caseId]) @@index([entryId]) + @@index([documentId]) @@index([uploadedById, createdAt]) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47ff250..5bab958 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,12 +85,15 @@ importers: '@afis/db': specifier: workspace:* version: link:../packages/db + blurhash: + specifier: 2.0.5 + version: 2.0.5 next: specifier: 15.5.25 - version: 15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-auth: specifier: 5.0.0-beta.32 - version: 5.0.0-beta.32(next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 5.0.0-beta.32(next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) pg: specifier: 8.13.1 version: 8.13.1 @@ -100,6 +103,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.8(react@19.2.8) + sharp: + specifier: 0.34.4 + version: 0.34.4 uuidv7: specifier: 1.0.2 version: 1.0.2 @@ -628,145 +634,125 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.4': - resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} - engines: {node: '>=20.9.0'} + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.4': - resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} - engines: {node: '>=20.9.0'} + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.4': - resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} - engines: {node: '>=20.9.0'} - os: [freebsd] - - '@img/sharp-libvips-darwin-arm64@1.3.3': - resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.3': - resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.3': - resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.3.3': - resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.3.3': - resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-riscv64@1.3.3': - resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} - cpu: [riscv64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.3.3': - resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.3.3': - resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': - resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.3.3': - resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.35.4': - resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} - engines: {node: '>=20.9.0'} + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.35.4': - resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} - engines: {node: '>=20.9.0'} + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.35.4': - resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} - engines: {node: '>=20.9.0'} + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - '@img/sharp-linux-riscv64@0.35.4': - resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} - engines: {node: '>=20.9.0'} - cpu: [riscv64] - os: [linux] - - '@img/sharp-linux-s390x@0.35.4': - resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} - engines: {node: '>=20.9.0'} + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.35.4': - resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} - engines: {node: '>=20.9.0'} + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.35.4': - resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} - engines: {node: '>=20.9.0'} + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.35.4': - resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} - engines: {node: '>=20.9.0'} + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.35.4': - resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} - engines: {node: '>=20.9.0'} - - '@img/sharp-webcontainers-wasm32@0.35.4': - resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} - engines: {node: '>=20.9.0'} + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.4': - resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} - engines: {node: '>=20.9.0'} + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.4': - resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} - engines: {node: ^20.9.0} + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.4': - resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} - engines: {node: '>=20.9.0'} + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -1699,6 +1685,9 @@ packages: better-result@2.10.0: resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} + blurhash@2.0.5: + resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} + brace-expansion@1.1.18: resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} @@ -3243,14 +3232,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.35.4: - resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} - engines: {node: '>=20.9.0'} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -4075,111 +4059,92 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.1.0': - optional: true + '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.35.4': + '@img/sharp-darwin-arm64@0.34.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-arm64': 1.2.3 optional: true - '@img/sharp-darwin-x64@0.35.4': + '@img/sharp-darwin-x64@0.34.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.4': - dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64@1.2.3': optional: true - '@img/sharp-libvips-darwin-arm64@1.3.3': + '@img/sharp-libvips-darwin-x64@1.2.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.3': + '@img/sharp-libvips-linux-arm64@1.2.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.3': + '@img/sharp-libvips-linux-arm@1.2.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.3': + '@img/sharp-libvips-linux-ppc64@1.2.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.3': + '@img/sharp-libvips-linux-s390x@1.2.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.3': + '@img/sharp-libvips-linux-x64@1.2.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.3': + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.3': + '@img/sharp-libvips-linuxmusl-x64@1.2.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.3.3': - optional: true - - '@img/sharp-linux-arm64@0.35.4': + '@img/sharp-linux-arm64@0.34.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 optional: true - '@img/sharp-linux-arm@0.35.4': + '@img/sharp-linux-arm@0.34.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.2.3 optional: true - '@img/sharp-linux-ppc64@0.35.4': + '@img/sharp-linux-ppc64@0.34.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 optional: true - '@img/sharp-linux-riscv64@0.35.4': + '@img/sharp-linux-s390x@0.34.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 optional: true - '@img/sharp-linux-s390x@0.35.4': + '@img/sharp-linux-x64@0.34.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.2.3 optional: true - '@img/sharp-linux-x64@0.35.4': + '@img/sharp-linuxmusl-arm64@0.34.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.4': + '@img/sharp-linuxmusl-x64@0.34.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 - optional: true - - '@img/sharp-wasm32@0.35.4': + '@img/sharp-wasm32@0.34.4': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.4': - dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-win32-arm64@0.34.4': optional: true - '@img/sharp-win32-arm64@0.35.4': + '@img/sharp-win32-ia32@0.34.4': optional: true - '@img/sharp-win32-ia32@0.35.4': - optional: true - - '@img/sharp-win32-x64@0.35.4': + '@img/sharp-win32-x64@0.34.4': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -5114,6 +5079,8 @@ snapshots: better-result@2.10.0: {} + blurhash@2.0.5: {} + brace-expansion@1.1.18: dependencies: balanced-match: 1.0.2 @@ -5545,8 +5512,8 @@ snapshots: '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.5(jiti@2.7.0)) @@ -5565,7 +5532,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -5576,22 +5543,22 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5602,7 +5569,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -6370,13 +6337,13 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.32(next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): + next-auth@5.0.0-beta.32(next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): dependencies: '@auth/core': 0.41.3 - next: 15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 - next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@next/env': 15.5.25 '@swc/helpers': 0.5.15 @@ -6395,10 +6362,9 @@ snapshots: '@next/swc-win32-arm64-msvc': 15.5.25 '@next/swc-win32-x64-msvc': 15.5.25 '@playwright/test': 1.62.1 - sharp: 0.35.4(@types/node@22.20.1) + sharp: 0.34.4 transitivePeerDependencies: - '@babel/core' - - '@types/node' - babel-plugin-macros node-exports-info@1.6.2: @@ -6805,39 +6771,34 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.35.4(@types/node@22.20.1): + sharp@0.34.4: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.4 - '@img/sharp-darwin-x64': 0.35.4 - '@img/sharp-freebsd-wasm32': 0.35.4 - '@img/sharp-libvips-darwin-arm64': 1.3.3 - '@img/sharp-libvips-darwin-x64': 1.3.3 - '@img/sharp-libvips-linux-arm': 1.3.3 - '@img/sharp-libvips-linux-arm64': 1.3.3 - '@img/sharp-libvips-linux-ppc64': 1.3.3 - '@img/sharp-libvips-linux-riscv64': 1.3.3 - '@img/sharp-libvips-linux-s390x': 1.3.3 - '@img/sharp-libvips-linux-x64': 1.3.3 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 - '@img/sharp-linux-arm': 0.35.4 - '@img/sharp-linux-arm64': 0.35.4 - '@img/sharp-linux-ppc64': 0.35.4 - '@img/sharp-linux-riscv64': 0.35.4 - '@img/sharp-linux-s390x': 0.35.4 - '@img/sharp-linux-x64': 0.35.4 - '@img/sharp-linuxmusl-arm64': 0.35.4 - '@img/sharp-linuxmusl-x64': 0.35.4 - '@img/sharp-webcontainers-wasm32': 0.35.4 - '@img/sharp-win32-arm64': 0.35.4 - '@img/sharp-win32-ia32': 0.35.4 - '@img/sharp-win32-x64': 0.35.4 - '@types/node': 22.20.1 - optional: true + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 shebang-command@2.0.0: dependencies: diff --git a/start.sh b/start.sh index 3e060e7..1357e1b 100755 --- a/start.sh +++ b/start.sh @@ -89,6 +89,22 @@ pnpm db:deploy >/dev/null info "Grundzustand einspielen" pnpm db:seed >/dev/null +# --- Dateiablage ------------------------------------------------------------- +# Im Stack ist das ein Volume, hier ein Verzeichnis. Ohne es scheitert der +# erste Upload mit einem Fehler, der nichts erklärt. +FILES_DIR_LOKAL="$(grep -E '^FILES_DIR=' .env 2>/dev/null | cut -d= -f2- || true)" +FILES_DIR_LOKAL="${FILES_DIR_LOKAL:-/tmp/afis-files}" +mkdir -p "${FILES_DIR_LOKAL}/quarantine" "${FILES_DIR_LOKAL}/store" +info "Dateiablage unter ${FILES_DIR_LOKAL}" + +# Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in +# der Quarantäne — sie gehen nicht ungeprüft durch. Siehe docs/07. +CLAMAV_SOCKET_LOKAL="$(grep -E '^CLAMAV_SOCKET=' .env 2>/dev/null | cut -d= -f2- || true)" +[ -S "${CLAMAV_SOCKET_LOKAL:-/var/run/clamav/clamd.sock}" ] \ + || warn "Kein clamd erreichbar: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand" +command -v qpdf >/dev/null \ + || warn "qpdf fehlt: PDF-Uploads landen auf 'Prüfung unvollständig' (im Container ist es enthalten)" + if [ "$NUR_DB" = "1" ]; then info "Datenbank läuft auf localhost:${DB_PORT}. Beenden mit: docker stop ${DB_CONTAINER}" exit 0 diff --git a/web/app/(app)/admin/richtlinie/PolicyForm.tsx b/web/app/(app)/admin/richtlinie/PolicyForm.tsx index cda3a21..6162a49 100644 --- a/web/app/(app)/admin/richtlinie/PolicyForm.tsx +++ b/web/app/(app)/admin/richtlinie/PolicyForm.tsx @@ -1,7 +1,8 @@ 'use client' import { useState, useTransition } from 'react' -import { de } from '@/lib/i18n/de' +import { settingsText } from '@/lib/i18n/admin' +import { caseText } from '@/lib/i18n/cases' import { savePolicy } from './actions' const POLICIES = ['APPEND_ONLY', 'GRACE_WINDOW', 'OPEN'] as const @@ -24,7 +25,7 @@ export function PolicyForm({ const [pending, startTransition] = useTransition() function submit() { - if (!confirm(de.settings.confirm)) return + if (!confirm(settingsText.confirm)) return setError(null) startTransition(async () => { const result = await savePolicy({ @@ -40,7 +41,7 @@ export function PolicyForm({ return (
- {de.settings.policy} + {settingsText.policy}
{POLICIES.map((value) => ( ))}
@@ -96,10 +97,10 @@ export function PolicyForm({ disabled={pending} className="border border-line bg-raised px-4 py-2 font-condensed text-xs text-accent disabled:text-text-dim" > - {pending ? de.case.saving : de.settings.save} + {pending ? caseText.saving : settingsText.save} {saved ? ( - {de.settings.saved} + {settingsText.saved} ) : null} {error ? {error} : null}
diff --git a/web/app/(app)/admin/rollen/RoleRow.tsx b/web/app/(app)/admin/rollen/RoleRow.tsx index 9ea301f..065ceff 100644 --- a/web/app/(app)/admin/rollen/RoleRow.tsx +++ b/web/app/(app)/admin/rollen/RoleRow.tsx @@ -1,7 +1,7 @@ 'use client' import { useState, useTransition } from 'react' -import { de } from '@/lib/i18n/de' +import { roleAdminText } from '@/lib/i18n/admin' import { OPT_IN_PERMISSIONS, PERMISSIONS, type Permission } from '@/lib/auth/permissions' import { saveRoleMapping } from './actions' @@ -52,7 +52,7 @@ export function RoleRow({ } function submit() { - if (!confirm(de.roleAdmin.confirm)) return + if (!confirm(roleAdminText.confirm)) return setError(null) startTransition(async () => { const result = await saveRoleMapping({ @@ -77,14 +77,14 @@ export function RoleRow({ {role.discordRoleId} {!role.isPresent ? ( - {de.roleAdmin.missingFromGuild} + {roleAdminText.missingFromGuild} ) : null}
diff --git a/web/app/(app)/admin/vorlagen/TemplateEditor.tsx b/web/app/(app)/admin/vorlagen/TemplateEditor.tsx index 1eb7a6c..85c6c7e 100644 --- a/web/app/(app)/admin/vorlagen/TemplateEditor.tsx +++ b/web/app/(app)/admin/vorlagen/TemplateEditor.tsx @@ -1,7 +1,8 @@ 'use client' import { useMemo, useState, useTransition } from 'react' -import { de } from '@/lib/i18n/de' +import { templateAdminText } from '@/lib/i18n/admin' +import { caseText, sectionText } from '@/lib/i18n/cases' import { isInputField, looksLikeFields, type TemplateField } from '@/lib/templates/fields' import { saveTemplate } from './actions' @@ -41,11 +42,11 @@ export function TemplateEditor({ vorlage }: { vorlage: Vorlage }) { // Nur eine grobe Formprüfung für die Vorschau. Ob die Vorlage // gespeichert werden darf, entscheidet allein der Server. if (!looksLikeFields(roh)) { - return { ok: false as const, fehler: de.templateAdmin.fieldsInvalid } + return { ok: false as const, fehler: templateAdminText.fieldsInvalid } } return { ok: true as const, fields: roh } } catch { - return { ok: false as const, fehler: de.templateAdmin.invalid } + return { ok: false as const, fehler: templateAdminText.invalid } } }, [json]) @@ -62,7 +63,7 @@ export function TemplateEditor({ vorlage }: { vorlage: Vorlage }) { numberPadding, fields: geparst.fields, }) - if (antwort.ok) setResult(de.templateAdmin.saved(antwort.version)) + if (antwort.ok) setResult(templateAdminText.saved(antwort.version)) else setError(antwort.error) }) } @@ -73,14 +74,14 @@ export function TemplateEditor({ vorlage }: { vorlage: Vorlage }) {

{vorlage.name}

{vorlage.key} - {de.templateAdmin.version(vorlage.version)} + {templateAdminText.version(vorlage.version)}