Compare commits

..

No commits in common. "main" and "v1.10.2" have entirely different histories.

45 changed files with 2078 additions and 5107 deletions

5
.gitignore vendored
View File

@ -9,11 +9,6 @@ node_modules/
.env
.env.local
# Telegram credentials
scripts/.env.telegram
scripts/node_modules/
scripts/package-lock.json
# IDE
.vscode/
.idea/

View File

@ -1,22 +1,5 @@
# 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
### ✨ Features
@ -47,7 +30,7 @@
- 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)
@ -146,7 +129,7 @@
---
## feature/security
## [Unreleased] - Branch: feature/security
### 🔐 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)
@ -417,7 +400,7 @@
---
## Preload Image
## [Unreleased] - Branch: feature/PreloadImage
### 🚀 Slideshow Optimization (November 2025)
@ -454,7 +437,7 @@
---
## Delete Unproved Groups
## [Unreleased] - Branch: feature/DeleteUnprovedGroups
### ✨ Automatic Cleanup Feature (November 2025)
@ -521,7 +504,7 @@
---
## Image Description
## [Unreleased] - Branch: feature/ImageDescription
### ✨ 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)

View File

@ -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

View File

@ -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

View File

@ -302,35 +302,6 @@ describe('Example API', () => {
# 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
```bash

155
README.md
View File

@ -5,7 +5,6 @@ A self-hosted image uploader with multi-image upload capabilities and automatic
## Features
**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
**Automatic Cleanup**: 🆕 Unapproved groups are automatically deleted after 7 days
**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
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
@ -176,31 +262,31 @@ The application automatically generates optimized preview thumbnails for all upl
## 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/
├── .env.backend.example # Backend environment variables documentation
├── .env.frontend.example # Frontend environment variables documentation
├── dev/ # Development environment
│ ├── .env # 🆕 Central dev secrets (gitignored)
│ ├── .env.example # Dev environment template
│ ├── docker-compose.yml # All ENV vars defined here
│ ├── docker-compose.yml # Development services configuration
│ ├── backend/
│ │ ├── config/.env # Development backend configuration
│ │ └── Dockerfile # Development backend container
│ └── frontend/
│ ├── config/env.sh # Generates window._env_ from ENV
│ ├── config/.env # Development frontend configuration
│ ├── config/env.sh # Runtime configuration script
│ ├── Dockerfile # Development frontend container
│ ├── nginx.conf # Development nginx configuration
│ └── start.sh # Development startup script
└── prod/ # Production environment
├── .env # 🆕 Central prod secrets (gitignored)
├── .env.example # Production environment template
├── docker-compose.yml # All ENV vars defined here
├── docker-compose.yml # Production services configuration
├── backend/
│ ├── config/.env # Production backend configuration
│ └── Dockerfile # Production backend container
└── 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
├── Dockerfile # Production frontend container
└── nginx.conf # Production nginx configuration
@ -208,20 +294,6 @@ docker/
### 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
- **Production**: Uses `docker/prod/` configuration with optimized builds
- **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)
## Configuration
### 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 |
|----------|---------|-------------|
| `API_URL` | `http://localhost:5001` | Backend API endpoint (frontend → backend) |
| `PUBLIC_HOST` | `public.test.local` | Public upload subdomain (no admin access) |
| `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.
| `API_URL` | `http://localhost:5001` | Backend API endpoint |
| `CLIENT_URL` | `http://localhost` | Frontend application URL |
### Volume Configuration
- **Upload Limits**: 100MB maximum file size for batch uploads

View File

@ -101,7 +101,7 @@ Neue Struktur: Datenbank in src/data/db und bilder in src/data/images
[x] 🎨 Drag & Drop Reihenfolge ändern
[x] 📊 Upload-Progress mit Details
[x] 🖼️ Thumbnail-Navigation in Slideshow
[ ] 🔄 Batch-Operations (alle entfernen, etc.)
### Future Features
- 👤 User-Management

View File

@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "Project Image Uploader API",
"version": "2.0.1",
"version": "1.10.2",
"description": "Auto-generated OpenAPI spec with correct mount prefixes"
},
"servers": [
@ -39,9 +39,6 @@
{
"name": "Admin - Cleanup"
},
{
"name": "Admin - Telegram"
},
{
"name": "Admin - Monitoring"
},
@ -388,15 +385,6 @@
},
"description": {
"example": "any"
},
"year": {
"example": "any"
},
"title": {
"example": "any"
},
"name": {
"example": "any"
}
}
}
@ -1070,38 +1058,22 @@
},
"/api/manage/{token}/reorder": {
"put": {
"tags": [
"Management Portal"
],
"summary": "Reorder images in group",
"description": "Reorder images within the managed group (token-based access)",
"description": "",
"parameters": [
{
"name": "token",
"in": "path",
"required": true,
"type": "string",
"description": "Management token (UUID v4)",
"example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
"type": "string"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"imageIds": {
"type": "array",
"example": [
1,
3,
2,
4
],
"items": {
"type": "number"
}
"example": "any"
}
}
}
@ -1109,29 +1081,13 @@
],
"responses": {
"200": {
"description": "Images reordered successfully",
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"updatedCount": {
"type": "number",
"example": 4
}
},
"xml": {
"name": "main"
}
}
"description": "OK"
},
"400": {
"description": "Invalid token format or imageIds"
"description": "Bad Request"
},
"404": {
"description": "Token not found or group deleted"
"description": "Not Found"
},
"429": {
"description": "Too Many Requests"
@ -1195,46 +1151,25 @@
},
"/api/admin/groups/{groupId}/consents": {
"post": {
"tags": [
"Consent Management"
],
"summary": "Save or update consents for a group",
"description": "Store workshop consent and social media consents for a specific group",
"description": "",
"parameters": [
{
"name": "groupId",
"in": "path",
"required": true,
"type": "string",
"description": "Group ID",
"example": "abc123def456"
"type": "string"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"workshopConsent": {
"type": "boolean",
"example": true
"example": "any"
},
"socialMediaConsents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"platformId": {
"type": "number",
"example": 2
},
"consented": {
"type": "boolean",
"example": false
}
}
}
"example": "any"
}
}
}
@ -1242,26 +1177,10 @@
],
"responses": {
"200": {
"description": "Consents saved successfully",
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Consents saved successfully"
}
},
"xml": {
"name": "main"
}
}
"description": "OK"
},
"400": {
"description": "Invalid request data"
"description": "Bad Request"
},
"403": {
"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": {
"get": {
"tags": [

View File

@ -26,9 +26,5 @@ module.exports = {
// Run tests serially to avoid DB conflicts
maxWorkers: 1,
// Force exit after tests complete
forceExit: true,
// Transform ESM modules in node_modules
transformIgnorePatterns: [
'node_modules/(?!(uuid)/)'
]
forceExit: true
};

View File

@ -1,6 +1,6 @@
{
"name": "backend",
"version": "2.0.1",
"version": "1.10.2",
"description": "",
"main": "src/index.js",
"scripts": {
@ -31,7 +31,6 @@
"find-remove": "^2.0.3",
"fs": "^0.0.1-security",
"node-cron": "^4.2.1",
"node-telegram-bot-api": "^0.66.0",
"sharp": "^0.34.4",
"shortid": "^2.2.16",
"sqlite3": "^5.1.7",

View File

@ -16,7 +16,7 @@ const endpointsFiles = routerMappings.map(r => path.join(routesDir, r.file));
const doc = {
info: {
title: 'Project Image Uploader API',
version: '2.0.1',
version: '1.10.2',
description: 'Auto-generated OpenAPI spec with correct mount prefixes'
},
host: 'localhost:5001',

View File

@ -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) => {
/*
#swagger.tags = ['Admin - Cleanup']

View File

@ -6,10 +6,6 @@ const UploadGroup = require('../models/uploadGroup');
const groupRepository = require('../repositories/GroupRepository');
const dbManager = require('../database/DatabaseManager');
const ImagePreviewService = require('../services/ImagePreviewService');
const TelegramNotificationService = require('../services/TelegramNotificationService');
// Singleton-Instanz des Telegram Service
const telegramService = new TelegramNotificationService();
const router = Router();
@ -121,12 +117,6 @@ router.post('/upload/batch', async (req, res) => {
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)
if (!consents.workshopConsent) {
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`);
// 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
res.json({
groupId: group.groupId,

View File

@ -58,37 +58,16 @@ router.get('/social-media/platforms', async (req, res) => {
// 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) => {
/*
#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 {
const { groupId } = req.params;
const { workshopConsent, socialMediaConsents } = req.body;

View File

@ -5,10 +5,6 @@ const deletionLogRepository = require('../repositories/DeletionLogRepository');
const dbManager = require('../database/DatabaseManager');
const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter');
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
router.use(rateLimitMiddleware);
@ -215,20 +211,6 @@ router.put('/:token/consents', async (req, res) => {
[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({
success: true,
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({
success: true,
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)`);
// 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({
success: true,
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) => {
/*
#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 {
const { token } = req.params;
const { imageIds } = req.body;

View File

@ -4,10 +4,6 @@ const path = require('path');
const initiateResources = require('./utils/initiate-resources');
const dbManager = require('./database/DatabaseManager');
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
let swaggerUi = null;
@ -82,19 +78,8 @@ class Server {
console.log(`✅ Server läuft auf Port ${this._port}`);
console.log(`📊 SQLite Datenbank aktiv`);
// Speichere SchedulerService in app für Admin-Endpoints
this._app.set('schedulerService', SchedulerService);
// Starte Scheduler für automatisches Cleanup
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) {
console.error('💥 Fehler beim Serverstart:', error);

View File

@ -1,11 +1,9 @@
const cron = require('node-cron');
const GroupCleanupService = require('./GroupCleanupService');
const TelegramNotificationService = require('./TelegramNotificationService');
class SchedulerService {
constructor() {
this.tasks = [];
this.telegramService = new TelegramNotificationService();
}
start() {
@ -32,35 +30,7 @@ class SchedulerService {
this.tasks.push(cleanupTask);
// Telegram Warning-Job: Jeden Tag um 09:00 Uhr (1 Stunde vor Cleanup)
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)');
console.log('✓ Scheduler started - Daily cleanup at 10:00 AM (Europe/Berlin)');
// Für Development: Manueller Trigger
if (process.env.NODE_ENV === 'development') {
@ -80,42 +50,6 @@ class SchedulerService {
console.log('[Scheduler] Manual cleanup triggered...');
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();

View File

@ -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;

View File

@ -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)
});
});
});
});

View File

@ -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

View File

@ -10,22 +10,6 @@ NODE_ENV=development
# Port for the backend server
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)
# DB_HOST=localhost
# DB_PORT=3306

View File

@ -6,4 +6,7 @@
# Production: http://backend:5000 (container-to-container)
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

View File

@ -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

View File

@ -12,8 +12,8 @@ RUN npm install
# Copy backend source code
COPY backend/ .
# Note: Environment variables are set via docker-compose.yml
# No .env file needed in the image
# Copy development environment configuration
COPY docker/dev/backend/config/.env ./.env
# Expose port
EXPOSE 5000

View File

@ -15,6 +15,7 @@ services:
volumes:
- ../../frontend:/app:cached
- dev_frontend_node_modules:/app/node_modules
- ./frontend/config/.env:/app/.env:ro
environment:
- CHOKIDAR_USEPOLLING=true
- API_URL=http://localhost:5001
@ -37,20 +38,14 @@ services:
volumes:
- ../../backend:/usr/src/app:cached
- dev_backend_node_modules:/usr/src/app/node_modules
- ./backend/config/.env:/usr/src/app/.env:ro
environment:
- NODE_ENV=development
- PORT=5000
- REMOVE_IMAGES=false
- ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET:-dev-session-secret-change-me}
- PUBLIC_HOST=public.test.local
- INTERNAL_HOST=internal.test.local
- ENABLE_HOST_RESTRICTION=true
- TRUST_PROXY_HOPS=0
- 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:
- dev-internal
command: [ "npm", "run", "server" ]

View File

@ -13,9 +13,9 @@ WORKDIR /app
# Copy package files first to leverage Docker cache for npm install
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
# Note: ENV variables are set via docker-compose.yml, not from .env file
COPY docker/dev/frontend/config/.env ./.env
# Make env.sh executable
RUN chmod +x ./env.sh

View File

@ -7,18 +7,23 @@ touch ./env-config.js
# Add assignment
echo "window._env_ = {" >> ./env-config.js
# List of environment variables to export (add more as needed)
ENV_VARS="API_URL PUBLIC_HOST INTERNAL_HOST"
# Read each environment variable and add to config
for varname in $ENV_VARS; do
# Get value from environment using indirect expansion
value="${!varname}"
# Only add if value exists
if [ -n "$value" ]; then
echo " $varname: \"$value\"," >> ./env-config.js
# Read each line in .env file
# Each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
# Split env variables by character `=`
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
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

View File

@ -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

View File

@ -15,8 +15,8 @@ services:
- backend
environment:
- API_URL=http://backend:5000
- PUBLIC_HOST=public.test.local
- INTERNAL_HOST=internal.test.local
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
networks:
- npm-nw
@ -42,18 +42,13 @@ services:
# ⚠️ Für HTTP-only Labs per Override auf "false" setzen (nicht im Repo committen)
- ADMIN_SESSION_COOKIE_SECURE=true
# Host Configuration (Public/Internal Separation)
- PUBLIC_HOST=public.test.local
- INTERNAL_HOST=internal.test.local
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
- ENABLE_HOST_RESTRICTION=true
- PUBLIC_UPLOAD_RATE_LIMIT=20
- PUBLIC_UPLOAD_RATE_WINDOW=3600000
# Trust nginx-proxy-manager (1 hop)
- 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

View File

@ -20,10 +20,10 @@ COPY --from=build /app/build /usr/share/nginx/html
# Default port exposure
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
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
RUN apk add --no-cache bash

View File

@ -7,18 +7,23 @@ touch ./env-config.js
# Add assignment
echo "window._env_ = {" >> ./env-config.js
# List of environment variables to export (add more as needed)
ENV_VARS="API_URL PUBLIC_HOST INTERNAL_HOST"
# Read each environment variable and add to config
for varname in $ENV_VARS; do
# Get value from environment using indirect expansion
value="${!varname}"
# Only add if value exists
if [ -n "$value" ]; then
echo " $varname: \"$value\"," >> ./env-config.js
# Read each line in .env file
# Each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
# Split env variables by character `=`
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
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

View File

@ -1,12 +1,12 @@
{
"name": "frontend",
"version": "2.0.1",
"version": "1.10.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "2.0.1",
"version": "1.10.2",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "2.0.1",
"version": "1.10.2",
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@ -171,7 +171,7 @@ function MultiUploadPage() {
<p className="page-subtitle">
Lade ein oder mehrere Bilder von deinem Projekt hoch und beschreibe dein Projekt in wenigen Worten.
<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>
{!uploading ? (
@ -318,10 +318,10 @@ function MultiUploadPage() {
</div>
<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 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>
</div>
)}

4067
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "project-image-uploader",
"version": "2.0.0",
"version": "1.2.0",
"private": true,
"scripts": {
"release": "./scripts/release.sh patch",

32
prod.sh
View File

@ -52,36 +52,8 @@ case $choice in
;;
3)
echo -e "${GREEN}Pushe Production Images zur Registry...${NC}"
# Hole aktuelle Version aus package.json
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}"
docker compose -f docker/prod/docker-compose.yml push
echo -e "${GREEN}Production Images erfolgreich gepusht!${NC}"
;;
4)
echo -e "${GREEN}Baue Container neu...${NC}"

View File

@ -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

View File

@ -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)"
```

View File

@ -39,8 +39,6 @@ changed = False
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]+)')
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):
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)
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:
path.write_text(new_text)
print('UPDATED')

View File

@ -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"
}
}

View File

@ -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();