From d60ffcbeb48191a8c961cfebf5e3d0d5391da9e5 Mon Sep 17 00:00:00 2001 From: Simon Luthe Date: Wed, 24 Jul 2024 02:30:37 +0200 Subject: [PATCH] feat(produktion): Implementiere Docker-basierte Produktionsumgebung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🐳 Erstelle Dockerfile zur Containerisierung - 🚀 Richte automatischen Build und Push zu DockerHub ein - 📄 Entwickle docker-compose.yml für einfache Bereitstellung - 🔧 Ersetze hartcodierte Pfade durch API-Aufrufe zur Vermeidung von CORS-Problemen - 🏠 Vereinfache Self-Hosting-Prozess durch Konfiguration mit einer einzigen URL - 🖥️ Passe server.js an neue Produktionsstruktur an - 🐛 Behebe Fehler, die durch Übergang zur Produktionsumgebung entstanden sind Dieser Commit markiert den **Übergang von der Entwicklungs- zu einer robusten, containerisierten Produktionsumgebung**. Er verbessert die Bereitstellbarkeit und reduziert die Konfigurationskomplexität für Endbenutzer. --- .dockerignore | 4 + .github/workflows/docker-publish.yml | 25 ++ Dockerfile | 32 ++ README.md | 40 +- backend/package-lock.json | 439 +++++++++++++++++++++- backend/package.json | 33 +- backend/server.js | 538 +++++++++++++-------------- docker-compose.yml | 32 ++ frontend/admin.html | 2 +- frontend/admin.js | 10 +- frontend/index.html | 6 +- frontend/script.js | 54 +-- init.sql | 23 ++ 13 files changed, 888 insertions(+), 350 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 init.sql diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7136e9e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +.git +.gitignore \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..ba903c4 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,25 @@ +name: Docker + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag hymnoscripe:$(date +%s) + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: revisoren + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Push to Docker Hub + uses: docker/build-push-action@v6 + with: + push: true + tags: revisoren/hymnoscripe:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b74c43c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM node:22 as build + +WORKDIR /app + +# Kopieren und Installieren der Backend-Abhängigkeiten +COPY backend/package*.json ./backend/ +RUN cd backend && npm install + +# Kopieren und Bauen des Frontends +COPY frontend ./frontend + +# Kopieren der Backend-Dateien +COPY backend ./backend + +# Kopieren der Konfigurationsdateien +COPY init.sql ./ + +FROM node:22-slim + +WORKDIR /app + +# Kopieren der gebauten Anwendung aus dem Build-Stage +COPY --from=build /app/backend ./backend +COPY --from=build /app/frontend ./frontend +COPY --from=build /app/init.sql ./ + +WORKDIR /app/backend +RUN npm install --only=production + +EXPOSE 9615 + +CMD ["node", "backend/server.js"] \ No newline at end of file diff --git a/README.md b/README.md index afa4683..990b3b3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# Gottesdienst-Liedblatt Generator +# HymnoScribe -Dieses Projekt ist ein umfassendes Tool zur Erstellung von Gottesdienst-Liedblättern. Es bietet eine benutzerfreundliche Oberfläche zum Erstellen, Bearbeiten und Verwalten von Gottesdienstobjekten sowie zur Generierung von PDF-Liedblättern. +[![Docker](https://github.com/Revisor01/HymnoScribe/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/Revisor01/HymnoScribe/actions/workflows/docker-publish.yml) + +HymnoScribe ist ein umfassendes Tool zur Erstellung von Gottesdienst-Liedblättern. Es bietet eine benutzerfreundliche Oberfläche zum Erstellen, Bearbeiten und Verwalten von Gottesdienstobjekten sowie zur Generierung von PDF-Liedblättern. ## Funktionen @@ -15,14 +17,16 @@ Dieses Projekt ist ein umfassendes Tool zur Erstellung von Gottesdienst-Liedblä ## Installation und Nutzung +### Lokale Installation + 1. Klonen Sie das Repository: ``` - git clone https://github.com/ihr-benutzername/gottesdienst-liedblatt-generator.git + git clone https://github.com/Revisor01/HymnoScribe.git ``` 2. Navigieren Sie in das Projektverzeichnis: ``` - cd gottesdienst-liedblatt-generator + cd HymnoScribe ``` 3. Installieren Sie die Abhängigkeiten: @@ -37,29 +41,17 @@ Dieses Projekt ist ein umfassendes Tool zur Erstellung von Gottesdienst-Liedblä 5. Öffnen Sie einen Webbrowser und navigieren Sie zu `http://localhost:3000` -## Docker-Nutzung - -1. Bauen Sie das Docker-Image: - ``` - docker build -t gottesdienst-liedblatt-generator . - ``` - -2. Starten Sie den Container: - ``` - docker run -p 3000:3000 -v /pfad/zum/upload/ordner:/app/uploads gottesdienst-liedblatt-generator - ``` +### Docker-Nutzung -## Hinweise zur Nutzung +Sie können HymnoScribe auch über Docker ausführen: -- **Admin-Oberfläche**: Verwenden Sie die Admin-Oberfläche, um neue Gottesdienstobjekte hinzuzufügen oder bestehende zu bearbeiten. -- **Liedblatt-Erstellung**: Ziehen Sie Objekte aus dem Pool in den Arbeitsbereich, um Ihr Liedblatt zu gestalten. -- **PDF-Generierung**: Wählen Sie das gewünschte Format und klicken Sie auf "PDF generieren", um Ihr Liedblatt als PDF zu erstellen. -- **Sessions und Vorlagen**: Nutzen Sie das Session-Management, um Ihre Arbeit zu speichern und später fortzusetzen. Erstellen Sie Vorlagen für wiederkehrende Liedblatt-Strukturen. +``` +docker run -p 3000:3000 -v /pfad/zum/upload/ordner:/app/uploads revisoren/hymnoscripe:latest +``` -## Hinweise zur Ausgabe +## Entwicklung -- Die generierten PDFs sind optimiert für die Druckausgabe in A4 und A3 duplex. -- Die Broschüren-Option ermöglicht es, Liedblätter im Booklet-Format zu erstellen, ideal für gefaltete Gottesdienstordnungen. +HymnoScribe verwendet GitHub Actions für kontinuierliche Integration und Bereitstellung. Bei jedem Push in den `main`-Branch wird automatisch ein neues Docker-Image gebaut und auf Docker Hub veröffentlicht. ## Beitrag @@ -67,4 +59,4 @@ Beiträge zum Projekt sind willkommen. Bitte erstellen Sie einen Pull Request od ## Lizenz -Dieses Projekt ist unter der MIT-Lizenz lizenziert. Siehe die [LICENSE](LICENSE) Datei für Details. +Dieses Projekt ist unter der MIT-Lizenz lizenziert. Siehe die [LICENSE](LICENSE) Datei für Details. \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index f681def..a0f6be3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,20 +1,43 @@ { - "name": "liedblatt-backend", + "name": "hymnoscribe", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "liedblatt-backend", + "name": "hymnoscribe", "version": "1.0.0", + "license": "MIT", "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", "fs": "^0.0.1-security", + "mariadb": "^3.3.1", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "mysql2": "^3.10.3", "uuid": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^2.0.22" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" } }, "node_modules/accepts": { @@ -30,6 +53,20 @@ "node": ">= 0.6" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -51,6 +88,13 @@ "node": ">= 6.0.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", @@ -60,6 +104,19 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -84,6 +141,30 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -129,6 +210,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", @@ -346,6 +459,19 @@ "node": ">= 0.10.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -388,6 +514,21 @@ "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -425,6 +566,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -437,6 +591,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -513,6 +677,13 @@ "node": ">=0.10.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -528,6 +699,52 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -555,6 +772,40 @@ "node": ">=16.14" } }, + "node_modules/mariadb": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.3.1.tgz", + "integrity": "sha512-L8bh4iuZU3J8H7Co7rQ6OY9FDLItAN1rGy8kPA7Dyxo8AiHADuuONoypKKp1pE09drs6e5LR7UW9luLZ/A4znA==", + "license": "LGPL-2.1-or-later", + "dependencies": { + "@types/geojson": "^7946.0.14", + "@types/node": "^20.11.17", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mariadb/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mariadb/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -612,6 +863,19 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -764,6 +1028,62 @@ "node": ">= 0.6" } }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -812,6 +1132,19 @@ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "license": "MIT" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -831,6 +1164,13 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -891,6 +1231,19 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -917,6 +1270,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -1008,6 +1371,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/sqlstring": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", @@ -1049,6 +1435,32 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1058,6 +1470,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1077,6 +1499,19 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 6aafc0f..513752c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,17 +1,38 @@ { - "name": "liedblatt-backend", + "name": "hymnoscribe", "version": "1.0.0", + "description": "Ein innovativer Liedblatt Generator", "main": "server.js", "scripts": { - "start": "node server.js" + "start": "node server.js", + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", "fs": "^0.0.1-security", + "mysql2": "^2.3.3", "multer": "^1.4.5-lts.1", - "mysql": "^2.18.1", - "mysql2": "^3.10.3", "uuid": "^10.0.0" - } -} + }, + "devDependencies": { + "nodemon": "^2.0.22" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/Revisor01/HymnoScribe.git" + }, + "keywords": [ + "gottesdienst", + "liedblatt", + "generator", + "kirche", + "musik" + ], + "author": "Simon Luthe", + "license": "MIT" +} \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 2fb23c1..529c2fc 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,17 +2,27 @@ const express = require('express'); const cors = require('cors'); const multer = require('multer'); const path = require('path'); -const mysql = require('mysql2/promise'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); +const mysql = require('mysql2/promise'); + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + connectionLimit: 5 +}); const app = express(); +const PORT = process.env.PORT || 3000; + app.use(express.json()); -app.use(cors()); -app.use((req, res, next) => { - console.log(`Received request for: ${req.url}`); - next(); -}); +app.use(cors({ + origin: process.env.URL.split(','), + methods: ['GET', 'POST', 'PUT', 'DELETE'], + allowedHeaders: ['Content-Type', 'Authorization'] +})); const customImageStorage = multer.diskStorage({ destination: function (req, file, cb) { @@ -27,16 +37,6 @@ const customImageStorage = multer.diskStorage({ const uploadCustomImage = multer({ storage: customImageStorage }); -app.post('/upload-custom-image', uploadCustomImage.single('customImage'), (req, res) => { - if (req.file) { - const imagePath = `/uploads/custom/${req.file.filename}`; - res.json({ success: true, imagePath }); - } else { - res.status(400).json({ success: false, message: 'Kein Bild hochgeladen' }); - } -}); - -// Multer-Setup für das Hochladen von Dateien const storage = multer.diskStorage({ destination: function (req, file, cb) { let uploadPath; @@ -54,10 +54,8 @@ const storage = multer.diskStorage({ }, filename: function (req, file, cb) { if (file.fieldname === 'logo') { - // Für Logo-Uploads behalten wir den Originalnamen bei cb(null, file.originalname); } else if (file.fieldname === 'customImage') { - // Für benutzerdefinierte Bilder verwenden wir einen Zeitstempel cb(null, Date.now() + '-' + file.originalname); } else { const titel = req.body.titel.replace(/[^a-z0-9]/gi, '_').toLowerCase(); @@ -69,31 +67,20 @@ const storage = multer.diskStorage({ const upload = multer({ storage: storage }); -app.post('/upload-logo', upload.single('logo'), (req, res) => { - if (req.file) { - const logoPath = `/uploads/logos/${req.file.filename}`; - console.log("Logo uploaded successfully:", logoPath); - res.json({ success: true, logoPath }); - } else { - console.log("No logo file received"); - res.status(400).json({ success: false, message: 'Kein Bild hochgeladen' }); - } -}); +const apiRouter = express.Router(); -app.post('/upload-custom-image', upload.single('customImage'), (req, res) => { +apiRouter.post('/upload-custom-image', uploadCustomImage.single('customImage'), (req, res) => { if (req.file) { - const imagePath = `/uploads/custom/${req.file.filename}`; - console.log("Custom image uploaded successfully:", imagePath); + const imagePath = `/api/uploads/custom/${req.file.filename}`; res.json({ success: true, imagePath }); } else { - console.log("No custom image file received"); res.status(400).json({ success: false, message: 'Kein Bild hochgeladen' }); } }); -app.post('/upload-logo', upload.single('logo'), (req, res) => { +apiRouter.post('/upload-logo', upload.single('logo'), (req, res) => { if (req.file) { - const logoPath = `/uploads/logos/${req.file.filename}`; + const logoPath = `/api/uploads/logos/${req.file.filename}`; console.log("Logo uploaded successfully:", logoPath); res.json({ success: true, logoPath }); } else { @@ -102,23 +89,228 @@ app.post('/upload-logo', upload.single('logo'), (req, res) => { } }); -app.use('/icons', express.static(path.join(__dirname, 'icons'))); -app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); -app.use('/ttf', express.static(path.join(__dirname, 'ttf'))); +apiRouter.post('/objekte', upload.fields([ + { name: 'notenbild', maxCount: 1 }, + { name: 'notenbildMitText', maxCount: 1 } +]), async (req, res) => { + try { + const { typ, titel, inhalt, strophen, copyright } = req.body; + const notenbild = req.files && req.files['notenbild'] + ? `/api/uploads/${path.relative(path.join(__dirname, 'uploads'), req.files['notenbild'][0].path)}` + : null; + const notenbildMitText = req.files && req.files['notenbildMitText'] + ? `/api/uploads/${path.relative(path.join(__dirname, 'uploads'), req.files['notenbildMitText'][0].path)}` + : null; + + console.log('Prepared data:', { typ, titel, inhalt, strophen, notenbild, notenbildMitText, copyright }); + + const safeInhalt = inhalt === undefined ? null : inhalt; + const safeStrophen = strophen === undefined ? null : strophen; + console.log('Received request body:', req.body); + + const [result] = await pool.query( + 'INSERT INTO objekte (typ, titel, inhalt, notenbild, notenbildMitText, strophen, copyright) VALUES (?, ?, ?, ?, ?, ?, ?)', + [typ, titel, safeInhalt, notenbild, notenbildMitText, safeStrophen, copyright] + ); + + console.log('Database insert result:', result); + + res.status(201).json({ id: result.insertId, message: 'Objekt erfolgreich gespeichert' }); + } catch (error) { + console.error('Server-Fehler:', error); + res.status(500).json({ error: 'Interner Serverfehler', details: error.message }); + } +}); -const PORT = 3000; +apiRouter.get('/objekte', async (req, res) => { + try { + console.log('GET /objekte aufgerufen'); + const [results] = await pool.query('SELECT * FROM objekte'); + console.log('Abgerufene Objekte:', results); + res.json(results); + } catch (error) { + console.error('Fehler beim Abrufen der Objekte: ', error); + res.status(500).json({ error: 'Interner Serverfehler', details: error.message }); + } +}); + +apiRouter.put('/objekte/:id', upload.fields([ + { name: 'notenbild', maxCount: 1 }, + { name: 'notenbildMitText', maxCount: 1 } +]), async (req, res) => { + try { + const { id } = req.params; + const { typ, titel, inhalt, strophen, copyright } = req.body; + + console.log('Empfangene Daten:', { id, typ, titel, inhalt, strophen, copyright }); + + const [existingObjekt] = await pool.query('SELECT * FROM objekte WHERE id = ?', [id]); + + if (existingObjekt.length === 0) { + return res.status(404).json({ message: 'Objekt nicht gefunden' }); + } + + let notenbild = existingObjekt[0].notenbild; + let notenbildMitText = existingObjekt[0].notenbildMitText; + + if (req.files && req.files['notenbild']) { + notenbild = `/api/uploads/${path.relative(path.join(__dirname, 'uploads'), req.files['notenbild'][0].path)}`; + } + if (req.files && req.files['notenbildMitText']) { + notenbildMitText = `/api/uploads/${path.relative(path.join(__dirname, 'uploads'), req.files['notenbildMitText'][0].path)}`; + } + + const query = 'UPDATE objekte SET typ = ?, titel = ?, inhalt = ?, strophen = ?, notenbild = ?, notenbildMitText = ?, copyright = ? WHERE id = ?'; + const params = [typ, titel, inhalt || null, strophen || null, notenbild, notenbildMitText, copyright || null, id]; + + console.log('SQL Query:', query); + console.log('SQL Params:', params); + + const [result] = await pool.query(query, params); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Objekt nicht gefunden' }); + } + res.json({ message: 'Objekt erfolgreich aktualisiert' }); + } catch (error) { + console.error('Detaillierter Fehler:', error); + res.status(500).json({ error: 'Interner Serverfehler', details: error.message, stack: error.stack }); + } +}); + +apiRouter.delete('/objekte/:id', async (req, res) => { + try { + const { id } = req.params; + const [result] = await pool.query('DELETE FROM objekte WHERE id = ?', [id]); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Objekt nicht gefunden' }); + } + res.json({ message: 'Objekt erfolgreich gelöscht' }); + } catch (error) { + console.error('Fehler beim Löschen des Objekts: ', error); + res.status(500).json({ error: 'Interner Serverfehler', details: error.message }); + } +}); + +apiRouter.post('/sessions', async (req, res) => { + try { + const { name, data } = req.body; + const id = uuidv4(); + await pool.query('INSERT INTO sessions (id, name, data) VALUES (?, ?, ?)', [id, name, JSON.stringify(data)]); + res.status(201).json({ id, message: 'Session erfolgreich gespeichert' }); + } catch (error) { + console.error('Fehler beim Speichern der Session:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.get('/sessions', async (req, res) => { + try { + const [rows] = await pool.query('SELECT id, name, created_at FROM sessions ORDER BY created_at DESC'); + res.json(rows); + } catch (error) { + console.error('Fehler beim Abrufen der Sessions:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.get('/sessions/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM sessions WHERE id = ?', [req.params.id]); + if (rows.length === 0) { + res.status(404).json({ error: 'Session nicht gefunden' }); + } else { + console.log("Gefundene Session:", rows[0]); + res.json(rows[0]); + } + } catch (error) { + console.error('Fehler beim Abrufen der Session:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.delete('/sessions/:id', async (req, res) => { + try { + const [result] = await pool.query('DELETE FROM sessions WHERE id = ?', [req.params.id]); + if (result.affectedRows === 0) { + res.status(404).json({ error: 'Session nicht gefunden' }); + } else { + res.json({ message: 'Session erfolgreich gelöscht' }); + } + } catch (error) { + console.error('Fehler beim Löschen der Session:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.post('/vorlagen', async (req, res) => { + try { + const { name, data } = req.body; + const id = uuidv4(); + await pool.query('INSERT INTO vorlagen (id, name, data) VALUES (?, ?, ?)', [id, name, JSON.stringify(data)]); + res.status(201).json({ id, message: 'Vorlage erfolgreich gespeichert' }); + } catch (error) { + console.error('Fehler beim Speichern der Vorlage:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.get('/vorlagen', async (req, res) => { + try { + const [rows] = await pool.query('SELECT id, name FROM vorlagen'); + res.json(rows); + } catch (error) { + console.error('Fehler beim Abrufen der Vorlagen:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.get('/vorlagen/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM vorlagen WHERE id = ?', [req.params.id]); + if (rows.length === 0) { + res.status(404).json({ error: 'Vorlage nicht gefunden' }); + } else { + res.json(rows[0]); + } + } catch (error) { + console.error('Fehler beim Abrufen der Vorlage:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +apiRouter.delete('/vorlagen/:id', async (req, res) => { + try { + const [result] = await pool.query('DELETE FROM vorlagen WHERE id = ?', [req.params.id]); + if (result.affectedRows === 0) { + res.status(404).json({ error: 'Vorlage nicht gefunden' }); + } else { + res.json({ message: 'Vorlage erfolgreich gelöscht' }); + } + } catch (error) { + console.error('Fehler beim Löschen der Vorlage:', error); + res.status(500).json({ error: 'Interner Serverfehler' }); + } +}); + +app.use('/api', apiRouter); + +app.use('/api/icons', express.static(path.join(__dirname, 'icons'))); +app.use('/api/uploads', express.static(path.join(__dirname, 'uploads'))); +app.use('/api/ttf', express.static(path.join(__dirname, 'ttf'))); + +app.use(express.static(path.join(__dirname, '../frontend'))); + +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/index.html')); +}); async function initializeDatabase() { try { - const connection = await mysql.createConnection({ - host: 'localhost', - user: 'root', - password: '', - database: 'gottesdienst' - }); + const conn = await pool.getConnection(); - // Überprüfen und Aktualisieren der Datenbankstruktur - await connection.execute(` + await conn.execute(` CREATE TABLE IF NOT EXISTS objekte ( id INT AUTO_INCREMENT PRIMARY KEY, typ VARCHAR(255) NOT NULL, @@ -131,14 +323,13 @@ async function initializeDatabase() { ) `); - // Überprüfen, ob die copyright-Spalte bereits existiert, falls nicht, fügen wir sie hinzu - const [columns] = await connection.execute("SHOW COLUMNS FROM objekte LIKE 'copyright'"); + const [columns] = await conn.execute("SHOW COLUMNS FROM objekte LIKE 'copyright'"); if (columns.length === 0) { - await connection.execute("ALTER TABLE objekte ADD COLUMN copyright VARCHAR(255)"); + await conn.execute("ALTER TABLE objekte ADD COLUMN copyright VARCHAR(255)"); console.log('Copyright-Spalte zur objekte-Tabelle hinzugefügt.'); } - - await connection.execute(` + + await conn.execute(` CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(36) PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -146,250 +337,33 @@ async function initializeDatabase() { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); - - await connection.execute(` + + await conn.execute(` CREATE TABLE IF NOT EXISTS vorlagen ( id VARCHAR(36) PRIMARY KEY, name VARCHAR(255) NOT NULL, data JSON NOT NULL ) `); - + console.log('Datenbankstruktur überprüft und aktualisiert.'); - return connection; + conn.release(); } catch (error) { console.error('Fehler bei der Datenbankinitialisierung:', error); process.exit(1); } } -async function startServer() { - const db = await initializeDatabase(); - - app.post('/objekte', upload.fields([ - { name: 'notenbild', maxCount: 1 }, - { name: 'notenbildMitText', maxCount: 1 } - ]), async (req, res) => { - try { - console.log('Received request body:', req.body); - console.log('Received files:', req.files); - - const { typ, titel, inhalt, strophen, copyright } = req.body; - const notenbild = req.files && req.files['notenbild'] - ? path.join('/uploads', path.relative(path.join(__dirname, 'uploads'), req.files['notenbild'][0].path)) - : null; - const notenbildMitText = req.files && req.files['notenbildMitText'] - ? path.join('/uploads', path.relative(path.join(__dirname, 'uploads'), req.files['notenbildMitText'][0].path)) - : null; - - console.log('Prepared data:', { typ, titel, inhalt, strophen, notenbild, notenbildMitText, copyright }); - - const safeInhalt = inhalt === undefined ? null : inhalt; - const safeStrophen = strophen === undefined ? null : strophen; - - const [result] = await db.execute( - 'INSERT INTO objekte (typ, titel, inhalt, notenbild, notenbildMitText, strophen, copyright) VALUES (?, ?, ?, ?, ?, ?, ?)', - [typ, titel, safeInhalt, notenbild, notenbildMitText, safeStrophen, copyright] - ); - - console.log('Database insert result:', result); - - res.status(201).json({ id: result.insertId, message: 'Objekt erfolgreich gespeichert' }); - } catch (error) { - console.error('Server-Fehler:', error); - res.status(500).json({ error: 'Interner Serverfehler', details: error.message }); - } - }); - - app.get('/objekte', async (req, res) => { - try { - console.log('GET /objekte aufgerufen'); - const [results] = await db.execute('SELECT * FROM objekte'); - console.log('Abgerufene Objekte:', results); - res.json(results); - } catch (error) { - console.error('Fehler beim Abrufen der Objekte: ', error); - res.status(500).json({ error: 'Interner Serverfehler', details: error.message }); - } - }); - - app.put('/objekte/:id', upload.fields([ - { name: 'notenbild', maxCount: 1 }, - { name: 'notenbildMitText', maxCount: 1 } - ]), async (req, res) => { - try { - const { id } = req.params; - const { typ, titel, inhalt, strophen, copyright } = req.body; - - console.log('Empfangene Daten:', { id, typ, titel, inhalt, strophen, copyright }); - - // Holen Sie das existierende Objekt aus der Datenbank - const [existingObjekt] = await db.execute('SELECT * FROM objekte WHERE id = ?', [id]); - - if (existingObjekt.length === 0) { - return res.status(404).json({ message: 'Objekt nicht gefunden' }); - } - - let notenbild = existingObjekt[0].notenbild; - let notenbildMitText = existingObjekt[0].notenbildMitText; - - if (req.files && req.files['notenbild']) { - notenbild = path.join('/uploads', path.relative(path.join(__dirname, 'uploads'), req.files['notenbild'][0].path)); - } - if (req.files && req.files['notenbildMitText']) { - notenbildMitText = path.join('/uploads', path.relative(path.join(__dirname, 'uploads'), req.files['notenbildMitText'][0].path)); - } - - const query = 'UPDATE objekte SET typ = ?, titel = ?, inhalt = ?, strophen = ?, notenbild = ?, notenbildMitText = ?, copyright = ? WHERE id = ?'; - const params = [ - typ, - titel, - inhalt || null, - strophen || null, - notenbild, - notenbildMitText, - copyright || null, // Erlaubt NULL, wenn kein Copyright angegeben ist - id - ]; - - console.log('SQL Query:', query); - console.log('SQL Params:', params); - - const [result] = await db.execute(query, params); - - if (result.affectedRows === 0) { - return res.status(404).json({ message: 'Objekt nicht gefunden' }); - } - res.json({ message: 'Objekt erfolgreich aktualisiert' }); - } catch (error) { - console.error('Detaillierter Fehler:', error); - res.status(500).json({ error: 'Interner Serverfehler', details: error.message, stack: error.stack }); - } - }); - app.delete('/objekte/:id', async (req, res) => { - try { - const { id } = req.params; - const [result] = await db.execute('DELETE FROM objekte WHERE id = ?', [id]); - - if (result.affectedRows === 0) { - return res.status(404).json({ message: 'Objekt nicht gefunden' }); - } - res.json({ message: 'Objekt erfolgreich gelöscht' }); - } catch (error) { - console.error('Fehler beim Löschen des Objekts: ', error); - res.status(500).json({ error: 'Interner Serverfehler', details: error.message }); - } - }); - - // Neue Routen für Session-Management - app.post('/sessions', async (req, res) => { - try { - const { name, data } = req.body; - const id = uuidv4(); - await db.execute('INSERT INTO sessions (id, name, data) VALUES (?, ?, ?)', [id, name, JSON.stringify(data)]); - res.status(201).json({ id, message: 'Session erfolgreich gespeichert' }); - } catch (error) { - console.error('Fehler beim Speichern der Session:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - app.get('/sessions', async (req, res) => { - try { - const [rows] = await db.execute('SELECT id, name, created_at FROM sessions ORDER BY created_at DESC'); - res.json(rows); - } catch (error) { - console.error('Fehler beim Abrufen der Sessions:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - app.get('/sessions/:id', async (req, res) => { - try { - const [rows] = await db.execute('SELECT * FROM sessions WHERE id = ?', [req.params.id]); - if (rows.length === 0) { - res.status(404).json({ error: 'Session nicht gefunden' }); - } else { - console.log("Gefundene Session:", rows[0]); - res.json(rows[0]); - } - } catch (error) { - console.error('Fehler beim Abrufen der Session:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - app.delete('/sessions/:id', async (req, res) => { - try { - const [result] = await db.execute('DELETE FROM sessions WHERE id = ?', [req.params.id]); - if (result.affectedRows === 0) { - res.status(404).json({ error: 'Session nicht gefunden' }); - } else { - res.json({ message: 'Session erfolgreich gelöscht' }); - } - } catch (error) { - console.error('Fehler beim Löschen der Session:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - - - // Routen für Vorlagen - app.post('/vorlagen', async (req, res) => { - try { - const { name, data } = req.body; - const id = uuidv4(); - await db.execute('INSERT INTO vorlagen (id, name, data) VALUES (?, ?, ?)', [id, name, JSON.stringify(data)]); - res.status(201).json({ id, message: 'Vorlage erfolgreich gespeichert' }); - } catch (error) { - console.error('Fehler beim Speichern der Vorlage:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - app.get('/vorlagen', async (req, res) => { - try { - const [rows] = await db.execute('SELECT id, name FROM vorlagen'); - res.json(rows); - } catch (error) { - console.error('Fehler beim Abrufen der Vorlagen:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - app.get('/vorlagen/:id', async (req, res) => { - try { - const [rows] = await db.execute('SELECT * FROM vorlagen WHERE id = ?', [req.params.id]); - if (rows.length === 0) { - res.status(404).json({ error: 'Vorlage nicht gefunden' }); - } else { - res.json(rows[0]); - } - } catch (error) { - console.error('Fehler beim Abrufen der Vorlage:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); - - app.delete('/vorlagen/:id', async (req, res) => { - try { - const [result] = await db.execute('DELETE FROM vorlagen WHERE id = ?', [req.params.id]); - if (result.affectedRows === 0) { - res.status(404).json({ error: 'Vorlage nicht gefunden' }); - } else { - res.json({ message: 'Vorlage erfolgreich gelöscht' }); - } - } catch (error) { - console.error('Fehler beim Löschen der Vorlage:', error); - res.status(500).json({ error: 'Interner Serverfehler' }); - } - }); +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); - app.listen(PORT, () => { - console.log(`Server läuft auf http://localhost:${PORT}`); +initializeDatabase().then(() => { + app.listen(PORT, '0.0.0.0', () => { + console.log(`Server läuft auf http://0.0.0.0:${PORT}`); }); -} - -startServer(); \ No newline at end of file +}).catch(error => { + console.error('Fehler beim Starten des Servers:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6521f6d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' +services: + hymnoscribe: + image: revisoren/hymnoscripe:latest + ports: + - "9615:3000" + environment: + - NODE_ENV=${NODE_ENV:-production} + - DB_HOST=${DB_HOST:-db} + - DB_USER=${DB_USER:-hymnoscribe} + - DB_PASSWORD=${DB_PASSWORD:-hymnoscribe9715} + - DB_NAME=${DB_NAME:-hymnoscribe} + - URL=${URL:-https://hymnoscripe.godsapp.de} + - API_URL=${API_URL:-https://api.hymnoscripe.godsapp.de} + volumes: + - ./backend/uploads:/app/backend/uploads + depends_on: + - db + + db: + image: mysql:9.0 + volumes: + - ./hymnoscribe_db:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-hymnoscribe9715} + MYSQL_DATABASE: ${DB_NAME:-hymnoscribe} + MYSQL_USER: ${DB_USER:-hymnoscribe} + MYSQL_PASSWORD: ${DB_PASSWORD:-hymnoscribe9715} + +volumes: + hymnoscribe_db: diff --git a/frontend/admin.html b/frontend/admin.html index 6884a44..e5d87e3 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -11,7 +11,7 @@
-

Admin-Oberfläche für Gottesdienstobjekte

+

HymnoScribe - Der Liedblatt Generator - Administration

Zurück zum Liedblatt-Generator
diff --git a/frontend/admin.js b/frontend/admin.js index 6f775a1..319336a 100644 --- a/frontend/admin.js +++ b/frontend/admin.js @@ -249,7 +249,7 @@ async function handleFormSubmit(e) { strophen: null, notenbild: null, notenbildMitText: null, - copyright: document.getElementById('copyright').value + copyright: document.getElementById('copyright').value || null }; console.log('Initial objektData:', JSON.stringify(objektData)); @@ -265,7 +265,7 @@ async function handleFormSubmit(e) { console.log('Prepared objektData:', JSON.stringify(objektData)); try { - const url = objektId ? `http://localhost:3000/objekte/${objektId}` : 'http://localhost:3000/objekte'; + const url = objektId ? `/api/objekte/${objektId}` : '/api/objekte'; const method = objektId ? 'PUT' : 'POST'; const formData = new FormData(); @@ -365,7 +365,7 @@ function resetForm() { async function loadObjekte() { try { - const response = await fetch('http://localhost:3000/objekte'); + const response = await fetch('/api/objekte'); if (!response.ok) { throw new Error('Fehler beim Abrufen der Objekte'); } @@ -478,7 +478,7 @@ function editObjekt(id) { } function getImagePath(objekt, imageType) { - const basePath = 'http://localhost:3000/'; + const basePath = ''; let imagePath; if (imageType === 'notenbild') { @@ -541,7 +541,7 @@ function customConfirm(message) { async function deleteObjekt(id) { if (await customConfirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')) { try { - const response = await fetch(`http://localhost:3000/objekte/${id}`, { + const response = await fetch(`/api/objekte/${id}`, { method: 'DELETE' }); if (!response.ok) { diff --git a/frontend/index.html b/frontend/index.html index 67ff1d8..ef1a3b4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,12 +13,12 @@ - Gottesdienst-Liedblatt Generator + HymnoScribe - Der Liedblatt Generator
-

Gottesdienst-Liedblatt Generator

+

HymnoScribe - Der Liedblatt Generator