Compare commits
No commits in common. "main" and "v1.10.2" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,7 +1,6 @@
|
||||||
# Development data backups
|
# Development data backups
|
||||||
data-backup/
|
data-backup/
|
||||||
.copilot/
|
.copilot/
|
||||||
.playwridght-mcp/
|
|
||||||
|
|
||||||
# Node modules
|
# Node modules
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
@ -10,11 +9,6 @@ node_modules/
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
# Telegram credentials
|
|
||||||
scripts/.env.telegram
|
|
||||||
scripts/node_modules/
|
|
||||||
scripts/package-lock.json
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
# AI Browser Debugging – Setup für GitHub Copilot in VS Code
|
|
||||||
|
|
||||||
Dieses Dokument beschreibt, wie GitHub Copilot (KI) Zugriff auf einen echten Browser bekommt,
|
|
||||||
um Console-Fehler, Netzwerkanfragen und Screenshots der laufenden React-App zu analysieren.
|
|
||||||
|
|
||||||
**Umgebung:** Windows 11 · Docker Desktop · WSL 2 (Ubuntu) · VS Code (Remote – WSL)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Was wird dadurch möglich?
|
|
||||||
|
|
||||||
Wenn dieses Setup aktiv ist, kann Copilot folgende Browser-Aktionen selbstständig durchführen:
|
|
||||||
|
|
||||||
| Fähigkeit | Beschreibung |
|
|
||||||
|---|---|
|
|
||||||
| **Screenshots** | Seite oder einzelne Elemente aufnehmen |
|
|
||||||
| **Console-Logs** | Alle `console.error`, `console.warn`, etc. lesen |
|
|
||||||
| **Netzwerkanfragen** | API-Requests und Responses analysieren |
|
|
||||||
| **JS ausführen** | Beliebiges JavaScript in der laufenden Seite auswerten |
|
|
||||||
| **Navigation** | URLs aufrufen, zurücknavigieren |
|
|
||||||
| **UI-Interaktionen** | Klicken, Felder ausfüllen, Hover |
|
|
||||||
|
|
||||||
Der Mechanismus dahinter ist der **Playwright MCP Server** – ein lokaler Prozess in WSL 2,
|
|
||||||
der einen headless Chromium-Browser startet und diesen über das MCP-Protokoll
|
|
||||||
für Copilot-Tools bereitstellt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Voraussetzungen
|
|
||||||
|
|
||||||
- VS Code mit **GitHub Copilot Chat** Extension (empfohlen: ≥ 1.99)
|
|
||||||
- Node.js in WSL 2 (bereits vorhanden, da das Projekt Node.js nutzt)
|
|
||||||
- Docker Desktop läuft und die Dev-Umgebung ist gestartet (`./dev.sh`)
|
|
||||||
→ Frontend erreichbar auf `http://localhost:3000`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Setup-Schritte
|
|
||||||
|
|
||||||
### Schritt 1 – Node.js-Version prüfen (WSL 2)
|
|
||||||
|
|
||||||
Playwright benötigt **Node.js ≥ 18**. Die System-Node-Version in Ubuntu ist oft veraltet. Prüfen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node --version
|
|
||||||
```
|
|
||||||
|
|
||||||
Falls die Ausgabe **v12 oder v16** zeigt, Node.js 20 nachinstallieren:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
||||||
sudo apt-get install -y nodejs
|
|
||||||
|
|
||||||
# Prüfen:
|
|
||||||
node --version # sollte v20.x.x zeigen
|
|
||||||
npm --version # sollte 10.x zeigen
|
|
||||||
```
|
|
||||||
|
|
||||||
> Bei Nutzung von **nvm**: aktive Version mit `node --version` prüfen, ggf. `nvm use 20` ausführen.
|
|
||||||
|
|
||||||
### Schritt 2 – Playwright MCP Server installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g @playwright/mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schritt 3 – Chromium-Browser und System-Abhängigkeiten installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Chromium herunterladen (Playwright-eigene Version)
|
|
||||||
npx playwright install chromium
|
|
||||||
|
|
||||||
# System-Bibliotheken installieren
|
|
||||||
# WICHTIG: sudo env "PATH=$PATH" verwenden, damit sudo die korrekte
|
|
||||||
# Node.js-Version (≥ 18) findet und nicht die System-Version (v12)
|
|
||||||
sudo env "PATH=$PATH" npx playwright install-deps chromium
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Warum `sudo env "PATH=$PATH"`?**
|
|
||||||
> `sudo` setzt standardmäßig einen leeren PATH und findet dann nur den System-Node.js (oft v12),
|
|
||||||
> der zu alt für Playwright ist. Mit `env "PATH=$PATH"` wird der aktuelle PATH weitergegeben,
|
|
||||||
> sodass sudo die richtige Node-Version verwendet.
|
|
||||||
|
|
||||||
> **Hinweis:** `install-deps` installiert fehlende Linux-Bibliotheken (libglib, libnss, etc.),
|
|
||||||
> die Chromium in WSL 2 benötigt. Ohne diesen Schritt startet der Browser nicht.
|
|
||||||
|
|
||||||
### Schritt 4 – VS Code MCP-Konfiguration anlegen
|
|
||||||
|
|
||||||
Datei `.vscode/mcp.json` im Projekt-Root anlegen.
|
|
||||||
|
|
||||||
**Wichtig bei nvm:** VS Code startet MCP-Server-Prozesse ohne das nvm-Environment,
|
|
||||||
deshalb muss der **absolute Pfad** zu `node` und dem CLI-Script angegeben werden.
|
|
||||||
|
|
||||||
Zuerst die benötigten Pfade ermitteln:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
which node # z. B. /home/USER/.nvm/versions/node/v21.7.1/bin/node
|
|
||||||
npm list -g @playwright/mcp # zeigt Installationspfad
|
|
||||||
```
|
|
||||||
|
|
||||||
Dann `.vscode/mcp.json` entsprechend anlegen (Pfade anpassen!):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"servers": {
|
|
||||||
"playwright": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "/home/USER/.nvm/versions/node/vX.Y.Z/bin/node",
|
|
||||||
"args": [
|
|
||||||
"/home/USER/.nvm/versions/node/vX.Y.Z/lib/node_modules/@playwright/mcp/cli.js",
|
|
||||||
"--browser", "chromium",
|
|
||||||
"--no-sandbox"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Ohne nvm** (system-weites Node.js ≥ 18): `"command": "npx"` mit
|
|
||||||
> `"args": ["@playwright/mcp@latest", "--browser", "chromium", "--no-sandbox"]` funktioniert direkt.
|
|
||||||
|
|
||||||
> Nach dem Anlegen der Datei: `Ctrl+Shift+P` → *Developer: Reload Window*,
|
|
||||||
> dann **MCP: List Servers** → `playwright` → **Start Server**.
|
|
||||||
> Im Output-Log sollte `Discovered X tools` erscheinen.
|
|
||||||
|
|
||||||
### Schritt 5 – Browser-Seiteninhalt für Copilot freischalten
|
|
||||||
|
|
||||||
Damit Copilot auf Console-Logs, Netzwerkanfragen und Screenshots zugreifen kann,
|
|
||||||
muss eine zusätzliche VS Code-Einstellung aktiviert werden:
|
|
||||||
|
|
||||||
1. `Ctrl+Shift+P` → **Open User Settings (JSON)**
|
|
||||||
2. Eintragen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"workbench.browser.enableChatTools": true
|
|
||||||
```
|
|
||||||
|
|
||||||
3. `Ctrl+Shift+P` → **Developer: Reload Window**
|
|
||||||
|
|
||||||
> **Ohne diese Einstellung** öffnet Copilot zwar den Browser, kann aber weder
|
|
||||||
> Seiteninhalte lesen noch Console-Fehler oder Netzwerkanfragen analysieren.
|
|
||||||
|
|
||||||
### Schritt 6 – Source Maps ✅ (kein Handlungsbedarf)
|
|
||||||
|
|
||||||
Der Frontend-Container startet via `start.sh` den CRA Dev-Server (`react-scripts start`).
|
|
||||||
Im **Development-Modus** von Create React App sind Source Maps **standardmäßig immer aktiv** –
|
|
||||||
das Setzen von `GENERATE_SOURCEMAP` ist nur für Production-Builds (`react-scripts build`) relevant.
|
|
||||||
|
|
||||||
**Kein Eingriff in `docker-compose.yml` oder `package.json` nötig.** Fehler werden
|
|
||||||
automatisch auf die korrekten Quelldateien in `frontend/src/` zurückgeführt.
|
|
||||||
|
|
||||||
### Schritt 7 (optional) – VS Code launch.json für manuelles Developer-Debugging
|
|
||||||
|
|
||||||
Diese Konfiguration gilt für das **eigene Debugging** (nicht für die KI), ermöglicht aber
|
|
||||||
in VS Code Breakpoints direkt im React-Code zu setzen.
|
|
||||||
|
|
||||||
Datei `.vscode/launch.json` anlegen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Frontend debuggen (Chrome, Port 3000)",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}/frontend/src",
|
|
||||||
"sourceMapPathOverrides": {
|
|
||||||
"/app/src/*": "${workspaceFolder}/frontend/src/*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "msedge",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Frontend debuggen (Edge, Port 3000)",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}/frontend/src",
|
|
||||||
"sourceMapPathOverrides": {
|
|
||||||
"/app/src/*": "${workspaceFolder}/frontend/src/*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> `sourceMapPathOverrides` ist notwendig, weil der Code im Docker-Container
|
|
||||||
> unter `/app/src/` liegt, die lokalen Dateien aber unter `frontend/src/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Funktionsweise (Überblick)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ Windows 11 Host │
|
|
||||||
│ │
|
|
||||||
│ VS Code ──── Remote WSL ────► WSL 2 Ubuntu │
|
|
||||||
│ │ │
|
|
||||||
│ Copilot-Tools │
|
|
||||||
│ │ │
|
|
||||||
│ Playwright MCP Server │
|
|
||||||
│ (npx @playwright/mcp) │
|
|
||||||
│ │ │
|
|
||||||
│ Chromium (headless) │
|
|
||||||
│ │ │
|
|
||||||
│ http://localhost:3000 │
|
|
||||||
│ │ │
|
|
||||||
│ ┌───────────────┘ │
|
|
||||||
│ ▼ │
|
|
||||||
│ Docker Desktop │
|
|
||||||
│ └── image-uploader-frontend-dev │
|
|
||||||
│ (nginx, Port 3000 → 80 im Container) │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Copilot ruft ein Browser-Tool auf (z. B. `take_screenshot` oder `list_console_messages`).
|
|
||||||
2. VS Code leitet den Aufruf an den Playwright MCP Server weiter.
|
|
||||||
3. Playwright steuert headless Chromium in WSL 2.
|
|
||||||
4. Chromium lädt `http://localhost:3000` – Docker Desktop leitet den Port durch.
|
|
||||||
5. Das Ergebnis (Screenshot, Logs, etc.) kommt zurück zu Copilot.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verwendung – Copilot ansprechen
|
|
||||||
|
|
||||||
Wenn das Setup aktiv ist, können Anfragen wie folgt gestellt werden:
|
|
||||||
|
|
||||||
```
|
|
||||||
Öffne die App auf localhost:3000, navigiere zur Upload-Seite
|
|
||||||
und zeige mir alle Console-Fehler.
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
Mach einen Screenshot der aktuellen Ansicht und erkläre,
|
|
||||||
was auf der Seite zu sehen ist.
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
Schau, welche API-Requests beim Laden der Galerie-Seite
|
|
||||||
gesendet werden und ob es Fehler gibt.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Problem: `install-deps` schlägt fehl
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Chromium über das System-Paket installieren als Fallback
|
|
||||||
sudo apt-get update && sudo apt-get install -y chromium-browser
|
|
||||||
|
|
||||||
# Dann Playwright auf den System-Chromium zeigen
|
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npx playwright install chromium
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem: Playwright startet nicht (no usable sandbox)
|
|
||||||
|
|
||||||
Chromium in WSL 2 benötigt das `--no-sandbox` Flag. Das Flag wird direkt als
|
|
||||||
Top-Level-Argument übergeben (nicht als `--browser-arg`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"servers": {
|
|
||||||
"playwright": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "/home/USER/.nvm/versions/node/vX.Y.Z/bin/node",
|
|
||||||
"args": [
|
|
||||||
"/home/USER/.nvm/versions/node/vX.Y.Z/lib/node_modules/@playwright/mcp/cli.js",
|
|
||||||
"--browser", "chromium",
|
|
||||||
"--no-sandbox"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem: `localhost:3000` nicht erreichbar
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Prüfen, ob die Dev-Container laufen
|
|
||||||
docker compose -f docker/dev/docker-compose.yml ps
|
|
||||||
|
|
||||||
# Falls nicht: Dev-Umgebung starten
|
|
||||||
./dev.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem: MCP-Server startet, aber Tools sind nicht verfügbar
|
|
||||||
|
|
||||||
- VS Code-Fenster neu laden: `Ctrl+Shift+P` → *Developer: Reload Window*
|
|
||||||
- Sicherstellen, dass `"workbench.browser.enableChatTools": true` in den User Settings gesetzt ist
|
|
||||||
- Copilot Chat Extension ≥ 1.99 prüfen
|
|
||||||
- `.vscode/mcp.json` auf JSON-Syntaxfehler prüfen
|
|
||||||
- Im MCP-Output-Log prüfen: `Ctrl+Shift+P` → *Output: Show Output Channels* → **MCP**
|
|
||||||
|
|
||||||
### Problem: Source Maps zeigen falschen Pfad
|
|
||||||
|
|
||||||
Der `sourceMapPathOverrides`-Eintrag in `launch.json` muss den Container-Pfad
|
|
||||||
(`/app/src/*`) auf den lokalen Pfad (`frontend/src/*`) abbilden.
|
|
||||||
Prüfen, ob der Pfad im Container korrekt ist:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec image-uploader-frontend-dev ls /app/src
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Weiterführende Links
|
|
||||||
|
|
||||||
- [Playwright MCP auf npm](https://www.npmjs.com/package/@playwright/mcp)
|
|
||||||
- [VS Code MCP-Dokumentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
|
|
||||||
- [VS Code Chrome Debugger](https://code.visualstudio.com/docs/nodejs/browser-debugging)
|
|
||||||
- [Playwright in WSL 2](https://playwright.dev/docs/browsers#install-system-dependencies)
|
|
||||||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -1,22 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [2.0.1] - 2025-12-01
|
|
||||||
|
|
||||||
|
|
||||||
## [2.0.0] - 2025-11-30
|
|
||||||
|
|
||||||
### ✨ Features
|
|
||||||
- ENV-Struktur massiv vereinfacht (Phase 6)
|
|
||||||
- Add consent change and deletion notifications (Phase 4)
|
|
||||||
- Add upload notifications to Telegram Bot (Phase 3)
|
|
||||||
- Add TelegramNotificationService (Phase 2)
|
|
||||||
- Add Telegram Bot standalone test (Phase 1)
|
|
||||||
- Add Telegram notification feature request and improve prod.sh Docker registry push
|
|
||||||
|
|
||||||
### 🔧 Chores
|
|
||||||
- Add package.json for Telegram test scripts
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.2] - 2025-11-29
|
## [1.10.2] - 2025-11-29
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
|
|
@ -47,7 +30,7 @@
|
||||||
- Improve release script with tag-based commit detection
|
- Improve release script with tag-based commit detection
|
||||||
|
|
||||||
|
|
||||||
## Public/Internal Host Separation (November 25, 2025)
|
## [Unreleased] - Branch: feature/public-internal-hosts
|
||||||
|
|
||||||
### 🌐 Public/Internal Host Separation (November 25, 2025)
|
### 🌐 Public/Internal Host Separation (November 25, 2025)
|
||||||
|
|
||||||
|
|
@ -146,7 +129,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## feature/security
|
## [Unreleased] - Branch: feature/security
|
||||||
|
|
||||||
### 🔐 Session-Based Admin Authentication & Multi-Admin Support (November 23, 2025)
|
### 🔐 Session-Based Admin Authentication & Multi-Admin Support (November 23, 2025)
|
||||||
|
|
||||||
|
|
@ -168,7 +151,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## feature/SocialMedia
|
## [Unreleased] - Branch: feature/SocialMedia
|
||||||
|
|
||||||
### 🧪 Comprehensive Test Suite & Admin API Security (November 16, 2025)
|
### 🧪 Comprehensive Test Suite & Admin API Security (November 16, 2025)
|
||||||
|
|
||||||
|
|
@ -417,7 +400,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Preload Image
|
## [Unreleased] - Branch: feature/PreloadImage
|
||||||
|
|
||||||
### 🚀 Slideshow Optimization (November 2025)
|
### 🚀 Slideshow Optimization (November 2025)
|
||||||
|
|
||||||
|
|
@ -454,7 +437,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Delete Unproved Groups
|
## [Unreleased] - Branch: feature/DeleteUnprovedGroups
|
||||||
|
|
||||||
### ✨ Automatic Cleanup Feature (November 2025)
|
### ✨ Automatic Cleanup Feature (November 2025)
|
||||||
|
|
||||||
|
|
@ -521,7 +504,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Image Description
|
## [Unreleased] - Branch: feature/ImageDescription
|
||||||
|
|
||||||
### ✨ Image Descriptions Feature (November 2025)
|
### ✨ Image Descriptions Feature (November 2025)
|
||||||
|
|
||||||
|
|
@ -595,7 +578,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Upgrade Deps: React & Node (October 2025)
|
## [Unreleased] - Branch: upgrade/deps-react-node-20251028
|
||||||
|
|
||||||
### 🎯 Major Framework Upgrades (October 2025)
|
### 🎯 Major Framework Upgrades (October 2025)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
<!--
|
|
||||||
Feature Request: Validierungsfeedback & Auto-Scroll bei fehlenden Pflichtfeldern im Upload-Formular
|
|
||||||
Zielgruppe: Entwickler / KI-Implementierer
|
|
||||||
-->
|
|
||||||
|
|
||||||
# FEATURE_REQUEST: Upload-Formular – Validierungsfeedback bei fehlenden Pflichtfeldern
|
|
||||||
|
|
||||||
**Status**: ⏳ Geplant
|
|
||||||
**Erstellt**: 1. April 2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Das Upload-Formular (`MultiUploadPage`) blockiert den Upload-Vorgang still, wenn Pflichtfelder
|
|
||||||
fehlen. Die Funktion `handleUpload` bricht ohne Benutzerrückmeldung ab:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Aktuelles Verhalten in handleUpload():
|
|
||||||
if (selectedImages.length === 0) return;
|
|
||||||
if (!metadata.year || !metadata.title.trim()) return;
|
|
||||||
if (!consents.workshopConsent) return;
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Nutzer klickt auf den Upload-Button – nichts passiert. Kein Hinweis, welches Feld fehlt,
|
|
||||||
keine visuelle Hervorhebung, kein Scroll zum Problem.
|
|
||||||
|
|
||||||
### Betroffene Pflichtfelder
|
|
||||||
|
|
||||||
| Feld | Bedingung |
|
|
||||||
|---|---|
|
|
||||||
| Bilder auswählen | `selectedImages.length === 0` |
|
|
||||||
| Jahr | `!metadata.year` |
|
|
||||||
| Titel der Slideshow | `!metadata.title.trim()` |
|
|
||||||
| Workshop-Einwilligung (Checkbox) | `!consents.workshopConsent` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gewünschtes Verhalten
|
|
||||||
|
|
||||||
1. **Klick auf Upload-Button** → Validierung aller Pflichtfelder
|
|
||||||
2. **Bei einem oder mehreren fehlenden Feldern:**
|
|
||||||
- Das erste fehlende Feld wird visuell hervorgehoben (roter Rahmen / Fehlermeldung unterhalb)
|
|
||||||
- Die Seite scrollt automatisch zum ersten fehlenden Feld
|
|
||||||
- Der Upload wird nicht ausgeführt
|
|
||||||
3. **Fehlermeldungen verschwinden**, sobald der Nutzer das jeweilige Feld korrekt ausfüllt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### UC-1: Kein Titel eingegeben
|
|
||||||
- Nutzer wählt Bilder aus, füllt Jahr aus, setzt Einwilligung, aber lässt Titel leer
|
|
||||||
- Klick auf Upload-Button
|
|
||||||
- → Seite scrollt zum Titel-Feld, Feld wird rot umrandet, Meldung: *„Bitte gib einen Titel für die Slideshow ein."*
|
|
||||||
|
|
||||||
### UC-2: Kein Jahr gewählt
|
|
||||||
- → Seite scrollt zum Jahr-Feld, Fehlermeldung: *„Bitte wähle ein Jahr aus."*
|
|
||||||
|
|
||||||
### UC-3: Einwilligung fehlt
|
|
||||||
- → Seite scrollt zur Checkbox, Fehlermeldung: *„Bitte bestätige die Einwilligung."*
|
|
||||||
|
|
||||||
### UC-4: Mehrere Felder fehlen gleichzeitig
|
|
||||||
- → Scroll zum **ersten** fehlenden Feld (Reihenfolge: Bilder → Jahr → Titel → Einwilligung)
|
|
||||||
- Alle fehlenden Felder werden gleichzeitig markiert
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technische Umsetzung
|
|
||||||
|
|
||||||
### Implementierungsschritte
|
|
||||||
|
|
||||||
1. **Branch anlegen**: `feature/upload-form-validation`
|
|
||||||
2. **Plan-Datei anlegen**: `FeatureRequests/FEATURE_PLAN-upload-form-validation.md`
|
|
||||||
3. Fragen zur Umsetzung stellen (siehe unten)
|
|
||||||
4. Code-Implementierung
|
|
||||||
|
|
||||||
### Technischer Ansatz
|
|
||||||
|
|
||||||
**State für Validierungsfehler** in `MultiUploadPage`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const [validationErrors, setValidationErrors] = useState({
|
|
||||||
images: false,
|
|
||||||
year: false,
|
|
||||||
title: false,
|
|
||||||
workshopConsent: false,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Refs für Auto-Scroll** zu jedem Pflichtfeld:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const imagesSectionRef = useRef(null);
|
|
||||||
const yearFieldRef = useRef(null);
|
|
||||||
const titleFieldRef = useRef(null);
|
|
||||||
const consentRef = useRef(null);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validierungslogik** vor dem Upload:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const validate = () => {
|
|
||||||
const errors = {
|
|
||||||
images: selectedImages.length === 0,
|
|
||||||
year: !metadata.year,
|
|
||||||
title: !metadata.title.trim(),
|
|
||||||
workshopConsent: !consents.workshopConsent,
|
|
||||||
};
|
|
||||||
setValidationErrors(errors);
|
|
||||||
|
|
||||||
// Zum ersten Fehler scrollen
|
|
||||||
if (errors.images) imagesSectionRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
else if (errors.year) yearFieldRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
else if (errors.title) titleFieldRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
else if (errors.workshopConsent) consentRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
|
|
||||||
return !Object.values(errors).some(Boolean);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehler-Reset** bei Änderungen an den Feldern (jeweils im jeweiligen onChange-Handler):
|
|
||||||
|
|
||||||
```js
|
|
||||||
setValidationErrors(prev => ({ ...prev, title: false }));
|
|
||||||
```
|
|
||||||
|
|
||||||
**Visuelle Hervorhebung**: Felder-Komponenten erhalten ein `error`-Prop (MUI-Standard):
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<TextField
|
|
||||||
ref={titleFieldRef}
|
|
||||||
error={validationErrors.title}
|
|
||||||
helperText={validationErrors.title ? 'Bitte gib einen Titel ein.' : ''}
|
|
||||||
...
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Klärungsfragen vor der Umsetzung
|
|
||||||
|
|
||||||
1. Werden die Eingabe-Komponenten für Jahr und Titel als MUI-`TextField`/`Select` gerendert
|
|
||||||
oder als eigene Custom-Komponenten? (Bestimmt, ob `error`-Prop direkt nutzbar ist)
|
|
||||||
2. Soll die Einwilligungs-Checkbox ebenfalls eine inline Fehlermeldung erhalten oder reicht
|
|
||||||
ein Scroll + farbliche Markierung des Checkbox-Labels?
|
|
||||||
3. Soll der Upload-Button während der Validierung deaktiviert (`disabled`) bleiben, oder erst
|
|
||||||
nach dem ersten Klickversuch aktiviert werden? (aktuelle Implementierung: Button ist immer aktiv)
|
|
||||||
|
|
@ -1,385 +0,0 @@
|
||||||
# Feature Plan: Telegram Bot Integration
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Implementierung eines Telegram Bots zur automatischen Benachrichtigung der Werkstatt-Gruppe über wichtige Events im Image Uploader System.
|
|
||||||
|
|
||||||
**Basis:** [FEATURE_REQUEST-telegram.md](./FEATURE_REQUEST-telegram.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phasen-Aufteilung
|
|
||||||
|
|
||||||
### Phase 1: Bot Setup & Standalone-Test
|
|
||||||
**Ziel:** Telegram Bot erstellen und isoliert testen (ohne App-Integration)
|
|
||||||
|
|
||||||
**Status:** 🟢 Abgeschlossen
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [x] Telegram Bot via BotFather erstellt
|
|
||||||
- [x] Bot zu Test-Telegram-Gruppe hinzugefügt
|
|
||||||
- [x] Chat-ID ermittelt
|
|
||||||
- [x] `scripts/telegram-test.js` - Standalone Test-Script
|
|
||||||
- [x] `scripts/README.telegram.md` - Setup-Anleitung
|
|
||||||
- [x] `.env.telegram` - Template für Bot-Credentials
|
|
||||||
- [x] Erfolgreiche Test-Nachricht versendet
|
|
||||||
|
|
||||||
**Akzeptanzkriterium:**
|
|
||||||
✅ Bot sendet erfolgreich Nachricht an Testgruppe
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 2: Backend-Service Integration
|
|
||||||
**Ziel:** TelegramNotificationService in Backend integrieren
|
|
||||||
|
|
||||||
**Status:** 🟢 Abgeschlossen
|
|
||||||
|
|
||||||
**Dependencies:** Phase 1 abgeschlossen
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [x] `backend/src/services/TelegramNotificationService.js`
|
|
||||||
- [x] ENV-Variablen in `docker/dev/backend/config/.env`
|
|
||||||
- [x] Unit-Tests für Service
|
|
||||||
- [x] Docker Dev Environment funktioniert
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 3: Upload-Benachrichtigungen
|
|
||||||
**Ziel:** Automatische Benachrichtigungen bei neuem Upload
|
|
||||||
|
|
||||||
**Status:** 🟢 Abgeschlossen
|
|
||||||
|
|
||||||
**Dependencies:** Phase 2 abgeschlossen
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [x] Integration in `routes/batchUpload.js`
|
|
||||||
- [x] `sendUploadNotification()` Methode
|
|
||||||
- [x] Formatierung mit Icons/Emojis
|
|
||||||
- [x] Integration-Tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 4: User-Änderungs-Benachrichtigungen
|
|
||||||
**Ziel:** Benachrichtigungen bei Consent-Änderungen & Löschungen
|
|
||||||
|
|
||||||
**Status:** 🟢 Abgeschlossen
|
|
||||||
|
|
||||||
**Dependencies:** Phase 3 abgeschlossen
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [x] Integration in `routes/management.js` (PUT/DELETE)
|
|
||||||
- [x] `sendConsentChangeNotification()` Methode
|
|
||||||
- [x] `sendGroupDeletedNotification()` Methode
|
|
||||||
- [x] Integration-Tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 5: Tägliche Lösch-Warnungen
|
|
||||||
**Ziel:** Cron-Job für bevorstehende Löschungen
|
|
||||||
|
|
||||||
**Status:** 🟢 Abgeschlossen
|
|
||||||
|
|
||||||
**Dependencies:** Phase 4 abgeschlossen
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [x] Cron-Job Setup (node-cron)
|
|
||||||
- [x] `sendDeletionWarning()` Methode
|
|
||||||
- [x] Admin-Route für manuellen Trigger (`POST /api/admin/telegram/warning`)
|
|
||||||
- [x] SchedulerService Integration (09:00 daily)
|
|
||||||
- [x] Docker ENV-Variablen konfiguriert
|
|
||||||
- [x] README.md Update
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 6: Production Deployment
|
|
||||||
**Ziel:** Rollout in Production-Umgebung + ENV-Vereinfachung
|
|
||||||
|
|
||||||
**Status:** 🟢 Abgeschlossen
|
|
||||||
|
|
||||||
**Dependencies:** Phase 1-5 abgeschlossen + getestet
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [x] ENV-Struktur vereinfachen (zu viele .env-Dateien!)
|
|
||||||
- [x] Production ENV-Variablen in docker/prod/.env konfigurieren
|
|
||||||
- [x] docker/prod/docker-compose.yml mit Telegram-ENV erweitern
|
|
||||||
- [x] Consent-Änderung Bug Fix (platform_name statt name)
|
|
||||||
- [x] README.md Update mit ENV-Struktur Dokumentation
|
|
||||||
- ⏭️ Bot in echte Werkstatt-Gruppe einfügen (optional, bei Bedarf)
|
|
||||||
- ⏭️ Production Testing (optional, bei Bedarf)
|
|
||||||
|
|
||||||
**ENV-Vereinfachung (Abgeschlossen):**
|
|
||||||
```
|
|
||||||
Vorher: 16 .env-Dateien mit redundanter Konfiguration
|
|
||||||
Nachher: 2 zentrale .env-Dateien
|
|
||||||
✅ docker/dev/.env (alle dev secrets)
|
|
||||||
✅ docker/prod/.env (alle prod secrets)
|
|
||||||
✅ docker-compose.yml nutzt ${VAR} Platzhalter
|
|
||||||
✅ Gemountete .env-Dateien entfernt (wurden überschrieben)
|
|
||||||
✅ Alle ENV-Variablen in docker-compose environment
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 - Detaillierter Plan
|
|
||||||
|
|
||||||
### 1. Vorbereitung (5 min)
|
|
||||||
|
|
||||||
**Auf Windows 11 Host-System:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Node.js Version prüfen
|
|
||||||
node --version # Sollte >= 18.x sein
|
|
||||||
|
|
||||||
# Projektverzeichnis öffnen
|
|
||||||
cd /home/lotzm/gitea.hobbyhimmel/Project-Image-Uploader/scripts
|
|
||||||
|
|
||||||
# Dependencies installieren (lokal)
|
|
||||||
npm init -y # Falls noch keine package.json
|
|
||||||
npm install node-telegram-bot-api dotenv
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Telegram Bot erstellen (10 min)
|
|
||||||
|
|
||||||
**Anleitung:** Siehe `scripts/README.telegram.md`
|
|
||||||
|
|
||||||
**Schritte:**
|
|
||||||
1. Telegram öffnen (Windows 11 App)
|
|
||||||
2. [@BotFather](https://t.me/botfather) suchen
|
|
||||||
3. `/newbot` Command
|
|
||||||
4. Bot-Name: "Werkstatt Image Uploader Bot"
|
|
||||||
5. Username: `werkstatt_uploader_bot` (oder verfügbar)
|
|
||||||
6. **Token kopieren** → `.env.telegram`
|
|
||||||
|
|
||||||
### 3. Test-Gruppe erstellen & Bot hinzufügen (5 min)
|
|
||||||
|
|
||||||
**Schritte:**
|
|
||||||
1. Neue Telegram-Gruppe erstellen: "Werkstatt Upload Bot Test"
|
|
||||||
2. Bot zur Gruppe hinzufügen: @werkstatt_uploader_bot
|
|
||||||
3. **Chat-ID ermitteln** (siehe README.telegram.md)
|
|
||||||
4. Chat-ID speichern → `.env.telegram`
|
|
||||||
|
|
||||||
### 4. Test-Script erstellen (10 min)
|
|
||||||
|
|
||||||
**Datei:** `scripts/telegram-test.js`
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Lädt `.env.telegram`
|
|
||||||
- Validiert Bot-Token
|
|
||||||
- Sendet Test-Nachricht
|
|
||||||
- Error-Handling
|
|
||||||
|
|
||||||
### 5. Erste Nachricht senden (2 min)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd scripts
|
|
||||||
node telegram-test.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erwartete Ausgabe:**
|
|
||||||
```
|
|
||||||
✅ Telegram Bot erfolgreich verbunden!
|
|
||||||
Bot-Name: Werkstatt Image Uploader Bot
|
|
||||||
Bot-Username: @werkstatt_uploader_bot
|
|
||||||
|
|
||||||
📤 Sende Test-Nachricht an Chat -1001234567890...
|
|
||||||
✅ Nachricht erfolgreich gesendet!
|
|
||||||
```
|
|
||||||
|
|
||||||
**In Telegram-Gruppe:**
|
|
||||||
```
|
|
||||||
🤖 Telegram Bot Test
|
|
||||||
|
|
||||||
Dies ist eine Test-Nachricht vom Werkstatt Image Uploader Bot.
|
|
||||||
|
|
||||||
Status: ✅ Erfolgreich verbunden!
|
|
||||||
Zeitstempel: 2025-11-29 14:23:45
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dateistruktur (Phase 1)
|
|
||||||
|
|
||||||
```
|
|
||||||
scripts/
|
|
||||||
├── README.telegram.md # Setup-Anleitung (NEU)
|
|
||||||
├── telegram-test.js # Test-Script (NEU)
|
|
||||||
├── .env.telegram.example # ENV-Template (NEU)
|
|
||||||
├── .env.telegram # Echte Credentials (gitignored, NEU)
|
|
||||||
├── package.json # Lokale Dependencies (NEU)
|
|
||||||
└── node_modules/ # npm packages (gitignored)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables (Phase 1)
|
|
||||||
|
|
||||||
**Datei:** `scripts/.env.telegram`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Telegram Bot Configuration
|
|
||||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
||||||
TELEGRAM_CHAT_ID=-1001234567890
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies (Phase 1)
|
|
||||||
|
|
||||||
**Package:** `scripts/package.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "telegram-test-scripts",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Standalone Telegram Bot Testing",
|
|
||||||
"main": "telegram-test.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "node telegram-test.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
|
||||||
"dotenv": "^16.3.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sicherheit (Phase 1)
|
|
||||||
|
|
||||||
**`.gitignore` ergänzen:**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Telegram Credentials
|
|
||||||
scripts/.env.telegram
|
|
||||||
scripts/node_modules/
|
|
||||||
scripts/package-lock.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Wichtig:**
|
|
||||||
- ❌ Niemals `.env.telegram` committen!
|
|
||||||
- ✅ Nur `.env.telegram.example` (ohne echte Tokens) committen
|
|
||||||
- ✅ Bot-Token regenerieren, falls versehentlich exposed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist (Phase 1)
|
|
||||||
|
|
||||||
- [x] Node.js Version >= 18.x
|
|
||||||
- [x] Telegram App installiert (Windows 11)
|
|
||||||
- [x] Bot via BotFather erstellt
|
|
||||||
- [x] Bot-Token gespeichert in `.env.telegram`
|
|
||||||
- [x] Test-Gruppe erstellt
|
|
||||||
- [x] Bot zur Gruppe hinzugefügt
|
|
||||||
- [x] Chat-ID ermittelt
|
|
||||||
- [x] Chat-ID gespeichert in `.env.telegram`
|
|
||||||
- [x] Privacy Mode deaktiviert
|
|
||||||
- [x] Test-Nachricht erfolgreich gesendet
|
|
||||||
- [ ] `npm install` erfolgreich
|
|
||||||
- [ ] `node telegram-test.js` läuft ohne Fehler
|
|
||||||
- [ ] Test-Nachricht in Telegram-Gruppe empfangen
|
|
||||||
- [ ] Formatierung (Emojis, Zeilenumbrüche) korrekt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting (Phase 1)
|
|
||||||
|
|
||||||
### Problem: "Unauthorized (401)"
|
|
||||||
**Lösung:** Bot-Token falsch → BotFather prüfen, `.env.telegram` korrigieren
|
|
||||||
|
|
||||||
### Problem: "Bad Request: chat not found"
|
|
||||||
**Lösung:** Chat-ID falsch → Neue Nachricht in Gruppe senden, Chat-ID neu ermitteln
|
|
||||||
|
|
||||||
### Problem: "ETELEGRAM: 403 Forbidden"
|
|
||||||
**Lösung:** Bot wurde aus Gruppe entfernt → Bot erneut zur Gruppe hinzufügen
|
|
||||||
|
|
||||||
### Problem: "Module not found: node-telegram-bot-api"
|
|
||||||
**Lösung:**
|
|
||||||
```bash
|
|
||||||
cd scripts
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nächste Schritte (nach Phase 1)
|
|
||||||
|
|
||||||
1. **Code-Review:** `scripts/telegram-test.js`
|
|
||||||
2. **Dokumentation Review:** `scripts/README.telegram.md`
|
|
||||||
3. **Commit:**
|
|
||||||
```bash
|
|
||||||
git add scripts/
|
|
||||||
git commit -m "feat: Add Telegram Bot standalone test (Phase 1)"
|
|
||||||
```
|
|
||||||
4. **Phase 2 starten:** Backend-Integration planen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Zeitschätzung
|
|
||||||
|
|
||||||
| Phase | Aufwand | Beschreibung |
|
|
||||||
|-------|---------|--------------|
|
|
||||||
| **Phase 1** | **~45 min** | Bot Setup + Standalone-Test |
|
|
||||||
| Phase 2 | ~2h | Backend-Service |
|
|
||||||
| Phase 3 | ~2h | Upload-Benachrichtigungen |
|
|
||||||
| Phase 4 | ~2h | Änderungs-Benachrichtigungen |
|
|
||||||
| Phase 5 | ~2h | Cron-Job |
|
|
||||||
| Phase 6 | ~1h | Production Deployment |
|
|
||||||
| **Gesamt** | **~9-10h** | Vollständige Integration |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conventional Commits (ab Phase 1)
|
|
||||||
|
|
||||||
**Phase 1:**
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: Add Telegram Bot test script"
|
|
||||||
git commit -m "docs: Add Telegram Bot setup guide"
|
|
||||||
git commit -m "chore: Add node-telegram-bot-api dependency to scripts"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Phase 2:**
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: Add TelegramNotificationService"
|
|
||||||
git commit -m "test: Add TelegramNotificationService unit tests"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Phase 3-6:**
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: Add upload notification to Telegram"
|
|
||||||
git commit -m "feat: Add consent change notifications"
|
|
||||||
git commit -m "feat: Add daily deletion warnings cron job"
|
|
||||||
git commit -m "docs: Update README with Telegram features"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Release-Planung
|
|
||||||
|
|
||||||
**Phase 1:** Kein Release (interne Tests)
|
|
||||||
|
|
||||||
**Phase 6 (Final):**
|
|
||||||
- **Version:** `2.0.0` (Major Release)
|
|
||||||
- **Branch:** `feature/telegram-notifications`
|
|
||||||
- **Release-Command:** `npm run release:major`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status-Tracking
|
|
||||||
|
|
||||||
**Letzte Aktualisierung:** 2025-11-30
|
|
||||||
|
|
||||||
| Phase | Status | Datum |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Phase 1 | 🟢 Abgeschlossen | 2025-11-29 |
|
|
||||||
| Phase 2 | 🟢 Abgeschlossen | 2025-11-29 |
|
|
||||||
| Phase 3 | 🟢 Abgeschlossen | 2025-11-29 |
|
|
||||||
| Phase 4 | 🟢 Abgeschlossen | 2025-11-30 |
|
|
||||||
| Phase 5 | 🟢 Abgeschlossen | 2025-11-30 |
|
|
||||||
| Phase 6 | 🟡 ENV vereinfacht | 2025-11-30 |
|
|
||||||
|
|
||||||
**Legende:**
|
|
||||||
- 🟢 Abgeschlossen
|
|
||||||
- 🟡 In Arbeit
|
|
||||||
- 🔴 Blockiert
|
|
||||||
- ⚪ Ausstehend
|
|
||||||
|
|
@ -1,450 +0,0 @@
|
||||||
# Feature Request: Telegram Bot für Benachrichtigungen
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Integration eines Telegram Bots zur automatischen Benachrichtigung der Werkstatt-Gruppe über wichtige Events im Image Uploader System.
|
|
||||||
|
|
||||||
## Ziel
|
|
||||||
|
|
||||||
Werkstatt-Mitarbeiter sollen zeitnah über neue Uploads, Änderungen und bevorstehende Löschungen informiert werden, ohne ständig das Admin-Panel prüfen zu müssen.
|
|
||||||
|
|
||||||
## Use Case
|
|
||||||
|
|
||||||
Die Offene Werkstatt hat eine Telegram Gruppe, in der das Team kommuniziert. Der Bot wird zu dieser Gruppe hinzugefügt und sendet automatisierte Benachrichtigungen bei relevanten Events.
|
|
||||||
|
|
||||||
## Funktionale Anforderungen
|
|
||||||
|
|
||||||
### 1. Benachrichtigung: Neuer Upload
|
|
||||||
|
|
||||||
**Trigger:** Erfolgreicher Batch-Upload über `/api/upload-batch`
|
|
||||||
|
|
||||||
**Nachricht enthält:**
|
|
||||||
- 📸 Upload-Icon
|
|
||||||
- Name des Uploaders
|
|
||||||
- Anzahl der hochgeladenen Bilder
|
|
||||||
- Jahr der Gruppe
|
|
||||||
- Titel der Gruppe
|
|
||||||
- Workshop-Consent Status (✅ Ja / ❌ Nein)
|
|
||||||
- Social Media Consents (Facebook, Instagram, TikTok Icons)
|
|
||||||
- Link zum Admin-Panel (Moderation)
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
📸 Neuer Upload!
|
|
||||||
|
|
||||||
Uploader: Max Mustermann
|
|
||||||
Bilder: 12
|
|
||||||
Gruppe: 2024 - Schweißkurs November
|
|
||||||
Workshop: ✅ Ja
|
|
||||||
Social Media: 📘 Instagram, 🎵 TikTok
|
|
||||||
|
|
||||||
🔗 Zur Freigabe: https://internal.hobbyhimmel.de/moderation
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Benachrichtigung: User-Änderungen
|
|
||||||
|
|
||||||
**Trigger:**
|
|
||||||
- `PUT /api/manage/:token` (Consent-Änderung)
|
|
||||||
- `DELETE /api/manage/:token/groups/:groupId` (Gruppenl löschung durch User)
|
|
||||||
|
|
||||||
**Nachricht enthält:**
|
|
||||||
- ⚙️ Änderungs-Icon
|
|
||||||
- Art der Änderung (Consent Update / Gruppe gelöscht)
|
|
||||||
- Betroffene Gruppe (Jahr + Titel)
|
|
||||||
- Uploader-Name
|
|
||||||
- Neue Consent-Werte (bei Update)
|
|
||||||
|
|
||||||
**Beispiel (Consent-Änderung):**
|
|
||||||
```
|
|
||||||
⚙️ User-Änderung
|
|
||||||
|
|
||||||
Aktion: Consent aktualisiert
|
|
||||||
Gruppe: 2024 - Schweißkurs November
|
|
||||||
Uploader: Max Mustermann
|
|
||||||
|
|
||||||
Neu:
|
|
||||||
Workshop: ❌ Nein (vorher: ✅)
|
|
||||||
Social Media: 📘 Instagram (TikTok entfernt)
|
|
||||||
|
|
||||||
🔗 Details: https://internal.hobbyhimmel.de/moderation
|
|
||||||
```
|
|
||||||
|
|
||||||
**Beispiel (Gruppe gelöscht):**
|
|
||||||
```
|
|
||||||
⚙️ User-Änderung
|
|
||||||
|
|
||||||
Aktion: Gruppe gelöscht
|
|
||||||
Gruppe: 2024 - Schweißkurs November
|
|
||||||
Uploader: Max Mustermann
|
|
||||||
Bilder: 12
|
|
||||||
|
|
||||||
ℹ️ User hat Gruppe selbst über Management-Link gelöscht
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Benachrichtigung: Ablauf Freigabe / Löschung in 1 Tag
|
|
||||||
|
|
||||||
**Trigger:** Täglicher Cron-Job (z.B. 09:00 Uhr)
|
|
||||||
|
|
||||||
**Prüfung:**
|
|
||||||
- Alle nicht-freigegebenen Gruppen mit `created_at < NOW() - 6 days`
|
|
||||||
- Werden in 24 Stunden durch Cleanup-Service gelöscht
|
|
||||||
|
|
||||||
**Nachricht enthält:**
|
|
||||||
- ⏰ Warnung-Icon
|
|
||||||
- Liste aller betroffenen Gruppen
|
|
||||||
- Countdown bis Löschung
|
|
||||||
- Hinweis auf Freigabe-Möglichkeit
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
⏰ Löschung in 24 Stunden!
|
|
||||||
|
|
||||||
Folgende Gruppen werden morgen automatisch gelöscht:
|
|
||||||
|
|
||||||
1. 2024 - Schweißkurs November
|
|
||||||
Uploader: Max Mustermann
|
|
||||||
Bilder: 12
|
|
||||||
Hochgeladen: 20.11.2024
|
|
||||||
|
|
||||||
2. 2024 - Holzarbeiten Workshop
|
|
||||||
Uploader: Anna Schmidt
|
|
||||||
Bilder: 8
|
|
||||||
Hochgeladen: 21.11.2024
|
|
||||||
|
|
||||||
💡 Jetzt freigeben oder Freigabe bleibt aus!
|
|
||||||
🔗 Zur Moderation: https://internal.hobbyhimmel.de/moderation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technische Anforderungen
|
|
||||||
|
|
||||||
### Backend-Integration
|
|
||||||
|
|
||||||
**Neue Umgebungsvariablen:**
|
|
||||||
```bash
|
|
||||||
TELEGRAM_BOT_TOKEN=<bot-token>
|
|
||||||
TELEGRAM_CHAT_ID=<werkstatt-gruppen-id>
|
|
||||||
TELEGRAM_ENABLED=true
|
|
||||||
```
|
|
||||||
|
|
||||||
**Neue Service-Datei:** `backend/src/services/TelegramNotificationService.js`
|
|
||||||
|
|
||||||
**Methoden:**
|
|
||||||
- `sendUploadNotification(groupData)`
|
|
||||||
- `sendConsentChangeNotification(oldConsents, newConsents, groupData)`
|
|
||||||
- `sendGroupDeletedNotification(groupData)`
|
|
||||||
- `sendDeletionWarning(groupsList)`
|
|
||||||
|
|
||||||
**Integration Points:**
|
|
||||||
- `routes/batchUpload.js` → Nach erfolgreichem Upload
|
|
||||||
- `routes/management.js` → PUT/DELETE Endpoints
|
|
||||||
- `services/GroupCleanupService.js` → Neue Methode für tägliche Prüfung
|
|
||||||
|
|
||||||
### Telegram Bot Setup
|
|
||||||
|
|
||||||
**Bot erstellen:**
|
|
||||||
1. Mit [@BotFather](https://t.me/botfather) sprechen
|
|
||||||
2. `/newbot` → Bot-Name: "Werkstatt Image Uploader Bot"
|
|
||||||
3. Token speichern → `.env`
|
|
||||||
|
|
||||||
**Bot zur Gruppe hinzufügen:**
|
|
||||||
1. Bot zu Werkstatt-Gruppe einladen
|
|
||||||
2. Chat-ID ermitteln: `https://api.telegram.org/bot<TOKEN>/getUpdates`
|
|
||||||
3. Chat-ID speichern → `.env`
|
|
||||||
|
|
||||||
**Berechtigungen:**
|
|
||||||
- ✅ Can send messages
|
|
||||||
- ✅ Can send photos (optional, für Vorschau-Bilder)
|
|
||||||
- ❌ Keine Admin-Rechte nötig
|
|
||||||
|
|
||||||
### Cron-Job für tägliche Prüfung
|
|
||||||
|
|
||||||
**Optionen:**
|
|
||||||
|
|
||||||
**A) Node-Cron (empfohlen für Development):**
|
|
||||||
```javascript
|
|
||||||
// backend/src/services/TelegramScheduler.js
|
|
||||||
const cron = require('node-cron');
|
|
||||||
|
|
||||||
// Jeden Tag um 09:00 Uhr
|
|
||||||
cron.schedule('0 9 * * *', async () => {
|
|
||||||
await checkPendingDeletions();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**B) System Cron (Production):**
|
|
||||||
```bash
|
|
||||||
# crontab -e
|
|
||||||
0 9 * * * curl -X POST http://localhost:5000/api/admin/telegram/check-deletions
|
|
||||||
```
|
|
||||||
|
|
||||||
**Neue Route:** `POST /api/admin/telegram/check-deletions` (Admin-Auth)
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
**Neue NPM Packages:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
|
||||||
"node-cron": "^3.0.3"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
### Development (.env)
|
|
||||||
```bash
|
|
||||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
||||||
TELEGRAM_CHAT_ID=-1001234567890
|
|
||||||
TELEGRAM_ENABLED=true
|
|
||||||
TELEGRAM_DAILY_CHECK_TIME=09:00
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
- Gleiche Variablen in `docker/prod/backend/config/.env`
|
|
||||||
- Cron-Job via Node-Cron oder System-Cron
|
|
||||||
|
|
||||||
## Sicherheit
|
|
||||||
|
|
||||||
- ✅ Bot-Token niemals committen (`.env` nur)
|
|
||||||
- ✅ Chat-ID validieren (nur bekannte Gruppen)
|
|
||||||
- ✅ Keine sensiblen Daten in Nachrichten (keine Email, keine vollständigen Token)
|
|
||||||
- ✅ Rate-Limiting für Telegram API (max 30 msg/sec)
|
|
||||||
- ✅ Error-Handling: Wenn Telegram down → Upload funktioniert trotzdem
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
**Manuell:**
|
|
||||||
```bash
|
|
||||||
# Trigger Upload-Benachrichtigung
|
|
||||||
curl -X POST http://localhost:5001/api/upload-batch \
|
|
||||||
-F "images=@test.jpg" \
|
|
||||||
-F "year=2024" \
|
|
||||||
-F "title=Test Upload" \
|
|
||||||
-F "name=Test User" \
|
|
||||||
-F 'consents={"workshopConsent":true,"socialMediaConsents":[]}'
|
|
||||||
|
|
||||||
# Trigger Consent-Änderung
|
|
||||||
curl -X PUT http://localhost:5001/api/manage/<TOKEN> \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"workshopConsent":false,"socialMediaConsents":[]}'
|
|
||||||
|
|
||||||
# Trigger tägliche Prüfung (Admin)
|
|
||||||
curl -X POST http://localhost:5001/api/admin/telegram/check-deletions \
|
|
||||||
-b cookies.txt -H "X-CSRF-Token: $CSRF"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Automatisiert:**
|
|
||||||
- Unit-Tests für `TelegramNotificationService.js`
|
|
||||||
- Mock Telegram API mit `nock`
|
|
||||||
- Prüfe Nachrichtenformat + Escaping
|
|
||||||
|
|
||||||
## Optional: Zukünftige Erweiterungen
|
|
||||||
|
|
||||||
- 📊 Wöchentlicher Statistik-Report (Uploads, Freigaben, Löschungen)
|
|
||||||
- 🖼️ Preview-Bild im Telegram (erstes Bild der Gruppe)
|
|
||||||
- 💬 Interaktive Buttons (z.B. "Freigeben", "Ablehnen") → Webhook
|
|
||||||
- 🔔 Admin-Befehle (`/stats`, `/pending`, `/cleanup`)
|
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
|
||||||
|
|
||||||
- [ ] Bot sendet Nachricht bei neuem Upload
|
|
||||||
- [ ] Bot sendet Nachricht bei Consent-Änderung
|
|
||||||
- [ ] Bot sendet Nachricht bei User-Löschung
|
|
||||||
- [ ] Bot sendet tägliche Warnung für bevorstehende Löschungen (09:00 Uhr)
|
|
||||||
- [ ] Alle Nachrichten enthalten relevante Informationen + Link
|
|
||||||
- [ ] Telegram-Fehler brechen Upload/Änderungen nicht ab
|
|
||||||
- [ ] ENV-Variable `TELEGRAM_ENABLED=false` deaktiviert alle Benachrichtigungen
|
|
||||||
- [ ] README.dev.md enthält Setup-Anleitung
|
|
||||||
|
|
||||||
## Aufwandsschätzung
|
|
||||||
|
|
||||||
- Backend-Integration: ~4-6 Stunden
|
|
||||||
- Cron-Job Setup: ~2 Stunden
|
|
||||||
- Testing: ~2 Stunden
|
|
||||||
- Dokumentation: ~1 Stunde
|
|
||||||
|
|
||||||
**Gesamt: ~9-11 Stunden**
|
|
||||||
|
|
||||||
## Priorität
|
|
||||||
|
|
||||||
**Medium** - Verbessert Workflow, aber nicht kritisch für Kernfunktion
|
|
||||||
|
|
||||||
## Release-Planung
|
|
||||||
|
|
||||||
**Target Version:** `2.0.0` (Major Version)
|
|
||||||
|
|
||||||
**Begründung für Major Release:**
|
|
||||||
- Neue Infrastruktur-Abhängigkeit (Telegram Bot)
|
|
||||||
- Neue Umgebungsvariablen erforderlich
|
|
||||||
- Breaking Change: Optional, aber empfohlene Konfiguration
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### 1. Feature Branch erstellen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout -b feature/telegram-notifications
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Conventional Commits verwenden
|
|
||||||
|
|
||||||
**Wichtig:** Alle Commits nach [Conventional Commits](https://www.conventionalcommits.org/) formatieren!
|
|
||||||
|
|
||||||
**Beispiele:**
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: Add TelegramNotificationService"
|
|
||||||
git commit -m "feat: Add upload notification endpoint"
|
|
||||||
git commit -m "feat: Add daily deletion warning cron job"
|
|
||||||
git commit -m "chore: Add node-telegram-bot-api dependency"
|
|
||||||
git commit -m "docs: Update README with Telegram setup"
|
|
||||||
git commit -m "test: Add TelegramNotificationService unit tests"
|
|
||||||
git commit -m "fix: Handle Telegram API rate limiting"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Commit-Typen:**
|
|
||||||
- `feat:` - Neue Features
|
|
||||||
- `fix:` - Bugfixes
|
|
||||||
- `docs:` - Dokumentation
|
|
||||||
- `test:` - Tests
|
|
||||||
- `chore:` - Dependencies, Config
|
|
||||||
- `refactor:` - Code-Umstrukturierung
|
|
||||||
|
|
||||||
→ **Wird automatisch im CHANGELOG.md gruppiert!**
|
|
||||||
|
|
||||||
### 3. Development Setup
|
|
||||||
|
|
||||||
**Docker Dev Environment nutzen:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Container starten
|
|
||||||
./dev.sh
|
|
||||||
|
|
||||||
# .env konfigurieren (Backend)
|
|
||||||
# docker/dev/backend/config/.env
|
|
||||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
||||||
TELEGRAM_CHAT_ID=-1001234567890
|
|
||||||
TELEGRAM_ENABLED=true
|
|
||||||
TELEGRAM_DAILY_CHECK_TIME=09:00
|
|
||||||
|
|
||||||
# Backend neu starten (lädt neue ENV-Variablen)
|
|
||||||
docker compose -f docker/dev/docker-compose.yml restart backend-dev
|
|
||||||
|
|
||||||
# Logs verfolgen
|
|
||||||
docker compose -f docker/dev/docker-compose.yml logs -f backend-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tests ausführen:**
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
npm test -- tests/unit/TelegramNotificationService.test.js
|
|
||||||
npm test -- tests/api/telegram.test.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Dokumentation aktualisieren
|
|
||||||
|
|
||||||
**README.md** - User-Dokumentation ergänzen:
|
|
||||||
- [ ] Telegram-Bot Setup-Anleitung
|
|
||||||
- [ ] Benachrichtigungs-Features beschreiben
|
|
||||||
- [ ] ENV-Variablen dokumentieren
|
|
||||||
|
|
||||||
**README.dev.md** - Development-Doku ergänzen:
|
|
||||||
- [ ] Telegram-Bot Testing-Anleitung
|
|
||||||
- [ ] Cron-Job Debugging
|
|
||||||
- [ ] TelegramNotificationService API-Referenz
|
|
||||||
- [ ] Beispiel-Curl-Commands für manuelle Trigger
|
|
||||||
|
|
||||||
**Sektion in README.dev.md einfügen (z.B. nach "Cleanup-System testen"):**
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### Telegram-Benachrichtigungen testen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Bot-Token validieren:
|
|
||||||
curl https://api.telegram.org/bot<TOKEN>/getMe
|
|
||||||
|
|
||||||
# Chat-ID ermitteln:
|
|
||||||
curl https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
||||||
|
|
||||||
# Upload-Benachrichtigung testen:
|
|
||||||
# → Einfach Upload durchführen, Telegram-Gruppe prüfen
|
|
||||||
|
|
||||||
# Consent-Änderung testen:
|
|
||||||
curl -X PUT http://localhost:5001/api/manage/<TOKEN> \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"workshopConsent":false,"socialMediaConsents":[]}'
|
|
||||||
|
|
||||||
# Tägliche Löschwarnung manuell triggern:
|
|
||||||
curl -b cookies.txt -H "X-CSRF-Token: $CSRF" \
|
|
||||||
-X POST http://localhost:5001/api/admin/telegram/check-deletions
|
|
||||||
```
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 5. Testing Checklist
|
|
||||||
|
|
||||||
- [ ] Unit-Tests für `TelegramNotificationService.js` (min. 80% Coverage)
|
|
||||||
- [ ] Integration-Tests für alle 3 Benachrichtigungstypen
|
|
||||||
- [ ] Manueller Test: Upload → Telegram-Nachricht kommt an
|
|
||||||
- [ ] Manueller Test: Consent-Änderung → Telegram-Nachricht kommt an
|
|
||||||
- [ ] Manueller Test: User-Löschung → Telegram-Nachricht kommt an
|
|
||||||
- [ ] Manueller Test: Cron-Job (tägliche Warnung) funktioniert
|
|
||||||
- [ ] Error-Handling: Telegram down → Upload funktioniert trotzdem
|
|
||||||
- [ ] ENV `TELEGRAM_ENABLED=false` → Keine Nachrichten
|
|
||||||
|
|
||||||
### 6. Release erstellen
|
|
||||||
|
|
||||||
**Nach erfolgreicher Implementierung:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Alle Änderungen committen (Conventional Commits!)
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: Complete Telegram notification system"
|
|
||||||
|
|
||||||
# Feature Branch pushen
|
|
||||||
git push origin feature/telegram-notifications
|
|
||||||
|
|
||||||
# Merge in main (nach Review)
|
|
||||||
git checkout main
|
|
||||||
git merge feature/telegram-notifications
|
|
||||||
|
|
||||||
# Major Release erstellen (2.0.0)
|
|
||||||
npm run release:major
|
|
||||||
|
|
||||||
# CHANGELOG prüfen (wurde automatisch generiert!)
|
|
||||||
cat CHANGELOG.md
|
|
||||||
|
|
||||||
# Push mit Tags
|
|
||||||
git push --follow-tags
|
|
||||||
|
|
||||||
# Docker Images bauen und pushen
|
|
||||||
./prod.sh # Option 3
|
|
||||||
```
|
|
||||||
|
|
||||||
**Release Notes (automatisch in CHANGELOG.md):**
|
|
||||||
- ✨ Features: Telegram-Bot Integration (Upload, Änderungen, Lösch-Warnungen)
|
|
||||||
- 📚 Documentation: README.md + README.dev.md Updates
|
|
||||||
- 🧪 Tests: TelegramNotificationService Tests
|
|
||||||
|
|
||||||
### 7. Deployment
|
|
||||||
|
|
||||||
**Production .env updaten:**
|
|
||||||
```bash
|
|
||||||
# docker/prod/backend/config/.env
|
|
||||||
TELEGRAM_BOT_TOKEN=<production-token>
|
|
||||||
TELEGRAM_CHAT_ID=<production-chat-id>
|
|
||||||
TELEGRAM_ENABLED=true
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container neu starten:**
|
|
||||||
```bash
|
|
||||||
./prod.sh # Option 4: Container neu bauen und starten
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wichtige Hinweise
|
|
||||||
|
|
||||||
⚠️ **Vor dem Release prüfen:**
|
|
||||||
- README.md enthält User-Setup-Anleitung
|
|
||||||
- README.dev.md enthält Developer-Anleitung
|
|
||||||
- Alle Tests bestehen (`npm test`)
|
|
||||||
- Docker Dev Setup funktioniert
|
|
||||||
- Conventional Commits verwendet
|
|
||||||
- CHANGELOG.md ist korrekt generiert
|
|
||||||
|
|
@ -302,35 +302,6 @@ describe('Example API', () => {
|
||||||
# 5. Log prüfen: GET http://localhost:5001/api/admin/deletion-log
|
# 5. Log prüfen: GET http://localhost:5001/api/admin/deletion-log
|
||||||
```
|
```
|
||||||
|
|
||||||
### Telegram-Benachrichtigungen testen
|
|
||||||
|
|
||||||
**Voraussetzung:** Bot-Setup abgeschlossen (siehe `scripts/README.telegram.md`)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. ENV-Variablen in docker/dev/backend/config/.env konfigurieren:
|
|
||||||
TELEGRAM_ENABLED=true
|
|
||||||
TELEGRAM_BOT_TOKEN=<dein-bot-token>
|
|
||||||
TELEGRAM_CHAT_ID=<deine-chat-id>
|
|
||||||
|
|
||||||
# 2. Backend neu starten (lädt neue ENV-Variablen):
|
|
||||||
docker compose -f docker/dev/docker-compose.yml restart backend-dev
|
|
||||||
|
|
||||||
# 3. Test-Nachricht wird automatisch beim Server-Start gesendet
|
|
||||||
docker compose -f docker/dev/docker-compose.yml logs -f backend-dev
|
|
||||||
|
|
||||||
# 4. Upload-Benachrichtigung testen (Phase 3+):
|
|
||||||
curl -X POST http://localhost:5001/api/upload-batch \
|
|
||||||
-F "images=@test.jpg" \
|
|
||||||
-F "year=2024" \
|
|
||||||
-F "title=Test Upload" \
|
|
||||||
-F "name=Test User" \
|
|
||||||
-F 'consents={"workshopConsent":true,"socialMediaConsents":[]}'
|
|
||||||
# → Prüfe Telegram-Gruppe auf Benachrichtigung
|
|
||||||
|
|
||||||
# 5. Service manuell deaktivieren:
|
|
||||||
TELEGRAM_ENABLED=false
|
|
||||||
```
|
|
||||||
|
|
||||||
### API-Tests
|
### API-Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
155
README.md
155
README.md
|
|
@ -5,7 +5,6 @@ A self-hosted image uploader with multi-image upload capabilities and automatic
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
**Multi-Image Upload**: Upload multiple images at once with batch processing
|
**Multi-Image Upload**: Upload multiple images at once with batch processing
|
||||||
**Telegram Notifications**: 🆕 Real-time notifications for uploads, consent changes, deletions, and daily warnings
|
|
||||||
**Social Media Consent Management**: 🆕 GDPR-compliant consent system for workshop display and social media publishing
|
**Social Media Consent Management**: 🆕 GDPR-compliant consent system for workshop display and social media publishing
|
||||||
**Automatic Cleanup**: 🆕 Unapproved groups are automatically deleted after 7 days
|
**Automatic Cleanup**: 🆕 Unapproved groups are automatically deleted after 7 days
|
||||||
**Deletion Log**: 🆕 Complete audit trail of automatically deleted content
|
**Deletion Log**: 🆕 Complete audit trail of automatically deleted content
|
||||||
|
|
@ -21,7 +20,94 @@ A self-hosted image uploader with multi-image upload capabilities and automatic
|
||||||
## What's New
|
## What's New
|
||||||
This project extends the original [Image-Uploader by vallezw](https://github.com/vallezw/Image-Uploader) with enhanced multi-upload and slideshow capabilities.
|
This project extends the original [Image-Uploader by vallezw](https://github.com/vallezw/Image-Uploader) with enhanced multi-upload and slideshow capabilities.
|
||||||
|
|
||||||
See the [CHANGELOG](CHANGELOG.md) for a detailed list of improvements and new features.
|
### 🆕 Latest Features (November 2025)
|
||||||
|
|
||||||
|
- **🌐 Public/Internal Host Separation** (Nov 25):
|
||||||
|
- Subdomain-based feature separation for production deployment
|
||||||
|
- Public host (`deinprojekt.hobbyhimmel.de`): Upload + UUID Management only
|
||||||
|
- Internal host (`deinprojekt.lan.hobbyhimmel.de`): Full admin access
|
||||||
|
- Frontend code splitting with React.lazy() for optimized bundle size
|
||||||
|
- Backend API protection via hostGate middleware
|
||||||
|
- Rate limiting: 20 uploads/hour on public host
|
||||||
|
- Audit log tracking with source host information
|
||||||
|
- Complete local testing support via /etc/hosts entries
|
||||||
|
- Zero configuration overhead for single-host deployments
|
||||||
|
|
||||||
|
- **🧪 Comprehensive Test Suite** (Nov 16):
|
||||||
|
- 45 automated tests covering all API endpoints (100% passing)
|
||||||
|
- Jest + Supertest integration testing framework
|
||||||
|
- Unit tests for authentication middleware
|
||||||
|
- API tests for admin, consent, migration, and upload endpoints
|
||||||
|
- In-memory SQLite database for isolated testing
|
||||||
|
- Coverage: 26% statements, 15% branches (realistic starting point)
|
||||||
|
- Test execution time: ~10 seconds for full suite
|
||||||
|
- CI/CD ready with proper teardown and cleanup
|
||||||
|
|
||||||
|
- **🔒 Admin Session Authentication** (Nov 16):
|
||||||
|
- Server-managed HTTP sessions for all admin/system endpoints
|
||||||
|
- CSRF protection on every mutating request via `X-CSRF-Token`
|
||||||
|
- Secure `ADMIN_SESSION_SECRET` configuration keeps cookies tamper-proof
|
||||||
|
- Protected routes: `/api/admin/*`, `/api/system/migration/migrate`, `/api/system/migration/rollback`
|
||||||
|
- Session-aware moderation UI with login + first-admin setup wizard
|
||||||
|
- Complete authentication documentation in `AUTHENTICATION.md`
|
||||||
|
|
||||||
|
- **📋 API Route Documentation** (Nov 16):
|
||||||
|
- Single Source of Truth: `backend/src/routes/routeMappings.js`
|
||||||
|
- Comprehensive route overview in `backend/src/routes/README.md`
|
||||||
|
- Critical Express routing order documented (specific before generic)
|
||||||
|
- Frontend-ready route reference with authentication requirements
|
||||||
|
- OpenAPI specification auto-generation integrated
|
||||||
|
|
||||||
|
- **🔐 Social Media Consent Management** (Phase 1 Complete - Nov 9-10):
|
||||||
|
- GDPR-compliant consent system for image usage
|
||||||
|
- Mandatory workshop display consent (no upload without approval)
|
||||||
|
- Optional per-platform consents (Facebook, Instagram, TikTok)
|
||||||
|
- Consent badges and filtering in moderation panel
|
||||||
|
- CSV/JSON export for legal documentation
|
||||||
|
- Group ID tracking for consent withdrawal requests
|
||||||
|
- **🔑 Self-Service Management Portal** (Phase 2 Complete - Nov 11-15):
|
||||||
|
- Secure UUID-based management tokens for user self-service
|
||||||
|
- Frontend portal at `/manage/:token` for consent management
|
||||||
|
- Revoke/restore consents for workshop and social media
|
||||||
|
- Edit metadata (title, description) after upload
|
||||||
|
- Add/delete images after upload (with moderation re-approval)
|
||||||
|
- Complete group deletion with audit trail
|
||||||
|
- IP-based rate limiting (10 requests/hour)
|
||||||
|
- Brute-force protection (20 failed attempts → 24h ban)
|
||||||
|
- Management audit log for security tracking
|
||||||
|
- **🎨 Modular UI Architecture** (Nov 15):
|
||||||
|
- Reusable components: ConsentManager, GroupMetadataEditor, ImageDescriptionManager
|
||||||
|
- Multi-mode support: upload/edit/moderate modes for maximum reusability
|
||||||
|
- Code reduction: 62% in ModerationGroupImagesPage (281→107 lines)
|
||||||
|
- Consistent design: HTML buttons, Paper boxes, Material-UI Alerts
|
||||||
|
- Individual save/discard per component section
|
||||||
|
- Zero code duplication between pages
|
||||||
|
- **<EFBFBD> Slideshow Optimization**: Intelligent image preloading eliminates loading delays and duplicate images
|
||||||
|
- **📅 Chronological Display**: Slideshows now play in chronological order (year → upload date)
|
||||||
|
- **Automatic Cleanup**: Unapproved groups are automatically deleted after 7 days
|
||||||
|
- **Deletion Log**: Complete audit trail with statistics (groups, images, storage freed)
|
||||||
|
- **Countdown Display**: Visual indicator showing days until automatic deletion
|
||||||
|
- **Approval Feedback**: SweetAlert2 notifications for moderation actions
|
||||||
|
- **Manual Cleanup Trigger**: Admin API endpoints for testing and manual cleanup
|
||||||
|
- **Image Descriptions**: Add optional descriptions to individual images (max 200 characters)
|
||||||
|
- **Edit Mode**: Edit descriptions for uploaded images in upload preview and moderation interface
|
||||||
|
- **Slideshow Display**: Image descriptions shown as overlays during slideshow presentation
|
||||||
|
- **Public Display**: Descriptions visible in public group views and galleries
|
||||||
|
|
||||||
|
### Previous Features (October 2025)
|
||||||
|
- **Drag-and-Drop Image Reordering**: Admins can now reorder images using intuitive drag-and-drop
|
||||||
|
- **Touch-Friendly Interface**: Mobile-optimized controls with always-visible drag handles
|
||||||
|
- **Slideshow Integration**: Custom image order automatically applies to slideshow mode
|
||||||
|
- **Optimistic UI Updates**: Immediate visual feedback with error recovery
|
||||||
|
- **Comprehensive Admin Panel**: Dedicated moderation interface for content curation
|
||||||
|
|
||||||
|
### Core Features
|
||||||
|
- Multi-image batch upload with progress tracking
|
||||||
|
- Automatic slideshow presentation mode
|
||||||
|
- Image grouping with descriptions and metadata
|
||||||
|
- Random slideshow rotation with custom ordering support
|
||||||
|
- Keyboard navigation support (Slideshow: Space/Arrow keys, Escape to exit)
|
||||||
|
- Mobile-responsive design with touch-first interactions
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -176,31 +262,31 @@ The application automatically generates optimized preview thumbnails for all upl
|
||||||
|
|
||||||
## Docker Structure
|
## Docker Structure
|
||||||
|
|
||||||
The application uses separate Docker configurations for development and production with **simplified environment variable management**:
|
The application uses separate Docker configurations for development and production:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker/
|
docker/
|
||||||
├── .env.backend.example # Backend environment variables documentation
|
├── .env.backend.example # Backend environment variables documentation
|
||||||
├── .env.frontend.example # Frontend environment variables documentation
|
├── .env.frontend.example # Frontend environment variables documentation
|
||||||
├── dev/ # Development environment
|
├── dev/ # Development environment
|
||||||
│ ├── .env # 🆕 Central dev secrets (gitignored)
|
│ ├── docker-compose.yml # Development services configuration
|
||||||
│ ├── .env.example # Dev environment template
|
|
||||||
│ ├── docker-compose.yml # All ENV vars defined here
|
|
||||||
│ ├── backend/
|
│ ├── backend/
|
||||||
|
│ │ ├── config/.env # Development backend configuration
|
||||||
│ │ └── Dockerfile # Development backend container
|
│ │ └── Dockerfile # Development backend container
|
||||||
│ └── frontend/
|
│ └── frontend/
|
||||||
│ ├── config/env.sh # Generates window._env_ from ENV
|
│ ├── config/.env # Development frontend configuration
|
||||||
|
│ ├── config/env.sh # Runtime configuration script
|
||||||
│ ├── Dockerfile # Development frontend container
|
│ ├── Dockerfile # Development frontend container
|
||||||
│ ├── nginx.conf # Development nginx configuration
|
│ ├── nginx.conf # Development nginx configuration
|
||||||
│ └── start.sh # Development startup script
|
│ └── start.sh # Development startup script
|
||||||
└── prod/ # Production environment
|
└── prod/ # Production environment
|
||||||
├── .env # 🆕 Central prod secrets (gitignored)
|
├── docker-compose.yml # Production services configuration
|
||||||
├── .env.example # Production environment template
|
|
||||||
├── docker-compose.yml # All ENV vars defined here
|
|
||||||
├── backend/
|
├── backend/
|
||||||
|
│ ├── config/.env # Production backend configuration
|
||||||
│ └── Dockerfile # Production backend container
|
│ └── Dockerfile # Production backend container
|
||||||
└── frontend/
|
└── frontend/
|
||||||
├── config/env.sh # Generates window._env_ from ENV
|
├── config/.env # Production frontend configuration
|
||||||
|
├── config/env.sh # Runtime configuration script
|
||||||
├── config/htpasswd # HTTP Basic Auth credentials
|
├── config/htpasswd # HTTP Basic Auth credentials
|
||||||
├── Dockerfile # Production frontend container
|
├── Dockerfile # Production frontend container
|
||||||
└── nginx.conf # Production nginx configuration
|
└── nginx.conf # Production nginx configuration
|
||||||
|
|
@ -208,20 +294,6 @@ docker/
|
||||||
|
|
||||||
### Environment Configuration
|
### Environment Configuration
|
||||||
|
|
||||||
**🆕 Simplified ENV Structure (Nov 2025):**
|
|
||||||
- **2 central `.env` files** (down from 16 files!)
|
|
||||||
- `docker/dev/.env` - All development secrets
|
|
||||||
- `docker/prod/.env` - All production secrets
|
|
||||||
- **docker-compose.yml** - All environment variables defined in `environment:` sections
|
|
||||||
- **No .env files in Docker images** - All configuration via docker-compose
|
|
||||||
- **Frontend env.sh** - Generates `window._env_` JavaScript object from ENV variables at runtime
|
|
||||||
|
|
||||||
**How it works:**
|
|
||||||
1. Docker Compose automatically reads `.env` from the same directory
|
|
||||||
2. Variables are injected into containers via `environment:` sections using `${VAR}` placeholders
|
|
||||||
3. Frontend `env.sh` script reads ENV variables and generates JavaScript config at container startup
|
|
||||||
4. Secrets stay in gitignored `.env` files, never in code or images
|
|
||||||
|
|
||||||
- **Development**: Uses `docker/dev/` configuration with live reloading
|
- **Development**: Uses `docker/dev/` configuration with live reloading
|
||||||
- **Production**: Uses `docker/prod/` configuration with optimized builds
|
- **Production**: Uses `docker/prod/` configuration with optimized builds
|
||||||
- **Scripts**: Use `./dev.sh` or `./prod.sh` for easy deployment
|
- **Scripts**: Use `./dev.sh` or `./prod.sh` for easy deployment
|
||||||
|
|
@ -519,41 +591,12 @@ The application includes comprehensive testing tools for the automatic cleanup f
|
||||||
For detailed testing instructions, see: [`tests/TESTING-CLEANUP.md`](tests/TESTING-CLEANUP.md)
|
For detailed testing instructions, see: [`tests/TESTING-CLEANUP.md`](tests/TESTING-CLEANUP.md)
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
**Simplified ENV Management (Nov 2025):**
|
|
||||||
All environment variables are now managed through **2 central `.env` files** and `docker-compose.yml`:
|
|
||||||
|
|
||||||
**Core Variables:**
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `API_URL` | `http://localhost:5001` | Backend API endpoint (frontend → backend) |
|
| `API_URL` | `http://localhost:5001` | Backend API endpoint |
|
||||||
| `PUBLIC_HOST` | `public.test.local` | Public upload subdomain (no admin access) |
|
| `CLIENT_URL` | `http://localhost` | Frontend application URL |
|
||||||
| `INTERNAL_HOST` | `internal.test.local` | Internal admin subdomain (full access) |
|
|
||||||
| `ADMIN_SESSION_SECRET` | - | Secret for admin session cookies (required) |
|
|
||||||
|
|
||||||
**Telegram Notifications (Optional):**
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `TELEGRAM_ENABLED` | `false` | Enable/disable Telegram notifications |
|
|
||||||
| `TELEGRAM_BOT_TOKEN` | - | Telegram Bot API token (from @BotFather) |
|
|
||||||
| `TELEGRAM_CHAT_ID` | - | Telegram chat/group ID for notifications |
|
|
||||||
| `TELEGRAM_SEND_TEST_ON_START` | `false` | Send test message on service startup (dev only) |
|
|
||||||
|
|
||||||
**Configuration Files:**
|
|
||||||
- `docker/dev/.env` - Development secrets (gitignored)
|
|
||||||
- `docker/prod/.env` - Production secrets (gitignored)
|
|
||||||
- `docker/dev/.env.example` - Development template (committed)
|
|
||||||
- `docker/prod/.env.example` - Production template (committed)
|
|
||||||
|
|
||||||
**How to configure:**
|
|
||||||
1. Copy `.env.example` to `.env` in the respective environment folder
|
|
||||||
2. Edit `.env` and set your secrets (ADMIN_SESSION_SECRET, Telegram tokens, etc.)
|
|
||||||
3. Docker Compose automatically reads `.env` and injects variables into containers
|
|
||||||
4. Never commit `.env` files (already in `.gitignore`)
|
|
||||||
|
|
||||||
**Telegram Setup:** See `scripts/README.telegram.md` for complete configuration guide.
|
|
||||||
|
|
||||||
### Volume Configuration
|
### Volume Configuration
|
||||||
- **Upload Limits**: 100MB maximum file size for batch uploads
|
- **Upload Limits**: 100MB maximum file size for batch uploads
|
||||||
|
|
|
||||||
2
TODO.md
2
TODO.md
|
|
@ -101,7 +101,7 @@ Neue Struktur: Datenbank in src/data/db und bilder in src/data/images
|
||||||
[x] 🎨 Drag & Drop Reihenfolge ändern
|
[x] 🎨 Drag & Drop Reihenfolge ändern
|
||||||
[x] 📊 Upload-Progress mit Details
|
[x] 📊 Upload-Progress mit Details
|
||||||
[x] 🖼️ Thumbnail-Navigation in Slideshow
|
[x] 🖼️ Thumbnail-Navigation in Slideshow
|
||||||
|
[ ] 🔄 Batch-Operations (alle entfernen, etc.)
|
||||||
|
|
||||||
### Future Features
|
### Future Features
|
||||||
- 👤 User-Management
|
- 👤 User-Management
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"openapi": "3.0.0",
|
"openapi": "3.0.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Project Image Uploader API",
|
"title": "Project Image Uploader API",
|
||||||
"version": "2.0.1",
|
"version": "1.10.2",
|
||||||
"description": "Auto-generated OpenAPI spec with correct mount prefixes"
|
"description": "Auto-generated OpenAPI spec with correct mount prefixes"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
|
|
@ -39,9 +39,6 @@
|
||||||
{
|
{
|
||||||
"name": "Admin - Cleanup"
|
"name": "Admin - Cleanup"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Admin - Telegram"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Admin - Monitoring"
|
"name": "Admin - Monitoring"
|
||||||
},
|
},
|
||||||
|
|
@ -388,15 +385,6 @@
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"example": "any"
|
"example": "any"
|
||||||
},
|
|
||||||
"year": {
|
|
||||||
"example": "any"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"example": "any"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"example": "any"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1070,38 +1058,22 @@
|
||||||
},
|
},
|
||||||
"/api/manage/{token}/reorder": {
|
"/api/manage/{token}/reorder": {
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"description": "",
|
||||||
"Management Portal"
|
|
||||||
],
|
|
||||||
"summary": "Reorder images in group",
|
|
||||||
"description": "Reorder images within the managed group (token-based access)",
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "token",
|
"name": "token",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"description": "Management token (UUID v4)",
|
|
||||||
"example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "body",
|
"name": "body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"imageIds": {
|
"imageIds": {
|
||||||
"type": "array",
|
"example": "any"
|
||||||
"example": [
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
2,
|
|
||||||
4
|
|
||||||
],
|
|
||||||
"items": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1109,29 +1081,13 @@
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Images reordered successfully",
|
"description": "OK"
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true
|
|
||||||
},
|
|
||||||
"updatedCount": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "main"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid token format or imageIds"
|
"description": "Bad Request"
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Token not found or group deleted"
|
"description": "Not Found"
|
||||||
},
|
},
|
||||||
"429": {
|
"429": {
|
||||||
"description": "Too Many Requests"
|
"description": "Too Many Requests"
|
||||||
|
|
@ -1195,46 +1151,25 @@
|
||||||
},
|
},
|
||||||
"/api/admin/groups/{groupId}/consents": {
|
"/api/admin/groups/{groupId}/consents": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"description": "",
|
||||||
"Consent Management"
|
|
||||||
],
|
|
||||||
"summary": "Save or update consents for a group",
|
|
||||||
"description": "Store workshop consent and social media consents for a specific group",
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "groupId",
|
"name": "groupId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"description": "Group ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "body",
|
"name": "body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"workshopConsent": {
|
"workshopConsent": {
|
||||||
"type": "boolean",
|
"example": "any"
|
||||||
"example": true
|
|
||||||
},
|
},
|
||||||
"socialMediaConsents": {
|
"socialMediaConsents": {
|
||||||
"type": "array",
|
"example": "any"
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"platformId": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 2
|
|
||||||
},
|
|
||||||
"consented": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1242,26 +1177,10 @@
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Consents saved successfully",
|
"description": "OK"
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Consents saved successfully"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "main"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request data"
|
"description": "Bad Request"
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "Forbidden"
|
"description": "Forbidden"
|
||||||
|
|
@ -1798,46 +1717,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/admin/telegram/warning": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Admin - Telegram"
|
|
||||||
],
|
|
||||||
"summary": "Manually trigger Telegram deletion warning",
|
|
||||||
"description": "Sends deletion warning to Telegram for testing (normally runs daily at 09:00)",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Warning sent successfully",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true
|
|
||||||
},
|
|
||||||
"groupsWarned": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 2
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Deletion warning sent for 2 groups"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xml": {
|
|
||||||
"name": "main"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/admin/cleanup/preview": {
|
"/api/admin/cleanup/preview": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,5 @@ module.exports = {
|
||||||
// Run tests serially to avoid DB conflicts
|
// Run tests serially to avoid DB conflicts
|
||||||
maxWorkers: 1,
|
maxWorkers: 1,
|
||||||
// Force exit after tests complete
|
// Force exit after tests complete
|
||||||
forceExit: true,
|
forceExit: true
|
||||||
// Transform ESM modules in node_modules
|
|
||||||
transformIgnorePatterns: [
|
|
||||||
'node_modules/(?!(uuid)/)'
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "2.0.2",
|
"version": "1.10.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -31,7 +31,6 @@
|
||||||
"find-remove": "^2.0.3",
|
"find-remove": "^2.0.3",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
|
||||||
"sharp": "^0.34.4",
|
"sharp": "^0.34.4",
|
||||||
"shortid": "^2.2.16",
|
"shortid": "^2.2.16",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const endpointsFiles = routerMappings.map(r => path.join(routesDir, r.file));
|
||||||
const doc = {
|
const doc = {
|
||||||
info: {
|
info: {
|
||||||
title: 'Project Image Uploader API',
|
title: 'Project Image Uploader API',
|
||||||
version: '2.0.1',
|
version: '1.10.2',
|
||||||
description: 'Auto-generated OpenAPI spec with correct mount prefixes'
|
description: 'Auto-generated OpenAPI spec with correct mount prefixes'
|
||||||
},
|
},
|
||||||
host: 'localhost:5001',
|
host: 'localhost:5001',
|
||||||
|
|
|
||||||
|
|
@ -237,46 +237,6 @@ router.post('/cleanup/trigger', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/telegram/warning', async (req, res) => {
|
|
||||||
/*
|
|
||||||
#swagger.tags = ['Admin - Telegram']
|
|
||||||
#swagger.summary = 'Manually trigger Telegram deletion warning'
|
|
||||||
#swagger.description = 'Sends deletion warning to Telegram for testing (normally runs daily at 09:00)'
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
description: 'Warning sent successfully',
|
|
||||||
schema: {
|
|
||||||
success: true,
|
|
||||||
groupsWarned: 2,
|
|
||||||
message: 'Deletion warning sent for 2 groups'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
const schedulerService = req.app.get('schedulerService');
|
|
||||||
|
|
||||||
if (!schedulerService) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Scheduler service not available'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await schedulerService.triggerTelegramWarningNow();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
groupsWarned: result.groupCount,
|
|
||||||
message: result.message
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Admin API] Error triggering Telegram warning:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/cleanup/preview', async (req, res) => {
|
router.get('/cleanup/preview', async (req, res) => {
|
||||||
/*
|
/*
|
||||||
#swagger.tags = ['Admin - Cleanup']
|
#swagger.tags = ['Admin - Cleanup']
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ const UploadGroup = require('../models/uploadGroup');
|
||||||
const groupRepository = require('../repositories/GroupRepository');
|
const groupRepository = require('../repositories/GroupRepository');
|
||||||
const dbManager = require('../database/DatabaseManager');
|
const dbManager = require('../database/DatabaseManager');
|
||||||
const ImagePreviewService = require('../services/ImagePreviewService');
|
const ImagePreviewService = require('../services/ImagePreviewService');
|
||||||
const TelegramNotificationService = require('../services/TelegramNotificationService');
|
|
||||||
|
|
||||||
// Singleton-Instanz des Telegram Service
|
|
||||||
const telegramService = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -121,12 +117,6 @@ router.post('/upload/batch', async (req, res) => {
|
||||||
consents = {};
|
consents = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge separate form fields into metadata (backwards compatibility)
|
|
||||||
if (req.body.year) metadata.year = parseInt(req.body.year);
|
|
||||||
if (req.body.title) metadata.title = req.body.title;
|
|
||||||
if (req.body.name) metadata.name = req.body.name;
|
|
||||||
if (req.body.description) metadata.description = req.body.description;
|
|
||||||
|
|
||||||
// Validiere Workshop Consent (Pflichtfeld)
|
// Validiere Workshop Consent (Pflichtfeld)
|
||||||
if (!consents.workshopConsent) {
|
if (!consents.workshopConsent) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
|
|
@ -239,22 +229,6 @@ router.post('/upload/batch', async (req, res) => {
|
||||||
|
|
||||||
console.log(`Successfully saved group ${group.groupId} with ${files.length} images to database`);
|
console.log(`Successfully saved group ${group.groupId} with ${files.length} images to database`);
|
||||||
|
|
||||||
// Sende Telegram-Benachrichtigung (async, non-blocking)
|
|
||||||
if (telegramService.isAvailable()) {
|
|
||||||
telegramService.sendUploadNotification({
|
|
||||||
name: group.name,
|
|
||||||
year: group.year,
|
|
||||||
title: group.title,
|
|
||||||
imageCount: files.length,
|
|
||||||
workshopConsent: consents.workshopConsent,
|
|
||||||
socialMediaConsents: consents.socialMediaConsents || [],
|
|
||||||
token: createResult.managementToken
|
|
||||||
}).catch(err => {
|
|
||||||
// Fehler loggen, aber Upload nicht fehlschlagen lassen
|
|
||||||
console.error('[Telegram] Upload notification failed:', err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erfolgreiche Antwort mit Management-Token
|
// Erfolgreiche Antwort mit Management-Token
|
||||||
res.json({
|
res.json({
|
||||||
groupId: group.groupId,
|
groupId: group.groupId,
|
||||||
|
|
|
||||||
|
|
@ -58,37 +58,16 @@ router.get('/social-media/platforms', async (req, res) => {
|
||||||
// Group Consents
|
// Group Consents
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /groups/:groupId/consents
|
||||||
|
* Speichere oder aktualisiere Consents für eine Gruppe
|
||||||
|
*
|
||||||
|
* Body: {
|
||||||
|
* workshopConsent: boolean,
|
||||||
|
* socialMediaConsents: [{ platformId: number, consented: boolean }]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
router.post('/groups/:groupId/consents', async (req, res) => {
|
router.post('/groups/:groupId/consents', async (req, res) => {
|
||||||
/*
|
|
||||||
#swagger.tags = ['Consent Management']
|
|
||||||
#swagger.summary = 'Save or update consents for a group'
|
|
||||||
#swagger.description = 'Store workshop consent and social media consents for a specific group'
|
|
||||||
#swagger.parameters['groupId'] = {
|
|
||||||
in: 'path',
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
description: 'Group ID',
|
|
||||||
example: 'abc123def456'
|
|
||||||
}
|
|
||||||
#swagger.parameters['body'] = {
|
|
||||||
in: 'body',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: [
|
|
||||||
{ platformId: 1, consented: true },
|
|
||||||
{ platformId: 2, consented: false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
description: 'Consents saved successfully',
|
|
||||||
schema: { success: true, message: 'Consents saved successfully' }
|
|
||||||
}
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: 'Invalid request data'
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
try {
|
try {
|
||||||
const { groupId } = req.params;
|
const { groupId } = req.params;
|
||||||
const { workshopConsent, socialMediaConsents } = req.body;
|
const { workshopConsent, socialMediaConsents } = req.body;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,6 @@ const deletionLogRepository = require('../repositories/DeletionLogRepository');
|
||||||
const dbManager = require('../database/DatabaseManager');
|
const dbManager = require('../database/DatabaseManager');
|
||||||
const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter');
|
const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter');
|
||||||
const auditLogMiddleware = require('../middlewares/auditLog');
|
const auditLogMiddleware = require('../middlewares/auditLog');
|
||||||
const TelegramNotificationService = require('../services/TelegramNotificationService');
|
|
||||||
|
|
||||||
// Singleton-Instanz des Telegram Service
|
|
||||||
const telegramService = new TelegramNotificationService();
|
|
||||||
|
|
||||||
// Apply middleware to all management routes
|
// Apply middleware to all management routes
|
||||||
router.use(rateLimitMiddleware);
|
router.use(rateLimitMiddleware);
|
||||||
|
|
@ -215,20 +211,6 @@ router.put('/:token/consents', async (req, res) => {
|
||||||
[newValue, groupData.groupId]
|
[newValue, groupData.groupId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sende Telegram-Benachrichtigung (async, non-blocking)
|
|
||||||
if (telegramService.isAvailable()) {
|
|
||||||
telegramService.sendConsentChangeNotification({
|
|
||||||
name: groupData.name,
|
|
||||||
year: groupData.year,
|
|
||||||
title: groupData.title,
|
|
||||||
consentType: 'workshop',
|
|
||||||
action: action,
|
|
||||||
newValue: newValue === 1
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[Telegram] Consent change notification failed:', err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Workshop consent ${action}d successfully`,
|
message: `Workshop consent ${action}d successfully`,
|
||||||
|
|
@ -281,26 +263,6 @@ router.put('/:token/consents', async (req, res) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sende Telegram-Benachrichtigung (async, non-blocking)
|
|
||||||
if (telegramService.isAvailable()) {
|
|
||||||
// Hole Platform-Name für Benachrichtigung
|
|
||||||
const platform = await dbManager.get(
|
|
||||||
'SELECT platform_name FROM social_media_platforms WHERE id = ?',
|
|
||||||
[platformId]
|
|
||||||
);
|
|
||||||
|
|
||||||
telegramService.sendConsentChangeNotification({
|
|
||||||
name: groupData.name,
|
|
||||||
year: groupData.year,
|
|
||||||
title: groupData.title,
|
|
||||||
consentType: 'social_media',
|
|
||||||
action: action,
|
|
||||||
platform: platform ? platform.platform_name : `Platform ${platformId}`
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[Telegram] Consent change notification failed:', err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Social media consent ${action}d successfully`,
|
message: `Social media consent ${action}d successfully`,
|
||||||
|
|
@ -1045,18 +1007,6 @@ router.delete('/:token', async (req, res) => {
|
||||||
|
|
||||||
console.log(`✓ Group ${groupId} deleted via management token (${imageCount} images)`);
|
console.log(`✓ Group ${groupId} deleted via management token (${imageCount} images)`);
|
||||||
|
|
||||||
// Sende Telegram-Benachrichtigung (async, non-blocking)
|
|
||||||
if (telegramService.isAvailable()) {
|
|
||||||
telegramService.sendGroupDeletedNotification({
|
|
||||||
name: groupData.name,
|
|
||||||
year: groupData.year,
|
|
||||||
title: groupData.title,
|
|
||||||
imageCount: imageCount
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[Telegram] Group deletion notification failed:', err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Group and all associated data deleted successfully',
|
message: 'Group and all associated data deleted successfully',
|
||||||
|
|
@ -1076,36 +1026,18 @@ router.delete('/:token', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/manage/:token/reorder
|
||||||
|
* Reorder images within the managed group (token-based access)
|
||||||
|
*
|
||||||
|
* @param {string} token - Management token (UUID v4)
|
||||||
|
* @param {number[]} imageIds - Array of image IDs in new order
|
||||||
|
* @returns {Object} Success status and updated image count
|
||||||
|
* @throws {400} Invalid token format or imageIds
|
||||||
|
* @throws {404} Token not found or group deleted
|
||||||
|
* @throws {500} Server error
|
||||||
|
*/
|
||||||
router.put('/:token/reorder', async (req, res) => {
|
router.put('/:token/reorder', async (req, res) => {
|
||||||
/*
|
|
||||||
#swagger.tags = ['Management Portal']
|
|
||||||
#swagger.summary = 'Reorder images in group'
|
|
||||||
#swagger.description = 'Reorder images within the managed group (token-based access)'
|
|
||||||
#swagger.parameters['token'] = {
|
|
||||||
in: 'path',
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
description: 'Management token (UUID v4)',
|
|
||||||
example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
|
||||||
}
|
|
||||||
#swagger.parameters['body'] = {
|
|
||||||
in: 'body',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
imageIds: [1, 3, 2, 4]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
description: 'Images reordered successfully',
|
|
||||||
schema: { success: true, updatedCount: 4 }
|
|
||||||
}
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: 'Invalid token format or imageIds'
|
|
||||||
}
|
|
||||||
#swagger.responses[404] = {
|
|
||||||
description: 'Token not found or group deleted'
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
try {
|
try {
|
||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
const { imageIds } = req.body;
|
const { imageIds } = req.body;
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@ const path = require('path');
|
||||||
const initiateResources = require('./utils/initiate-resources');
|
const initiateResources = require('./utils/initiate-resources');
|
||||||
const dbManager = require('./database/DatabaseManager');
|
const dbManager = require('./database/DatabaseManager');
|
||||||
const SchedulerService = require('./services/SchedulerService');
|
const SchedulerService = require('./services/SchedulerService');
|
||||||
const TelegramNotificationService = require('./services/TelegramNotificationService');
|
|
||||||
|
|
||||||
// Singleton-Instanz des Telegram Service
|
|
||||||
const telegramService = new TelegramNotificationService();
|
|
||||||
|
|
||||||
// Dev: Swagger UI (mount only in non-production) — require lazily
|
// Dev: Swagger UI (mount only in non-production) — require lazily
|
||||||
let swaggerUi = null;
|
let swaggerUi = null;
|
||||||
|
|
@ -82,19 +78,8 @@ class Server {
|
||||||
console.log(`✅ Server läuft auf Port ${this._port}`);
|
console.log(`✅ Server läuft auf Port ${this._port}`);
|
||||||
console.log(`📊 SQLite Datenbank aktiv`);
|
console.log(`📊 SQLite Datenbank aktiv`);
|
||||||
|
|
||||||
// Speichere SchedulerService in app für Admin-Endpoints
|
|
||||||
this._app.set('schedulerService', SchedulerService);
|
|
||||||
|
|
||||||
// Starte Scheduler für automatisches Cleanup
|
// Starte Scheduler für automatisches Cleanup
|
||||||
SchedulerService.start();
|
SchedulerService.start();
|
||||||
|
|
||||||
// Teste Telegram-Service (optional, nur in Development wenn aktiviert)
|
|
||||||
if (process.env.NODE_ENV === 'development'
|
|
||||||
&& process.env.TELEGRAM_SEND_TEST_ON_START === 'true'
|
|
||||||
&& telegramService.isAvailable()) {
|
|
||||||
telegramService.sendTestMessage()
|
|
||||||
.catch(err => console.error('[Telegram] Test message failed:', err.message));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('💥 Fehler beim Serverstart:', error);
|
console.error('💥 Fehler beim Serverstart:', error);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
const cron = require('node-cron');
|
const cron = require('node-cron');
|
||||||
const GroupCleanupService = require('./GroupCleanupService');
|
const GroupCleanupService = require('./GroupCleanupService');
|
||||||
const TelegramNotificationService = require('./TelegramNotificationService');
|
|
||||||
|
|
||||||
class SchedulerService {
|
class SchedulerService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tasks = [];
|
this.tasks = [];
|
||||||
this.telegramService = new TelegramNotificationService();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|
@ -32,35 +30,7 @@ class SchedulerService {
|
||||||
|
|
||||||
this.tasks.push(cleanupTask);
|
this.tasks.push(cleanupTask);
|
||||||
|
|
||||||
// Telegram Warning-Job: Jeden Tag um 09:00 Uhr (1 Stunde vor Cleanup)
|
console.log('✓ Scheduler started - Daily cleanup at 10:00 AM (Europe/Berlin)');
|
||||||
const telegramWarningTask = cron.schedule('0 9 * * *', async () => {
|
|
||||||
console.log('[Scheduler] Running daily Telegram deletion warning at 09:00 AM...');
|
|
||||||
try {
|
|
||||||
if (this.telegramService.isAvailable()) {
|
|
||||||
const groupsForDeletion = await GroupCleanupService.findGroupsForDeletion();
|
|
||||||
|
|
||||||
if (groupsForDeletion && groupsForDeletion.length > 0) {
|
|
||||||
await this.telegramService.sendDeletionWarning(groupsForDeletion);
|
|
||||||
console.log(`[Scheduler] Sent deletion warning for ${groupsForDeletion.length} groups`);
|
|
||||||
} else {
|
|
||||||
console.log('[Scheduler] No groups pending deletion');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[Scheduler] Telegram service not available, skipping warning');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Scheduler] Telegram warning task failed:', error);
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
scheduled: true,
|
|
||||||
timezone: "Europe/Berlin"
|
|
||||||
});
|
|
||||||
|
|
||||||
this.tasks.push(telegramWarningTask);
|
|
||||||
|
|
||||||
console.log('✓ Scheduler started:');
|
|
||||||
console.log(' - Daily cleanup at 10:00 AM (Europe/Berlin)');
|
|
||||||
console.log(' - Daily Telegram warning at 09:00 AM (Europe/Berlin)');
|
|
||||||
|
|
||||||
// Für Development: Manueller Trigger
|
// Für Development: Manueller Trigger
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
|
@ -80,42 +50,6 @@ class SchedulerService {
|
||||||
console.log('[Scheduler] Manual cleanup triggered...');
|
console.log('[Scheduler] Manual cleanup triggered...');
|
||||||
return await GroupCleanupService.performScheduledCleanup();
|
return await GroupCleanupService.performScheduledCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Für Development: Manueller Telegram-Warning-Trigger
|
|
||||||
async triggerTelegramWarningNow() {
|
|
||||||
console.log('[Scheduler] Manual Telegram warning triggered...');
|
|
||||||
try {
|
|
||||||
if (!this.telegramService.isAvailable()) {
|
|
||||||
console.log('[Scheduler] Telegram service not available');
|
|
||||||
return { success: false, message: 'Telegram service not available' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupsForDeletion = await GroupCleanupService.findGroupsForDeletion();
|
|
||||||
|
|
||||||
if (!groupsForDeletion || groupsForDeletion.length === 0) {
|
|
||||||
console.log('[Scheduler] No groups pending deletion');
|
|
||||||
return { success: true, message: 'No groups pending deletion', groupCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.telegramService.sendDeletionWarning(groupsForDeletion);
|
|
||||||
console.log(`[Scheduler] Sent deletion warning for ${groupsForDeletion.length} groups`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Warning sent for ${groupsForDeletion.length} groups`,
|
|
||||||
groupCount: groupsForDeletion.length,
|
|
||||||
groups: groupsForDeletion.map(g => ({
|
|
||||||
groupId: g.groupId,
|
|
||||||
name: g.name,
|
|
||||||
year: g.year,
|
|
||||||
uploadDate: g.uploadDate
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Scheduler] Manual Telegram warning failed:', error);
|
|
||||||
return { success: false, message: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new SchedulerService();
|
module.exports = new SchedulerService();
|
||||||
|
|
|
||||||
|
|
@ -1,312 +0,0 @@
|
||||||
const TelegramBot = require('node-telegram-bot-api');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TelegramNotificationService
|
|
||||||
*
|
|
||||||
* Versendet automatische Benachrichtigungen über Telegram an die Werkstatt-Gruppe.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Upload-Benachrichtigungen (Phase 3)
|
|
||||||
* - Consent-Änderungs-Benachrichtigungen (Phase 4)
|
|
||||||
* - Gruppen-Lösch-Benachrichtigungen (Phase 4)
|
|
||||||
* - Tägliche Lösch-Warnungen (Phase 5)
|
|
||||||
*
|
|
||||||
* Phase 2: Backend-Service Integration (Basic Setup)
|
|
||||||
*/
|
|
||||||
class TelegramNotificationService {
|
|
||||||
constructor() {
|
|
||||||
this.enabled = process.env.TELEGRAM_ENABLED === 'true';
|
|
||||||
this.botToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
||||||
this.chatId = process.env.TELEGRAM_CHAT_ID;
|
|
||||||
this.bot = null;
|
|
||||||
|
|
||||||
if (this.enabled) {
|
|
||||||
this.initialize();
|
|
||||||
} else {
|
|
||||||
console.log('[Telegram] Service disabled (TELEGRAM_ENABLED=false)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialisiert den Telegram Bot
|
|
||||||
*/
|
|
||||||
initialize() {
|
|
||||||
try {
|
|
||||||
if (!this.botToken) {
|
|
||||||
throw new Error('TELEGRAM_BOT_TOKEN is not defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.chatId) {
|
|
||||||
throw new Error('TELEGRAM_CHAT_ID is not defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bot = new TelegramBot(this.botToken, { polling: false });
|
|
||||||
console.log('[Telegram] Service initialized successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Telegram] Initialization failed:', error.message);
|
|
||||||
this.enabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob der Service verfügbar ist
|
|
||||||
*/
|
|
||||||
isAvailable() {
|
|
||||||
return this.enabled && this.bot !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sendet eine Test-Nachricht
|
|
||||||
*
|
|
||||||
* @returns {Promise<Object>} Telegram API Response
|
|
||||||
*/
|
|
||||||
async sendTestMessage() {
|
|
||||||
if (!this.isAvailable()) {
|
|
||||||
console.log('[Telegram] Service not available, skipping test message');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timestamp = new Date().toLocaleString('de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = `
|
|
||||||
🤖 Telegram Service Test
|
|
||||||
|
|
||||||
Service erfolgreich initialisiert!
|
|
||||||
|
|
||||||
Zeitstempel: ${timestamp}
|
|
||||||
Environment: ${process.env.NODE_ENV || 'development'}
|
|
||||||
|
|
||||||
---
|
|
||||||
ℹ️ Dieser Bot sendet automatische Benachrichtigungen für den Image Uploader.
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const response = await this.bot.sendMessage(this.chatId, message);
|
|
||||||
console.log('[Telegram] Test message sent successfully');
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Telegram] Failed to send test message:', error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase 3: Sendet Benachrichtigung bei neuem Upload
|
|
||||||
*
|
|
||||||
* @param {Object} groupData - Gruppen-Informationen
|
|
||||||
* @param {string} groupData.name - Name des Uploaders
|
|
||||||
* @param {number} groupData.year - Jahr der Gruppe
|
|
||||||
* @param {string} groupData.title - Titel der Gruppe
|
|
||||||
* @param {number} groupData.imageCount - Anzahl hochgeladener Bilder
|
|
||||||
* @param {boolean} groupData.workshopConsent - Workshop-Consent Status
|
|
||||||
* @param {Array<string>} groupData.socialMediaConsents - Social Media Plattformen
|
|
||||||
* @param {string} groupData.token - Management-Token
|
|
||||||
*/
|
|
||||||
async sendUploadNotification(groupData) {
|
|
||||||
if (!this.isAvailable()) {
|
|
||||||
console.log('[Telegram] Service not available, skipping upload notification');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const workshopIcon = groupData.workshopConsent ? '✅' : '❌';
|
|
||||||
const socialMediaIcons = this.formatSocialMediaIcons(groupData.socialMediaConsents);
|
|
||||||
|
|
||||||
const message = `
|
|
||||||
📸 Neuer Upload!
|
|
||||||
|
|
||||||
Uploader: ${groupData.name}
|
|
||||||
Bilder: ${groupData.imageCount}
|
|
||||||
Gruppe: ${groupData.year} - ${groupData.title}
|
|
||||||
|
|
||||||
Workshop: ${workshopIcon} ${groupData.workshopConsent ? 'Ja' : 'Nein'}
|
|
||||||
Social Media: ${socialMediaIcons || '❌ Keine'}
|
|
||||||
|
|
||||||
🔗 Zur Freigabe: ${this.getAdminUrl()}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const response = await this.bot.sendMessage(this.chatId, message);
|
|
||||||
console.log(`[Telegram] Upload notification sent for group: ${groupData.title}`);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Telegram] Failed to send upload notification:', error.message);
|
|
||||||
// Fehler loggen, aber nicht werfen - Upload soll nicht fehlschlagen wegen Telegram
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase 4: Sendet Benachrichtigung bei Consent-Änderung
|
|
||||||
*
|
|
||||||
* @param {Object} changeData - Änderungs-Informationen
|
|
||||||
* @param {string} changeData.name - Name des Uploaders
|
|
||||||
* @param {number} changeData.year - Jahr
|
|
||||||
* @param {string} changeData.title - Titel
|
|
||||||
* @param {string} changeData.consentType - 'workshop' oder 'social_media'
|
|
||||||
* @param {string} changeData.action - 'revoke' oder 'restore'
|
|
||||||
* @param {string} [changeData.platform] - Plattform-Name (nur bei social_media)
|
|
||||||
* @param {boolean} [changeData.newValue] - Neuer Wert (nur bei workshop)
|
|
||||||
*/
|
|
||||||
async sendConsentChangeNotification(changeData) {
|
|
||||||
if (!this.isAvailable()) {
|
|
||||||
console.log('[Telegram] Service not available, skipping consent change notification');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { name, year, title, consentType, action, platform, newValue } = changeData;
|
|
||||||
|
|
||||||
let changeDescription;
|
|
||||||
if (consentType === 'workshop') {
|
|
||||||
const icon = newValue ? '✅' : '❌';
|
|
||||||
const status = newValue ? 'Ja' : 'Nein';
|
|
||||||
const actionText = action === 'revoke' ? 'widerrufen' : 'wiederhergestellt';
|
|
||||||
changeDescription = `Workshop-Consent ${actionText}\nNeuer Status: ${icon} ${status}`;
|
|
||||||
} else if (consentType === 'social_media') {
|
|
||||||
const actionText = action === 'revoke' ? 'widerrufen' : 'erteilt';
|
|
||||||
changeDescription = `Social Media Consent ${actionText}\nPlattform: ${platform}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = `
|
|
||||||
⚙️ Consent-Änderung
|
|
||||||
|
|
||||||
Gruppe: ${year} - ${title}
|
|
||||||
Uploader: ${name}
|
|
||||||
|
|
||||||
${changeDescription}
|
|
||||||
|
|
||||||
🔗 Details: ${this.getAdminUrl()}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const response = await this.bot.sendMessage(this.chatId, message);
|
|
||||||
console.log(`[Telegram] Consent change notification sent for: ${title}`);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Telegram] Failed to send consent change notification:', error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase 4: Sendet Benachrichtigung bei Gruppen-Löschung durch User
|
|
||||||
*
|
|
||||||
* @param {Object} groupData - Gruppen-Informationen
|
|
||||||
*/
|
|
||||||
async sendGroupDeletedNotification(groupData) {
|
|
||||||
if (!this.isAvailable()) {
|
|
||||||
console.log('[Telegram] Service not available, skipping group deleted notification');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = `
|
|
||||||
⚙️ User-Änderung
|
|
||||||
|
|
||||||
Aktion: Gruppe gelöscht
|
|
||||||
Gruppe: ${groupData.year} - ${groupData.title}
|
|
||||||
Uploader: ${groupData.name}
|
|
||||||
Bilder: ${groupData.imageCount}
|
|
||||||
|
|
||||||
ℹ️ User hat Gruppe selbst über Management-Link gelöscht
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const response = await this.bot.sendMessage(this.chatId, message);
|
|
||||||
console.log(`[Telegram] Group deleted notification sent for: ${groupData.title}`);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Telegram] Failed to send group deleted notification:', error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase 5: Sendet tägliche Warnung für bevorstehende Löschungen
|
|
||||||
*
|
|
||||||
* @param {Array<Object>} groupsList - Liste der zu löschenden Gruppen
|
|
||||||
*/
|
|
||||||
async sendDeletionWarning(groupsList) {
|
|
||||||
if (!this.isAvailable()) {
|
|
||||||
console.log('[Telegram] Service not available, skipping deletion warning');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!groupsList || groupsList.length === 0) {
|
|
||||||
console.log('[Telegram] No groups pending deletion, skipping warning');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let groupsText = groupsList.map((group, index) => {
|
|
||||||
const uploadDate = new Date(group.created_at).toLocaleDateString('de-DE');
|
|
||||||
return `${index + 1}. ${group.year} - ${group.title}
|
|
||||||
Uploader: ${group.name}
|
|
||||||
Bilder: ${group.imageCount}
|
|
||||||
Hochgeladen: ${uploadDate}`;
|
|
||||||
}).join('\n\n');
|
|
||||||
|
|
||||||
const message = `
|
|
||||||
⏰ Löschung in 24 Stunden!
|
|
||||||
|
|
||||||
Folgende Gruppen werden morgen automatisch gelöscht:
|
|
||||||
|
|
||||||
${groupsText}
|
|
||||||
|
|
||||||
💡 Jetzt freigeben oder Freigabe bleibt aus!
|
|
||||||
🔗 Zur Moderation: ${this.getAdminUrl()}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const response = await this.bot.sendMessage(this.chatId, message);
|
|
||||||
console.log(`[Telegram] Deletion warning sent for ${groupsList.length} groups`);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Telegram] Failed to send deletion warning:', error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Helper Methods
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formatiert Social Media Consents als Icons
|
|
||||||
*
|
|
||||||
* @param {Array<string>} platforms - Liste der Plattformen
|
|
||||||
* @returns {string} Formatierter String mit Icons
|
|
||||||
*/
|
|
||||||
formatSocialMediaIcons(platforms) {
|
|
||||||
if (!platforms || platforms.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconMap = {
|
|
||||||
'facebook': '📘 Facebook',
|
|
||||||
'instagram': '📷 Instagram',
|
|
||||||
'tiktok': '🎵 TikTok'
|
|
||||||
};
|
|
||||||
|
|
||||||
return platforms.map(p => iconMap[p.toLowerCase()] || p).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die Admin-URL zurück
|
|
||||||
*
|
|
||||||
* @returns {string} Admin-Panel URL
|
|
||||||
*/
|
|
||||||
getAdminUrl() {
|
|
||||||
const host = process.env.INTERNAL_HOST || 'internal.hobbyhimmel.de';
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const protocol = isProduction ? 'https' : 'http';
|
|
||||||
const port = isProduction ? '' : ':3000';
|
|
||||||
return `${protocol}://${host}${port}/moderation`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TelegramNotificationService;
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
/**
|
|
||||||
* Integration Tests für Telegram Upload-Benachrichtigungen
|
|
||||||
*
|
|
||||||
* Phase 3: Upload-Benachrichtigungen
|
|
||||||
*
|
|
||||||
* Diese Tests prüfen die Integration zwischen Upload-Route und Telegram-Service
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const { getRequest } = require('../testServer');
|
|
||||||
|
|
||||||
describe('Telegram Upload Notifications (Integration)', () => {
|
|
||||||
let TelegramNotificationService;
|
|
||||||
let sendUploadNotificationSpy;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// Spy auf TelegramNotificationService
|
|
||||||
TelegramNotificationService = require('../../src/services/TelegramNotificationService');
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Spy auf sendUploadNotification erstellen
|
|
||||||
sendUploadNotificationSpy = jest.spyOn(TelegramNotificationService.prototype, 'sendUploadNotification')
|
|
||||||
.mockResolvedValue({ message_id: 42 });
|
|
||||||
|
|
||||||
// isAvailable() immer true zurückgeben für Tests
|
|
||||||
jest.spyOn(TelegramNotificationService.prototype, 'isAvailable')
|
|
||||||
.mockReturnValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Restore alle Spys
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /api/upload/batch', () => {
|
|
||||||
const testImagePath = path.join(__dirname, '../utils/test-image.jpg');
|
|
||||||
|
|
||||||
// Erstelle Test-Bild falls nicht vorhanden
|
|
||||||
beforeAll(() => {
|
|
||||||
if (!fs.existsSync(testImagePath)) {
|
|
||||||
// Erstelle 1x1 px JPEG
|
|
||||||
const buffer = Buffer.from([
|
|
||||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46,
|
|
||||||
0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
|
|
||||||
0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
|
|
||||||
0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08,
|
|
||||||
0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C,
|
|
||||||
0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
|
|
||||||
0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D,
|
|
||||||
0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20,
|
|
||||||
0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
|
|
||||||
0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27,
|
|
||||||
0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34,
|
|
||||||
0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01,
|
|
||||||
0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4,
|
|
||||||
0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x03, 0xFF, 0xC4, 0x00, 0x14,
|
|
||||||
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
|
|
||||||
0x00, 0x00, 0x3F, 0x00, 0x37, 0xFF, 0xD9
|
|
||||||
]);
|
|
||||||
fs.writeFileSync(testImagePath, buffer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte Telegram-Benachrichtigung bei erfolgreichem Upload senden', async () => {
|
|
||||||
const response = await getRequest()
|
|
||||||
.post('/api/upload/batch')
|
|
||||||
.field('year', '2024')
|
|
||||||
.field('title', 'Test Upload')
|
|
||||||
.field('name', 'Test User')
|
|
||||||
.field('consents', JSON.stringify({
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: ['instagram', 'tiktok']
|
|
||||||
}))
|
|
||||||
.attach('images', testImagePath);
|
|
||||||
|
|
||||||
// Upload sollte erfolgreich sein
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body.message).toBe('Batch upload successful');
|
|
||||||
|
|
||||||
// Warte kurz auf async Telegram-Call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 150));
|
|
||||||
|
|
||||||
// Telegram-Service sollte aufgerufen worden sein
|
|
||||||
expect(sendUploadNotificationSpy).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
name: 'Test User',
|
|
||||||
year: 2024,
|
|
||||||
title: 'Test Upload',
|
|
||||||
imageCount: 1,
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: ['instagram', 'tiktok']
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte Upload nicht fehlschlagen wenn Telegram-Service nicht verfügbar', async () => {
|
|
||||||
// Restore mock und setze isAvailable auf false
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
jest.spyOn(TelegramNotificationService.prototype, 'isAvailable')
|
|
||||||
.mockReturnValue(false);
|
|
||||||
sendUploadNotificationSpy = jest.spyOn(TelegramNotificationService.prototype, 'sendUploadNotification');
|
|
||||||
|
|
||||||
const response = await getRequest()
|
|
||||||
.post('/api/upload/batch')
|
|
||||||
.field('year', '2024')
|
|
||||||
.field('title', 'Test Upload')
|
|
||||||
.field('name', 'Test User')
|
|
||||||
.field('consents', JSON.stringify({
|
|
||||||
workshopConsent: false,
|
|
||||||
socialMediaConsents: []
|
|
||||||
}))
|
|
||||||
.attach('images', testImagePath);
|
|
||||||
|
|
||||||
// Upload sollte trotzdem erfolgreich sein
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body.message).toBe('Batch upload successful');
|
|
||||||
|
|
||||||
// Telegram sollte nicht aufgerufen worden sein
|
|
||||||
expect(sendUploadNotificationSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte Upload nicht fehlschlagen wenn Telegram-Benachrichtigung fehlschlägt', async () => {
|
|
||||||
sendUploadNotificationSpy.mockRejectedValueOnce(
|
|
||||||
new Error('Telegram API Error')
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await getRequest()
|
|
||||||
.post('/api/upload/batch')
|
|
||||||
.field('year', '2024')
|
|
||||||
.field('title', 'Test Upload')
|
|
||||||
.field('name', 'Test User')
|
|
||||||
.field('consents', JSON.stringify({
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: []
|
|
||||||
}))
|
|
||||||
.attach('images', testImagePath);
|
|
||||||
|
|
||||||
// Upload sollte trotzdem erfolgreich sein
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body.message).toBe('Batch upload successful');
|
|
||||||
|
|
||||||
// Warte auf async error handling
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 150));
|
|
||||||
|
|
||||||
// Telegram wurde versucht aufzurufen
|
|
||||||
expect(sendUploadNotificationSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte korrekte Daten an Telegram-Service übergeben', async () => {
|
|
||||||
const response = await getRequest()
|
|
||||||
.post('/api/upload/batch')
|
|
||||||
.field('year', '2025')
|
|
||||||
.field('title', 'Schweißkurs November')
|
|
||||||
.field('name', 'Max Mustermann')
|
|
||||||
.field('consents', JSON.stringify({
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: ['facebook', 'instagram']
|
|
||||||
}))
|
|
||||||
.attach('images', testImagePath)
|
|
||||||
.attach('images', testImagePath);
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 150));
|
|
||||||
|
|
||||||
expect(sendUploadNotificationSpy).toHaveBeenCalledWith({
|
|
||||||
name: 'Max Mustermann',
|
|
||||||
year: 2025,
|
|
||||||
title: 'Schweißkurs November',
|
|
||||||
imageCount: 2,
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: ['facebook', 'instagram'],
|
|
||||||
token: expect.any(String)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
/**
|
|
||||||
* Unit Tests für TelegramNotificationService
|
|
||||||
*
|
|
||||||
* Phase 2: Basic Service Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TelegramNotificationService = require('../../src/services/TelegramNotificationService');
|
|
||||||
|
|
||||||
// Mock node-telegram-bot-api komplett
|
|
||||||
jest.mock('node-telegram-bot-api');
|
|
||||||
|
|
||||||
describe('TelegramNotificationService', () => {
|
|
||||||
let originalEnv;
|
|
||||||
let TelegramBot;
|
|
||||||
let mockBotInstance;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
TelegramBot = require('node-telegram-bot-api');
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Speichere originale ENV-Variablen
|
|
||||||
originalEnv = { ...process.env };
|
|
||||||
|
|
||||||
// Setze Test-ENV
|
|
||||||
process.env.TELEGRAM_ENABLED = 'true';
|
|
||||||
process.env.TELEGRAM_BOT_TOKEN = 'test-bot-token-123';
|
|
||||||
process.env.TELEGRAM_CHAT_ID = '-1001234567890';
|
|
||||||
|
|
||||||
// Erstelle Mock-Bot-Instanz
|
|
||||||
mockBotInstance = {
|
|
||||||
sendMessage: jest.fn().mockResolvedValue({
|
|
||||||
message_id: 42,
|
|
||||||
chat: { id: -1001234567890 },
|
|
||||||
text: 'Test'
|
|
||||||
}),
|
|
||||||
getMe: jest.fn().mockResolvedValue({
|
|
||||||
id: 123456,
|
|
||||||
first_name: 'Test Bot',
|
|
||||||
username: 'test_bot'
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock TelegramBot constructor
|
|
||||||
TelegramBot.mockImplementation(() => mockBotInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Restore original ENV
|
|
||||||
process.env = originalEnv;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Initialization', () => {
|
|
||||||
it('sollte erfolgreich initialisieren wenn TELEGRAM_ENABLED=true', () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
expect(service.isAvailable()).toBe(true);
|
|
||||||
expect(TelegramBot).toHaveBeenCalledWith('test-bot-token-123', { polling: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte nicht initialisieren wenn TELEGRAM_ENABLED=false', () => {
|
|
||||||
process.env.TELEGRAM_ENABLED = 'false';
|
|
||||||
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
expect(service.isAvailable()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte fehlschlagen wenn TELEGRAM_BOT_TOKEN fehlt', () => {
|
|
||||||
delete process.env.TELEGRAM_BOT_TOKEN;
|
|
||||||
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
expect(service.isAvailable()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte fehlschlagen wenn TELEGRAM_CHAT_ID fehlt', () => {
|
|
||||||
delete process.env.TELEGRAM_CHAT_ID;
|
|
||||||
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
expect(service.isAvailable()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sendTestMessage', () => {
|
|
||||||
it('sollte Test-Nachricht erfolgreich senden', async () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const result = await service.sendTestMessage();
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.message_id).toBe(42);
|
|
||||||
expect(mockBotInstance.sendMessage).toHaveBeenCalledWith(
|
|
||||||
'-1001234567890',
|
|
||||||
expect.stringContaining('Telegram Service Test')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte null zurückgeben wenn Service nicht verfügbar', async () => {
|
|
||||||
process.env.TELEGRAM_ENABLED = 'false';
|
|
||||||
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const result = await service.sendTestMessage();
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte Fehler werfen bei Telegram-API-Fehler', async () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
mockBotInstance.sendMessage.mockRejectedValueOnce(new Error('API Error'));
|
|
||||||
|
|
||||||
await expect(service.sendTestMessage()).rejects.toThrow('API Error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatSocialMediaIcons', () => {
|
|
||||||
it('sollte Social Media Plattformen korrekt formatieren', () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const result = service.formatSocialMediaIcons(['facebook', 'instagram', 'tiktok']);
|
|
||||||
|
|
||||||
expect(result).toBe('📘 Facebook, 📷 Instagram, 🎵 TikTok');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte leeren String bei leerer Liste zurückgeben', () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const result = service.formatSocialMediaIcons([]);
|
|
||||||
|
|
||||||
expect(result).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte case-insensitive sein', () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const result = service.formatSocialMediaIcons(['FACEBOOK', 'Instagram', 'TikTok']);
|
|
||||||
|
|
||||||
expect(result).toBe('📘 Facebook, 📷 Instagram, 🎵 TikTok');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAdminUrl', () => {
|
|
||||||
it('sollte Admin-URL mit PUBLIC_URL generieren', () => {
|
|
||||||
process.env.PUBLIC_URL = 'https://test.example.com';
|
|
||||||
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const url = service.getAdminUrl();
|
|
||||||
|
|
||||||
expect(url).toBe('https://test.example.com/moderation');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte Default-URL verwenden wenn PUBLIC_URL nicht gesetzt', () => {
|
|
||||||
delete process.env.PUBLIC_URL;
|
|
||||||
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const url = service.getAdminUrl();
|
|
||||||
|
|
||||||
expect(url).toBe('https://internal.hobbyhimmel.de/moderation');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sendUploadNotification (Phase 3)', () => {
|
|
||||||
it('sollte Upload-Benachrichtigung mit korrekten Daten senden', async () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
const groupData = {
|
|
||||||
name: 'Max Mustermann',
|
|
||||||
year: 2024,
|
|
||||||
title: 'Schweißkurs November',
|
|
||||||
imageCount: 12,
|
|
||||||
workshopConsent: true,
|
|
||||||
socialMediaConsents: ['instagram', 'tiktok'],
|
|
||||||
token: 'test-token-123'
|
|
||||||
};
|
|
||||||
|
|
||||||
await service.sendUploadNotification(groupData);
|
|
||||||
|
|
||||||
expect(mockBotInstance.sendMessage).toHaveBeenCalledWith(
|
|
||||||
'-1001234567890',
|
|
||||||
expect.stringContaining('📸 Neuer Upload!')
|
|
||||||
);
|
|
||||||
expect(mockBotInstance.sendMessage).toHaveBeenCalledWith(
|
|
||||||
'-1001234567890',
|
|
||||||
expect.stringContaining('Max Mustermann')
|
|
||||||
);
|
|
||||||
expect(mockBotInstance.sendMessage).toHaveBeenCalledWith(
|
|
||||||
'-1001234567890',
|
|
||||||
expect.stringContaining('Bilder: 12')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sollte null zurückgeben und nicht werfen bei Fehler', async () => {
|
|
||||||
const service = new TelegramNotificationService();
|
|
||||||
|
|
||||||
mockBotInstance.sendMessage.mockRejectedValueOnce(new Error('Network error'));
|
|
||||||
|
|
||||||
const groupData = {
|
|
||||||
name: 'Test User',
|
|
||||||
year: 2024,
|
|
||||||
title: 'Test',
|
|
||||||
imageCount: 5,
|
|
||||||
workshopConsent: false,
|
|
||||||
socialMediaConsents: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.sendUploadNotification(groupData);
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 159 B |
|
|
@ -10,22 +10,6 @@ NODE_ENV=development
|
||||||
# Port for the backend server
|
# Port for the backend server
|
||||||
PORT=5000
|
PORT=5000
|
||||||
|
|
||||||
# Admin Session Secret (IMPORTANT: Change in production!)
|
|
||||||
# Generate with: openssl rand -base64 32
|
|
||||||
ADMIN_SESSION_SECRET=change-me-in-production
|
|
||||||
|
|
||||||
# Telegram Bot Configuration (optional)
|
|
||||||
TELEGRAM_ENABLED=false
|
|
||||||
# Send test message on server start (development only)
|
|
||||||
TELEGRAM_SEND_TEST_ON_START=false
|
|
||||||
# Bot-Token from @BotFather
|
|
||||||
# Example: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz1234567890
|
|
||||||
TELEGRAM_BOT_TOKEN=your-bot-token-here
|
|
||||||
# Chat-ID of the Telegram group (negative for groups!)
|
|
||||||
# Get via: https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
||||||
# Example: -1001234567890
|
|
||||||
TELEGRAM_CHAT_ID=your-chat-id-here
|
|
||||||
|
|
||||||
# Database settings (if needed in future)
|
# Database settings (if needed in future)
|
||||||
# DB_HOST=localhost
|
# DB_HOST=localhost
|
||||||
# DB_PORT=3306
|
# DB_PORT=3306
|
||||||
|
|
@ -6,4 +6,7 @@
|
||||||
# Production: http://backend:5000 (container-to-container)
|
# Production: http://backend:5000 (container-to-container)
|
||||||
API_URL=http://backend:5000
|
API_URL=http://backend:5000
|
||||||
|
|
||||||
# Public/Internal host separation (optional)
|
# Client URL - the URL where users access the frontend
|
||||||
|
# Development: http://localhost:3000 (dev server)
|
||||||
|
# Production: http://localhost (nginx on port 80)
|
||||||
|
CLIENT_URL=http://localhost
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# Docker Compose Environment Variables for Development
|
|
||||||
# Copy this file to .env and adjust values
|
|
||||||
|
|
||||||
# Admin Session Secret (optional, has default: dev-session-secret-change-me)
|
|
||||||
#ADMIN_SESSION_SECRET=your-secret-here
|
|
||||||
|
|
||||||
# Telegram Bot Configuration (optional)
|
|
||||||
TELEGRAM_ENABLED=false
|
|
||||||
TELEGRAM_SEND_TEST_ON_START=false
|
|
||||||
# Bot-Token from @BotFather
|
|
||||||
# Example: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz1234567890
|
|
||||||
TELEGRAM_BOT_TOKEN=your-bot-token-here
|
|
||||||
# Chat-ID of the Telegram group (negative for groups!)
|
|
||||||
# Get via: https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
||||||
# Example: -1001234567890
|
|
||||||
TELEGRAM_CHAT_ID=your-chat-id-here
|
|
||||||
|
|
@ -12,8 +12,8 @@ RUN npm install
|
||||||
# Copy backend source code
|
# Copy backend source code
|
||||||
COPY backend/ .
|
COPY backend/ .
|
||||||
|
|
||||||
# Note: Environment variables are set via docker-compose.yml
|
# Copy development environment configuration
|
||||||
# No .env file needed in the image
|
COPY docker/dev/backend/config/.env ./.env
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../frontend:/app:cached
|
- ../../frontend:/app:cached
|
||||||
- dev_frontend_node_modules:/app/node_modules
|
- dev_frontend_node_modules:/app/node_modules
|
||||||
|
- ./frontend/config/.env:/app/.env:ro
|
||||||
environment:
|
environment:
|
||||||
- CHOKIDAR_USEPOLLING=true
|
- CHOKIDAR_USEPOLLING=true
|
||||||
- API_URL=http://localhost:5001
|
- API_URL=http://localhost:5001
|
||||||
|
|
@ -37,20 +38,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../backend:/usr/src/app:cached
|
- ../../backend:/usr/src/app:cached
|
||||||
- dev_backend_node_modules:/usr/src/app/node_modules
|
- dev_backend_node_modules:/usr/src/app/node_modules
|
||||||
|
- ./backend/config/.env:/usr/src/app/.env:ro
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- PORT=5000
|
|
||||||
- REMOVE_IMAGES=false
|
|
||||||
- ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET:-dev-session-secret-change-me}
|
|
||||||
- PUBLIC_HOST=public.test.local
|
- PUBLIC_HOST=public.test.local
|
||||||
- INTERNAL_HOST=internal.test.local
|
- INTERNAL_HOST=internal.test.local
|
||||||
- ENABLE_HOST_RESTRICTION=true
|
- ENABLE_HOST_RESTRICTION=true
|
||||||
- TRUST_PROXY_HOPS=0
|
- TRUST_PROXY_HOPS=0
|
||||||
- PUBLIC_UPLOAD_RATE_LIMIT=20
|
- PUBLIC_UPLOAD_RATE_LIMIT=20
|
||||||
- TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false}
|
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
||||||
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
||||||
- TELEGRAM_SEND_TEST_ON_START=${TELEGRAM_SEND_TEST_ON_START:-false}
|
|
||||||
networks:
|
networks:
|
||||||
- dev-internal
|
- dev-internal
|
||||||
command: [ "npm", "run", "server" ]
|
command: [ "npm", "run", "server" ]
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ WORKDIR /app
|
||||||
# Copy package files first to leverage Docker cache for npm install
|
# Copy package files first to leverage Docker cache for npm install
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
|
|
||||||
# Copy environment shell script (generates env-config.js from ENV at runtime)
|
# Copy environment configuration
|
||||||
COPY docker/dev/frontend/config/env.sh ./env.sh
|
COPY docker/dev/frontend/config/env.sh ./env.sh
|
||||||
# Note: ENV variables are set via docker-compose.yml, not from .env file
|
COPY docker/dev/frontend/config/.env ./.env
|
||||||
|
|
||||||
# Make env.sh executable
|
# Make env.sh executable
|
||||||
RUN chmod +x ./env.sh
|
RUN chmod +x ./env.sh
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,23 @@ touch ./env-config.js
|
||||||
# Add assignment
|
# Add assignment
|
||||||
echo "window._env_ = {" >> ./env-config.js
|
echo "window._env_ = {" >> ./env-config.js
|
||||||
|
|
||||||
# List of environment variables to export (add more as needed)
|
# Read each line in .env file
|
||||||
ENV_VARS="API_URL PUBLIC_HOST INTERNAL_HOST"
|
# Each line represents key=value pairs
|
||||||
|
while read -r line || [[ -n "$line" ]];
|
||||||
# Read each environment variable and add to config
|
do
|
||||||
for varname in $ENV_VARS; do
|
# Split env variables by character `=`
|
||||||
# Get value from environment using indirect expansion
|
if printf '%s\n' "$line" | grep -q -e '='; then
|
||||||
value="${!varname}"
|
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
|
||||||
|
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
|
||||||
# Only add if value exists
|
|
||||||
if [ -n "$value" ]; then
|
|
||||||
echo " $varname: \"$value\"," >> ./env-config.js
|
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
# Read value of current variable if exists as Environment variable
|
||||||
|
value=$(printf '%s\n' "${!varname}")
|
||||||
|
# Otherwise use value from .env file
|
||||||
|
[[ -z $value ]] && value=${varvalue}
|
||||||
|
|
||||||
|
# Append configuration property to JS file
|
||||||
|
echo " $varname: \"$value\"," >> ./env-config.js
|
||||||
|
done < .env
|
||||||
|
|
||||||
echo "}" >> ./env-config.js
|
echo "}" >> ./env-config.js
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# Docker Compose Environment Variables for Production
|
|
||||||
# Copy this file to .env and adjust values
|
|
||||||
# IMPORTANT: Keep this file secure and never commit .env to git!
|
|
||||||
|
|
||||||
# Admin Session Secret (REQUIRED: Generate new secret!)
|
|
||||||
# Generate with: openssl rand -base64 32
|
|
||||||
ADMIN_SESSION_SECRET=CHANGE-ME-IN-PRODUCTION
|
|
||||||
|
|
||||||
# Telegram Bot Configuration (optional)
|
|
||||||
# Set to true to enable Telegram notifications in production
|
|
||||||
TELEGRAM_ENABLED=false
|
|
||||||
# Bot-Token from @BotFather (production bot)
|
|
||||||
# Example: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz1234567890
|
|
||||||
TELEGRAM_BOT_TOKEN=your-production-bot-token-here
|
|
||||||
# Chat-ID of the production Telegram group (negative for groups!)
|
|
||||||
# Get via: https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
||||||
# Example: -1001234567890
|
|
||||||
TELEGRAM_CHAT_ID=your-production-chat-id-here
|
|
||||||
|
|
@ -6,7 +6,6 @@ services:
|
||||||
frontend:
|
frontend:
|
||||||
container_name: image-uploader-frontend
|
container_name: image-uploader-frontend
|
||||||
image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-frontend:latest
|
image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-frontend:latest
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
build:
|
build:
|
||||||
|
|
@ -16,8 +15,8 @@ services:
|
||||||
- backend
|
- backend
|
||||||
environment:
|
environment:
|
||||||
- API_URL=http://backend:5000
|
- API_URL=http://backend:5000
|
||||||
- PUBLIC_HOST=public.test.local
|
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
|
||||||
- INTERNAL_HOST=internal.test.local
|
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- npm-nw
|
- npm-nw
|
||||||
|
|
@ -26,7 +25,6 @@ services:
|
||||||
backend:
|
backend:
|
||||||
container_name: image-uploader-backend
|
container_name: image-uploader-backend
|
||||||
image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-backend:latest
|
image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-backend:latest
|
||||||
restart: unless-stopped
|
|
||||||
build:
|
build:
|
||||||
context: ../../
|
context: ../../
|
||||||
dockerfile: docker/prod/backend/Dockerfile
|
dockerfile: docker/prod/backend/Dockerfile
|
||||||
|
|
@ -44,18 +42,13 @@ services:
|
||||||
# ⚠️ Für HTTP-only Labs per Override auf "false" setzen (nicht im Repo committen)
|
# ⚠️ Für HTTP-only Labs per Override auf "false" setzen (nicht im Repo committen)
|
||||||
- ADMIN_SESSION_COOKIE_SECURE=true
|
- ADMIN_SESSION_COOKIE_SECURE=true
|
||||||
# Host Configuration (Public/Internal Separation)
|
# Host Configuration (Public/Internal Separation)
|
||||||
- PUBLIC_HOST=public.test.local
|
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
|
||||||
- INTERNAL_HOST=internal.test.local
|
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
|
||||||
- ENABLE_HOST_RESTRICTION=true
|
- ENABLE_HOST_RESTRICTION=true
|
||||||
- PUBLIC_UPLOAD_RATE_LIMIT=20
|
- PUBLIC_UPLOAD_RATE_LIMIT=20
|
||||||
- PUBLIC_UPLOAD_RATE_WINDOW=3600000
|
- PUBLIC_UPLOAD_RATE_WINDOW=3600000
|
||||||
# Trust nginx-proxy-manager (1 hop)
|
# Trust nginx-proxy-manager (1 hop)
|
||||||
- TRUST_PROXY_HOPS=1
|
- TRUST_PROXY_HOPS=1
|
||||||
# Telegram Bot Configuration (optional)
|
|
||||||
- TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false}
|
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
||||||
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
||||||
- TELEGRAM_SEND_TEST_ON_START=false
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ COPY --from=build /app/build /usr/share/nginx/html
|
||||||
# Default port exposure
|
# Default port exposure
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
# Copy .env shell script to container (generates env-config.js from ENV at runtime)
|
# Copy .env file and shell script to container
|
||||||
WORKDIR /usr/share/nginx/html
|
WORKDIR /usr/share/nginx/html
|
||||||
COPY docker/prod/frontend/config/env.sh ./env.sh
|
COPY docker/prod/frontend/config/env.sh ./env.sh
|
||||||
# Note: ENV variables are set via docker-compose.yml, not from .env file
|
COPY docker/prod/frontend/config/.env ./.env
|
||||||
|
|
||||||
# Add bash
|
# Add bash
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,23 @@ touch ./env-config.js
|
||||||
# Add assignment
|
# Add assignment
|
||||||
echo "window._env_ = {" >> ./env-config.js
|
echo "window._env_ = {" >> ./env-config.js
|
||||||
|
|
||||||
# List of environment variables to export (add more as needed)
|
# Read each line in .env file
|
||||||
ENV_VARS="API_URL PUBLIC_HOST INTERNAL_HOST"
|
# Each line represents key=value pairs
|
||||||
|
while read -r line || [[ -n "$line" ]];
|
||||||
# Read each environment variable and add to config
|
do
|
||||||
for varname in $ENV_VARS; do
|
# Split env variables by character `=`
|
||||||
# Get value from environment using indirect expansion
|
if printf '%s\n' "$line" | grep -q -e '='; then
|
||||||
value="${!varname}"
|
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
|
||||||
|
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
|
||||||
# Only add if value exists
|
|
||||||
if [ -n "$value" ]; then
|
|
||||||
echo " $varname: \"$value\"," >> ./env-config.js
|
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
# Read value of current variable if exists as Environment variable
|
||||||
|
value=$(printf '%s\n' "${!varname}")
|
||||||
|
# Otherwise use value from .env file
|
||||||
|
[[ -z $value ]] && value=${varvalue}
|
||||||
|
|
||||||
|
# Append configuration property to JS file
|
||||||
|
echo " $varname: \"$value\"," >> ./env-config.js
|
||||||
|
done < .env
|
||||||
|
|
||||||
echo "}" >> ./env-config.js
|
echo "}" >> ./env-config.js
|
||||||
|
|
|
||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "2.0.1",
|
"version": "1.10.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "2.0.1",
|
"version": "1.10.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "2.0.2",
|
"version": "1.10.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<script src="https://kit.fontawesome.com/fc1a4a5a71.js" crossorigin="anonymous"></script>
|
<script src="https://kit.fontawesome.com/fc1a4a5a71.js" crossorigin="anonymous"></script>
|
||||||
|
<link rel="stylesheet" href="@sweetalert2/theme-material-ui/material-ui.css">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@100&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@100&display=swap" rel="stylesheet">
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<script src="sweetalert2/dist/sweetalert2.min.js"></script>
|
||||||
<script src="%PUBLIC_URL%/env-config.js"></script>
|
<script src="%PUBLIC_URL%/env-config.js"></script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<AdminSessionProvider>
|
<AdminSessionProvider>
|
||||||
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
<Router>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public Routes - immer verfügbar */}
|
{/* Public Routes - immer verfügbar */}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ function ConsentManager({
|
||||||
token,
|
token,
|
||||||
groupId,
|
groupId,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
mode = 'edit',
|
mode = 'edit'
|
||||||
validationErrors,
|
|
||||||
onValidationClear
|
|
||||||
}) {
|
}) {
|
||||||
// Initialize with proper defaults
|
// Initialize with proper defaults
|
||||||
const defaultConsents = {
|
const defaultConsents = {
|
||||||
|
|
@ -212,8 +210,6 @@ function ConsentManager({
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
token={token}
|
token={token}
|
||||||
onSave={null}
|
onSave={null}
|
||||||
workshopConsentError={validationErrors?.workshopConsent}
|
|
||||||
onWorkshopConsentErrorClear={() => onValidationClear?.('workshopConsent')}
|
|
||||||
>
|
>
|
||||||
{/* Alerts and Buttons only in edit mode */}
|
{/* Alerts and Buttons only in edit mode */}
|
||||||
{!isUploadMode && (
|
{!isUploadMode && (
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ function GroupMetadataEditor({
|
||||||
token,
|
token,
|
||||||
groupId,
|
groupId,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
mode = 'edit',
|
mode = 'edit'
|
||||||
validationErrors,
|
|
||||||
onValidationClear
|
|
||||||
}) {
|
}) {
|
||||||
const [metadata, setMetadata] = useState(initialMetadata || {
|
const [metadata, setMetadata] = useState(initialMetadata || {
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
|
|
@ -152,8 +150,6 @@ function GroupMetadataEditor({
|
||||||
<DescriptionInput
|
<DescriptionInput
|
||||||
metadata={currentMetadata}
|
metadata={currentMetadata}
|
||||||
onMetadataChange={setCurrentMetadata}
|
onMetadataChange={setCurrentMetadata}
|
||||||
validationErrors={validationErrors}
|
|
||||||
onValidationClear={onValidationClear}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isUploadMode && hasChanges() && (
|
{!isUploadMode && hasChanges() && (
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function Navbar() {
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<NavLink className="logo" to="/">
|
<NavLink className="logo" exact to="/">
|
||||||
<img src={logo} className="imageNav" alt="Logo" />
|
<img src={logo} className="imageNav" alt="Logo" />
|
||||||
<p className="logo">Upload your Project Images</p>
|
<p className="logo">Upload your Project Images</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
@ -31,7 +31,7 @@ function Navbar() {
|
||||||
<ul className="nav__links">
|
<ul className="nav__links">
|
||||||
<li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li>
|
<li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li>
|
||||||
<li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li>
|
<li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li>
|
||||||
<li><NavLink className={({ isActive }) => isActive ? 'cta active' : 'cta'} to="/">Upload</NavLink></li>
|
<li><NavLink className="cta" exact to="/" activeClassName="active">Upload</NavLink></li>
|
||||||
{isManagementPage && (
|
{isManagementPage && (
|
||||||
<li><NavLink className="cta" to={location.pathname} activeClassName="active">Mein Upload</NavLink></li>
|
<li><NavLink className="cta" to={location.pathname} activeClassName="active">Mein Upload</NavLink></li>
|
||||||
)}
|
)}
|
||||||
|
|
@ -62,7 +62,7 @@ function Navbar() {
|
||||||
<NavLink to="/moderation" activeClassName="active" onClick={closeMenu}>
|
<NavLink to="/moderation" activeClassName="active" onClick={closeMenu}>
|
||||||
<LockIcon style={{ fontSize: 24, verticalAlign: 'text-bottom', marginRight: 12 }} aria-hidden="true" />Moderation
|
<LockIcon style={{ fontSize: 24, verticalAlign: 'text-bottom', marginRight: 12 }} aria-hidden="true" />Moderation
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink to="/" className={({ isActive }) => isActive ? 'active' : undefined} onClick={closeMenu}>Upload</NavLink>
|
<NavLink exact to="/" activeClassName="active" onClick={closeMenu}>Upload</NavLink>
|
||||||
{isManagementPage && (
|
{isManagementPage && (
|
||||||
<NavLink to={location.pathname} activeClassName="active" onClick={closeMenu}>Mein Upload</NavLink>
|
<NavLink to={location.pathname} activeClassName="active" onClick={closeMenu}>Mein Upload</NavLink>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ function Navbar() {
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<NavLink className="logo" to="/">
|
<NavLink className="logo" exact to="/">
|
||||||
<img src={logo} className="imageNav" alt="Logo" />
|
<img src={logo} className="imageNav" alt="Logo" />
|
||||||
<p className="logo">Upload your Project Images</p>
|
<p className="logo">Upload your Project Images</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,7 @@ function ConsentCheckboxes({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
mode = 'upload',
|
mode = 'upload',
|
||||||
groupId = null,
|
groupId = null,
|
||||||
children,
|
children
|
||||||
workshopConsentError = false,
|
|
||||||
onWorkshopConsentErrorClear
|
|
||||||
}) {
|
}) {
|
||||||
const [platforms, setPlatforms] = useState([]);
|
const [platforms, setPlatforms] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -73,7 +71,6 @@ function ConsentCheckboxes({
|
||||||
...consents,
|
...consents,
|
||||||
workshopConsent: event.target.checked
|
workshopConsent: event.target.checked
|
||||||
});
|
});
|
||||||
if (onWorkshopConsentErrorClear) onWorkshopConsentErrorClear();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSocialMediaChange = (platformId) => (event) => {
|
const handleSocialMediaChange = (platformId) => (event) => {
|
||||||
|
|
@ -171,11 +168,6 @@ function ConsentCheckboxes({
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{workshopConsentError && (
|
|
||||||
<Alert severity="error" sx={{ mt: 1 }}>
|
|
||||||
Bitte bestätige die Einwilligung zur Anzeige in der Werkstatt.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ import { TextField, Typography, Grid, Box } from '@mui/material';
|
||||||
|
|
||||||
function DescriptionInput({
|
function DescriptionInput({
|
||||||
metadata = {},
|
metadata = {},
|
||||||
onMetadataChange,
|
onMetadataChange
|
||||||
validationErrors = {},
|
|
||||||
onValidationClear
|
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const handleFieldChange = (field, value) => {
|
const handleFieldChange = (field, value) => {
|
||||||
|
|
@ -14,7 +12,6 @@ function DescriptionInput({
|
||||||
[field]: value
|
[field]: value
|
||||||
};
|
};
|
||||||
onMetadataChange(updatedMetadata);
|
onMetadataChange(updatedMetadata);
|
||||||
if (onValidationClear) onValidationClear(field);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
@ -89,8 +86,6 @@ function DescriptionInput({
|
||||||
min: 1900,
|
min: 1900,
|
||||||
max: currentYear + 10
|
max: currentYear + 10
|
||||||
}}
|
}}
|
||||||
error={!!validationErrors.year}
|
|
||||||
helperText={validationErrors.year ? 'Bitte wähle ein Jahr aus.' : ''}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
@ -105,8 +100,6 @@ function DescriptionInput({
|
||||||
onChange={(e) => handleFieldChange('title', e.target.value)}
|
onChange={(e) => handleFieldChange('title', e.target.value)}
|
||||||
placeholder="z.B. Wohnzimmer Renovierung"
|
placeholder="z.B. Wohnzimmer Renovierung"
|
||||||
inputProps={{ maxLength: 100 }}
|
inputProps={{ maxLength: 100 }}
|
||||||
error={!!validationErrors.title}
|
|
||||||
helperText={validationErrors.title ? 'Bitte gib einen Titel für die Slideshow ein.' : ''}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
function MultiImageDropzone({ onImagesSelected, selectedImages = [], hasError = false }) {
|
function MultiImageDropzone({ onImagesSelected, selectedImages = [] }) {
|
||||||
|
|
||||||
const handleFiles = (files) => {
|
const handleFiles = (files) => {
|
||||||
// Filter nur Bilddateien
|
// Filter nur Bilddateien
|
||||||
|
|
@ -57,21 +57,21 @@ function MultiImageDropzone({ onImagesSelected, selectedImages = [], hasError =
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropzoneSx = {
|
const dropzoneSx = {
|
||||||
border: `2px dashed ${hasError ? '#d32f2f' : '#cccccc'}`,
|
border: '2px dashed #cccccc',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '40px 20px',
|
padding: '40px 20px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
backgroundColor: hasError ? '#fff5f5' : '#fafafa',
|
backgroundColor: '#fafafa',
|
||||||
minHeight: '200px',
|
minHeight: '200px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
borderColor: hasError ? '#b71c1c' : '#999999',
|
borderColor: '#999999',
|
||||||
backgroundColor: hasError ? '#ffebee' : '#f0f0f0'
|
backgroundColor: '#f0f0f0'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload';
|
import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload';
|
||||||
|
|
@ -34,17 +34,6 @@ function MultiUploadPage() {
|
||||||
const [uploadResult, setUploadResult] = useState(null);
|
const [uploadResult, setUploadResult] = useState(null);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [imageDescriptions, setImageDescriptions] = useState({});
|
const [imageDescriptions, setImageDescriptions] = useState({});
|
||||||
const [validationErrors, setValidationErrors] = useState({
|
|
||||||
images: false,
|
|
||||||
year: false,
|
|
||||||
title: false,
|
|
||||||
workshopConsent: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refs für Auto-Scroll zu Pflichtfeldern
|
|
||||||
const dropzoneRef = useRef(null);
|
|
||||||
const metadataRef = useRef(null);
|
|
||||||
const consentRef = useRef(null);
|
|
||||||
|
|
||||||
// Cleanup object URLs when component unmounts
|
// Cleanup object URLs when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -119,34 +108,12 @@ function MultiUploadPage() {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearValidationError = (field) => {
|
|
||||||
setValidationErrors(prev => ({ ...prev, [field]: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = () => {
|
|
||||||
const errors = {
|
|
||||||
images: selectedImages.length === 0,
|
|
||||||
year: !metadata.year,
|
|
||||||
title: !metadata.title.trim(),
|
|
||||||
workshopConsent: !consents.workshopConsent,
|
|
||||||
};
|
|
||||||
setValidationErrors(errors);
|
|
||||||
|
|
||||||
if (errors.images) {
|
|
||||||
dropzoneRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
} else if (errors.year) {
|
|
||||||
metadataRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
} else if (errors.title) {
|
|
||||||
metadataRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
} else if (errors.workshopConsent) {
|
|
||||||
consentRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return !Object.values(errors).some(Boolean);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!validate()) return;
|
if (selectedImages.length === 0) return;
|
||||||
|
|
||||||
|
if (!metadata.year || !metadata.title.trim()) return;
|
||||||
|
|
||||||
|
if (!consents.workshopConsent) return;
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
|
@ -184,6 +151,13 @@ function MultiUploadPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canUpload = () => {
|
||||||
|
return selectedImages.length > 0 &&
|
||||||
|
metadata.year &&
|
||||||
|
metadata.title.trim() &&
|
||||||
|
consents.workshopConsent;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="allContainer">
|
<div className="allContainer">
|
||||||
{<NavbarUpload />}
|
{<NavbarUpload />}
|
||||||
|
|
@ -197,27 +171,16 @@ function MultiUploadPage() {
|
||||||
<p className="page-subtitle">
|
<p className="page-subtitle">
|
||||||
Lade ein oder mehrere Bilder von deinem Projekt hoch und beschreibe dein Projekt in wenigen Worten.
|
Lade ein oder mehrere Bilder von deinem Projekt hoch und beschreibe dein Projekt in wenigen Worten.
|
||||||
<br />
|
<br />
|
||||||
Die Bilder werden nur in der Hobbyhimmel Werkstatt auf den Monitoren gezeigt, es wird an keine Dritten weiter gegeben, sofern du deine Einwilligung nicht erteilst.
|
Die Bilder werden nur hier im Hobbyhimmel auf dem Monitor gezeigt, es wird an keine Dritten weiter gegeben.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{!uploading ? (
|
{!uploading ? (
|
||||||
<>
|
<>
|
||||||
{/* Image Dropzone - stays inline as it's upload-specific */}
|
{/* Image Dropzone - stays inline as it's upload-specific */}
|
||||||
<div ref={dropzoneRef}>
|
<MultiImageDropzone
|
||||||
<MultiImageDropzone
|
onImagesSelected={handleImagesSelected}
|
||||||
onImagesSelected={(imgs) => {
|
selectedImages={selectedImages}
|
||||||
handleImagesSelected(imgs);
|
/>
|
||||||
clearValidationError('images');
|
|
||||||
}}
|
|
||||||
selectedImages={selectedImages}
|
|
||||||
hasError={validationErrors.images}
|
|
||||||
/>
|
|
||||||
{validationErrors.images && (
|
|
||||||
<p style={{ color: '#d32f2f', fontSize: '13px', marginTop: '6px' }}>
|
|
||||||
Bitte wähle mindestens ein Bild aus.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Gallery with descriptions */}
|
{/* Image Gallery with descriptions */}
|
||||||
{selectedImages.length > 0 && (
|
{selectedImages.length > 0 && (
|
||||||
|
|
@ -238,31 +201,24 @@ function MultiUploadPage() {
|
||||||
{selectedImages.length > 0 && (
|
{selectedImages.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{/* Modular Components like ManagementPortalPage */}
|
{/* Modular Components like ManagementPortalPage */}
|
||||||
<div ref={metadataRef}>
|
<GroupMetadataEditor
|
||||||
<GroupMetadataEditor
|
metadata={metadata}
|
||||||
metadata={metadata}
|
onMetadataChange={setMetadata}
|
||||||
onMetadataChange={setMetadata}
|
mode="upload"
|
||||||
mode="upload"
|
/>
|
||||||
validationErrors={validationErrors}
|
|
||||||
onValidationClear={clearValidationError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref={consentRef}>
|
<ConsentManager
|
||||||
<ConsentManager
|
consents={consents}
|
||||||
consents={consents}
|
onConsentsChange={setConsents}
|
||||||
onConsentsChange={setConsents}
|
mode="upload"
|
||||||
mode="upload"
|
/>
|
||||||
validationErrors={validationErrors}
|
|
||||||
onValidationClear={clearValidationError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex-center">
|
<div className="flex-center">
|
||||||
<button
|
<button
|
||||||
className="btn btn-success"
|
className="btn btn-success"
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
|
disabled={!canUpload()}
|
||||||
>
|
>
|
||||||
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
|
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -362,10 +318,10 @@ function MultiUploadPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-small" style={{ color: '#666', marginBottom: '4px' }}>
|
<p className="text-small" style={{ color: '#666', marginBottom: '4px' }}>
|
||||||
⚠️ <strong>Wichtig:</strong> Bewahre diesen Link sicher auf! Jeder mit diesem Link kann Deinen Upload verwalten.
|
⚠️ <strong>Wichtig:</strong> Bewahren Sie diesen Link sicher auf! Jeder mit diesem Link kann Ihren Upload verwalten.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-small" style={{ color: '#666', fontStyle: 'italic' }}>
|
<p className="text-small" style={{ color: '#666', fontStyle: 'italic' }}>
|
||||||
ℹ️ <strong>Hinweis:</strong> Über diesen Link kannst Du nur die Bilder in der Werkstatt verwalten. Bereits auf Social Media Plattformen veröffentlichte Bilder müssen separat dort gelöscht werden.
|
ℹ️ <strong>Hinweis:</strong> Über diesen Link können Sie nur die Bilder in der Werkstatt verwalten. Bereits auf Social Media Plattformen veröffentlichte Bilder müssen separat dort gelöscht werden.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
4067
package-lock.json
generated
4067
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "project-image-uploader",
|
"name": "project-image-uploader",
|
||||||
"version": "2.0.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"release": "./scripts/release.sh patch",
|
"release": "./scripts/release.sh patch",
|
||||||
|
|
|
||||||
32
prod.sh
32
prod.sh
|
|
@ -52,36 +52,8 @@ case $choice in
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
echo -e "${GREEN}Pushe Production Images zur Registry...${NC}"
|
echo -e "${GREEN}Pushe Production Images zur Registry...${NC}"
|
||||||
|
docker compose -f docker/prod/docker-compose.yml push
|
||||||
# Hole aktuelle Version aus package.json
|
echo -e "${GREEN}Production Images erfolgreich gepusht!${NC}"
|
||||||
VERSION=$(node -p "require('./frontend/package.json').version")
|
|
||||||
REGISTRY="gitea.lan.hobbyhimmel.de/hobbyhimmel"
|
|
||||||
echo -e "${BLUE}Aktuelle Version: ${VERSION}${NC}"
|
|
||||||
echo -e "${BLUE}Registry: ${REGISTRY}${NC}"
|
|
||||||
|
|
||||||
# Baue Images mit korrekter Version
|
|
||||||
echo -e "${BLUE}Baue Images mit Version ${VERSION}...${NC}"
|
|
||||||
cd docker/prod
|
|
||||||
docker build -t ${REGISTRY}/image-uploader-frontend:${VERSION} -f frontend/Dockerfile ../../
|
|
||||||
docker build -t ${REGISTRY}/image-uploader-backend:${VERSION} -f backend/Dockerfile ../../
|
|
||||||
|
|
||||||
# Tag als 'latest'
|
|
||||||
docker tag ${REGISTRY}/image-uploader-frontend:${VERSION} ${REGISTRY}/image-uploader-frontend:latest
|
|
||||||
docker tag ${REGISTRY}/image-uploader-backend:${VERSION} ${REGISTRY}/image-uploader-backend:latest
|
|
||||||
|
|
||||||
# Push Images (max-concurrent-uploads=1 in Docker Desktop gesetzt)
|
|
||||||
echo -e "${BLUE}Pushe Frontend ${VERSION}...${NC}"
|
|
||||||
docker push ${REGISTRY}/image-uploader-frontend:${VERSION}
|
|
||||||
echo -e "${BLUE}Pushe Frontend latest...${NC}"
|
|
||||||
docker push ${REGISTRY}/image-uploader-frontend:latest
|
|
||||||
|
|
||||||
echo -e "${BLUE}Pushe Backend ${VERSION}...${NC}"
|
|
||||||
docker push ${REGISTRY}/image-uploader-backend:${VERSION}
|
|
||||||
echo -e "${BLUE}Pushe Backend latest...${NC}"
|
|
||||||
docker push ${REGISTRY}/image-uploader-backend:latest
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
echo -e "${GREEN}✓ Images v${VERSION} und latest erfolgreich gepusht!${NC}"
|
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
echo -e "${GREEN}Baue Container neu...${NC}"
|
echo -e "${GREEN}Baue Container neu...${NC}"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# Telegram Bot Configuration Template
|
|
||||||
#
|
|
||||||
# Kopiere diese Datei zu .env.telegram und trage deine echten Werte ein:
|
|
||||||
# cp .env.telegram.example .env.telegram
|
|
||||||
#
|
|
||||||
# WICHTIG: .env.telegram NIEMALS committen! (ist in .gitignore)
|
|
||||||
|
|
||||||
# Bot-Token von @BotFather
|
|
||||||
# Beispiel: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz1234567890
|
|
||||||
TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE
|
|
||||||
|
|
||||||
# Chat-ID der Telegram-Gruppe (negativ für Gruppen!)
|
|
||||||
# Ermitteln via: https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
||||||
# Beispiel: -1001234567890
|
|
||||||
TELEGRAM_CHAT_ID=YOUR_CHAT_ID_HERE
|
|
||||||
|
|
@ -1,506 +0,0 @@
|
||||||
# Telegram Bot Setup & Testing Guide
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Diese Anleitung beschreibt Schritt-für-Schritt, wie du den Telegram Bot für den Image Uploader erstellst und testest.
|
|
||||||
|
|
||||||
**Phase 1:** Standalone-Test (ohne Backend-Integration)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Voraussetzungen
|
|
||||||
|
|
||||||
- ✅ Windows 11 mit installiertem Telegram Desktop
|
|
||||||
- ✅ Telegram Account
|
|
||||||
- ✅ Node.js >= 18.x installiert
|
|
||||||
- ✅ Zugriff auf dieses Git-Repository
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Telegram Bot erstellen
|
|
||||||
|
|
||||||
### 1.1 BotFather öffnen
|
|
||||||
|
|
||||||
1. **Telegram Desktop** öffnen
|
|
||||||
2. In der Suche eingeben: `@BotFather`
|
|
||||||
3. Chat mit **BotFather** öffnen (offizieller Bot mit blauem Haken ✓)
|
|
||||||
|
|
||||||
### 1.2 Neuen Bot erstellen
|
|
||||||
|
|
||||||
**Commands im BotFather-Chat eingeben:**
|
|
||||||
|
|
||||||
```
|
|
||||||
/newbot
|
|
||||||
```
|
|
||||||
|
|
||||||
**BotFather fragt nach Namen:**
|
|
||||||
```
|
|
||||||
Alright, a new bot. How are we going to call it?
|
|
||||||
Please choose a name for your bot.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Antworten mit:**
|
|
||||||
```
|
|
||||||
Werkstatt Image Uploader Bot
|
|
||||||
```
|
|
||||||
|
|
||||||
**BotFather fragt nach Username:**
|
|
||||||
```
|
|
||||||
Good. Now let's choose a username for your bot.
|
|
||||||
It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Antworten mit (muss auf `bot` enden):**
|
|
||||||
```
|
|
||||||
werkstatt_uploader_bot
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ **Falls Username vergeben:** Anderen Namen wählen (z.B. `werkstatt_upload_bot`, `hobbyhimmel_uploader_bot`)
|
|
||||||
|
|
||||||
### 1.3 Bot-Token speichern
|
|
||||||
|
|
||||||
**BotFather antwortet mit:**
|
|
||||||
```
|
|
||||||
Done! Congratulations on your new bot.
|
|
||||||
You will find it at t.me/werkstatt_uploader_bot.
|
|
||||||
You can now add a description, about section and profile picture for your bot.
|
|
||||||
|
|
||||||
Use this token to access the HTTP API:
|
|
||||||
123456789:ABCdefGHIjklMNOpqrsTUVwxyz1234567890
|
|
||||||
|
|
||||||
Keep your token secure and store it safely, it can be used by anyone to control your bot.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Token kopieren** (z.B. `123456789:ABCdefGHIjklMNOpqrsTUVwxyz1234567890`)
|
|
||||||
|
|
||||||
➡️ **Diesen Token brauchst du gleich für `.env.telegram`!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Test-Telegram-Gruppe erstellen
|
|
||||||
|
|
||||||
### 2.1 Neue Gruppe erstellen
|
|
||||||
|
|
||||||
1. In Telegram: **Neuer Chat** → **Neue Gruppe**
|
|
||||||
2. Gruppennamen eingeben: `Werkstatt Upload Bot Test`
|
|
||||||
3. **Weiter** klicken
|
|
||||||
4. (Optional) Weitere Mitglieder hinzufügen (oder nur dich selbst)
|
|
||||||
5. **Erstellen** klicken
|
|
||||||
|
|
||||||
### 2.2 Bot zur Gruppe hinzufügen
|
|
||||||
|
|
||||||
1. Test-Gruppe öffnen
|
|
||||||
2. Auf Gruppennamen (oben) klicken → **Mitglieder hinzufügen**
|
|
||||||
3. Suche nach: `@werkstatt_uploader_bot` (dein Bot-Username)
|
|
||||||
4. Bot auswählen → **Hinzufügen**
|
|
||||||
|
|
||||||
**Telegram zeigt:**
|
|
||||||
```
|
|
||||||
werkstatt_uploader_bot wurde zur Gruppe hinzugefügt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 Privacy Mode deaktivieren (WICHTIG!)
|
|
||||||
|
|
||||||
⚠️ **Ohne diesen Schritt sieht der Bot keine Gruppennachrichten!**
|
|
||||||
|
|
||||||
1. **BotFather** öffnen
|
|
||||||
2. Command eingeben: `/mybots`
|
|
||||||
3. Deinen Bot auswählen: `@werkstatt_uploader_bot`
|
|
||||||
4. **Bot Settings** klicken
|
|
||||||
5. **Group Privacy** klicken
|
|
||||||
6. **Turn off** klicken
|
|
||||||
|
|
||||||
**BotFather bestätigt:**
|
|
||||||
```
|
|
||||||
Privacy mode is disabled for <bot-name>.
|
|
||||||
All messages will now be sent to the bot.
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.4 Bot als Admin hinzufügen (optional, aber empfohlen)
|
|
||||||
|
|
||||||
1. Gruppe öffnen → Gruppennamen klicken
|
|
||||||
2. **Administratoren** → **Administrator hinzufügen**
|
|
||||||
3. `@werkstatt_uploader_bot` auswählen
|
|
||||||
4. **Berechtigungen:**
|
|
||||||
- ✅ Nachrichten senden
|
|
||||||
- ❌ Alle anderen optional (nicht nötig)
|
|
||||||
5. **Speichern**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Chat-ID ermitteln
|
|
||||||
|
|
||||||
Die Chat-ID wird benötigt, um Nachrichten an die richtige Gruppe zu senden.
|
|
||||||
|
|
||||||
### Methode 1: Via Telegram API (empfohlen)
|
|
||||||
|
|
||||||
**Schritt 1:** Nachricht in Test-Gruppe senden
|
|
||||||
|
|
||||||
- Öffne die Test-Gruppe
|
|
||||||
- Sende eine beliebige Nachricht (z.B. "Test")
|
|
||||||
|
|
||||||
**Schritt 2:** Browser öffnen und folgende URL aufrufen:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://api.telegram.org/bot<DEIN_BOT_TOKEN>/getUpdates
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ersetze `<DEIN_BOT_TOKEN>`** mit deinem echten Token!
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
https://api.telegram.org/bot123456789:ABCdefGHIjklMNOpqrsTUVwxyz/getUpdates
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ **Wenn du `{"ok":true,"result":[]}` siehst (leeres result-Array):**
|
|
||||||
|
|
||||||
Das bedeutet, der Bot hat noch keine Nachrichten empfangen. **Checkliste:**
|
|
||||||
|
|
||||||
1. ✅ Hast du den Bot zur Gruppe hinzugefügt? (Schritt 2.2)
|
|
||||||
2. ✅ Hast du **NACH** dem Hinzufügen eine Nachricht gesendet? (Schritt 1)
|
|
||||||
3. ✅ War die Nachricht in der **richtigen Gruppe** (nicht im Bot-Direct-Chat)?
|
|
||||||
|
|
||||||
**Lösung - Mach jetzt folgendes:**
|
|
||||||
- Telegram öffnen
|
|
||||||
- **Test-Gruppe** öffnen (nicht den Bot direkt!)
|
|
||||||
- Prüfe, ob der Bot als Mitglied angezeigt wird
|
|
||||||
- Sende eine neue Nachricht: "Test"
|
|
||||||
- **Sofort** zurück zum Browser → Seite neu laden (F5)
|
|
||||||
|
|
||||||
**Schritt 3:** Im JSON-Response nach `chat` suchen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ok": true,
|
|
||||||
"result": [
|
|
||||||
{
|
|
||||||
"update_id": 123456789,
|
|
||||||
"message": {
|
|
||||||
"message_id": 1,
|
|
||||||
"from": { ... },
|
|
||||||
"chat": {
|
|
||||||
"id": -1001234567890, // ← DAS IST DEINE CHAT-ID!
|
|
||||||
"title": "Werkstatt Upload Bot Test",
|
|
||||||
"type": "supergroup"
|
|
||||||
},
|
|
||||||
"text": "Test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Chat-ID kopieren** (z.B. `-1001234567890`)
|
|
||||||
|
|
||||||
⚠️ **Wichtig:** Gruppen-Chat-IDs sind **negativ** und beginnen meist mit `-100`!
|
|
||||||
|
|
||||||
### Methode 2: Via curl (Linux/WSL)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl "https://api.telegram.org/bot<DEIN_BOT_TOKEN>/getUpdates" | jq '.result[0].message.chat.id'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Environment-Datei erstellen
|
|
||||||
|
|
||||||
### 4.1 Template kopieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd scripts
|
|
||||||
cp .env.telegram.example .env.telegram
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 `.env.telegram` bearbeiten
|
|
||||||
|
|
||||||
**Datei öffnen:** `scripts/.env.telegram`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Telegram Bot Configuration
|
|
||||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
||||||
TELEGRAM_CHAT_ID=-1001234567890
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deine echten Werte eintragen:**
|
|
||||||
- `TELEGRAM_BOT_TOKEN` → Token von BotFather (Schritt 1.3)
|
|
||||||
- `TELEGRAM_CHAT_ID` → Chat-ID aus Schritt 3 (negativ!)
|
|
||||||
|
|
||||||
**Speichern** und schließen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Dependencies installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd scripts
|
|
||||||
|
|
||||||
# package.json initialisieren (falls nicht vorhanden)
|
|
||||||
npm init -y
|
|
||||||
|
|
||||||
# Telegram Bot API installieren
|
|
||||||
npm install node-telegram-bot-api dotenv
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erwartete Ausgabe:**
|
|
||||||
```
|
|
||||||
added 15 packages, and audited 16 packages in 2s
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Test-Script ausführen
|
|
||||||
|
|
||||||
### 6.1 Script starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node telegram-test.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Erwartete Ausgabe (Erfolg)
|
|
||||||
|
|
||||||
```
|
|
||||||
🔧 Lade Telegram-Konfiguration...
|
|
||||||
✅ Konfiguration geladen!
|
|
||||||
|
|
||||||
🤖 Verbinde mit Telegram Bot...
|
|
||||||
✅ Telegram Bot erfolgreich verbunden!
|
|
||||||
|
|
||||||
Bot-Details:
|
|
||||||
Name: Werkstatt Image Uploader Bot
|
|
||||||
Username: @werkstatt_uploader_bot
|
|
||||||
ID: 1234567890
|
|
||||||
|
|
||||||
📤 Sende Test-Nachricht an Chat -1001234567890...
|
|
||||||
✅ Nachricht erfolgreich gesendet!
|
|
||||||
|
|
||||||
Message-ID: 42
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Telegram-Gruppe prüfen
|
|
||||||
|
|
||||||
**In der Test-Gruppe sollte jetzt erscheinen:**
|
|
||||||
|
|
||||||
```
|
|
||||||
🤖 Telegram Bot Test
|
|
||||||
|
|
||||||
Dies ist eine Test-Nachricht vom Werkstatt Image Uploader Bot.
|
|
||||||
|
|
||||||
Status: ✅ Erfolgreich verbunden!
|
|
||||||
Zeitstempel: 2025-11-29 14:23:45
|
|
||||||
|
|
||||||
---
|
|
||||||
ℹ️ Dieser Bot sendet automatische Benachrichtigungen für den Image Uploader.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Troubleshooting
|
|
||||||
|
|
||||||
### ❌ Fehler: "Error: Unauthorized (401)"
|
|
||||||
|
|
||||||
**Ursache:** Bot-Token ist falsch oder ungültig
|
|
||||||
|
|
||||||
**Lösung:**
|
|
||||||
1. BotFather öffnen
|
|
||||||
2. `/token` eingeben
|
|
||||||
3. Deinen Bot auswählen
|
|
||||||
4. Neuen Token kopieren
|
|
||||||
5. `.env.telegram` aktualisieren
|
|
||||||
6. Script erneut starten
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❌ Fehler: "Bad Request: chat not found"
|
|
||||||
|
|
||||||
**Ursache:** Chat-ID ist falsch
|
|
||||||
|
|
||||||
**Lösung:**
|
|
||||||
1. Test-Gruppe öffnen
|
|
||||||
2. Neue Nachricht senden
|
|
||||||
3. Chat-ID erneut ermitteln (Schritt 3)
|
|
||||||
4. `.env.telegram` aktualisieren
|
|
||||||
5. Script erneut starten
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❌ Fehler: "Error: ETELEGRAM: 403 Forbidden"
|
|
||||||
|
|
||||||
**Ursache:** Bot wurde aus der Gruppe entfernt oder kann nicht posten
|
|
||||||
|
|
||||||
**Lösung:**
|
|
||||||
1. Test-Gruppe öffnen
|
|
||||||
2. Prüfen, ob Bot noch Mitglied ist
|
|
||||||
3. Falls nicht: Bot erneut hinzufügen (Schritt 2.2)
|
|
||||||
4. Falls ja: Bot als Admin hinzufügen (Schritt 2.3)
|
|
||||||
5. Script erneut starten
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❌ Fehler: "Cannot find module 'node-telegram-bot-api'"
|
|
||||||
|
|
||||||
**Ursache:** Dependencies nicht installiert
|
|
||||||
|
|
||||||
**Lösung:**
|
|
||||||
```bash
|
|
||||||
cd scripts
|
|
||||||
npm install
|
|
||||||
node telegram-test.js
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❌ Fehler: "TELEGRAM_BOT_TOKEN is not defined"
|
|
||||||
|
|
||||||
**Ursache:** `.env.telegram` fehlt oder nicht korrekt
|
|
||||||
|
|
||||||
**Lösung:**
|
|
||||||
1. Prüfen, ob `.env.telegram` existiert: `ls -la scripts/.env.telegram`
|
|
||||||
2. Falls nicht: Template kopieren (Schritt 4.1)
|
|
||||||
3. Werte eintragen (Schritt 4.2)
|
|
||||||
4. Script erneut starten
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Erweiterte Tests
|
|
||||||
|
|
||||||
### Test 1: Formatierung mit Emojis
|
|
||||||
|
|
||||||
**Script anpassen:** `telegram-test.js`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const message = `
|
|
||||||
📸 Neuer Upload!
|
|
||||||
|
|
||||||
Uploader: Max Mustermann
|
|
||||||
Bilder: 12
|
|
||||||
Gruppe: 2024 - Schweißkurs November
|
|
||||||
|
|
||||||
Workshop: ✅ Ja
|
|
||||||
Social Media: 📘 Instagram, 🎵 TikTok
|
|
||||||
|
|
||||||
🔗 Zur Freigabe: https://internal.hobbyhimmel.de/moderation
|
|
||||||
`;
|
|
||||||
|
|
||||||
bot.sendMessage(chatId, message);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausführen:**
|
|
||||||
```bash
|
|
||||||
node telegram-test.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Telegram prüfen:** Emojis sollten korrekt angezeigt werden
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Test 2: HTML-Formatierung
|
|
||||||
|
|
||||||
**Script anpassen:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const message = `
|
|
||||||
<b>🤖 Telegram Bot Test</b>
|
|
||||||
|
|
||||||
<i>HTML-Formatierung funktioniert!</i>
|
|
||||||
|
|
||||||
<code>Status: ✅ Erfolgreich</code>
|
|
||||||
|
|
||||||
<a href="https://hobbyhimmel.de">Link zur Website</a>
|
|
||||||
`;
|
|
||||||
|
|
||||||
bot.sendMessage(chatId, message, { parse_mode: 'HTML' });
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausführen & prüfen:** Fetter Text, kursiver Text, Code, Link
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Test 3: Markdown-Formatierung
|
|
||||||
|
|
||||||
**Script anpassen:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const message = `
|
|
||||||
*🤖 Telegram Bot Test*
|
|
||||||
|
|
||||||
_Markdown-Formatierung funktioniert!_
|
|
||||||
|
|
||||||
\`Status: ✅ Erfolgreich\`
|
|
||||||
|
|
||||||
[Link zur Website](https://hobbyhimmel.de)
|
|
||||||
`;
|
|
||||||
|
|
||||||
bot.sendMessage(chatId, message, { parse_mode: 'Markdown' });
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Sicherheit
|
|
||||||
|
|
||||||
### ⚠️ Wichtig!
|
|
||||||
|
|
||||||
- ❌ **NIEMALS** `.env.telegram` committen!
|
|
||||||
- ❌ **NIEMALS** Bot-Token öffentlich teilen!
|
|
||||||
- ✅ `.env.telegram` ist in `.gitignore` eingetragen
|
|
||||||
- ✅ Nur `.env.telegram.example` (ohne echte Tokens) committen
|
|
||||||
|
|
||||||
### Bot-Token kompromittiert?
|
|
||||||
|
|
||||||
**Falls Token versehentlich exposed:**
|
|
||||||
|
|
||||||
1. BotFather öffnen
|
|
||||||
2. `/revoke` eingeben
|
|
||||||
3. Deinen Bot auswählen
|
|
||||||
4. **Neuen Token** kopieren
|
|
||||||
5. `.env.telegram` aktualisieren
|
|
||||||
6. Alle Services neu starten
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Nächste Schritte
|
|
||||||
|
|
||||||
### ✅ Phase 1 abgeschlossen?
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
- [x] Bot erstellt
|
|
||||||
- [x] Test-Gruppe erstellt
|
|
||||||
- [x] Bot zur Gruppe hinzugefügt
|
|
||||||
- [x] Chat-ID ermittelt
|
|
||||||
- [x] `.env.telegram` konfiguriert
|
|
||||||
- [x] `npm install` erfolgreich
|
|
||||||
- [x] `node telegram-test.js` läuft ohne Fehler
|
|
||||||
- [x] Test-Nachricht in Telegram empfangen
|
|
||||||
|
|
||||||
### ➡️ Weiter zu Phase 2
|
|
||||||
|
|
||||||
**Backend-Integration:**
|
|
||||||
1. `TelegramNotificationService.js` erstellen
|
|
||||||
2. Service in Docker Dev Environment integrieren
|
|
||||||
3. ENV-Variablen in Backend übertragen
|
|
||||||
4. Unit-Tests schreiben
|
|
||||||
|
|
||||||
**Siehe:** `FeatureRequests/FEATURE_PLAN-telegram.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Referenzen
|
|
||||||
|
|
||||||
- [Telegram Bot API Dokumentation](https://core.telegram.org/bots/api)
|
|
||||||
- [node-telegram-bot-api (npm)](https://www.npmjs.com/package/node-telegram-bot-api)
|
|
||||||
- [BotFather Commands](https://core.telegram.org/bots#botfather)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
**Bei Problemen:**
|
|
||||||
1. Troubleshooting-Sektion durchlesen (Schritt 7)
|
|
||||||
2. Telegram Bot API Logs prüfen
|
|
||||||
3. BotFather `/mybots` → Bot auswählen → API Token prüfen
|
|
||||||
4. Chat-ID erneut ermitteln
|
|
||||||
|
|
||||||
**Erfolgreicher Test? 🎉**
|
|
||||||
```bash
|
|
||||||
git add scripts/
|
|
||||||
git commit -m "feat: Add Telegram Bot standalone test (Phase 1)"
|
|
||||||
```
|
|
||||||
|
|
@ -39,8 +39,6 @@ changed = False
|
||||||
|
|
||||||
cookie_pattern = re.compile(r'(\-\s*ADMIN_SESSION_COOKIE_SECURE\s*=\s*)([^\n\r]+)')
|
cookie_pattern = re.compile(r'(\-\s*ADMIN_SESSION_COOKIE_SECURE\s*=\s*)([^\n\r]+)')
|
||||||
secret_pattern = re.compile(r'(\-\s*ADMIN_SESSION_SECRET\s*=\s*)([^\n\r]+)')
|
secret_pattern = re.compile(r'(\-\s*ADMIN_SESSION_SECRET\s*=\s*)([^\n\r]+)')
|
||||||
telegram_token_pattern = re.compile(r'(\-\s*TELEGRAM_BOT_TOKEN\s*=\s*)([^\n\r${}]+)')
|
|
||||||
telegram_chat_pattern = re.compile(r'(\-\s*TELEGRAM_CHAT_ID\s*=\s*)(-?\d{10,})')
|
|
||||||
|
|
||||||
def ensure_entry(text, *, pattern, value, anchor_line, expected_line, label):
|
def ensure_entry(text, *, pattern, value, anchor_line, expected_line, label):
|
||||||
match = pattern.search(text)
|
match = pattern.search(text)
|
||||||
|
|
@ -82,18 +80,6 @@ if secret_expected not in new_text:
|
||||||
print('ERROR: Failed to ensure ADMIN_SESSION_SECRET uses environment variable in docker-compose.yml', file=sys.stderr)
|
print('ERROR: Failed to ensure ADMIN_SESSION_SECRET uses environment variable in docker-compose.yml', file=sys.stderr)
|
||||||
sys.exit(4)
|
sys.exit(4)
|
||||||
|
|
||||||
telegram_token_match = telegram_token_pattern.search(new_text)
|
|
||||||
if telegram_token_match and telegram_token_match.group(2).strip() not in ['${TELEGRAM_BOT_TOKEN}', '']:
|
|
||||||
print(f'ERROR: TELEGRAM_BOT_TOKEN contains hardcoded secret: {telegram_token_match.group(2)[:20]}...', file=sys.stderr)
|
|
||||||
print(' Use ${TELEGRAM_BOT_TOKEN} placeholder instead!', file=sys.stderr)
|
|
||||||
sys.exit(5)
|
|
||||||
|
|
||||||
telegram_chat_match = telegram_chat_pattern.search(new_text)
|
|
||||||
if telegram_chat_match:
|
|
||||||
print(f'ERROR: TELEGRAM_CHAT_ID contains hardcoded value: {telegram_chat_match.group(2)}', file=sys.stderr)
|
|
||||||
print(' Use ${TELEGRAM_CHAT_ID} placeholder instead!', file=sys.stderr)
|
|
||||||
sys.exit(6)
|
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
path.write_text(new_text)
|
path.write_text(new_text)
|
||||||
print('UPDATED')
|
print('UPDATED')
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"name": "telegram-test-scripts",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Standalone Telegram Bot Testing Scripts for Image Uploader",
|
|
||||||
"main": "telegram-test.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "node telegram-test.js"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"telegram",
|
|
||||||
"bot",
|
|
||||||
"testing"
|
|
||||||
],
|
|
||||||
"author": "Werkstatt Hobbyhimmel",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"node-telegram-bot-api": "^0.66.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Telegram Bot Standalone Test Script
|
|
||||||
*
|
|
||||||
* Testet die grundlegende Funktionalität des Telegram Bots
|
|
||||||
* ohne Integration in das Backend.
|
|
||||||
*
|
|
||||||
* Phase 1: Bot Setup & Standalone-Test
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node telegram-test.js
|
|
||||||
*
|
|
||||||
* Prerequisites:
|
|
||||||
* - .env.telegram mit TELEGRAM_BOT_TOKEN und TELEGRAM_CHAT_ID
|
|
||||||
* - npm install node-telegram-bot-api dotenv
|
|
||||||
*/
|
|
||||||
|
|
||||||
require('dotenv').config({ path: '.env.telegram' });
|
|
||||||
const TelegramBot = require('node-telegram-bot-api');
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Configuration
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
||||||
const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Validation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
console.log('🔧 Lade Telegram-Konfiguration...');
|
|
||||||
|
|
||||||
if (!BOT_TOKEN) {
|
|
||||||
console.error('❌ FEHLER: TELEGRAM_BOT_TOKEN ist nicht definiert!');
|
|
||||||
console.error('');
|
|
||||||
console.error('Bitte .env.telegram erstellen und Bot-Token eintragen:');
|
|
||||||
console.error('');
|
|
||||||
console.error(' TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz');
|
|
||||||
console.error(' TELEGRAM_CHAT_ID=-1001234567890');
|
|
||||||
console.error('');
|
|
||||||
console.error('Siehe: scripts/README.telegram.md');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CHAT_ID) {
|
|
||||||
console.error('❌ FEHLER: TELEGRAM_CHAT_ID ist nicht definiert!');
|
|
||||||
console.error('');
|
|
||||||
console.error('Bitte .env.telegram erstellen und Chat-ID eintragen:');
|
|
||||||
console.error('');
|
|
||||||
console.error(' TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz');
|
|
||||||
console.error(' TELEGRAM_CHAT_ID=-1001234567890');
|
|
||||||
console.error('');
|
|
||||||
console.error('Chat-ID ermitteln: https://api.telegram.org/bot<TOKEN>/getUpdates');
|
|
||||||
console.error('Siehe: scripts/README.telegram.md');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Konfiguration geladen!');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Bot Initialization
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
console.log('🤖 Verbinde mit Telegram Bot...');
|
|
||||||
|
|
||||||
// Create bot instance (polling disabled for one-off script)
|
|
||||||
const bot = new TelegramBot(BOT_TOKEN, { polling: false });
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Main Test Function
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
try {
|
|
||||||
// Step 1: Verify bot connection
|
|
||||||
const botInfo = await bot.getMe();
|
|
||||||
|
|
||||||
console.log('✅ Telegram Bot erfolgreich verbunden!');
|
|
||||||
console.log('');
|
|
||||||
console.log('Bot-Details:');
|
|
||||||
console.log(` Name: ${botInfo.first_name}`);
|
|
||||||
console.log(` Username: @${botInfo.username}`);
|
|
||||||
console.log(` ID: ${botInfo.id}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Step 2: Prepare test message
|
|
||||||
const timestamp = new Date().toLocaleString('de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = `
|
|
||||||
🤖 Telegram Bot Test
|
|
||||||
|
|
||||||
Dies ist eine Test-Nachricht vom Werkstatt Image Uploader Bot.
|
|
||||||
|
|
||||||
Status: ✅ Erfolgreich verbunden!
|
|
||||||
Zeitstempel: ${timestamp}
|
|
||||||
|
|
||||||
---
|
|
||||||
ℹ️ Dieser Bot sendet automatische Benachrichtigungen für den Image Uploader.
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
// Step 3: Send test message
|
|
||||||
console.log(`📤 Sende Test-Nachricht an Chat ${CHAT_ID}...`);
|
|
||||||
|
|
||||||
const sentMessage = await bot.sendMessage(CHAT_ID, message);
|
|
||||||
|
|
||||||
console.log('✅ Nachricht erfolgreich gesendet!');
|
|
||||||
console.log('');
|
|
||||||
console.log(`Message-ID: ${sentMessage.message_id}`);
|
|
||||||
console.log('');
|
|
||||||
console.log('🎉 Test erfolgreich abgeschlossen!');
|
|
||||||
console.log('');
|
|
||||||
console.log('➡️ Nächste Schritte:');
|
|
||||||
console.log(' 1. Telegram-Gruppe öffnen und Nachricht prüfen');
|
|
||||||
console.log(' 2. Verschiedene Nachrichtenformate testen (siehe README.telegram.md)');
|
|
||||||
console.log(' 3. Phase 2 starten: Backend-Integration');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ FEHLER beim Senden der Nachricht:');
|
|
||||||
console.error('');
|
|
||||||
|
|
||||||
if (error.response && error.response.body) {
|
|
||||||
const errorData = error.response.body;
|
|
||||||
console.error(`Error Code: ${errorData.error_code}`);
|
|
||||||
console.error(`Description: ${errorData.description}`);
|
|
||||||
console.error('');
|
|
||||||
|
|
||||||
// Helpful error messages
|
|
||||||
if (errorData.error_code === 401) {
|
|
||||||
console.error('💡 Lösung: Bot-Token ist ungültig oder falsch.');
|
|
||||||
console.error(' → BotFather öffnen und Token prüfen');
|
|
||||||
console.error(' → .env.telegram aktualisieren');
|
|
||||||
} else if (errorData.error_code === 400 && errorData.description.includes('chat not found')) {
|
|
||||||
console.error('💡 Lösung: Chat-ID ist falsch oder ungültig.');
|
|
||||||
console.error(' → Chat-ID erneut ermitteln: https://api.telegram.org/bot<TOKEN>/getUpdates');
|
|
||||||
console.error(' → Neue Nachricht in Gruppe senden, dann getUpdates aufrufen');
|
|
||||||
console.error(' → .env.telegram aktualisieren');
|
|
||||||
} else if (errorData.error_code === 403) {
|
|
||||||
console.error('💡 Lösung: Bot hat keine Berechtigung, in diese Gruppe zu schreiben.');
|
|
||||||
console.error(' → Bot zur Gruppe hinzufügen');
|
|
||||||
console.error(' → Bot als Admin hinzufügen (empfohlen)');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('');
|
|
||||||
console.error('📖 Siehe Troubleshooting: scripts/README.telegram.md');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Execute Test
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
runTest();
|
|
||||||
Loading…
Reference in New Issue
Block a user