From 93051f791dc7babbbc0945f21e2a6e8a3419b92a Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 7 Sep 2026 12:21:53 +0200 Subject: [PATCH] First --- Dockerfile | 28 + README.md | 65 + backend/package-lock.json | 2366 ++++++++++++++++++++++++++++++ backend/package.json | 16 + backend/public/admin/admin.css | 46 + backend/public/admin/admin.js | 158 ++ backend/public/admin/index.html | 61 + backend/public/book/book.css | 52 + backend/public/book/book.js | 147 ++ backend/public/book/index.html | 32 + backend/public/friend/friend.css | 38 + backend/public/friend/friend.js | 270 ++++ backend/public/friend/index.html | 82 ++ backend/public/login/index.html | 53 + backend/public/shared/style.css | 126 ++ backend/server.js | 47 + backend/src/auth.js | 25 + backend/src/pdf.js | 167 +++ backend/src/routes/admin.js | 111 ++ backend/src/routes/friend.js | 159 ++ backend/src/routes/uploads.js | 27 + backend/src/utils/hash.js | 14 + backend/src/utils/storage.js | 115 ++ data/.gitkeep | 0 docker-compose.yml | 21 + 25 files changed, 4226 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/public/admin/admin.css create mode 100644 backend/public/admin/admin.js create mode 100644 backend/public/admin/index.html create mode 100644 backend/public/book/book.css create mode 100644 backend/public/book/book.js create mode 100644 backend/public/book/index.html create mode 100644 backend/public/friend/friend.css create mode 100644 backend/public/friend/friend.js create mode 100644 backend/public/friend/index.html create mode 100644 backend/public/login/index.html create mode 100644 backend/public/shared/style.css create mode 100644 backend/server.js create mode 100644 backend/src/auth.js create mode 100644 backend/src/pdf.js create mode 100644 backend/src/routes/admin.js create mode 100644 backend/src/routes/friend.js create mode 100644 backend/src/routes/uploads.js create mode 100644 backend/src/utils/hash.js create mode 100644 backend/src/utils/storage.js create mode 100644 data/.gitkeep create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5912eb5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:20-bookworm-slim + +# Chromium + fonts for Puppeteer (PDF-Export) and nice handwriting fonts in the export +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + fonts-liberation \ + fonts-noto-color-emoji \ + ca-certificates \ + wget \ + && rm -rf /var/lib/apt/lists/* + +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ + PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \ + NODE_ENV=production + +WORKDIR /app + +COPY backend/package.json ./ +RUN npm install --omit=dev + +COPY backend/ . + +RUN mkdir -p /data/friends +VOLUME ["/data"] + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..063f0e9 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# 📖 Freundesbuch (digital) + +Ein selbst gehostetes, digitales Freundschaftsbuch. Kein Benutzer-System – nur ein +Admin-Passwort fĂŒr dich, und pro Freund ein frei wĂ€hlbares Freigabe-Passwort zum Teilen. + +## Funktionen + +- **Ein Passwort** fĂŒr den Admin-Bereich (als SHA-256-Hash in `docker-compose.yml`, kein Klartext). +- Freunde anlegen – jeder Freund ist ein eigener Ordner auf dem Server (`Name-zufallsID`). +- Jeder Freund hat ein **Profilbild** und einen **Steckbrief** (klassische Freundebuch-Felder). +- Beliebig viele **Blöcke** pro Freund: đŸ–Œïž Bild, 📝 Text, đŸŽ™ïž Audio (MP3 o. Ă€.) – alles wird + im jeweiligen Freundes-Ordner gespeichert. +- **Freigabe-Link**: FĂŒr jeden Freund kannst du einen Link mit eigenem Passwort erzeugen. + Damit kann sich der Freund **nur** in seine eigene Seite einloggen und dort Inhalte + hinzufĂŒgen/bearbeiten. Die Freigabe kannst du jederzeit als Admin beenden. +- **Freundesbuch-Ansicht**: verspieltes Scrapbook-Layout mit schrĂ€g angeordneten + Polaroid-Fotos, Washi-Tape-Optik usw. – seitenweise durchblĂ€tterbar. +- **PDF-Export** des gesamten Freundesbuchs (serverseitig ĂŒber Puppeteer/Chromium gerendert). + +## Setup + +1. **Admin-Passwort festlegen** – Hash erzeugen: + ```bash + echo -n "DeinPasswort" | sha256sum + ``` + (macOS: `shasum -a 256`, Windows/PowerShell: `certutil -hashfile datei.txt SHA256` oder + ein Online-Tool deines Vertrauens – wichtig ist nur SHA-256 in Kleinbuchstaben-Hex.) + + Den Hash in `docker-compose.yml` bei `ADMIN_PASSWORD_HASH` eintragen. + + ⚠ Der mitgelieferte Standard-Hash entspricht dem Passwort `aendern123` – + bitte vor dem Livegang unbedingt Ă€ndern! + +2. **Session-Secret Ă€ndern** – `SESSION_SECRET` in `docker-compose.yml` durch eine lange, + zufĂ€llige Zeichenkette ersetzen (z. B. `openssl rand -hex 32`). + +3. **Starten**: + ```bash + docker compose up -d --build + ``` + Die Anwendung lĂ€uft danach auf `http://:3000`. + +4. Alle Daten (Freunde-Ordner, Fotos, Texte, Audios) landen im lokalen Ordner `./data`, + der als Volume eingebunden ist – so bleiben sie auch bei Container-Updates erhalten. + +## Nutzung + +- **/** – Login mit dem Admin-Passwort. +- **/admin** – Übersicht: Freunde anlegen, bearbeiten, Freigabe verwalten, löschen. +- **/friend/\** – Freund bearbeiten (Profilbild, Steckbrief, Blöcke) – fĂŒr dich als Admin. +- **/share/\** – der Link, den du deinem Freund gibst. Er meldet sich mit dem von dir + vergebenen Freigabe-Passwort an und kann **nur** dort etwas eintragen. +- **/book** – die verspielte Freundesbuch-Ansicht mit BlĂ€tterfunktion und PDF-Export + (Button oben rechts). + +## Hinweise + +- Ordnerstruktur pro Freund: `data/friends/-/` + mit `meta.json` (Steckbrief, Blöcke, Freigabe-Status) und Unterordner `uploads/` + fĂŒr Fotos/Audios. +- Erlaubte Upload-Typen: Bilder (jpg, png, gif, webp) und Audio (mp3, wav, ogg, m4a), + maximal 40 MB pro Datei. +- Der PDF-Export rendert das Buch serverseitig mit Chromium (im Docker-Image enthalten) – + dafĂŒr braucht der Container etwas RAM (mind. 512 MB empfohlen). +- Es gibt bewusst **keine** Benutzernamen oder E-Mail-Adressen – nur Passwörter. diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..c3001a3 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2366 @@ +{ + "name": "freundesbuch-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "freundesbuch-backend", + "version": "1.0.0", + "dependencies": { + "cookie-session": "^2.1.0", + "express": "^4.19.2", + "multer": "^2.0.0", + "puppeteer": "^22.15.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", + "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", + "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-session": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz", + "integrity": "sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==", + "license": "MIT", + "dependencies": { + "cookies": "0.9.1", + "debug": "3.2.7", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "license": "BSD-3-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz", + "integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==", + "deprecated": "< 24.15.0 is no longer supported", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1312386", + "puppeteer-core": "22.15.0" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..1a41ecf --- /dev/null +++ b/backend/package.json @@ -0,0 +1,16 @@ +{ + "name": "freundesbuch-backend", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "cookie-session": "^2.1.0", + "express": "^4.19.2", + "multer": "^2.0.0", + "puppeteer": "^22.15.0" + } +} diff --git a/backend/public/admin/admin.css b/backend/public/admin/admin.css new file mode 100644 index 0000000..97307e3 --- /dev/null +++ b/backend/public/admin/admin.css @@ -0,0 +1,46 @@ +.inline-form { display: flex; gap: 0.6em; flex-wrap: wrap; margin-top: 0.6em; } +.inline-form input { flex: 1; min-width: 220px; } + +.friend-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1.2em; + margin-top: 1em; +} + +.friend-card { + background: var(--paper-2); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.2em; + position: relative; + text-align: center; +} +.friend-card .tape { top: -10px; left: 50%; transform: translateX(-50%) rotate(-3deg); } + +.friend-thumb { + width: 84px; height: 84px; border-radius: 50%; + object-fit: cover; border: 4px solid #fff; box-shadow: var(--shadow); + margin-bottom: 0.5em; +} +.friend-thumb.placeholder { + display: flex; align-items: center; justify-content: center; + background: var(--rose); color: #fff; font-size: 1.8rem; font-family: 'Caveat', cursive; +} + +.friend-card h3 { margin-bottom: 0.15em; } +.badge { + display: inline-block; font-size: 0.72rem; font-weight: 700; border-radius: 999px; + padding: 0.15em 0.7em; margin-bottom: 0.6em; +} +.badge.on { background: var(--sage); color: #245a3a; } +.badge.off { background: #eee; color: #888; } + +.friend-actions { display: flex; gap: 0.4em; justify-content: center; flex-wrap: wrap; margin-top: 0.6em; } +.friend-actions button, .friend-actions a.btn { font-size: 0.85rem; padding: 0.5em 1em; } + +.modal-backdrop { + position: fixed; inset: 0; background: rgba(75,59,107,0.35); + display: flex; align-items: center; justify-content: center; z-index: 50; padding: 1em; +} +.modal { width: 100%; max-width: 420px; } diff --git a/backend/public/admin/admin.js b/backend/public/admin/admin.js new file mode 100644 index 0000000..352128f --- /dev/null +++ b/backend/public/admin/admin.js @@ -0,0 +1,158 @@ +let friends = []; +let shareTargetId = null; + +async function checkSession() { + const res = await fetch('/api/session'); + const data = await res.json(); + if (data.role !== 'admin') { + window.location.href = '/'; + } +} + +async function loadFriends() { + const res = await fetch('/api/friends'); + if (res.status === 401) { window.location.href = '/'; return; } + const data = await res.json(); + friends = data.friends || []; + renderFriends(); +} + +function renderFriends() { + const grid = document.getElementById('friendGrid'); + const emptyMsg = document.getElementById('emptyMsg'); + grid.innerHTML = ''; + emptyMsg.style.display = friends.length ? 'none' : 'block'; + + friends.forEach((f) => { + const el = document.createElement('div'); + el.className = 'friend-card'; + const thumbHtml = f.profileImage + ? `` + : `
${(f.name || '?')[0].toUpperCase()}
`; + el.innerHTML = ` +
+ ${thumbHtml} +

${escapeHtml(f.name)}

+ ${f.shareEnabled ? 'Freigegeben' : 'Nicht freigegeben'} +
+ ✏ Bearbeiten + + +
+ `; + grid.appendChild(el); + }); + + grid.querySelectorAll('[data-share]').forEach((btn) => { + btn.addEventListener('click', () => openShareModal(btn.dataset.share)); + }); + grid.querySelectorAll('[data-delete]').forEach((btn) => { + btn.addEventListener('click', () => deleteFriend(btn.dataset.delete)); + }); +} + +// wir brauchen den vollen Ordnernamen fĂŒr Bild-URLs; da wir ihn nicht separat liefern, +// nutzen wir /uploads/-Lookup ĂŒber eine kleine Konvention: Backend akzeptiert auch +// reine IDs nicht direkt, daher holen wir folderName aus friend-detail bei Bedarf. +// Einfachere Lösung: Backend liefert profileImage-Pfad bereits vollstĂ€ndig auf. +function folderFor(f) { + return f.folderName || f.id; +} + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str || ''; + return div.innerHTML; +} + +document.getElementById('createForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const name = document.getElementById('newName').value.trim(); + const errBox = document.getElementById('createErr'); + errBox.innerHTML = ''; + if (!name) return; + const res = await fetch('/api/friends', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }); + const data = await res.json(); + if (!res.ok) { + errBox.innerHTML = `

${data.error}

`; + return; + } + document.getElementById('newName').value = ''; + loadFriends(); +}); + +async function deleteFriend(id) { + const f = friends.find((x) => x.id === id); + if (!confirm(`"${f ? f.name : 'Freund'}" wirklich löschen? Alle Fotos und EintrÀge gehen dabei verloren.`)) return; + const res = await fetch(`/api/friends/${id}`, { method: 'DELETE' }); + if (res.ok) loadFriends(); +} + +function openShareModal(id) { + shareTargetId = id; + const f = friends.find((x) => x.id === id); + document.getElementById('shareFriendName').textContent = f ? f.name : ''; + document.getElementById('sharePw').value = ''; + document.getElementById('shareErr').innerHTML = ''; + const linkBox = document.getElementById('shareLinkBox'); + if (f && f.shareEnabled) { + linkBox.style.display = 'block'; + document.getElementById('shareLinkInput').value = `${window.location.origin}/share/${id}`; + } else { + linkBox.style.display = 'none'; + } + document.getElementById('shareModal').style.display = 'flex'; +} + +document.getElementById('shareCloseBtn').addEventListener('click', () => { + document.getElementById('shareModal').style.display = 'none'; +}); + +document.getElementById('shareEnableBtn').addEventListener('click', async () => { + const pw = document.getElementById('sharePw').value; + const errBox = document.getElementById('shareErr'); + errBox.innerHTML = ''; + const res = await fetch(`/api/friends/${shareTargetId}/share`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: true, password: pw }), + }); + const data = await res.json(); + if (!res.ok) { + errBox.innerHTML = `

${data.error}

`; + return; + } + document.getElementById('shareLinkBox').style.display = 'block'; + document.getElementById('shareLinkInput').value = `${window.location.origin}/share/${shareTargetId}`; + await loadFriends(); +}); + +document.getElementById('shareDisableBtn').addEventListener('click', async () => { + const res = await fetch(`/api/friends/${shareTargetId}/share`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: false }), + }); + if (res.ok) { + document.getElementById('shareLinkBox').style.display = 'none'; + await loadFriends(); + } +}); + +document.getElementById('copyLinkBtn').addEventListener('click', () => { + const input = document.getElementById('shareLinkInput'); + input.select(); + navigator.clipboard.writeText(input.value).catch(() => {}); +}); + +document.getElementById('logoutBtn').addEventListener('click', async () => { + await fetch('/api/logout', { method: 'POST' }); + window.location.href = '/'; +}); + +checkSession(); +loadFriends(); diff --git a/backend/public/admin/index.html b/backend/public/admin/index.html new file mode 100644 index 0000000..4a20b8b --- /dev/null +++ b/backend/public/admin/index.html @@ -0,0 +1,61 @@ + + + + + +Freundesbuch – Admin + + + + +
+

📖 Freundesbuch

+ +
+ +
+
+

Neuen Freund anlegen

+
+ + +
+
+
+ +
+

Deine Freunde

+
+ +
+
+ + + + + + + diff --git a/backend/public/book/book.css b/backend/public/book/book.css new file mode 100644 index 0000000..e6c76f4 --- /dev/null +++ b/backend/public/book/book.css @@ -0,0 +1,52 @@ +.book-root { max-width: 1000px; margin: 0 auto; padding: 2em 1.2em 6em; min-height: 60vh; } + +.friend-page { position: relative; } + +.page-head { text-align: center; margin-bottom: 1.4em; position: relative; } +.page-head .tape { top: -10px; left: 50%; transform: translateX(-50%) rotate(-4deg); } +.page-profile-pic { + width: 150px; height: 150px; border-radius: 50%; object-fit: cover; + border: 8px solid #fff; box-shadow: var(--shadow); transform: rotate(-3deg); +} +.page-profile-pic.placeholder { + display: flex; align-items: center; justify-content: center; + background: var(--rose); color: #fff; font-size: 3rem; font-family: 'Caveat', cursive; +} +.page-head h1 { font-size: 3.2rem; margin-top: 0.3em; } + +.steckbrief-card { + background: var(--paper-2); border: 2px dashed var(--mustard); border-radius: 14px; + padding: 1.2em 1.6em; margin: 0 auto 2em; max-width: 640px; transform: rotate(-0.4deg); + box-shadow: var(--shadow); +} +.steckbrief-card h2 { color: var(--rust); } +.sb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.6em 1.2em; } +.sb-row .sb-key { font-weight: 700; color: var(--ink-soft); font-size: 0.82rem; display: block; } +.sb-row .sb-val { font-size: 0.95rem; } + +.scrap-grid { + display: flex; flex-wrap: wrap; gap: 2em 1.6em; justify-content: center; padding: 1em 0.5em; +} + +.scrap-item { background: #fff; box-shadow: 0 6px 16px rgba(0,0,0,0.16); transition: transform 0.15s ease; } +.scrap-item:hover { transform: scale(1.04) rotate(0deg) !important; z-index: 2; } + +.scrap-item.polaroid { padding: 10px 10px 30px; width: 220px; } +.scrap-item.polaroid img { width: 100%; height: 190px; object-fit: cover; display: block; } + +.scrap-item.text-note { width: 220px; min-height: 160px; padding: 1.2em; background: #fef7d6; font-family: Georgia, serif; } + +.scrap-item.audio-note { width: 220px; padding: 1.2em; background: #e8f3ec; text-align: center; } +.scrap-item.audio-note audio { width: 100%; margin-top: 0.6em; } + +.scrap-caption { font-family: 'Caveat', cursive; font-size: 1.2rem; text-align: center; margin-top: 0.4em; color: var(--ink-soft); } + +.book-nav { + position: fixed; bottom: 0; left: 0; right: 0; + background: var(--paper-2); border-top: 2px solid var(--line); + display: flex; align-items: center; justify-content: center; gap: 1.4em; + padding: 0.8em; box-shadow: 0 -4px 12px rgba(0,0,0,0.08); +} +#pageIndicator { font-family: 'Caveat', cursive; font-size: 1.3rem; color: var(--ink-soft); } + +.empty-book { text-align: center; margin-top: 3em; } diff --git a/backend/public/book/book.js b/backend/public/book/book.js new file mode 100644 index 0000000..acec4b1 --- /dev/null +++ b/backend/public/book/book.js @@ -0,0 +1,147 @@ +let friends = []; +let currentIndex = 0; + +const STECKBRIEF_LABELS = { + spitzname: 'Spitzname', + geburtstag: 'Geburtstag', + adresse: 'Adresse / Wohnort', + lieblingsfarbe: 'Lieblingsfarbe', + lieblingsessen: 'Lieblingsessen', + lieblingstier: 'Lieblingstier', + hobbys: 'Hobbys', + lieblingsmusik: 'Lieblingsmusik', + lieblingsfilm: 'Lieblingsfilm/-serie', + traumberuf: 'Traumberuf', + bestefreunde: 'Beste Freunde', + motto: 'Motto / Lebensweisheit', + sonstiges: 'Sonstiges', +}; + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str || ''; + return div.innerHTML; +} + +function randomRotation(seed) { + const angles = [-6, -4, -2, 2, 4, 6, -3, 3, 5, -5]; + return angles[seed % angles.length]; +} + +async function load() { + const res = await fetch('/api/book'); + if (res.status === 401) { window.location.href = '/'; return; } + const data = await res.json(); + friends = data.friends || []; + if (!friends.length) { + document.getElementById('bookRoot').innerHTML = ` +
+

Noch ist dieses Buch leer 📖

+

Lege im Admin-Bereich Freunde an und teile den Link mit ihnen.

+
`; + return; + } + document.getElementById('bookNav').style.display = 'flex'; + renderPage(); +} + +function uploadUrl(folder, filename) { + return `/uploads/${encodeURIComponent(folder)}/${encodeURIComponent(filename)}`; +} + +function renderPage() { + const f = friends[currentIndex]; + const root = document.getElementById('bookRoot'); + + const profileHtml = f.profileImage + ? `` + : `
${(f.name || '?')[0].toUpperCase()}
`; + + const sbEntries = Object.entries(f.steckbrief || {}).filter(([, v]) => v && String(v).trim()); + const sbHtml = sbEntries.length + ? `
${sbEntries.map(([k, v]) => ` +
${escapeHtml(STECKBRIEF_LABELS[k] || k)}${escapeHtml(v)}
+ `).join('')}
` + : `

Noch nichts eingetragen.

`; + + const blocks = [...(f.blocks || [])].sort((a, b) => a.order - b.order); + const blocksHtml = blocks.length + ? blocks.map((b, i) => { + const rot = randomRotation(i + f.id.length); + if (b.type === 'image') { + return `
+ + ${b.caption ? `
${escapeHtml(b.caption)}
` : ''} +
`; + } + if (b.type === 'audio') { + return `
+
đŸŽ™ïž ${b.caption ? escapeHtml(b.caption) : 'Sprachnachricht'}
+ +
`; + } + return `
+

${escapeHtml(b.content).replace(/\n/g, '
')}

+ ${b.caption ? `
${escapeHtml(b.caption)}
` : ''} +
`; + }).join('') + : '

Noch keine Erinnerungen hinzugefĂŒgt.

'; + + root.innerHTML = ` +
+
+
+ ${profileHtml} +

${escapeHtml(f.name)}

+
+
+

Steckbrief

+ ${sbHtml} +
+
+ ${blocksHtml} +
+
+ `; + + document.getElementById('pageIndicator').textContent = `Seite ${currentIndex + 1} von ${friends.length}`; + document.getElementById('prevBtn').disabled = currentIndex === 0; + document.getElementById('nextBtn').disabled = currentIndex === friends.length - 1; +} + +document.getElementById('prevBtn').addEventListener('click', () => { + if (currentIndex > 0) { currentIndex--; renderPage(); window.scrollTo({ top: 0, behavior: 'smooth' }); } +}); +document.getElementById('nextBtn').addEventListener('click', () => { + if (currentIndex < friends.length - 1) { currentIndex++; renderPage(); window.scrollTo({ top: 0, behavior: 'smooth' }); } +}); +document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft') document.getElementById('prevBtn').click(); + if (e.key === 'ArrowRight') document.getElementById('nextBtn').click(); +}); + +document.getElementById('pdfBtn').addEventListener('click', async () => { + const btn = document.getElementById('pdfBtn'); + btn.disabled = true; + btn.textContent = 'Erzeuge PDF...'; + try { + const res = await fetch('/api/book/pdf'); + if (!res.ok) throw new Error('PDF-Export fehlgeschlagen.'); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'freundesbuch.pdf'; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch (err) { + alert(err.message); + } finally { + btn.disabled = false; + btn.textContent = 'âŹ‡ïž Als PDF exportieren'; + } +}); + +load(); diff --git a/backend/public/book/index.html b/backend/public/book/index.html new file mode 100644 index 0000000..27b1d3a --- /dev/null +++ b/backend/public/book/index.html @@ -0,0 +1,32 @@ + + + + + +Freundesbuch – Ansicht + + + + + +
+

🎠 Freundesbuch

+
+ + ← Zur Übersicht +
+
+ +
+

LĂ€dt...

+
+ + + + + + diff --git a/backend/public/friend/friend.css b/backend/public/friend/friend.css new file mode 100644 index 0000000..7be9fcd --- /dev/null +++ b/backend/public/friend/friend.css @@ -0,0 +1,38 @@ +.profile-preview { + width: 140px; height: 140px; border-radius: 50%; + object-fit: cover; border: 6px solid #fff; box-shadow: var(--shadow); + margin: 0.4em auto 0; display: block; +} +.profile-preview.placeholder { + display: flex; align-items: center; justify-content: center; + font-size: 2.4rem; background: var(--paper); color: var(--rose); +} + +.steckbrief-form { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1em; +} + +.saved-msg { margin-left: 0.8em; color: #3a8a5a; font-weight: 700; opacity: 0; transition: opacity 0.3s; } +.saved-msg.show { opacity: 1; } + +.add-block-tabs { display: flex; gap: 0.5em; margin: 0.8em 0; flex-wrap: wrap; } +.tab-btn { background: #fff; color: var(--ink-soft); border: 2px solid var(--line); box-shadow: none; padding: 0.5em 1em; } +.tab-btn.active { background: var(--ink-soft); color: #fff; border-color: var(--ink-soft); } + +.blocks-list { display: flex; flex-wrap: wrap; gap: 1.2em; margin-top: 1.4em; } +.block-item { + background: var(--paper-2); border-radius: 10px; box-shadow: var(--shadow); + padding: 1em; width: 220px; position: relative; +} +.block-item img { width: 100%; height: 140px; object-fit: cover; border-radius: 6px; } +.block-item audio { width: 100%; margin-top: 0.4em; } +.block-item textarea { min-height: 4em; font-size: 0.9rem; } +.block-item .block-caption { font-size: 0.82rem; margin-top: 0.5em; } +.block-item .block-actions { display: flex; justify-content: space-between; margin-top: 0.6em; gap: 0.4em; } +.block-item .block-actions button { font-size: 0.78rem; padding: 0.35em 0.7em; } +.block-type-tag { + position: absolute; top: -8px; left: 10px; font-size: 0.7rem; font-weight: 700; + background: var(--mustard); color: #533; padding: 0.1em 0.6em; border-radius: 999px; +} diff --git a/backend/public/friend/friend.js b/backend/public/friend/friend.js new file mode 100644 index 0000000..3f98610 --- /dev/null +++ b/backend/public/friend/friend.js @@ -0,0 +1,270 @@ +const STECKBRIEF_FIELDS = [ + ['spitzname', 'Spitzname'], + ['geburtstag', 'Geburtstag'], + ['adresse', 'Adresse / Wohnort'], + ['lieblingsfarbe', 'Lieblingsfarbe'], + ['lieblingsessen', 'Lieblingsessen'], + ['lieblingstier', 'Lieblingstier'], + ['hobbys', 'Hobbys'], + ['lieblingsmusik', 'Lieblingsmusik'], + ['lieblingsfilm', 'Lieblingsfilm/-serie'], + ['traumberuf', 'Traumberuf'], + ['bestefreunde', 'Beste Freunde'], + ['motto', 'Motto / Lebensweisheit'], + ['sonstiges', 'Sonstiges'], +]; + +const pathParts = window.location.pathname.split('/').filter(Boolean); // ['friend', ':id'] or ['share', ':id'] +const mode = pathParts[0]; // 'friend' | 'share' +const friendId = pathParts[1]; + +let currentFriend = null; +let currentFolder = null; +let selectedType = 'image'; + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str || ''; + return div.innerHTML; +} + +async function init() { + const sessionRes = await fetch('/api/session'); + const session = await sessionRes.json(); + + const isAdmin = session.role === 'admin'; + const isMatchingFriend = session.role === 'friend' && session.friendId === friendId; + + document.getElementById('adminBackWrap').style.display = isAdmin ? 'block' : 'none'; + + if (isAdmin || isMatchingFriend) { + loadFriend(); + } else if (mode === 'share') { + showLoginGate(); + } else { + // /friend/:id ohne Admin-Session -> zur Startseite + window.location.href = '/'; + } +} + +function showLoginGate() { + document.getElementById('loginGate').style.display = 'block'; + document.getElementById('editorRoot').style.display = 'none'; +} + +document.getElementById('gateForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const password = document.getElementById('gatePassword').value; + const errBox = document.getElementById('gateErr'); + errBox.innerHTML = ''; + const res = await fetch(`/api/share/${friendId}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + const data = await res.json(); + if (!res.ok) { + errBox.innerHTML = `

${data.error}

`; + return; + } + loadFriend(); +}); + +async function loadFriend() { + const res = await fetch(`/api/friends/${friendId}`); + if (res.status === 401) { showLoginGate(); return; } + if (res.status === 404) { document.body.innerHTML = '

Diesen Freund gibt es nicht (mehr).

'; return; } + const data = await res.json(); + currentFriend = data.friend; + currentFolder = currentFriend.folderName; + + document.getElementById('loginGate').style.display = 'none'; + document.getElementById('editorRoot').style.display = 'block'; + document.getElementById('pageTitle').textContent = `📖 ${currentFriend.name}`; + + renderProfileImage(); + renderSteckbrief(); + renderBlocks(); +} + +function uploadUrl(filename) { + return `/uploads/${encodeURIComponent(currentFolder)}/${encodeURIComponent(filename)}`; +} + +function renderProfileImage() { + const img = document.getElementById('profileImg'); + const placeholder = document.getElementById('profilePlaceholder'); + if (currentFriend.profileImage) { + img.src = uploadUrl(currentFriend.profileImage) + `?t=${Date.now()}`; + img.style.display = 'block'; + placeholder.style.display = 'none'; + } else { + img.style.display = 'none'; + placeholder.style.display = 'flex'; + } +} + +document.getElementById('profileInput').addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + const fd = new FormData(); + fd.append('image', file); + const res = await fetch(`/api/friends/${friendId}/profileimage`, { method: 'POST', body: fd }); + const data = await res.json(); + if (res.ok) { + currentFriend = data.friend; + renderProfileImage(); + } else { + alert(data.error || 'Upload fehlgeschlagen.'); + } +}); + +function renderSteckbrief() { + const form = document.getElementById('steckbriefForm'); + form.innerHTML = ''; + STECKBRIEF_FIELDS.forEach(([key, label]) => { + const wrap = document.createElement('div'); + const val = (currentFriend.steckbrief && currentFriend.steckbrief[key]) || ''; + wrap.innerHTML = ``; + form.appendChild(wrap); + }); +} + +document.getElementById('steckbriefForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const inputs = document.querySelectorAll('#steckbriefForm input'); + const payload = {}; + inputs.forEach((inp) => { payload[inp.dataset.key] = inp.value; }); + const res = await fetch(`/api/friends/${friendId}/steckbrief`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (res.ok) { + currentFriend = data.friend; + const savedEl = document.getElementById('sbSaved'); + savedEl.textContent = 'Gespeichert ✓'; + savedEl.classList.add('show'); + setTimeout(() => savedEl.classList.remove('show'), 1800); + } else { + alert(data.error || 'Speichern fehlgeschlagen.'); + } +}); + +// --- Block-Typ-Auswahl --- +document.querySelectorAll('.tab-btn').forEach((btn) => { + btn.addEventListener('click', () => { + selectedType = btn.dataset.type; + document.querySelectorAll('.tab-btn').forEach((b) => b.classList.toggle('active', b === btn)); + document.getElementById('blockType').value = selectedType; + const fileWrap = document.getElementById('blockFileWrap'); + const textWrap = document.getElementById('blockTextWrap'); + const fileLabel = document.getElementById('blockFileLabel'); + const fileInput = document.getElementById('blockFile'); + if (selectedType === 'text') { + fileWrap.style.display = 'none'; + textWrap.style.display = 'block'; + } else { + fileWrap.style.display = 'block'; + textWrap.style.display = 'none'; + fileLabel.textContent = selectedType === 'image' ? 'Bild auswĂ€hlen' : 'MP3 auswĂ€hlen'; + fileInput.accept = selectedType === 'image' ? 'image/*' : 'audio/*'; + } + }); +}); + +document.getElementById('blockForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const fd = new FormData(); + fd.append('type', selectedType); + fd.append('caption', document.getElementById('blockCaption').value); + if (selectedType === 'text') { + fd.append('content', document.getElementById('blockContent').value); + } else { + const file = document.getElementById('blockFile').files[0]; + if (!file) { alert('Bitte eine Datei auswĂ€hlen.'); return; } + fd.append('file', file); + } + const res = await fetch(`/api/friends/${friendId}/blocks`, { method: 'POST', body: fd }); + const data = await res.json(); + if (res.ok) { + currentFriend = data.friend; + document.getElementById('blockForm').reset(); + renderBlocks(); + } else { + alert(data.error || 'HinzufĂŒgen fehlgeschlagen.'); + } +}); + +function renderBlocks() { + const list = document.getElementById('blocksList'); + list.innerHTML = ''; + const blocks = [...(currentFriend.blocks || [])].sort((a, b) => a.order - b.order); + blocks.forEach((b, idx) => { + const el = document.createElement('div'); + el.className = 'block-item'; + const tag = { image: 'đŸ–Œïž Bild', text: '📝 Text', audio: 'đŸŽ™ïž Audio' }[b.type] || b.type; + let inner = `
${tag}
`; + if (b.type === 'image') { + inner += ``; + } else if (b.type === 'audio') { + inner += ``; + } else { + inner += ``; + } + inner += ``; + inner += `
+ + + + +
`; + el.innerHTML = inner; + list.appendChild(el); + }); + + list.querySelectorAll('[data-save]').forEach((btn) => btn.addEventListener('click', () => saveBlock(btn.dataset.save))); + list.querySelectorAll('[data-del]').forEach((btn) => btn.addEventListener('click', () => deleteBlock(btn.dataset.del))); + list.querySelectorAll('[data-up]').forEach((btn) => btn.addEventListener('click', () => moveBlock(btn.dataset.up, -1))); + list.querySelectorAll('[data-down]').forEach((btn) => btn.addEventListener('click', () => moveBlock(btn.dataset.down, 1))); +} + +async function saveBlock(id) { + const captionInput = document.querySelector(`[data-caption-for="${id}"]`); + const contentInput = document.querySelector(`[data-content-for="${id}"]`); + const payload = { caption: captionInput ? captionInput.value : '' }; + if (contentInput) payload.content = contentInput.value; + const res = await fetch(`/api/friends/${friendId}/blocks/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (res.ok) { currentFriend = data.friend; renderBlocks(); } +} + +async function deleteBlock(id) { + if (!confirm('Diesen Block wirklich löschen?')) return; + const res = await fetch(`/api/friends/${friendId}/blocks/${id}`, { method: 'DELETE' }); + const data = await res.json(); + if (res.ok) { currentFriend = data.friend; renderBlocks(); } +} + +async function moveBlock(id, dir) { + const blocks = [...(currentFriend.blocks || [])].sort((a, b) => a.order - b.order); + const idx = blocks.findIndex((b) => b.id === id); + const swapIdx = idx + dir; + if (swapIdx < 0 || swapIdx >= blocks.length) return; + [blocks[idx], blocks[swapIdx]] = [blocks[swapIdx], blocks[idx]]; + const order = blocks.map((b) => b.id); + const res = await fetch(`/api/friends/${friendId}/blocks/reorder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order }), + }); + const data = await res.json(); + if (res.ok) { currentFriend = data.friend; renderBlocks(); } +} + +init(); diff --git a/backend/public/friend/index.html b/backend/public/friend/index.html new file mode 100644 index 0000000..4fc4f81 --- /dev/null +++ b/backend/public/friend/index.html @@ -0,0 +1,82 @@ + + + + + +Freundesbuch + + + + + + + + + + + + diff --git a/backend/public/login/index.html b/backend/public/login/index.html new file mode 100644 index 0000000..b91f80d --- /dev/null +++ b/backend/public/login/index.html @@ -0,0 +1,53 @@ + + + + + +Freundesbuch + + + + + + + + + diff --git a/backend/public/shared/style.css b/backend/public/shared/style.css new file mode 100644 index 0000000..f8daa7c --- /dev/null +++ b/backend/public/shared/style.css @@ -0,0 +1,126 @@ +@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=Nunito:ital,wght@0,400;0,600;0,700;1,600&display=swap'); + +:root { + --paper: #faf3e6; + --paper-2: #fffdf7; + --ink: #4b3b6b; + --ink-soft: #6b4f8c; + --rust: #c9622f; + --mustard: #e8b84b; + --sage: #a8d5bA; + --rose: #e8a0bf; + --line: #d9c9a8; + --radius: 14px; + --shadow: 0 6px 18px rgba(75, 59, 107, 0.14); +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + min-height: 100%; + background: var(--paper); + background-image: + radial-gradient(circle at 8% 12%, rgba(232, 160, 191, 0.14), transparent 38%), + radial-gradient(circle at 92% 88%, rgba(168, 213, 186, 0.16), transparent 42%); + color: var(--ink); + font-family: 'Nunito', sans-serif; + font-size: 16px; + line-height: 1.5; +} + +h1, h2, h3, .display { + font-family: 'Caveat', cursive; + color: var(--ink-soft); + font-weight: 700; + margin: 0 0 0.3em; + letter-spacing: 0.2px; +} + +h1 { font-size: 2.6rem; } +h2 { font-size: 1.9rem; } +h3 { font-size: 1.5rem; } + +a { color: var(--rust); } + +button, .btn { + font-family: 'Nunito', sans-serif; + font-weight: 700; + border: none; + border-radius: 999px; + padding: 0.65em 1.4em; + cursor: pointer; + background: var(--ink-soft); + color: #fff; + box-shadow: var(--shadow); + transition: transform 0.12s ease, box-shadow 0.12s ease; +} +button:hover, .btn:hover { transform: translateY(-1px); } +button:active, .btn:active { transform: translateY(0); } +button:disabled { opacity: 0.55; cursor: not-allowed; transform: none; } + +button.secondary { background: #fff; color: var(--ink-soft); border: 2px solid var(--ink-soft); box-shadow: none; } +button.danger { background: var(--rust); } +button.ghost { background: transparent; color: var(--ink-soft); box-shadow: none; padding: 0.4em 0.8em; } + +input[type="text"], input[type="password"], textarea, select { + font-family: 'Nunito', sans-serif; + font-size: 1rem; + padding: 0.6em 0.8em; + border: 2px solid var(--line); + border-radius: 10px; + background: var(--paper-2); + color: var(--ink); + width: 100%; +} +textarea { resize: vertical; min-height: 5em; } +input:focus, textarea:focus, select:focus, button:focus-visible { + outline: 3px solid var(--rose); + outline-offset: 1px; +} +label { font-weight: 700; font-size: 0.85rem; display: block; margin-bottom: 0.3em; color: var(--ink-soft); } + +.card { + background: var(--paper-2); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.4em; +} + +.tape { + position: absolute; + width: 60px; + height: 22px; + background: rgba(232, 184, 75, 0.55); + box-shadow: 0 1px 2px rgba(0,0,0,0.08); +} + +.error-msg { + background: #fce8e8; + color: #a33; + border-radius: 10px; + padding: 0.6em 1em; + font-weight: 600; + font-size: 0.9rem; +} + +.muted { color: #9a8fb0; font-style: italic; } + +.top-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1em 1.5em; + flex-wrap: wrap; + gap: 0.6em; +} + +.container { + max-width: 980px; + margin: 0 auto; + padding: 1em 1.2em 4em; +} + +@media (prefers-reduced-motion: reduce) { + * { transition: none !important; animation: none !important; } +} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..6aeb667 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,47 @@ +const path = require('path'); +const express = require('express'); +const cookieSession = require('cookie-session'); + +const storage = require('./src/utils/storage'); +const adminRoutes = require('./src/routes/admin'); +const friendRoutes = require('./src/routes/friend'); +const uploadRoutes = require('./src/routes/uploads'); + +storage.ensureBaseDirs(); + +const app = express(); +app.disable('x-powered-by'); +app.use(express.json({ limit: '2mb' })); + +app.use( + cookieSession({ + name: 'fb_session', + secret: process.env.SESSION_SECRET || 'bitte-in-der-docker-compose-aendern', + maxAge: 30 * 24 * 60 * 60 * 1000, // 30 Tage + sameSite: 'lax', + }) +); + +// --- API --- +app.use('/api', adminRoutes); +app.use('/api', friendRoutes); +app.use('/uploads', uploadRoutes); + +// --- Frontend (statisch) --- +const PUBLIC_DIR = path.join(__dirname, 'public'); +app.use('/assets', express.static(path.join(PUBLIC_DIR, 'shared'))); +app.use('/admin', express.static(path.join(PUBLIC_DIR, 'admin'))); +app.use('/friend', express.static(path.join(PUBLIC_DIR, 'friend'))); +app.use('/book', express.static(path.join(PUBLIC_DIR, 'book'))); +app.get('/', (req, res) => res.sendFile(path.join(PUBLIC_DIR, 'login', 'index.html'))); + +// Friend-Share-Link: /share/ -> gleiche Seite wie /friend, JS erkennt ID aus der URL +app.get('/share/:id', (req, res) => res.sendFile(path.join(PUBLIC_DIR, 'friend', 'index.html'))); +app.get('/friend/:id', (req, res) => res.sendFile(path.join(PUBLIC_DIR, 'friend', 'index.html'))); + +app.get('/health', (req, res) => res.json({ ok: true })); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Freundesbuch lĂ€uft auf Port ${PORT}`); +}); diff --git a/backend/src/auth.js b/backend/src/auth.js new file mode 100644 index 0000000..ce0491f --- /dev/null +++ b/backend/src/auth.js @@ -0,0 +1,25 @@ +function requireAdmin(req, res, next) { + if (req.session && req.session.role === 'admin') return next(); + return res.status(401).json({ error: 'Nicht angemeldet.' }); +} + +// Zugriff erlaubt fĂŒr Admin ODER fĂŒr den Freund selbst (per Share-Link angemeldet) +function requireAdminOrFriend(getFriendId) { + return (req, res, next) => { + if (req.session && req.session.role === 'admin') return next(); + const targetId = typeof getFriendId === 'function' ? getFriendId(req) : req.params.id; + if (req.session && req.session.role === 'friend' && req.session.friendId === targetId) { + return next(); + } + return res.status(401).json({ error: 'Nicht angemeldet.' }); + }; +} + +function requireAnySession(req, res, next) { + if (req.session && (req.session.role === 'admin' || req.session.role === 'friend')) { + return next(); + } + return res.status(401).json({ error: 'Nicht angemeldet.' }); +} + +module.exports = { requireAdmin, requireAdminOrFriend, requireAnySession }; diff --git a/backend/src/pdf.js b/backend/src/pdf.js new file mode 100644 index 0000000..b1f7632 --- /dev/null +++ b/backend/src/pdf.js @@ -0,0 +1,167 @@ +const fs = require('fs'); +const path = require('path'); +const puppeteer = require('puppeteer'); +const storage = require('./utils/storage'); + +function fileToDataUri(fullPath) { + try { + const buf = fs.readFileSync(fullPath); + const ext = path.extname(fullPath).toLowerCase().replace('.', ''); + const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'jpeg'; + return `data:image/${mime};base64,${buf.toString('base64')}`; + } catch (e) { + return null; + } +} + +function esc(str) { + return String(str || '').replace(/[&<>"']/g, (c) => ( + { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] + )); +} + +const ROTATIONS = [-4, 3, -2, 5, -3, 2, -5, 4]; + +function buildFriendPageHtml(friend, folder) { + const uploadsDir = path.join(storage.friendFolderPath(folder), 'uploads'); + const profileUri = friend.profileImage ? fileToDataUri(path.join(uploadsDir, friend.profileImage)) : null; + + const steckbriefRows = Object.entries(friend.steckbrief || {}) + .filter(([, v]) => v && String(v).trim()) + .map(([k, v]) => `
${esc(k)}${esc(v)}
`) + .join(''); + + const blocks = [...(friend.blocks || [])].sort((a, b) => a.order - b.order); + const blockHtml = blocks + .map((b, i) => { + const rot = ROTATIONS[i % ROTATIONS.length]; + if (b.type === 'text') { + return `
+

${esc(b.content).replace(/\n/g, '
')}

+ ${b.caption ? `
${esc(b.caption)}
` : ''} +
`; + } + if (b.type === 'image') { + const uri = fileToDataUri(path.join(uploadsDir, b.filename)); + if (!uri) return ''; + return `
+ + ${b.caption ? `
${esc(b.caption)}
` : ''} +
`; + } + if (b.type === 'audio') { + return `
+
🎵 Audio-Erinnerung${b.caption ? ': ' + esc(b.caption) : ''}
(im Web-Freundesbuch anhörbar)
+
`; + } + return ''; + }) + .join('\n'); + + return ` +
+
+
+ ${profileUri ? `` : '
?
'} +

${esc(friend.name)}

+
+
+

Steckbrief

+ ${steckbriefRows || '

Noch nichts eingetragen.

'} +
+
+ ${blockHtml || '

Noch keine Erinnerungen hinzugefügt.

'} +
+
`; +} + +function buildFullHtml(friends) { + const pages = friends + .map((f) => buildFriendPageHtml(f, f.folderName)) + .join('\n'); + + return ` + + + + + + +${pages || '

Noch keine Freunde angelegt.

'} + +`; +} + +async function generateBookPdf() { + const friends = storage.getAllFriendsMeta(); + const html = buildFullHtml(friends); + + const browser = await puppeteer.launch({ + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + try { + const page = await browser.newPage(); + await page.setContent(html, { waitUntil: 'networkidle0' }); + const buffer = await page.pdf({ format: 'A4', printBackground: true }); + return buffer; + } finally { + await browser.close(); + } +} + +module.exports = { generateBookPdf, buildFullHtml }; diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js new file mode 100644 index 0000000..b3d9e82 --- /dev/null +++ b/backend/src/routes/admin.js @@ -0,0 +1,111 @@ +const express = require('express'); +const { sha256, safeEqual } = require('../utils/hash'); +const storage = require('../utils/storage'); +const { requireAdmin } = require('../auth'); +const { generateBookPdf } = require('../pdf'); + +const router = express.Router(); + +const ADMIN_PASSWORD_HASH = (process.env.ADMIN_PASSWORD_HASH || '').trim().toLowerCase(); + +// --- Admin Login --- +router.post('/login', (req, res) => { + const { password } = req.body || {}; + if (!password || !ADMIN_PASSWORD_HASH) { + return res.status(400).json({ error: 'Passwort erforderlich.' }); + } + const hash = sha256(password); + if (!safeEqual(hash, ADMIN_PASSWORD_HASH)) { + return res.status(401).json({ error: 'Falsches Passwort.' }); + } + req.session.role = 'admin'; + req.session.friendId = null; + return res.json({ ok: true }); +}); + +router.post('/logout', (req, res) => { + req.session = null; + res.json({ ok: true }); +}); + +router.get('/session', (req, res) => { + if (req.session && req.session.role) { + return res.json({ role: req.session.role, friendId: req.session.friendId || null }); + } + res.json({ role: null }); +}); + +// --- Freunde-Verwaltung (nur Admin) --- +router.get('/friends', requireAdmin, (req, res) => { + const friends = storage.getAllFriendsMeta().map((f) => ({ + id: f.id, + folderName: f.folderName, + name: f.name, + createdAt: f.createdAt, + profileImage: f.profileImage, + shareEnabled: !!(f.share && f.share.enabled), + blockCount: (f.blocks || []).length, + })); + res.json({ friends }); +}); + +router.post('/friends', requireAdmin, (req, res) => { + const { name } = req.body || {}; + if (!name || !String(name).trim()) { + return res.status(400).json({ error: 'Name erforderlich.' }); + } + const meta = storage.createFriend(String(name).trim()); + res.json({ friend: meta }); +}); + +router.delete('/friends/:id', requireAdmin, (req, res) => { + const folder = storage.findFolderById(req.params.id); + if (!folder) return res.status(404).json({ error: 'Nicht gefunden.' }); + storage.deleteFriend(folder); + res.json({ ok: true }); +}); + +// Freigabe (Share-Link) verwalten +router.post('/friends/:id/share', requireAdmin, (req, res) => { + const folder = storage.findFolderById(req.params.id); + if (!folder) return res.status(404).json({ error: 'Nicht gefunden.' }); + const meta = storage.readMeta(folder); + const { enabled, password } = req.body || {}; + + if (enabled === false) { + meta.share.enabled = false; + storage.writeMeta(folder, meta); + return res.json({ friend: meta }); + } + + if (!password || String(password).length < 4) { + return res.status(400).json({ error: 'Bitte ein Passwort mit mindestens 4 Zeichen vergeben.' }); + } + meta.share.enabled = true; + meta.share.passwordHash = sha256(password); + storage.writeMeta(folder, meta); + res.json({ friend: meta }); +}); + +// --- Freundesbuch-Ansicht (aggregiert, nur Admin) --- +router.get('/book', requireAdmin, (req, res) => { + const friends = storage.getAllFriendsMeta().map((f) => ({ + ...f, + share: { enabled: !!(f.share && f.share.enabled) }, // Passwort-Hash nie an den Client senden + })); + res.json({ friends }); +}); + +router.get('/book/pdf', requireAdmin, async (req, res) => { + try { + const pdfBuffer = await generateBookPdf(); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', 'attachment; filename="freundesbuch.pdf"'); + res.send(pdfBuffer); + } catch (err) { + console.error('PDF-Export fehlgeschlagen:', err); + res.status(500).json({ error: 'PDF-Export fehlgeschlagen.' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/friend.js b/backend/src/routes/friend.js new file mode 100644 index 0000000..9b47324 --- /dev/null +++ b/backend/src/routes/friend.js @@ -0,0 +1,159 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const multer = require('multer'); +const { sha256, safeEqual } = require('../utils/hash'); +const storage = require('../utils/storage'); +const { requireAdminOrFriend } = require('../auth'); + +const router = express.Router(); + +function loadFriendOr404(req, res, next) { + const folder = storage.findFolderById(req.params.id); + if (!folder) return res.status(404).json({ error: 'Freund nicht gefunden.' }); + req.friendFolder = folder; + req.friendMeta = storage.readMeta(folder); + next(); +} + +// --- Login ĂŒber Freigabe-Link (kein Admin nötig) --- +router.post('/share/:id/login', loadFriendOr404, (req, res) => { + const meta = req.friendMeta; + if (!meta.share || !meta.share.enabled || !meta.share.passwordHash) { + return res.status(403).json({ error: 'Diese Freigabe ist nicht (mehr) aktiv.' }); + } + const { password } = req.body || {}; + const hash = sha256(password || ''); + if (!safeEqual(hash, meta.share.passwordHash)) { + return res.status(401).json({ error: 'Falsches Passwort.' }); + } + req.session.role = 'friend'; + req.session.friendId = meta.id; + res.json({ ok: true, name: meta.name }); +}); + +// alles unterhalb: Admin ODER der eingeloggte Freund selbst +router.use('/friends/:id', loadFriendOr404, requireAdminOrFriend((req) => req.friendMeta.id)); + +router.get('/friends/:id', (req, res) => { + const meta = req.friendMeta; + res.json({ friend: { ...meta, share: { enabled: !!(meta.share && meta.share.enabled) } } }); +}); + +router.put('/friends/:id/steckbrief', (req, res) => { + const meta = req.friendMeta; + meta.steckbrief = { ...meta.steckbrief, ...(req.body || {}) }; + storage.writeMeta(req.friendFolder, meta); + res.json({ friend: meta }); +}); + +// --- Datei-Uploads (Profilbild, Bild-/Audio-Blöcke) --- +function makeUpload() { + const storageEngine = multer.diskStorage({ + destination: (req, file, cb) => { + const dir = path.join(storage.friendFolderPath(req.friendFolder), 'uploads'); + fs.mkdirSync(dir, { recursive: true }); + cb(null, dir); + }, + filename: (req, file, cb) => { + const id = crypto.randomBytes(6).toString('hex'); + const ext = path.extname(file.originalname || '').toLowerCase() || guessExt(file.mimetype); + cb(null, `${id}${ext}`); + }, + }); + return multer({ + storage: storageEngine, + limits: { fileSize: 40 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + const allowed = /^image\/(png|jpe?g|gif|webp)$|^audio\/(mpeg|mp3|wav|ogg|x-m4a|mp4)$/i; + if (allowed.test(file.mimetype)) return cb(null, true); + cb(new Error('Dateityp nicht erlaubt.')); + }, + }); +} +function guessExt(mime) { + if (mime === 'image/jpeg') return '.jpg'; + if (mime === 'image/png') return '.png'; + if (mime === 'image/gif') return '.gif'; + if (mime === 'image/webp') return '.webp'; + if (mime.startsWith('audio/')) return '.mp3'; + return ''; +} +const upload = makeUpload(); + +router.post('/friends/:id/profileimage', upload.single('image'), (req, res) => { + if (!req.file) return res.status(400).json({ error: 'Kein Bild erhalten.' }); + const meta = req.friendMeta; + // altes Profilbild aufrĂ€umen + if (meta.profileImage) { + const oldPath = path.join(storage.friendFolderPath(req.friendFolder), 'uploads', meta.profileImage); + fs.rm(oldPath, { force: true }, () => {}); + } + meta.profileImage = req.file.filename; + storage.writeMeta(req.friendFolder, meta); + res.json({ friend: meta }); +}); + +// --- Blöcke: Bild, Text, Audio --- +router.post('/friends/:id/blocks', upload.single('file'), (req, res) => { + const meta = req.friendMeta; + const type = req.body.type; + if (!['image', 'text', 'audio'].includes(type)) { + return res.status(400).json({ error: 'Unbekannter Block-Typ.' }); + } + const block = { + id: crypto.randomBytes(5).toString('hex'), + type, + caption: req.body.caption || '', + createdAt: new Date().toISOString(), + order: meta.blocks.length, + }; + if (type === 'text') { + block.content = req.body.content || ''; + } else { + if (!req.file) return res.status(400).json({ error: 'Keine Datei erhalten.' }); + block.filename = req.file.filename; + } + meta.blocks.push(block); + storage.writeMeta(req.friendFolder, meta); + res.json({ friend: meta }); +}); + +router.put('/friends/:id/blocks/:blockId', (req, res) => { + const meta = req.friendMeta; + const block = meta.blocks.find((b) => b.id === req.params.blockId); + if (!block) return res.status(404).json({ error: 'Block nicht gefunden.' }); + if (typeof req.body.content === 'string') block.content = req.body.content; + if (typeof req.body.caption === 'string') block.caption = req.body.caption; + if (typeof req.body.order === 'number') block.order = req.body.order; + storage.writeMeta(req.friendFolder, meta); + res.json({ friend: meta }); +}); + +router.delete('/friends/:id/blocks/:blockId', (req, res) => { + const meta = req.friendMeta; + const idx = meta.blocks.findIndex((b) => b.id === req.params.blockId); + if (idx === -1) return res.status(404).json({ error: 'Block nicht gefunden.' }); + const [removed] = meta.blocks.splice(idx, 1); + if (removed.filename) { + const p = path.join(storage.friendFolderPath(req.friendFolder), 'uploads', removed.filename); + fs.rm(p, { force: true }, () => {}); + } + storage.writeMeta(req.friendFolder, meta); + res.json({ friend: meta }); +}); + +router.post('/friends/:id/blocks/reorder', (req, res) => { + const meta = req.friendMeta; + const order = req.body.order || []; // Array von Block-IDs in neuer Reihenfolge + order.forEach((blockId, i) => { + const b = meta.blocks.find((x) => x.id === blockId); + if (b) b.order = i; + }); + meta.blocks.sort((a, b) => a.order - b.order); + storage.writeMeta(req.friendFolder, meta); + res.json({ friend: meta }); +}); + +module.exports = router; diff --git a/backend/src/routes/uploads.js b/backend/src/routes/uploads.js new file mode 100644 index 0000000..a8870fc --- /dev/null +++ b/backend/src/routes/uploads.js @@ -0,0 +1,27 @@ +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const storage = require('../utils/storage'); + +const router = express.Router(); + +// /uploads// +router.get('/:folder/:filename', (req, res) => { + const { folder, filename } = req.params; + const session = req.session || {}; + const isAdmin = session.role === 'admin'; + const isMatchingFriend = session.role === 'friend' && folder.endsWith('-' + session.friendId); + + if (!isAdmin && !isMatchingFriend) { + return res.status(401).send('Nicht angemeldet.'); + } + + const safeFolder = path.basename(folder); + const safeFile = path.basename(filename); + const filePath = path.join(storage.friendFolderPath(safeFolder), 'uploads', safeFile); + + if (!fs.existsSync(filePath)) return res.status(404).send('Nicht gefunden.'); + res.sendFile(filePath); +}); + +module.exports = router; diff --git a/backend/src/utils/hash.js b/backend/src/utils/hash.js new file mode 100644 index 0000000..ae19f67 --- /dev/null +++ b/backend/src/utils/hash.js @@ -0,0 +1,14 @@ +const crypto = require('crypto'); + +function sha256(input) { + return crypto.createHash('sha256').update(String(input), 'utf8').digest('hex'); +} + +function safeEqual(a, b) { + const bufA = Buffer.from(String(a || ''), 'utf8'); + const bufB = Buffer.from(String(b || ''), 'utf8'); + if (bufA.length !== bufB.length) return false; + return crypto.timingSafeEqual(bufA, bufB); +} + +module.exports = { sha256, safeEqual }; diff --git a/backend/src/utils/storage.js b/backend/src/utils/storage.js new file mode 100644 index 0000000..899de2a --- /dev/null +++ b/backend/src/utils/storage.js @@ -0,0 +1,115 @@ +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const DATA_DIR = process.env.DATA_DIR || '/data'; +const FRIENDS_DIR = path.join(DATA_DIR, 'friends'); + +function ensureBaseDirs() { + fs.mkdirSync(FRIENDS_DIR, { recursive: true }); +} + +function slugify(name) { + return String(name) + .trim() + .toLowerCase() + .replace(/[ÀÄ]/g, 'ae') + .replace(/[öÖ]/g, 'oe') + .replace(/[ĂŒĂœ]/g, 'ue') + .replace(/ß/g, 'ss') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 40) || 'freund'; +} + +function randomId() { + return crypto.randomBytes(4).toString('hex'); +} + +function friendFolderPath(folderName) { + return path.join(FRIENDS_DIR, folderName); +} + +function metaPath(folderName) { + return path.join(friendFolderPath(folderName), 'meta.json'); +} + +function listFriendFolders() { + ensureBaseDirs(); + return fs + .readdirSync(FRIENDS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); +} + +function readMeta(folderName) { + const p = metaPath(folderName); + if (!fs.existsSync(p)) return null; + try { + return JSON.parse(fs.readFileSync(p, 'utf8')); + } catch (e) { + return null; + } +} + +function writeMeta(folderName, meta) { + const p = metaPath(folderName); + fs.writeFileSync(p, JSON.stringify(meta, null, 2), 'utf8'); +} + +function findFolderById(id) { + return listFriendFolders().find((f) => f.endsWith('-' + id) || f === id); +} + +function createFriend(name) { + ensureBaseDirs(); + const id = randomId(); + const folderName = `${slugify(name)}-${id}`; + const dir = friendFolderPath(folderName); + fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(path.join(dir, 'uploads'), { recursive: true }); + const meta = { + id, + folderName, + name, + createdAt: new Date().toISOString(), + profileImage: null, + steckbrief: {}, + blocks: [], + share: { + enabled: false, + passwordHash: null, + }, + }; + writeMeta(folderName, meta); + return meta; +} + +function deleteFriend(folderName) { + const dir = friendFolderPath(folderName); + fs.rmSync(dir, { recursive: true, force: true }); +} + +function getAllFriendsMeta() { + return listFriendFolders() + .map((f) => readMeta(f)) + .filter(Boolean) + .sort((a, b) => a.name.localeCompare(b.name, 'de')); +} + +module.exports = { + DATA_DIR, + FRIENDS_DIR, + ensureBaseDirs, + slugify, + randomId, + friendFolderPath, + metaPath, + listFriendFolders, + readMeta, + writeMeta, + findFolderById, + createFriend, + deleteFriend, + getAllFriendsMeta, +}; diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1eb6889 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + freundesbuch: + build: + context: . + dockerfile: Dockerfile + container_name: freundesbuch + restart: unless-stopped + ports: + - "3000:3000" + environment: + # SHA256-Hash des Admin-Passworts. Erzeugen z. B. mit: + # echo -n "DeinPasswort" | sha256sum + # (unter macOS: echo -n "DeinPasswort" | shasum -a 256) + # Standardwert unten entspricht dem Passwort "aendern123" -- unbedingt ersetzen! + ADMIN_PASSWORD_HASH: "4a2e027f974978aeccc1eee8748722f865f37284b245b223512d311348ec4c01" + # ZufĂ€lliger, langer Zeichenstring zum Signieren der Login-Sessions + SESSION_SECRET: "bitte-durch-einen-langen-zufaelligen-string-ersetzen" + PORT: "3000" + DATA_DIR: "/data" + volumes: + - ./data:/data