diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 8996a22..181a31a 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -23,7 +23,32 @@
"Bash(timeout /t 5 /nobreak)",
"Bash(start chrome:*)",
"WebSearch",
- "Bash(wc:*)"
+ "Bash(wc:*)",
+ "Bash(sqlite3:*)",
+ "Bash(ls:*)",
+ "Bash(docker restart:*)",
+ "Bash(grep:*)",
+ "Bash(docker build:*)",
+ "Bash(docker stop:*)",
+ "Bash(docker rm:*)",
+ "Bash(docker run:*)",
+ "Bash(find:*)",
+ "Bash(rg:*)",
+ "Bash(cp:*)",
+ "Bash(rm:*)",
+ "Bash(docker start:*)",
+ "Bash(apt list:*)",
+ "Bash(sudo apt:*)",
+ "Bash(sudo apt install:*)",
+ "Bash(echo:*)",
+ "Bash(docker inspect:*)",
+ "Bash(touch:*)",
+ "Bash(docker port:*)",
+ "Bash(docker-compose down:*)",
+ "Bash(__NEW_LINE__ cp /app/data/taskmate.db.backup-20260103-201322 /app/data/taskmate.db)",
+ "Bash(docker system prune:*)",
+ "Bash(docker cp:*)",
+ "Bash(mv:*)"
]
}
-}
+}
\ No newline at end of file
diff --git a/ANWENDUNGSBESCHREIBUNG.txt b/ANWENDUNGSBESCHREIBUNG.txt
index 050f041..6bcaa07 100644
--- a/ANWENDUNGSBESCHREIBUNG.txt
+++ b/ANWENDUNGSBESCHREIBUNG.txt
@@ -52,7 +52,7 @@ ADMINISTRATOREN
Standard-Admin-Zugangsdaten:
- Benutzername: admin
-- Passwort: Kx9#mP2$vL7@nQ4!wR
+- Passwort: [Vom Administrator gesetzt]
Nach der Anmeldung als regulärer Benutzer sehen Sie das Kanban-Board.
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 6d8c141..fe3e390 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,602 @@
TASKMATE - CHANGELOG
====================
+================================================================================
+03.01.2025 - GIT-INTEGRATION CODING-KACHELN
+================================================================================
+✅ Git-Repository-Erkennung für TaskMate-Kachel repariert
+✅ Docker-Pfad-Mapping: /home/claude-dev/TaskMate → /app/taskmate-source
+✅ Git-Status und Commit-Funktionen funktionieren wieder
+✅ Debug-Logging für Git-Operationen hinzugefügt
+
+Technische Details:
+- windowsToContainerPath() Funktion erweitert
+- Spezialfall für TaskMate-Repository implementiert
+- Container-Volume-Mapping berücksichtigt
+- Cache-Version auf 189 erhöht
+
+================================================================================
+03.01.2025 - SICHERHEITSVERBESSERUNGEN PHASE 1
+================================================================================
+
+SCHRITT 1: HARTCODIERTE CREDENTIALS ENTFERNT
+--------------------------------------------------------------------------------
+- Hartcodierte Credentials aus CLAUDE.md entfernt
+- Admin-Passwort aus ANWENDUNGSBESCHREIBUNG.txt entfernt
+- Gitea-Token nicht mehr im Klartext in Dokumentation
+- JWT_SECRET Mindestlänge von 32 Zeichen erzwungen
+- Fallback für unsicheres JWT_SECRET entfernt
+
+SCHRITT 2: TOKEN-ROTATION & REFRESH-TOKENS
+--------------------------------------------------------------------------------
+- Refresh-Token System implementiert (7 Tage Gültigkeit)
+- Access-Tokens haben nur noch 15 Minuten Gültigkeit
+- Neue Datenbank-Tabelle refresh_tokens
+- Automatische Bereinigung abgelaufener Tokens
+- Logout widerruft alle Refresh-Tokens des Benutzers
+
+TECHNISCHE ÄNDERUNGEN
+--------------------------------------------------------------------------------
+- dotenv Package zum Backend hinzugefügt
+- server.js lädt nun .env Datei beim Start
+- Dockerfile angepasst (npm install statt npm ci)
+- auth.js erweitert um Refresh-Token Funktionen
+- Frontend API-Client unterstützt Refresh-Tokens
+- Service Worker Version: 181 → 182
+
+SICHERHEITSVERBESSERUNGEN
+--------------------------------------------------------------------------------
+- Kürzere Token-Lebensdauer reduziert Angriffsfenster
+- Refresh-Tokens ermöglichen sichere lange Sessions
+- Token-Rotation bei jedem Refresh
+- IP und User-Agent werden geloggt
+
+AUTOMATISCHES TOKEN-REFRESH IMPLEMENTIERT
+--------------------------------------------------------------------------------
+- Proaktiver Token-Refresh nach 10 Minuten (bevor 15min Limit erreicht)
+- Automatischer Fallback-Refresh bei 401-Fehlern
+- Benutzer bleiben 7 Tage eingeloggt ohne Unterbrechung
+- Nahtlose Erneuerung im Hintergrund - keine Logout-Unterbrechungen
+
+API-ÄNDERUNGEN (Rückwärtskompatibel)
+--------------------------------------------------------------------------------
+- POST /api/auth/login gibt zusätzlich refreshToken zurück
+- POST /api/auth/refresh akzeptiert refreshToken im Body
+- Legacy-Support für alte Clients ohne Breaking Changes
+
+SCHRITT 3: XSS-SCHUTZ & EINGABEVALIDIERUNG VERSTÄRKT
+--------------------------------------------------------------------------------
+- Erweiterte Content Security Policy (CSP) implementiert
+- DOMPurify für doppelte Markdown-Bereinigung hinzugefügt
+- Strikte File-Upload Validierung gegen gefährliche Dateien
+- URL-Validierung gegen SSRF und JavaScript-Injection
+- Automatisches Input-Sanitizing für alle API-Requests
+- Zusätzliche Security Headers (HSTS, Referrer Policy, etc.)
+
+NEUE SICHERHEITS-FEATURES
+--------------------------------------------------------------------------------
+- Executable Dateien (.exe, .bat, etc.) werden komplett blockiert
+- Doppelte Dateiendungen (.txt.exe) werden abgelehnt
+- Lokale URLs (localhost, 192.168.x.x) sind nicht erlaubt (SSRF-Schutz)
+- Gefährliche Dateinamen mit Pfad-Traversal werden blockiert
+- MIME-Type Validierung gegen Spoofing-Angriffe
+
+SECURITY HEADERS
+--------------------------------------------------------------------------------
+- Content-Security-Policy mit strict-dynamic
+- HTTP Strict Transport Security (HSTS)
+- X-Content-Type-Options: nosniff
+- Referrer-Policy: strict-origin-when-cross-origin
+- Permissions-Policy: Kamera/Mikrofon deaktiviert
+
+FRONTEND-VERBESSERUNGEN
+--------------------------------------------------------------------------------
+- Automatisches Token-Management im API-Client
+- Retry-Logic für abgelaufene Tokens
+- Service Worker Version: 183 → 184
+
+PHASE 2: DATENBANK UND BACKUP-VERSCHLÜSSELUNG IMPLEMENTIERT
+--------------------------------------------------------------------------------
+- Vollständige Backup-Verschlüsselung mit AES-256-CBC implementiert
+- Neue encryption.js Bibliothek für sichere Verschlüsselung
+- Automatische verschlüsselte Backups (.enc Dateien)
+- 256-bit Verschlüsselungsschlüssel über Umgebungsvariablen
+- Kompatible Backups: sowohl verschlüsselt als auch unverschlüsselt
+- Sichere Wiederherstellung mit Entschlüsselung
+- PBKDF2 Key-Derivation für passwort-basierte Verschlüsselung
+
+NEUE VERSCHLÜSSELUNGS-FEATURES
+--------------------------------------------------------------------------------
+- Header-basiertes Dateiformat für Versionierung (TMENC001)
+- Salt und IV für jede Verschlüsselung einzigartig
+- Automatisches Fallback bei fehlgeschlagener Verschlüsselung
+- Admin-Endpunkte für manuelle Backup-Erstellung (/api/admin/backup)
+- Backup-Liste mit verschlüsselten Dateien anzeigen
+
+DOCKER UND INFRASTRUKTUR
+--------------------------------------------------------------------------------
+- Docker-Container mit Verschlüsselungsunterstützung neu gebaut
+- ENCRYPTION_KEY über docker-compose.yml Umgebungsvariablen
+- Korrekte Portmapping (3001 extern → 3000 intern)
+- Automatische Backup-Erstellung beim Server-Start getestet
+
+================================================================================
+03.01.2025 - CLAUDE.MD NEUSTRUKTURIERUNG & DATENSCHUTZ
+================================================================================
+
+DOKUMENTATION
+--------------------------------------------------------------------------------
+CLAUDE.md komplett neu strukturiert für bessere Entwickler-Erfahrung.
+
+WICHTIGER HINWEIS FÜR KI-ASSISTENTEN
+--------------------------------------------------------------------------------
+- Prominenter Hinweis: Anwender hat KEINE Programmierkenntnisse
+- Klare Anweisung: KI übernimmt ALLE technischen Aufgaben
+- Kommunikations-Regeln mit Richtig/Falsch Beispielen
+- Arbeitsweise-Sektion für nicht-technische Anwender
+
+NEUE STRUKTUR
+--------------------------------------------------------------------------------
+- Quick Start Sektion mit wichtigsten Befehlen ganz oben
+- Kritische Regeln prominent am Anfang platziert
+- Klare Gliederung nach typischen Entwicklungsaufgaben
+- Erweiterte Troubleshooting-Sektion mit Lösungen
+- Code-Patterns und Best Practices hinzugefügt
+- Performance- und Sicherheitshinweise dokumentiert
+
+DATENSCHUTZ & PROJEKTSICHERHEIT
+--------------------------------------------------------------------------------
+- Neue Sektion für Schutz von Produktivdaten hinzugefügt
+- Warnung: Projekt "AegisSight" niemals beeinträchtigen
+- Warnung: Bestehende Benutzer niemals ändern/löschen
+- Backup-Anweisung vor Datenbank-Arbeiten
+- Rollback-Strategie für Live-System dokumentiert
+- Anforderung: JEDE Änderung muss umkehrbar sein
+- Docker-Image Backup-Befehle hinzugefügt
+- Änderungs-Workflow für Live-Betrieb definiert
+
+HIGHLIGHTS
+--------------------------------------------------------------------------------
+- Docker-Befehle direkt im Quick Start
+- Echtzeit-Update Patterns mit Code-Beispielen
+- Datums-Formatierung mit richtig/falsch Beispielen
+- Deployment-Checkliste als Copy&Paste Template
+- Debug-Tipps für Frontend und Backend
+
+================================================================================
+03.01.2026 - LISTE: MEHRERE AVATARE FÜR MEHRFACHZUWEISUNG
+================================================================================
+
+FEATURE ENHANCEMENT
+--------------------------------------------------------------------------------
+Listen-Ansicht zeigt jetzt alle zugewiesenen Benutzer als separate Avatare an.
+
+NEUE FUNKTIONEN
+--------------------------------------------------------------------------------
+- Mehrere Avatar-Symbole nebeneinander bei Mehrfachzuweisung
+- Avatare werden aus task_assignees Tabelle und assignedTo kombiniert
+- Container für mehrere Avatare mit 2px Abstand
+- Hover-Effekt: Avatare vergrößern sich bei Mouse-Over
+- Alle Avatare sind klickbar für Bearbeitung
+
+TECHNISCHE DETAILS
+--------------------------------------------------------------------------------
+- JavaScript: Sammelt User-IDs aus task.assignees Array
+- CSS: .avatar-container für Flexbox-Layout mehrerer Avatare
+- Backend nutzt bereits vorhandene getFullTask() Funktion
+- Service Worker Cache-Version: 178 -> 179
+
+VERHALTEN
+--------------------------------------------------------------------------------
+- Task mit User 1 + 4: Zeigt 2 Avatare nebeneinander
+- Task mit nur User 1: Zeigt 1 Avatar
+- Task ohne Zuweisung: Zeigt "?" Placeholder
+- Klick auf beliebigen Avatar: Öffnet Bearbeitung-Dropdown
+
+================================================================================
+03.01.2026 - LISTE: NUR AVATAR-SYMBOLE BEI ZUGEWIESEN
+================================================================================
+
+UX-VERBESSERUNG
+--------------------------------------------------------------------------------
+In der Listen-Ansicht werden bei der Spalte "Zugewiesen" nur noch Symbole angezeigt.
+
+ÄNDERUNGEN
+--------------------------------------------------------------------------------
+- Benutzernamen werden nicht mehr neben Avataren angezeigt
+- Nur noch farbige Avatar-Symbole mit Initialen sichtbar
+- Tooltip zeigt Namen beim Hover über Avatar
+- Platzhalter "?" für nicht zugewiesene Aufgaben
+- Klick auf Avatar öffnet Dropdown zur Bearbeitung
+
+TECHNISCHE DETAILS
+--------------------------------------------------------------------------------
+- list.js: Dropdown standardmäßig versteckt (display: none)
+- CSS: Neue Klassen für avatar-empty und editing-Modus
+- JavaScript: Avatar-Click-Handler für Bearbeitung
+- Service Worker Cache-Version: 177 -> 178
+
+BEDIENUNG
+--------------------------------------------------------------------------------
+1. In Listen-Ansicht sind nur Avatar-Symbole sichtbar
+2. Hover zeigt Namen als Tooltip
+3. Klick auf Avatar öffnet Benutzer-Dropdown
+4. Auswahl ändert Zuweisung und versteckt Dropdown wieder
+
+================================================================================
+03.01.2026 - BACKUP MIT AEGISSIGHT-PROJEKT ERSTELLT
+================================================================================
+
+BACKUP-WIEDERHERSTELLUNG
+--------------------------------------------------------------------------------
+Erfolgreiches Backup mit allen wiederhergestellten AegisSight-Daten erstellt.
+
+BACKUP-DETAILS
+--------------------------------------------------------------------------------
+- Datei: backup_2026-01-03T00-38-47-492Z.db
+- Inhalt: AegisSight-Projekt mit 22 Aufgaben
+- Benutzer: 3 (HG, MH, admin)
+- Status: Vollständig und verifiziert
+
+TECHNICAL
+--------------------------------------------------------------------------------
+- Docker-Container mit korrekten Volume-Mounts neu gestartet
+- Datenbank-Paths korrekt gemappt: /home/claude-dev/TaskMate/data → /app/data
+- WAL-Dateien korrekt synchronisiert
+
+ERGEBNIS
+--------------------------------------------------------------------------------
+Alle AegisSight-Projektdaten sind wiederhergestellt und gesichert.
+
+================================================================================
+02.01.2026 - ADMIN: PASSWORT-BEARBEITUNG IMPLEMENTIERT
+================================================================================
+
+NEUE FUNKTION
+--------------------------------------------------------------------------------
+Admins können jetzt Benutzer-Passwörter im Admin-Bereich bearbeiten.
+
+FUNKTIONEN
+--------------------------------------------------------------------------------
+- Passwort-Bearbeitung: Klick auf Stift-Symbol aktiviert Bearbeitungsmodus
+- Passwort-Generator: Klick auf Refresh-Symbol generiert starkes Passwort
+- Beim Bearbeiten von Benutzern: Passwort optional ändern oder leer lassen
+- Automatische Validierung: Mindestens 8 Zeichen erforderlich
+
+TECHNISCHE DETAILS
+--------------------------------------------------------------------------------
+- HTML: Neue Button-Gruppe für Passwort-Eingabe hinzugefügt
+- CSS: Styling für password-input-group implementiert
+- JavaScript: togglePasswordEdit() und generatePassword() Methoden
+- Backend: Nutzt vorhandene PUT /api/admin/users/:id Route
+- Service Worker Cache-Version: 176 -> 177
+
+BEDIENUNG
+--------------------------------------------------------------------------------
+1. Benutzer im Admin-Bereich bearbeiten
+2. Stift-Symbol bei Passwort klicken → Eingabefeld wird bearbeitbar
+3. Neues Passwort eingeben ODER Generator-Button für zufälliges Passwort
+4. Formular speichern → Passwort wird sofort aktualisiert
+
+================================================================================
+02.01.2026 - DATENBANK WIEDERHERGESTELLT
+================================================================================
+
+KRITISCHER BUGFIX
+--------------------------------------------------------------------------------
+Datenbank-Verlust durch Container-Neustart behoben - alle Daten wiederhergestellt.
+
+PROBLEM
+--------------------------------------------------------------------------------
+- Beim Docker-Container Neustart wurde eine neue, leere Datenbank erstellt
+- Alle Benutzer, Aufgaben, Board-Einträge und Einstellungen waren verloren
+- Nur Standard-Benutzer (HG, MH) vorhanden
+
+LÖSUNG
+--------------------------------------------------------------------------------
+- Backup vom 02.01.2026 23:46 Uhr wiederhergestellt
+- Originale Benutzerdaten und Inhalte sind wieder verfügbar
+- Login mit ursprünglichen Benutzerkonten funktioniert wieder
+
+ERGEBNIS
+--------------------------------------------------------------------------------
+Alle Daten sind wieder da - Login mit ursprünglichen Credentials möglich.
+
+================================================================================
+02.01.2026 - BUGFIX: LOGIN-FEHLER BEHOBEN
+================================================================================
+
+BUGFIX
+--------------------------------------------------------------------------------
+Login-Problem behoben: NotificationManager-Fehler beim Login korrigiert.
+
+TECHNISCHE DETAILS
+--------------------------------------------------------------------------------
+- notifications.js: Sicherheitscheck für this.badge hinzugefügt
+- Verhindert "Cannot read properties of undefined (reading 'classList')" Fehler
+- Service Worker Cache-Version: 175 -> 176
+
+AUSWIRKUNG
+--------------------------------------------------------------------------------
+Login funktioniert wieder korrekt ohne JavaScript-Fehler.
+
+================================================================================
+02.01.2026 - CODING-TAB: GITEA INTEGRATION CACHE-FIX
+================================================================================
+
+BUGFIX
+--------------------------------------------------------------------------------
+Browser-Cache Problem behoben: Gitea Repository-Dropdown zeigt wieder Repos an.
+
+TECHNISCHE DETAILS
+--------------------------------------------------------------------------------
+- Service Worker Cache-Version: 170 -> 175 (aggressiver Cache-Bust)
+- Docker Container komplett neu gebaut und gestartet
+- getGiteaRepositories() API-Fix wird jetzt geladen
+
+ERGEBNIS
+--------------------------------------------------------------------------------
+Repository-Dropdown in Coding-Anwendungen funktioniert wieder korrekt.
+
+================================================================================
+02.01.2026 - CODING-TAB: CLAUDE.MD ALS POPUP MODAL
+================================================================================
+
+NEUE FUNKTION
+--------------------------------------------------------------------------------
+CLAUDE.md wird jetzt in einem separaten Vollbild-Modal angezeigt:
+
+VERBESSERUNGEN
+--------------------------------------------------------------------------------
+- Klickbarer Link statt kleine Box
+- Vollbild-Modal mit 70% Viewport-Höhe
+- Zeigt Dateigröße im Link (z.B. "7KB")
+- Bessere Lesbarkeit für längere Dokumentation
+- ESC-Taste zum Schließen
+- Service Worker Cache-Version: 168 -> 169
+
+BEDIENUNG
+--------------------------------------------------------------------------------
+1. Klick auf "CLAUDE.md anzeigen (XKB)" öffnet Modal
+2. ESC oder X schließt Modal
+3. Klick außerhalb schließt Modal
+
+================================================================================
+02.01.2026 - CODING-TAB: CLAUDE.MD ANZEIGE FINAL BEHOBEN
+================================================================================
+
+BUGFIX
+--------------------------------------------------------------------------------
+Problem mit unsichtbarer CLAUDE.md endgültig gelöst:
+
+BEHOBENE PROBLEME
+--------------------------------------------------------------------------------
+- Backend: Fallback-Pfad für TaskMate (/app/taskmate-source) implementiert
+- CSS: claude-content war standardmäßig versteckt (display: none)
+- HTML: Überflüssige Hinweistexte entfernt
+- Service Worker Cache-Version: 167 -> 168
+
+ERGEBNIS
+--------------------------------------------------------------------------------
+CLAUDE.md wird jetzt korrekt angezeigt mit "Test für TaskMate" am Ende.
+Nur-Lesen-Modus funktioniert wie gewünscht.
+
+================================================================================
+02.01.2026 - CODING-TAB: UX VERBESSERUNGEN
+================================================================================
+
+UX-VERBESSERUNGEN
+--------------------------------------------------------------------------------
+- Kacheln sind jetzt direkt klickbar (ohne Drei-Punkte-Menü)
+- Drei-Punkte-Menü entfernt - weniger Verwirrung
+- Cursor zeigt Klickbarkeit an
+- CLAUDE.md Badge korrigiert - zeigt jetzt wieder CLAUDE.md an
+- Service Worker Cache-Version: 164 -> 165
+
+================================================================================
+02.01.2026 - CODING-TAB: CLAUDE.MD NUR NOCH READONLY
+================================================================================
+
+ÄNDERUNG
+--------------------------------------------------------------------------------
+CLAUDE.md im Coding-Bereich ist jetzt nur noch lesbar (readonly).
+
+DETAILS
+--------------------------------------------------------------------------------
+- Bearbeiten-Tab entfernt - nur noch Ansicht verfügbar
+- CLAUDE.md kann nicht mehr über TaskMate bearbeitet werden
+- Zeigt immer die aktuellen Inhalte aus dem Dateisystem
+- Verhindert versehentliches Überschreiben wichtiger Projektanweisungen
+- Service Worker Cache-Version: 163 -> 164
+
+BEGRÜNDUNG
+--------------------------------------------------------------------------------
+- Sicherheit: Keine Schreibrechte-Konflikte mehr
+- Klarheit: CLAUDE.md sollte außerhalb von TaskMate gepflegt werden
+- Konsistenz: Immer aktuelle Inhalte aus dem Dateisystem
+
+================================================================================
+02.01.2026 - CODING-TAB: CLAUDE.MD ANZEIGE BEHOBEN
+================================================================================
+
+BUGFIX
+--------------------------------------------------------------------------------
+Problem behoben: CLAUDE.md wird im Coding-Bereich jetzt korrekt angezeigt.
+
+TECHNISCHE DETAILS
+--------------------------------------------------------------------------------
+- Backend PUT/POST-Routes erweitert: claudeMdFromDisk in Antworten
+- CLAUDE.md wird nach dem Speichern aus dem Dateisystem gelesen
+- Service Worker Cache-Version: 162 -> 163
+
+AUSWIRKUNG
+--------------------------------------------------------------------------------
+Im Coding-Bereich werden alle bisherigen Änderungen der CLAUDE.md nun
+korrekt im Editor-Fenster angezeigt.
+
+================================================================================
+02.01.2026 - CLAUDE.MD DOKUMENTATION ERWEITERT
+================================================================================
+
+DOKUMENTATION
+--------------------------------------------------------------------------------
+Erweiterte CLAUDE.md mit hilfreichen Informationen für effizientere Entwicklung:
+
+NEUE ABSCHNITTE
+--------------------------------------------------------------------------------
+- Projektübersicht: Kurzbeschreibung und Hauptfunktionen
+- Architektur-Kurzübersicht: Technologie-Stack auf einen Blick
+- Wichtige Dateien & Einstiegspunkte: Zentrale Dateien für schnellen Einstieg
+- Häufige Entwicklungsaufgaben: Schritt-für-Schritt Anleitungen
+- Testing & Debugging: Logs, häufige Probleme und Lösungen
+- Deployment-Checkliste: Strukturierte Schritte für sicheres Deployment
+
+ZWECK
+--------------------------------------------------------------------------------
+- Schnelleres Verständnis der Projektstruktur
+- Effizientere Entwicklung durch klare Anleitungen
+- Weniger Fehler durch dokumentierte Best Practices
+
+================================================================================
+02.01.2026 - CODING-TAB IMPLEMENTIERUNG
+================================================================================
+
+NEUE FEATURES
+--------------------------------------------------------------------------------
+Neuer "Coding"-Tab ersetzt den bisherigen "Gitea"-Tab mit erweiterter
+Funktionalitaet zur Verwaltung von Entwicklungsverzeichnissen.
+
+CODING-TAB
+--------------------------------------------------------------------------------
+- Projektubergreifende Verwaltung von Entwicklungsverzeichnissen
+- Kachel-basiertes Grid-Layout
+- Claude Code Button (orange) - Startet Claude Code im Verzeichnis
+- Codex Button (gruen) - Startet OpenAI Codex im Verzeichnis
+- Server-Pfade: Direkte Ausfuehrung auf dem Linux-Server
+- Windows-Pfade: Befehl zum manuellen Kopieren fuer WSL
+- Optionale Gitea-Repository-Verknuepfung pro Verzeichnis
+- Git-Operationen (Fetch, Pull, Push, Commit) bei Verknuepfung
+- Auto-Refresh des Git-Status alle 30 Sekunden
+- Farbauswahl pro Verzeichnis
+
+BACKEND
+--------------------------------------------------------------------------------
+- Neue Datenbank-Tabelle: coding_directories
+- Neue Route: /api/coding mit 12 Endpunkten
+- Terminal-Start-Logik fuer Claude/Codex
+
+FRONTEND
+--------------------------------------------------------------------------------
+- Neuer Manager: coding.js
+- Neues Styling: coding.css
+- Modals: Verzeichnis-Verwaltung, Befehl-Anzeige
+- Service Worker Cache-Version: 154 -> 155
+
+================================================================================
+31.12.2025 - MOBILE OPTIMIERUNG
+================================================================================
+
+NEUE FEATURES
+--------------------------------------------------------------------------------
+Vollstaendige Mobile-Optimierung der Anwendung mit Touch-Unterstuetzung.
+
+HAMBURGER-MENU
+--------------------------------------------------------------------------------
+- Slide-in Navigation von links
+- Projekt-Auswahl im Menu
+- Alle Views ueber Menu erreichbar
+- Benutzer-Info und Logout
+- Smooth Animation (Hamburger zu X)
+
+SWIPE-GESTEN
+--------------------------------------------------------------------------------
+- Horizontal wischen zum View-Wechsel
+- Swipe rechts: vorheriger View
+- Swipe links: naechster View
+- Visuelle Indikatoren am Bildschirmrand
+
+TOUCH DRAG & DROP
+--------------------------------------------------------------------------------
+- Long-Press (300ms) startet Task-Drag
+- Visuelles Feedback beim Ziehen
+- Auto-Scroll am Bildschirmrand
+- Drop-Zonen werden hervorgehoben
+
+BOARD-ANSICHT
+--------------------------------------------------------------------------------
+- Horizontal Scroll mit Scroll-Snap
+- Spalten snappen am Viewport
+
+BETROFFENE DATEIEN
+--------------------------------------------------------------------------------
+- frontend/css/mobile.css (NEU)
+- frontend/js/mobile.js (NEU)
+- frontend/index.html: Hamburger-Button, Mobile-Menu, Swipe-Indikatoren
+- frontend/js/app.js: Mobile-Modul Integration
+- frontend/sw.js: Cache-Version auf 154
+
+
+================================================================================
+30.12.2025 - BUGFIX: HTML-Entity-Encoding in Textfeldern
+================================================================================
+
+PROBLEM
+--------------------------------------------------------------------------------
+Sonderzeichen wie "&" wurden beim Speichern zu "&" konvertiert.
+Beispiel: "Claude&Codex" wurde zu "Claude&Codex" gespeichert.
+
+URSACHE
+--------------------------------------------------------------------------------
+Die sanitize-html Bibliothek encoded HTML-Entities auch wenn alle Tags
+entfernt werden (allowedTags: []). Dies führte zu unerwünschter Konvertierung
+von & zu &, < zu <, etc.
+
+LÖSUNG
+--------------------------------------------------------------------------------
+- Neue Funktion decodeHtmlEntities() in validation.js
+- stripHtml() dekodiert nun Entities nach der Bereinigung
+- Ampersand (&), Klammern (<>), Anführungszeichen bleiben erhalten
+
+BETROFFENE DATEIEN
+--------------------------------------------------------------------------------
+- backend/middleware/validation.js: decodeHtmlEntities() hinzugefügt
+
+
+================================================================================
+30.12.2025 - ADMINBEREICH: Dateiendungen frei definierbar
+================================================================================
+
+NEUES FEATURE
+--------------------------------------------------------------------------------
+Vereinfachtes System zur Verwaltung erlaubter Dateiendungen im Adminbereich.
+Das bisherige komplexe Kategorien-System (Bilder, Dokumente, Office, etc.)
+wurde durch eine einfache, flexible Dateiendungs-Verwaltung ersetzt.
+
+FUNKTIONSWEISE
+--------------------------------------------------------------------------------
+- Standard-Endungen: pdf, docx, txt (können geändert werden)
+- Tags-System: Aktive Endungen werden als Tags mit ×-Button angezeigt
+- Freifeld: Beliebige Endungen manuell hinzufügen (z.B. xlsx, png, zip)
+- Vorschläge: Schnellauswahl häufiger Endungen per Klick
+- Validierung: Backend prüft Dateiendung UND MIME-Type
+
+VORGESCHLAGENE ENDUNGEN
+--------------------------------------------------------------------------------
+Office: xlsx, pptx, doc, xls, ppt, odt, ods, rtf
+Bilder: png, jpg, gif, svg, webp
+Daten: csv, json, xml, md
+Archive: zip, rar, 7z
+
+BETROFFENE DATEIEN
+--------------------------------------------------------------------------------
+- backend/routes/admin.js: Neues Format (allowedExtensions statt allowedTypes)
+- backend/middleware/upload.js: Extension-basierte Validierung, MIME-Mapping
+- frontend/index.html: Neues UI mit Tags, Input, Vorschläge
+- frontend/js/admin.js: Neue Render- und Event-Logik
+- frontend/css/admin.css: Styles für Extension-Tags und Vorschläge
+- frontend/sw.js: Cache-Version auf 153 erhöht
+
+
================================================================================
30.12.2025 - BUGFIX: Login-Problem behoben (Sofort-Logout nach Login)
================================================================================
diff --git a/CLAUDE.md b/CLAUDE.md
index 383437c..abcfa01 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,60 +1,464 @@
-# TaskMate - Projektanweisungen
+# TaskMate - Entwicklerdokumentation
-## Infrastruktur/Server
-- **Docker-Container**: `taskmate` (hauptsächlich Backend), läuft auf Port 3001 intern → 3000 im Container
-- **Frontend-Domain**: https://taskmate.aegis-sight.de
-- **Gitea-Repository**: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate
-- **Gitea-Token**: `7c62fea51bfe0506a25131bd50ac710ac5aa7e3a9dca37a962e7822bdc7db840`
-- **Projektverzeichnis auf Server**: `/home/claude-dev/TaskMate`
+## ⚠️ WICHTIGER HINWEIS FÜR KI-ASSISTENTEN
+Der Anwender hat **KEINE Programmierkenntnisse**. Das bedeutet:
+- **DU übernimmst ALLE technischen Aufgaben vollständig**
+- **Erkläre in einfachen Worten**, was du tust und warum
+- **Frage NIEMALS nach technischen Details** oder Code-Schnipseln
+- **Führe ALLE Schritte selbstständig aus**
+- Der Anwender kann nur bestätigen/ablehnen, nicht selbst coden
-## Allgemein
-- Sprache: Deutsch für Benutzer-Kommunikation
-- Änderungen immer in CHANGELOG.txt dokumentieren nach bisher bekanntem Schema in der Datei
-- Beim Start ANWENDUNGSBESCHREIBUNG.txt lesen
-- Cache-Version in frontend/sw.js erhöhen nach Änderungen
-- Ich bin kein Mensch mit Fachwissen im Bereich Coding, daher musst du sämtliche Aufgaben in der Regel übernehmen.
+### Kommunikations-Regeln
+✅ **RICHTIG**: "Ich werde jetzt die Benutzeroberfläche anpassen, damit..."
+❌ **FALSCH**: "Kannst du mir den Code aus Zeile 42 zeigen?"
-## Technologie
-- Frontend: Vanilla JavaScript (kein Framework)
-- Backend: Node.js mit Express
-- Datenbank: SQLite
+✅ **RICHTIG**: "Ich starte jetzt den Server neu. Das dauert etwa 30 Sekunden."
+❌ **FALSCH**: "Führe bitte folgenden Befehl aus: docker restart..."
-## Konventionen
-- CSS-Variablen in frontend/css/variables.css
-- Deutsche Umlaute (ä, ö, ü) in Texten verwenden
+## 🚀 Quick Start
-## Datumsformatierung (WICHTIG)
-- NIEMALS `toISOString()` für Datumsvergleiche oder -anzeigen verwenden!
-- `toISOString()` konvertiert in UTC und verursacht Zeitzonenverschiebungen (z.B. 28.12. wird zu 27.12.)
-- Stattdessen lokale Formatierung verwenden:
- ```javascript
- // Richtig: Lokale Formatierung
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const dateStr = `${year}-${month}-${day}`;
+### Wichtigste Befehle
+```bash
+# Docker Container neu starten (nach Backend-Änderungen)
+docker restart taskmate
- // Falsch: UTC-Konvertierung
- const dateStr = date.toISOString().split('T')[0]; // NICHT VERWENDEN!
- ```
+# Container neu bauen (bei Dependency-Änderungen)
+docker build -t taskmate . && docker restart taskmate
-## Echtzeit-Aktualisierung (KRITISCH)
-- ALLE Nutzeranpassungen müssen SOFORT und ÜBERALL in der Anwendung sichtbar sein
-- Der Nutzer darf NIEMALS den Browser aktualisieren müssen (F5), um Änderungen zu sehen
-- Beispiele für Änderungen, die sofort überall wirken müssen:
- - Spaltenfarbe ändern → Board, Kalender, Wochenstreifen sofort aktualisieren
- - Aufgabe erstellen/bearbeiten/löschen → alle Ansichten sofort aktualisieren
- - Labels, Benutzer, Projekte ändern → überall sofort sichtbar
-- Technische Umsetzung:
- - `store.subscribe('tasks', callback)` - für Aufgabenänderungen
- - `store.subscribe('columns', callback)` - für Spaltenänderungen
- - `store.subscribe('labels', callback)` - für Label-Änderungen
- - `store.subscribe('users', callback)` - für Benutzeränderungen
- - `window.addEventListener('app:refresh', callback)` - für allgemeine Aktualisierungen
- - `window.addEventListener('modal:close', callback)` - nach Modal-Schließung
-- Bei JEDER neuen Komponente diese Event-Listener einbauen
-- Bei JEDER Datenänderung prüfen: Welche UI-Bereiche müssen aktualisiert werden?
+# Logs anzeigen
+docker logs taskmate -f
-## Berechtigungen/Aktionen
-- Du sollst den Dockercontainer eigenständig - sofern erforderlich - neu starten/neu bauen, dass Änderungen wirksam werden
-- Erreichbarkeit der Anwendung über https://taskmate.aegis-sight.de (keine automatische Browser-Öffnung)
+# Health Check
+curl http://localhost:3000/api/health
+```
+
+### Kritische Regeln - NIEMALS VERGESSEN! ⚠️
+1. **Cache-Version erhöhen** nach Frontend-Änderungen: `frontend/sw.js` → `CACHE_VERSION`
+2. **CHANGELOG.txt** bei JEDER Änderung aktualisieren
+3. **Keine `toISOString()`** für Datums-Operationen (UTC-Problem!)
+4. **Echtzeit-Updates** - User darf NIE F5 drücken müssen
+5. **Docker restart** nach Backend-Änderungen
+
+### Datenschutz & Projektsicherheit 🔐
+**ABSOLUT KRITISCH**: Das Projekt "AegisSight" ist produktiv im Einsatz!
+
+- **Projekt "AegisSight" NIEMALS löschen, ändern oder beeinträchtigen**
+- **Bestehende Benutzer NIEMALS zurücksetzen, löschen oder verändern**
+- **Produktivdaten sind TABU** - keine Testdaten in echte Projekte
+- **Keine Datenbank-Resets** ohne explizite Anweisung
+- **JEDE Änderung MUSS umkehrbar sein** - Live-System!
+- **Backup vor kritischen Änderungen** ist Pflicht
+
+### Rollback-Strategie für Live-Betrieb
+Bei JEDER Änderung sicherstellen:
+
+```bash
+# 1. Vor Änderungen - Backup erstellen
+cp data/taskmate.db data/taskmate.db.backup-$(date +%Y%m%d-%H%M%S)
+docker commit taskmate taskmate-backup-$(date +%Y%m%d-%H%M%S)
+
+# 2. Bei Problemen - Rollback durchführen
+docker stop taskmate
+docker run -d --name taskmate-temp taskmate-backup-TIMESTAMP
+# Nach Test: docker rm -f taskmate && docker rename taskmate-temp taskmate
+
+# 3. Code-Rollback via Git
+git stash # Aktuelle Änderungen sichern
+git checkout HEAD~1 # Zum vorherigen Commit
+docker build -t taskmate . && docker restart taskmate
+```
+
+**Änderungs-Workflow für Live-System:**
+1. Backup von Datenbank UND Docker-Image
+2. Kleine, inkrementelle Änderungen
+3. Sofortiger Test nach jeder Änderung
+4. Rollback-Plan dokumentieren
+5. Bei kritischen Änderungen: Wartungsfenster planen
+
+### Arbeitsweise mit nicht-technischem Anwender
+**Der Anwender versteht KEIN Coding!** Daher:
+
+1. **Vollständige Übernahme**: Du führst ALLE technischen Schritte durch
+2. **Einfache Erklärungen**: "Ich passe jetzt X an, damit Y funktioniert"
+3. **Status-Updates**: "Änderung abgeschlossen, teste jetzt..."
+4. **Keine technischen Fragen**: Niemals nach Code, Logs oder Befehlen fragen
+5. **Proaktives Handeln**: Selbstständig debuggen und lösen
+
+**Beispiel-Kommunikation:**
+- ✅ "Ich habe ein Problem gefunden und behebe es jetzt..."
+- ❌ "Welche Version von Node.js ist installiert?"
+- ✅ "Die Änderung ist fertig. Bitte die Seite neu laden und testen."
+- ❌ "Kannst du mal in die Console schauen?"
+
+### Zugriff & Domains
+- **Frontend**: https://taskmate.aegis-sight.de
+- **Lokaler Port**: 3001 → Container Port 3000
+- **Gitea**: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate
+- **Gitea-Token**: Siehe `.env` Datei (NIEMALS in Git einchecken!)
+
+## 📁 Projektstruktur
+
+### Wichtige Dateien - Hier starten!
+```
+frontend/js/app.js # Hauptanwendung & View-Controller
+backend/server.js # Express Server mit Socket.io
+backend/database.js # Datenbankschema (20+ Tabellen)
+frontend/js/store.js # State Management (Pub-Sub)
+frontend/js/api.js # API Client mit Auth/CSRF
+frontend/sw.js # Service Worker → CACHE_VERSION!
+CHANGELOG.txt # Änderungsprotokoll
+```
+
+### Frontend-Module (22 Dateien)
+```
+# Core
+app.js # Hauptanwendung
+store.js # State Management
+api.js # Backend-Kommunikation
+auth.js # Login/Token-Verwaltung
+utils.js # Hilfsfunktionen
+
+# Views
+board.js # Kanban-Board mit Drag&Drop
+calendar.js # Kalender (Monat/Woche)
+list.js # Listenansicht
+dashboard.js # Statistik-Dashboard
+proposals.js # Vorschlagssystem
+knowledge.js # Wissensdatenbank
+admin.js # Benutzerverwaltung
+
+# Features
+task-modal.js # Aufgaben-Details
+notifications.js # Benachrichtigungen
+sync.js # Socket.io Echtzeit
+mobile.js # Mobile Features
+```
+
+### Backend-Routes (22 Module)
+```
+/api/auth # Login/Logout
+/api/tasks # Aufgaben CRUD
+/api/projects # Projekte
+/api/columns # Kanban-Spalten
+/api/comments # Kommentare
+/api/files # Datei-Upload
+/api/proposals # Vorschläge
+/api/gitea # Git-Integration
+/api/knowledge # Wissensdatenbank
+```
+
+## 🔧 Entwicklung
+
+### Neue View/Ansicht hinzufügen
+```javascript
+// 1. Datei erstellen: frontend/js/myview.js
+export function initMyView() {
+ // KRITISCH: Echtzeit-Updates registrieren!
+ store.subscribe('tasks', updateView);
+ window.addEventListener('app:refresh', updateView);
+
+ function updateView() {
+ // UI aktualisieren
+ }
+}
+
+// 2. In index.html einbinden
+
+
+// 3. Navigation erweitern in navigation.js
+```
+
+### Neue API-Route erstellen
+```javascript
+// 1. Route-Datei: backend/routes/myroute.js
+const router = require('express').Router();
+const auth = require('../middleware/auth');
+
+router.get('/', auth, (req, res) => {
+ // Implementation
+});
+
+module.exports = router;
+
+// 2. In server.js registrieren
+app.use('/api/myroute', require('./routes/myroute'));
+
+// 3. Frontend API-Call in api.js
+async myRouteCall() {
+ return this.request('/api/myroute');
+}
+```
+
+### Datums-Formatierung (RICHTIG!)
+```javascript
+// ✅ RICHTIG - Lokale Zeit
+const year = date.getFullYear();
+const month = String(date.getMonth() + 1).padStart(2, '0');
+const day = String(date.getDate()).padStart(2, '0');
+const dateStr = `${year}-${month}-${day}`;
+
+// ❌ FALSCH - UTC-Konvertierung
+const dateStr = date.toISOString().split('T')[0]; // NIEMALS!
+```
+
+### Echtzeit-Updates implementieren
+```javascript
+// PFLICHT für ALLE Komponenten!
+// 1. Store-Subscriptions
+store.subscribe('tasks', () => renderTasks());
+store.subscribe('columns', () => updateColumns());
+store.subscribe('labels', () => refreshLabels());
+
+// 2. Event-Listener
+window.addEventListener('app:refresh', () => {
+ // Komplette UI aktualisieren
+});
+
+window.addEventListener('modal:close', () => {
+ // Nach Modal-Schließung
+});
+
+// 3. Socket.io Events in sync.js
+socket.on('task:update', (data) => {
+ store.updateTask(data);
+});
+```
+
+## 💾 Datenbank
+
+### Wichtige Tabellen
+```sql
+users # Benutzer mit Rollen
+projects # Projekte
+columns # Kanban-Spalten
+tasks # Aufgaben
+task_labels # M:N Labels
+task_assignees # M:N Zuweisungen
+comments # Kommentare
+attachments # Dateianhänge
+proposals # Vorschläge
+notifications # Benachrichtigungen
+knowledge_* # Wissensdatenbank
+```
+
+### Schema ändern
+```javascript
+// 1. In backend/database.js anpassen
+CREATE TABLE new_table (
+ id INTEGER PRIMARY KEY,
+ ...
+);
+
+// 2. Datenbank neu initialisieren
+rm data/taskmate.db*
+docker restart taskmate
+
+// 3. API & Frontend anpassen
+```
+
+## 🚢 Deployment
+
+### Deployment-Checkliste
+```bash
+# 1. Vor Deployment
+- [ ] Keine console.log() im Code
+- [ ] Alle Features getestet
+- [ ] Keine Testdaten in DB
+
+# 2. Deployment durchführen
+- [ ] Cache-Version erhöhen: frontend/sw.js
+- [ ] CHANGELOG.txt aktualisieren
+- [ ] Git commit & push
+- [ ] docker build -t taskmate .
+- [ ] docker restart taskmate
+
+# 3. Nach Deployment
+- [ ] https://taskmate.aegis-sight.de testen
+- [ ] Browser-Cache leeren (Strg+F5)
+- [ ] Login, Aufgabe erstellen, etc. testen
+```
+
+### Docker-Befehle
+```bash
+# Container Status
+docker ps -a | grep taskmate
+
+# Container stoppen/starten
+docker stop taskmate
+docker start taskmate
+
+# Container neu erstellen
+docker rm -f taskmate
+docker-compose up -d
+
+# In Container Shell
+docker exec -it taskmate sh
+
+# Logs live verfolgen
+docker logs taskmate -f --tail 100
+```
+
+## 🐛 Troubleshooting
+
+### Häufige Probleme
+
+**401 Unauthorized**
+- Token abgelaufen → Neu einloggen
+- Prüfen: localStorage.getItem('token')
+
+**CSRF Token ungültig**
+- Browser-Cache/Cookies löschen
+- Neu einloggen
+
+**Änderungen nicht sichtbar**
+- Service Worker Cache → sw.js Version erhöhen!
+- Browser: Strg+F5
+- Prüfen: Echtzeit-Updates implementiert?
+
+**Docker startet nicht**
+```bash
+docker logs taskmate
+docker ps -a | grep taskmate
+netstat -tulpn | grep 3001
+```
+
+**Datenbank-Fehler**
+```bash
+# Backup erstellen
+cp data/taskmate.db data/taskmate.db.backup
+
+# Integrität prüfen
+sqlite3 data/taskmate.db "PRAGMA integrity_check;"
+
+# Schema anzeigen
+sqlite3 data/taskmate.db ".schema"
+```
+
+### Debug-Tipps
+
+**Frontend Debugging**
+```javascript
+// Store-Status prüfen
+console.log(store.getState());
+
+// API-Calls tracken
+window.api.debug = true;
+
+// Socket-Events loggen
+window.socket.on('*', console.log);
+```
+
+**Backend Debugging**
+```javascript
+// In server.js
+app.use((req, res, next) => {
+ console.log(`${req.method} ${req.path}`);
+ next();
+});
+
+// SQL Queries loggen
+db.prepare(sql).run(); // Vorher console.log(sql)
+```
+
+## 📋 Code-Patterns
+
+### API Response Format
+```javascript
+// Erfolg
+res.json({
+ success: true,
+ data: result
+});
+
+// Fehler
+res.status(400).json({
+ success: false,
+ error: 'Fehlermeldung'
+});
+```
+
+### Store Update Pattern
+```javascript
+// Daten aktualisieren
+store.updateTasks(tasks);
+
+// Wird automatisch ausgelöst:
+// - Alle task-Subscriber
+// - Socket.io Broadcast
+// - UI-Updates
+```
+
+### Modal Pattern
+```javascript
+// Modal öffnen
+modal.show();
+
+// WICHTIG: Bei Schließung
+modal.addEventListener('close', () => {
+ window.dispatchEvent(new CustomEvent('modal:close'));
+ // Triggert UI-Updates!
+});
+```
+
+## 🔒 Sicherheit
+
+### Authentifizierung
+- JWT Token mit 24h Gültigkeit
+- Refresh bei jeder Aktivität
+- Token in localStorage
+
+### CSRF-Schutz
+- Token bei Login generiert
+- Bei jeder Mutation mitgesendet
+- Header: `X-CSRF-Token`
+
+### Berechtigungen
+- Admin: Nur Benutzerverwaltung
+- User: Alles außer Admin-Bereich
+- Projekt-basierte Rechte
+
+## 📝 Wichtige Konventionen
+
+- **Sprache**: Deutsch für UI, Englisch für Code
+- **Umlaute**: ä, ö, ü verwenden (keine ae, oe, ue)
+- **CSS**: Variablen in `frontend/css/variables.css`
+- **Keine Emojis** in Code/UI (nur Doku)
+- **Auto-Save**: Änderungen werden automatisch gespeichert
+
+## 🎯 Performance
+
+### Frontend
+- Lazy Loading für Views
+- Debouncing bei Suche/Filter
+- Virtual Scrolling bei langen Listen
+- Service Worker Caching
+
+### Backend
+- SQLite mit WAL Mode
+- Prepared Statements
+- Index auf häufig gefilterte Spalten
+- Pagination bei großen Datenmengen
+
+## 🔄 Git Workflow
+
+### Lokales Repository
+```bash
+# Status prüfen
+git status
+
+# Commit erstellen
+git add .
+git commit -m "Beschreibung der Änderung"
+
+# Zu Gitea pushen
+git push origin main
+```
+
+### Gitea Integration
+- Automatischer Push bei Commits
+- Repository-Projekt Verknüpfung
+- Branch-Verwaltung in UI
+
+---
+
+**Hinweis**: Diese Dokumentation ist für die KI-gestützte Entwicklung optimiert. Bei Fragen die `ANWENDUNGSBESCHREIBUNG.txt` für Endnutzer-Dokumentation konsultieren.
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 2b2346c..1721af3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,7 +22,7 @@ RUN git config --system user.email "taskmate@local" && \
COPY backend/package*.json ./
# Abhängigkeiten installieren
-RUN npm ci --only=production
+RUN npm install --only=production
# Build-Abhängigkeiten entfernen (kleineres Image)
RUN apk del python3 make g++
diff --git a/backend/database.js b/backend/database.js
index b7d1c4d..bdb6789 100644
--- a/backend/database.js
+++ b/backend/database.js
@@ -368,6 +368,25 @@ function createTables() {
)
`);
+ // Refresh Tokens für sichere Token-Rotation
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS refresh_tokens (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ token TEXT NOT NULL UNIQUE,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ last_used DATETIME,
+ user_agent TEXT,
+ ip_address TEXT,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ `);
+
+ // Index für Token-Lookup
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token)`);
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON refresh_tokens(expires_at)`);
+
// Anwendungen (Git-Repositories pro Projekt)
db.exec(`
CREATE TABLE IF NOT EXISTS applications (
@@ -457,6 +476,34 @@ function createTables() {
)
`);
+ // Coding-Verzeichnisse (projektübergreifend)
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS coding_directories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ local_path TEXT NOT NULL UNIQUE,
+ description TEXT,
+ color TEXT DEFAULT '#4F46E5',
+ gitea_repo_url TEXT,
+ gitea_repo_owner TEXT,
+ gitea_repo_name TEXT,
+ default_branch TEXT DEFAULT 'main',
+ last_sync DATETIME,
+ position INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ created_by INTEGER,
+ FOREIGN KEY (created_by) REFERENCES users(id)
+ )
+ `);
+
+ // Migration: Add claude_instructions column to coding_directories
+ const codingDirColumns = db.prepare("PRAGMA table_info(coding_directories)").all();
+ const hasClaudeInstructions = codingDirColumns.some(col => col.name === 'claude_instructions');
+ if (!hasClaudeInstructions) {
+ db.exec('ALTER TABLE coding_directories ADD COLUMN claude_instructions TEXT');
+ logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt');
+ }
+
// Indizes für Performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
@@ -476,17 +523,36 @@ function createTables() {
CREATE INDEX IF NOT EXISTS idx_applications_project ON applications(project_id);
CREATE INDEX IF NOT EXISTS idx_knowledge_entries_category ON knowledge_entries(category_id);
CREATE INDEX IF NOT EXISTS idx_knowledge_attachments_entry ON knowledge_attachments(entry_id);
+ CREATE INDEX IF NOT EXISTS idx_coding_directories_position ON coding_directories(position);
`);
logger.info('Datenbank-Tabellen erstellt');
}
/**
- * Standard-Benutzer erstellen
+ * Standard-Benutzer erstellen und Admin-Passwort korrigieren
*/
async function createDefaultUsers() {
const existingUsers = db.prepare('SELECT COUNT(*) as count FROM users').get();
+ // Admin-Passwort korrigieren (falls aus .env verschieden)
+ const adminExists = db.prepare('SELECT id, password_hash FROM users WHERE username = ? AND role = ?').get('admin', 'admin');
+ if (adminExists) {
+ const correctAdminPassword = process.env.ADMIN_PASSWORD || 'admin123';
+ const bcrypt = require('bcryptjs');
+
+ // Prüfen ob das Passwort bereits korrekt ist
+ const isCorrect = await bcrypt.compare(correctAdminPassword, adminExists.password_hash);
+ if (!isCorrect) {
+ logger.info('Admin-Passwort wird aus .env aktualisiert');
+ const correctHash = await bcrypt.hash(correctAdminPassword, 12);
+ db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(correctHash, adminExists.id);
+ logger.info('Admin-Passwort erfolgreich aktualisiert');
+ } else {
+ logger.info('Admin-Passwort bereits korrekt');
+ }
+ }
+
if (existingUsers.count === 0) {
// Benutzer aus Umgebungsvariablen
const user1 = {
@@ -510,10 +576,10 @@ async function createDefaultUsers() {
// Admin-Benutzer
const adminUser = {
- username: 'admin',
- password: 'Kx9#mP2$vL7@nQ4!wR',
- displayName: 'Administrator',
- color: '#8B5CF6'
+ username: process.env.ADMIN_USERNAME || 'admin',
+ password: process.env.ADMIN_PASSWORD || 'admin123',
+ displayName: process.env.ADMIN_DISPLAYNAME || 'Administrator',
+ color: process.env.ADMIN_COLOR || '#8B5CF6'
};
// Passwoerter hashen und Benutzer erstellen
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
index 62efcaa..a89b675 100644
--- a/backend/middleware/auth.js
+++ b/backend/middleware/auth.js
@@ -5,15 +5,22 @@
*/
const jwt = require('jsonwebtoken');
+const crypto = require('crypto');
const logger = require('../utils/logger');
+const { getDb } = require('../database');
-const JWT_SECRET = process.env.JWT_SECRET || 'UNSICHER_BITTE_AENDERN';
+const JWT_SECRET = process.env.JWT_SECRET;
+if (!JWT_SECRET || JWT_SECRET.length < 32) {
+ throw new Error('JWT_SECRET muss in .env gesetzt und mindestens 32 Zeichen lang sein!');
+}
+const ACCESS_TOKEN_EXPIRY = 15; // Minuten (kürzer für mehr Sicherheit)
+const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60; // 7 Tage in Minuten
const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten
/**
- * JWT-Token generieren
+ * JWT Access-Token generieren (kurze Lebensdauer)
*/
-function generateToken(user) {
+function generateAccessToken(user) {
// Permissions parsen falls als String gespeichert
let permissions = user.permissions || [];
if (typeof permissions === 'string') {
@@ -31,13 +38,38 @@ function generateToken(user) {
displayName: user.display_name,
color: user.color,
role: user.role || 'user',
- permissions: permissions
+ permissions: permissions,
+ type: 'access'
},
JWT_SECRET,
- { expiresIn: `${SESSION_TIMEOUT}m` }
+ { expiresIn: `${ACCESS_TOKEN_EXPIRY}m` }
);
}
+/**
+ * Refresh-Token generieren (lange Lebensdauer)
+ */
+function generateRefreshToken(userId, ipAddress, userAgent) {
+ const db = getDb();
+ const token = crypto.randomBytes(32).toString('hex');
+ const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY * 60 * 1000);
+
+ // Token in Datenbank speichern
+ db.prepare(`
+ INSERT INTO refresh_tokens (user_id, token, expires_at, ip_address, user_agent)
+ VALUES (?, ?, ?, ?, ?)
+ `).run(userId, token, expiresAt.toISOString(), ipAddress, userAgent);
+
+ return token;
+}
+
+/**
+ * Legacy generateToken für Rückwärtskompatibilität
+ */
+function generateToken(user) {
+ return generateAccessToken(user);
+}
+
/**
* JWT-Token verifizieren
*/
@@ -179,8 +211,72 @@ function generateCsrfToken() {
return randomBytes(32).toString('hex');
}
+/**
+ * Refresh-Token validieren und neuen Access-Token generieren
+ */
+async function refreshAccessToken(refreshToken, ipAddress, userAgent) {
+ const db = getDb();
+
+ // Token in Datenbank suchen
+ const tokenRecord = db.prepare(`
+ SELECT rt.*, u.* FROM refresh_tokens rt
+ JOIN users u ON rt.user_id = u.id
+ WHERE rt.token = ? AND rt.expires_at > datetime('now')
+ `).get(refreshToken);
+
+ if (!tokenRecord) {
+ throw new Error('Ungültiger oder abgelaufener Refresh-Token');
+ }
+
+ // Token als benutzt markieren
+ db.prepare(`
+ UPDATE refresh_tokens SET last_used = CURRENT_TIMESTAMP WHERE id = ?
+ `).run(tokenRecord.id);
+
+ // Neuen Access-Token generieren
+ const user = {
+ id: tokenRecord.user_id,
+ username: tokenRecord.username,
+ display_name: tokenRecord.display_name,
+ color: tokenRecord.color,
+ role: tokenRecord.role,
+ permissions: tokenRecord.permissions
+ };
+
+ return generateAccessToken(user);
+}
+
+/**
+ * Alle Refresh-Tokens eines Benutzers löschen (Logout auf allen Geräten)
+ */
+function revokeAllRefreshTokens(userId) {
+ const db = getDb();
+ db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').run(userId);
+}
+
+/**
+ * Abgelaufene Refresh-Tokens aufräumen
+ */
+function cleanupExpiredTokens() {
+ const db = getDb();
+ const result = db.prepare(`
+ DELETE FROM refresh_tokens WHERE expires_at < datetime('now')
+ `).run();
+
+ if (result.changes > 0) {
+ logger.info(`Bereinigt: ${result.changes} abgelaufene Refresh-Tokens`);
+ }
+}
+
+// Cleanup alle 6 Stunden
+setInterval(cleanupExpiredTokens, 6 * 60 * 60 * 1000);
+
module.exports = {
generateToken,
+ generateAccessToken,
+ generateRefreshToken,
+ refreshAccessToken,
+ revokeAllRefreshTokens,
verifyToken,
authenticateToken,
authenticateSocket,
diff --git a/backend/middleware/upload.js b/backend/middleware/upload.js
index f4f841f..e7f1ecc 100644
--- a/backend/middleware/upload.js
+++ b/backend/middleware/upload.js
@@ -7,6 +7,7 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
+const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const logger = require('../utils/logger');
@@ -18,18 +19,54 @@ if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
+// Mapping: Dateiendung -> erlaubte MIME-Types
+const EXTENSION_TO_MIME = {
+ // Dokumente
+ 'pdf': ['application/pdf'],
+ 'doc': ['application/msword'],
+ 'docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
+ 'xls': ['application/vnd.ms-excel'],
+ 'xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
+ 'ppt': ['application/vnd.ms-powerpoint'],
+ 'pptx': ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
+ 'odt': ['application/vnd.oasis.opendocument.text'],
+ 'ods': ['application/vnd.oasis.opendocument.spreadsheet'],
+ 'odp': ['application/vnd.oasis.opendocument.presentation'],
+ 'rtf': ['application/rtf', 'text/rtf'],
+ // Text
+ 'txt': ['text/plain'],
+ 'csv': ['text/csv', 'application/csv', 'text/comma-separated-values'],
+ 'md': ['text/markdown', 'text/x-markdown', 'text/plain'],
+ 'json': ['application/json', 'text/json'],
+ 'xml': ['application/xml', 'text/xml'],
+ 'html': ['text/html'],
+ 'log': ['text/plain'],
+ // Bilder
+ 'jpg': ['image/jpeg'],
+ 'jpeg': ['image/jpeg'],
+ 'png': ['image/png'],
+ 'gif': ['image/gif'],
+ 'webp': ['image/webp'],
+ 'svg': ['image/svg+xml'],
+ 'bmp': ['image/bmp'],
+ 'ico': ['image/x-icon', 'image/vnd.microsoft.icon'],
+ // Archive
+ 'zip': ['application/zip', 'application/x-zip-compressed'],
+ 'rar': ['application/x-rar-compressed', 'application/vnd.rar'],
+ '7z': ['application/x-7z-compressed'],
+ 'tar': ['application/x-tar'],
+ 'gz': ['application/gzip', 'application/x-gzip'],
+ // Code/Skripte (als text/plain akzeptiert)
+ 'sql': ['application/sql', 'text/plain'],
+ 'js': ['text/javascript', 'application/javascript', 'text/plain'],
+ 'css': ['text/css', 'text/plain'],
+ 'py': ['text/x-python', 'text/plain'],
+ 'sh': ['application/x-sh', 'text/plain']
+};
+
// Standard-Werte (Fallback)
let MAX_FILE_SIZE = (parseInt(process.env.MAX_FILE_SIZE_MB) || 15) * 1024 * 1024;
-let ALLOWED_MIME_TYPES = [
- 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
- 'application/pdf',
- 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- 'text/plain', 'text/csv', 'text/markdown',
- 'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
- 'application/json'
-];
+let ALLOWED_EXTENSIONS = ['pdf', 'docx', 'txt'];
/**
* Lädt Upload-Einstellungen aus der Datenbank
@@ -43,17 +80,9 @@ function loadUploadSettings() {
if (settings) {
MAX_FILE_SIZE = (settings.maxFileSizeMB || 15) * 1024 * 1024;
- // Erlaubte MIME-Types aus den aktiven Kategorien zusammenstellen
- const types = [];
- if (settings.allowedTypes) {
- Object.values(settings.allowedTypes).forEach(category => {
- if (category.enabled && Array.isArray(category.types)) {
- types.push(...category.types);
- }
- });
- }
- if (types.length > 0) {
- ALLOWED_MIME_TYPES = types;
+ // Erlaubte Endungen aus den Einstellungen
+ if (Array.isArray(settings.allowedExtensions) && settings.allowedExtensions.length > 0) {
+ ALLOWED_EXTENSIONS = settings.allowedExtensions;
}
}
} catch (error) {
@@ -67,7 +96,7 @@ function loadUploadSettings() {
*/
function getCurrentSettings() {
loadUploadSettings();
- return { maxFileSize: MAX_FILE_SIZE, allowedMimeTypes: ALLOWED_MIME_TYPES };
+ return { maxFileSize: MAX_FILE_SIZE, allowedExtensions: ALLOWED_EXTENSIONS };
}
/**
@@ -99,19 +128,83 @@ const storage = multer.diskStorage({
});
/**
- * Datei-Filter
+ * Gefährliche Dateinamen prüfen
+ */
+function isSecureFilename(filename) {
+ // Null-Bytes, Pfad-Traversal, Steuerzeichen blocken
+ const dangerousPatterns = [
+ /\x00/, // Null-Bytes
+ /\.\./, // Path traversal
+ /[<>:"\\|?*]/, // Windows-spezifische gefährliche Zeichen
+ /[\x00-\x1F]/, // Steuerzeichen
+ /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i, // Windows reservierte Namen
+ ];
+
+ return !dangerousPatterns.some(pattern => pattern.test(filename));
+}
+
+/**
+ * Datei-Filter: Erweiterte Sicherheitsprüfungen
*/
const fileFilter = (req, file, cb) => {
// Aktuelle Einstellungen laden
const settings = getCurrentSettings();
- // MIME-Type prüfen
- if (settings.allowedMimeTypes.includes(file.mimetype)) {
- cb(null, true);
- } else {
- logger.warn(`Abgelehnter Upload: ${file.originalname} (${file.mimetype})`);
- cb(new Error(`Dateityp nicht erlaubt: ${file.mimetype}`), false);
+ // Sicherheitsprüfungen für Dateinamen
+ if (!isSecureFilename(file.originalname)) {
+ logger.warn(`Unsicherer Dateiname abgelehnt: ${file.originalname}`);
+ cb(new Error('Dateiname enthält nicht erlaubte Zeichen'), false);
+ return;
}
+
+ // Dateiname-Länge prüfen
+ if (file.originalname.length > 255) {
+ logger.warn(`Dateiname zu lang: ${file.originalname}`);
+ cb(new Error('Dateiname ist zu lang (max. 255 Zeichen)'), false);
+ return;
+ }
+
+ // Dateiendung extrahieren (ohne Punkt, lowercase)
+ const ext = path.extname(file.originalname).toLowerCase().replace('.', '');
+
+ // Doppelte Dateiendungen verhindern (z.B. script.txt.exe)
+ const nameWithoutExt = path.basename(file.originalname, path.extname(file.originalname));
+ if (path.extname(nameWithoutExt)) {
+ logger.warn(`Doppelte Dateiendung abgelehnt: ${file.originalname}`);
+ cb(new Error('Dateien mit mehreren Endungen sind nicht erlaubt'), false);
+ return;
+ }
+
+ // Prüfen ob Endung erlaubt ist
+ if (!settings.allowedExtensions.includes(ext)) {
+ logger.warn(`Abgelehnter Upload (Endung): ${file.originalname} (.${ext})`);
+ cb(new Error(`Dateityp .${ext} nicht erlaubt`), false);
+ return;
+ }
+
+ // Executable Dateien zusätzlich blocken
+ const executableExtensions = [
+ 'exe', 'bat', 'cmd', 'com', 'scr', 'pif', 'vbs', 'vbe', 'js', 'jar',
+ 'app', 'deb', 'pkg', 'dmg', 'run', 'bin', 'msi', 'gadget'
+ ];
+ if (executableExtensions.includes(ext)) {
+ logger.warn(`Executable Datei abgelehnt: ${file.originalname}`);
+ cb(new Error('Ausführbare Dateien sind nicht erlaubt'), false);
+ return;
+ }
+
+ // MIME-Type gegen bekannte Typen prüfen
+ const expectedMimes = EXTENSION_TO_MIME[ext];
+ if (expectedMimes && !expectedMimes.includes(file.mimetype)) {
+ logger.warn(`MIME-Mismatch: ${file.originalname} (erwartet: ${expectedMimes.join('/')}, bekommen: ${file.mimetype})`);
+ // Bei kritischen Mismatches ablehnen
+ if (file.mimetype === 'application/octet-stream' || file.mimetype.startsWith('application/x-')) {
+ cb(new Error('Verdächtiger Dateityp erkannt'), false);
+ return;
+ }
+ }
+
+ cb(null, true);
};
/**
@@ -194,5 +287,6 @@ module.exports = {
getCurrentSettings,
UPLOAD_DIR,
MAX_FILE_SIZE,
- ALLOWED_MIME_TYPES
+ ALLOWED_EXTENSIONS,
+ EXTENSION_TO_MIME
};
diff --git a/backend/middleware/validation.js b/backend/middleware/validation.js
index 7c5aa71..d2986a7 100644
--- a/backend/middleware/validation.js
+++ b/backend/middleware/validation.js
@@ -5,24 +5,52 @@
*/
const sanitizeHtml = require('sanitize-html');
+const createDOMPurify = require('dompurify');
+const { JSDOM } = require('jsdom');
+
+// DOMPurify für Server-side Rendering initialisieren
+const window = new JSDOM('').window;
+const DOMPurify = createDOMPurify(window);
/**
- * HTML-Tags entfernen (für reine Text-Felder)
+ * HTML-Entities dekodieren
*/
-function stripHtml(input) {
- if (typeof input !== 'string') return input;
- return sanitizeHtml(input, {
- allowedTags: [],
- allowedAttributes: {}
- }).trim();
+function decodeHtmlEntities(str) {
+ if (typeof str !== 'string') return str;
+ const entities = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ ''': "'",
+ ''': "'",
+ ''': "'"
+ };
+ return str.replace(/&(amp|lt|gt|quot|#039|#x27|apos);/g, match => entities[match] || match);
}
/**
- * Markdown-sichere Bereinigung (erlaubt bestimmte Tags)
+ * HTML-Tags entfernen (für reine Text-Felder)
+ * Wichtig: sanitize-html encoded &-Zeichen zu &, daher dekodieren wir danach
+ */
+function stripHtml(input) {
+ if (typeof input !== 'string') return input;
+ const sanitized = sanitizeHtml(input, {
+ allowedTags: [],
+ allowedAttributes: {}
+ }).trim();
+ // Entities wieder dekodieren, da sanitize-html sie encoded
+ return decodeHtmlEntities(sanitized);
+}
+
+/**
+ * Markdown-sichere Bereinigung mit DOMPurify (doppelte Sicherheit)
*/
function sanitizeMarkdown(input) {
if (typeof input !== 'string') return input;
- return sanitizeHtml(input, {
+
+ // Erste Bereinigung mit sanitize-html
+ const firstPass = sanitizeHtml(input, {
allowedTags: [
'p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
'ul', 'ol', 'li', 'blockquote', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
@@ -44,6 +72,16 @@ function sanitizeMarkdown(input) {
}
}
});
+
+ // Zweite Bereinigung mit DOMPurify (zusätzliche Sicherheit)
+ return DOMPurify.sanitize(firstPass, {
+ ALLOWED_TAGS: [
+ 'p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
+ 'ul', 'ol', 'li', 'blockquote', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
+ ],
+ ALLOWED_ATTR: ['href', 'title', 'target', 'rel'],
+ ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
+ });
}
/**
@@ -65,7 +103,15 @@ function sanitizeObject(obj, options = {}) {
for (const [key, value] of Object.entries(obj)) {
// Bestimmte Felder dürfen Markdown enthalten
const allowHtml = ['description', 'content'].includes(key);
- sanitized[key] = sanitizeObject(value, { allowHtml });
+
+ // Passwort-Felder NICHT sanitizen (Sonderzeichen erhalten)
+ const skipSanitization = ['password', 'oldPassword', 'newPassword', 'confirmPassword'].includes(key);
+
+ if (skipSanitization) {
+ sanitized[key] = value; // Passwort unverändert lassen
+ } else {
+ sanitized[key] = sanitizeObject(value, { allowHtml });
+ }
}
return sanitized;
}
@@ -119,12 +165,32 @@ const validators = {
},
/**
- * URL-Format prüfen
+ * URL-Format prüfen (erweiterte Sicherheit)
*/
url: (value, fieldName) => {
try {
if (value) {
- new URL(value);
+ const url = new URL(value);
+
+ // Nur HTTP/HTTPS erlauben
+ if (!['http:', 'https:'].includes(url.protocol)) {
+ return `${fieldName} muss HTTP oder HTTPS verwenden`;
+ }
+
+ // Localhost und private IPs blocken (SSRF-Schutz)
+ const hostname = url.hostname;
+ if (hostname === 'localhost' ||
+ hostname === '127.0.0.1' ||
+ hostname.startsWith('192.168.') ||
+ hostname.startsWith('10.') ||
+ hostname.startsWith('172.')) {
+ return `${fieldName} darf nicht auf lokale Adressen verweisen`;
+ }
+
+ // JavaScript URLs blocken
+ if (url.href.toLowerCase().startsWith('javascript:')) {
+ return `${fieldName} enthält ungültigen JavaScript-Code`;
+ }
}
return null;
} catch {
diff --git a/backend/package.json b/backend/package.json
index 48f7252..f8ff7ab 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -20,7 +20,10 @@
"cookie-parser": "^1.4.6",
"express-rate-limiter": "^1.3.1",
"sanitize-html": "^2.11.0",
- "marked": "^11.1.0"
+ "marked": "^11.1.0",
+ "dompurify": "^3.0.6",
+ "jsdom": "^23.0.1",
+ "dotenv": "^16.3.1"
},
"engines": {
"node": ">=18.0.0"
diff --git a/backend/query_users.js b/backend/query_users.js
new file mode 100644
index 0000000..45c19ba
--- /dev/null
+++ b/backend/query_users.js
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+/**
+ * Script zum Abfragen der Benutzer aus der SQLite-Datenbank
+ * Verwendung: node query_users.js
+ */
+
+const Database = require('better-sqlite3');
+const path = require('path');
+
+// Datenbank-Pfad - angepasst für Docker-Container
+const DB_PATH = process.env.DB_PATH || './data/taskmate.db';
+
+try {
+ console.log('Verbinde zur Datenbank:', DB_PATH);
+
+ // Datenbank öffnen
+ const db = new Database(DB_PATH);
+
+ // Alle Benutzer abfragen
+ const users = db.prepare(`
+ SELECT
+ id,
+ username,
+ display_name,
+ color,
+ role,
+ email,
+ repositories_base_path,
+ created_at,
+ last_login,
+ failed_attempts,
+ locked_until
+ FROM users
+ ORDER BY id
+ `).all();
+
+ console.log('\n=== BENUTZER IN DER DATENBANK ===\n');
+
+ if (users.length === 0) {
+ console.log('Keine Benutzer gefunden!');
+ } else {
+ users.forEach(user => {
+ console.log(`ID: ${user.id}`);
+ console.log(`Benutzername: ${user.username}`);
+ console.log(`Anzeigename: ${user.display_name}`);
+ console.log(`Farbe: ${user.color}`);
+ console.log(`Rolle: ${user.role || 'user'}`);
+ console.log(`E-Mail: ${user.email || 'nicht gesetzt'}`);
+ console.log(`Repository-Basispfad: ${user.repositories_base_path || 'nicht gesetzt'}`);
+ console.log(`Erstellt am: ${user.created_at}`);
+ console.log(`Letzter Login: ${user.last_login || 'noch nie'}`);
+ console.log(`Fehlgeschlagene Versuche: ${user.failed_attempts}`);
+ console.log(`Gesperrt bis: ${user.locked_until || 'nicht gesperrt'}`);
+ console.log('-----------------------------------');
+ });
+
+ console.log(`\nGesamt: ${users.length} Benutzer gefunden`);
+ }
+
+ // Prüfe auch Login-Audit für weitere Informationen
+ const recentAttempts = db.prepare(`
+ SELECT
+ la.timestamp,
+ la.ip_address,
+ la.success,
+ la.user_agent,
+ u.username
+ FROM login_audit la
+ LEFT JOIN users u ON la.user_id = u.id
+ ORDER BY la.timestamp DESC
+ LIMIT 10
+ `).all();
+
+ if (recentAttempts.length > 0) {
+ console.log('\n=== LETZTE LOGIN-VERSUCHE ===\n');
+ recentAttempts.forEach(attempt => {
+ console.log(`${attempt.timestamp}: ${attempt.username || 'Unbekannt'} - ${attempt.success ? 'ERFOLGREICH' : 'FEHLGESCHLAGEN'} - IP: ${attempt.ip_address}`);
+ });
+ }
+
+ // Datenbank schließen
+ db.close();
+
+} catch (error) {
+ console.error('Fehler beim Abfragen der Datenbank:', error.message);
+ process.exit(1);
+}
\ No newline at end of file
diff --git a/backend/routes/admin.js b/backend/routes/admin.js
index 08afdfb..aecf264 100644
--- a/backend/routes/admin.js
+++ b/backend/routes/admin.js
@@ -10,45 +10,14 @@ const router = express.Router();
const { getDb } = require('../database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
+const backup = require('../utils/backup');
/**
- * Standard-Upload-Einstellungen
+ * Standard-Upload-Einstellungen (neues Format mit Dateiendungen)
*/
const DEFAULT_UPLOAD_SETTINGS = {
maxFileSizeMB: 15,
- allowedTypes: {
- images: {
- enabled: true,
- types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
- },
- documents: {
- enabled: true,
- types: ['application/pdf']
- },
- office: {
- enabled: true,
- types: [
- 'application/msword',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'application/vnd.ms-excel',
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'application/vnd.ms-powerpoint',
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
- ]
- },
- text: {
- enabled: true,
- types: ['text/plain', 'text/csv', 'text/markdown']
- },
- archives: {
- enabled: true,
- types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed']
- },
- data: {
- enabled: true,
- types: ['application/json']
- }
- }
+ allowedExtensions: ['pdf', 'docx', 'txt']
};
// Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle
@@ -351,6 +320,17 @@ router.get('/upload-settings', (req, res) => {
if (setting) {
const settings = JSON.parse(setting.value);
+
+ // Migration: Altes Format (allowedTypes) auf neues Format (allowedExtensions) umstellen
+ if (settings.allowedTypes && !settings.allowedExtensions) {
+ // Altes Format erkannt - auf Standard-Einstellungen zurücksetzen
+ logger.info('Migriere Upload-Einstellungen auf neues Format (allowedExtensions)');
+ db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)')
+ .run('upload_settings', JSON.stringify(DEFAULT_UPLOAD_SETTINGS));
+ res.json(DEFAULT_UPLOAD_SETTINGS);
+ return;
+ }
+
res.json(settings);
} else {
// Standard-Einstellungen zurückgeben und speichern
@@ -369,24 +349,36 @@ router.get('/upload-settings', (req, res) => {
*/
router.put('/upload-settings', (req, res) => {
try {
- const { maxFileSizeMB, allowedTypes } = req.body;
+ const { maxFileSizeMB, allowedExtensions } = req.body;
// Validierung
if (typeof maxFileSizeMB !== 'number' || maxFileSizeMB < 1 || maxFileSizeMB > 100) {
return res.status(400).json({ error: 'Maximale Dateigröße muss zwischen 1 und 100 MB liegen' });
}
- if (!allowedTypes || typeof allowedTypes !== 'object') {
- return res.status(400).json({ error: 'Ungültige Dateityp-Konfiguration' });
+ if (!Array.isArray(allowedExtensions) || allowedExtensions.length === 0) {
+ return res.status(400).json({ error: 'Mindestens eine Dateiendung muss erlaubt sein' });
}
- const settings = { maxFileSizeMB, allowedTypes };
+ // Endungen validieren (nur alphanumerisch, 1-10 Zeichen)
+ const validExtensions = allowedExtensions
+ .map(ext => ext.toLowerCase().replace(/^\./, '')) // Punkt am Anfang entfernen
+ .filter(ext => /^[a-z0-9]{1,10}$/.test(ext));
+
+ if (validExtensions.length === 0) {
+ return res.status(400).json({ error: 'Keine gültigen Dateiendungen angegeben' });
+ }
+
+ // Duplikate entfernen
+ const uniqueExtensions = [...new Set(validExtensions)];
+
+ const settings = { maxFileSizeMB, allowedExtensions: uniqueExtensions };
const db = getDb();
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)')
.run('upload_settings', JSON.stringify(settings));
- logger.info(`Admin ${req.user.username} hat Upload-Einstellungen geändert`);
+ logger.info(`Admin ${req.user.username} hat Upload-Einstellungen geändert: ${uniqueExtensions.join(', ')}`);
res.json(settings);
} catch (error) {
@@ -404,7 +396,12 @@ function getUploadSettings() {
const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('upload_settings');
if (setting) {
- return JSON.parse(setting.value);
+ const settings = JSON.parse(setting.value);
+ // Bei altem Format oder fehlendem allowedExtensions: Standard verwenden
+ if (!settings.allowedExtensions || !Array.isArray(settings.allowedExtensions)) {
+ return DEFAULT_UPLOAD_SETTINGS;
+ }
+ return settings;
}
return DEFAULT_UPLOAD_SETTINGS;
} catch (error) {
@@ -413,6 +410,42 @@ function getUploadSettings() {
}
}
+/**
+ * POST /api/admin/backup/create - Sofortiges verschlüsseltes Backup erstellen
+ */
+router.post('/backup/create', (req, res) => {
+ try {
+ const backupPath = backup.createBackup();
+
+ if (backupPath) {
+ logger.info(`Admin ${req.user.username} hat manuelles Backup erstellt`);
+ res.json({
+ success: true,
+ message: 'Verschlüsseltes Backup erfolgreich erstellt',
+ backupPath: backupPath.split('/').pop() // Nur Dateiname zurückgeben
+ });
+ } else {
+ res.status(500).json({ error: 'Backup-Erstellung fehlgeschlagen' });
+ }
+ } catch (error) {
+ logger.error('Backup-Erstellung durch Admin fehlgeschlagen:', error);
+ res.status(500).json({ error: 'Interner Fehler beim Erstellen des Backups' });
+ }
+});
+
+/**
+ * GET /api/admin/backup/list - Liste aller verschlüsselten Backups
+ */
+router.get('/backup/list', (req, res) => {
+ try {
+ const backups = backup.listBackups();
+ res.json(backups);
+ } catch (error) {
+ logger.error('Fehler beim Auflisten der Backups:', error);
+ res.status(500).json({ error: 'Fehler beim Auflisten der Backups' });
+ }
+});
+
module.exports = router;
module.exports.getUploadSettings = getUploadSettings;
module.exports.DEFAULT_UPLOAD_SETTINGS = DEFAULT_UPLOAD_SETTINGS;
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index 01eb6a7..6efc438 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -8,7 +8,7 @@ const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const { getDb } = require('../database');
-const { generateToken, authenticateToken } = require('../middleware/auth');
+const { generateToken, generateRefreshToken, refreshAccessToken, revokeAllRefreshTokens, authenticateToken } = require('../middleware/auth');
const { getTokenForUser } = require('../middleware/csrf');
const { validatePassword } = require('../middleware/validation');
const logger = require('../utils/logger');
@@ -37,13 +37,8 @@ router.post('/login', async (req, res) => {
// Benutzer suchen: Zuerst nach Username "admin", dann nach E-Mail
let user;
- if (username.toLowerCase() === 'admin') {
- // Admin-User per Username suchen
- user = db.prepare('SELECT * FROM users WHERE username = ?').get('admin');
- } else {
- // Normale User per E-Mail suchen
- user = db.prepare('SELECT * FROM users WHERE email = ?').get(username);
- }
+ // User per Username suchen (kann E-Mail-Adresse oder admin sein)
+ user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
// Audit-Log Eintrag vorbereiten
const logAttempt = (userId, success) => {
@@ -111,8 +106,11 @@ router.post('/login', async (req, res) => {
logAttempt(user.id, true);
- // JWT-Token generieren
- const token = generateToken(user);
+ // JWT Access-Token generieren (kurze Lebensdauer)
+ const accessToken = generateToken(user);
+
+ // Refresh-Token generieren (lange Lebensdauer)
+ const refreshToken = generateRefreshToken(user.id, ip, userAgent);
// CSRF-Token generieren
const csrfToken = getTokenForUser(user.id);
@@ -128,7 +126,8 @@ router.post('/login', async (req, res) => {
}
res.json({
- token,
+ token: accessToken,
+ refreshToken,
csrfToken,
user: {
id: user.id,
@@ -147,13 +146,19 @@ router.post('/login', async (req, res) => {
/**
* POST /api/auth/logout
- * Benutzer abmelden
+ * Benutzer abmelden und Refresh-Tokens widerrufen
*/
router.post('/logout', authenticateToken, (req, res) => {
- // Bei JWT gibt es serverseitig nichts zu tun
- // Client muss Token löschen
- logger.info(`Logout: ${req.user.username}`);
- res.json({ message: 'Erfolgreich abgemeldet' });
+ try {
+ // Alle Refresh-Tokens des Benutzers löschen
+ revokeAllRefreshTokens(req.user.id);
+
+ logger.info(`Logout: ${req.user.username}`);
+ res.json({ message: 'Erfolgreich abgemeldet' });
+ } catch (error) {
+ logger.error('Logout-Fehler:', { error: error.message });
+ res.status(500).json({ error: 'Logout fehlgeschlagen' });
+ }
});
/**
@@ -200,27 +205,68 @@ router.get('/me', authenticateToken, (req, res) => {
/**
* POST /api/auth/refresh
- * Token erneuern
+ * Token mit Refresh-Token erneuern
*/
-router.post('/refresh', authenticateToken, (req, res) => {
+router.post('/refresh', async (req, res) => {
try {
- const db = getDb();
- const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
-
- if (!user) {
- return res.status(404).json({ error: 'Benutzer nicht gefunden' });
+ const { refreshToken } = req.body;
+ const ip = req.ip || req.connection.remoteAddress;
+ const userAgent = req.headers['user-agent'];
+
+ if (!refreshToken) {
+ // Fallback für alte Clients - mit Access Token authentifizieren
+ if (req.headers.authorization) {
+ return legacyRefresh(req, res);
+ }
+ return res.status(400).json({ error: 'Refresh-Token erforderlich' });
}
- const token = generateToken(user);
- const csrfToken = getTokenForUser(user.id);
+ // Neuen Access-Token mit Refresh-Token generieren
+ const accessToken = await refreshAccessToken(refreshToken, ip, userAgent);
+ const db = getDb();
+
+ // User-Daten für CSRF-Token abrufen
+ const decoded = require('jsonwebtoken').decode(accessToken);
+ const csrfToken = getTokenForUser(decoded.id);
- res.json({ token, csrfToken });
+ res.json({
+ token: accessToken,
+ csrfToken
+ });
} catch (error) {
logger.error('Token-Refresh Fehler:', { error: error.message });
- res.status(500).json({ error: 'Interner Serverfehler' });
+ res.status(401).json({ error: 'Token-Erneuerung fehlgeschlagen' });
}
});
+// Legacy Refresh für Rückwärtskompatibilität
+function legacyRefresh(req, res) {
+ // Prüfe Authorization Header
+ const authHeader = req.headers['authorization'];
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({ error: 'Nicht authentifiziert' });
+ }
+
+ const token = authHeader.substring(7);
+ const user = require('../middleware/auth').verifyToken(token);
+
+ if (!user) {
+ return res.status(401).json({ error: 'Token ungültig' });
+ }
+
+ const db = getDb();
+ const dbUser = db.prepare('SELECT * FROM users WHERE id = ?').get(user.id);
+
+ if (!dbUser) {
+ return res.status(404).json({ error: 'Benutzer nicht gefunden' });
+ }
+
+ const newToken = generateToken(dbUser);
+ const csrfToken = getTokenForUser(dbUser.id);
+
+ res.json({ token: newToken, csrfToken });
+}
+
/**
* PUT /api/auth/password
* Passwort ändern
diff --git a/backend/routes/coding.js b/backend/routes/coding.js
new file mode 100644
index 0000000..361945e
--- /dev/null
+++ b/backend/routes/coding.js
@@ -0,0 +1,643 @@
+/**
+ * TASKMATE - Coding Routes
+ * ========================
+ * Verwaltung von Server-Anwendungen mit Claude/Codex Integration
+ */
+
+const express = require('express');
+const router = express.Router();
+const fs = require('fs');
+const path = require('path');
+const { getDb } = require('../database');
+const logger = require('../utils/logger');
+const gitService = require('../services/gitService');
+
+/**
+ * Prüft ob ein Pfad ein Server-Pfad (Linux) ist
+ */
+function isServerPath(localPath) {
+ return localPath && localPath.startsWith('/');
+}
+
+/**
+ * Schreibt CLAUDE.md in ein Verzeichnis
+ */
+function writeCLAUDEmd(directoryPath, content) {
+ if (!content || !directoryPath) return false;
+
+ try {
+ const claudePath = path.join(directoryPath, 'CLAUDE.md');
+ fs.writeFileSync(claudePath, content, 'utf8');
+ logger.info(`CLAUDE.md geschrieben: ${claudePath}`);
+ return true;
+ } catch (e) {
+ logger.error('CLAUDE.md schreiben fehlgeschlagen:', e);
+ return false;
+ }
+}
+
+/**
+ * Liest CLAUDE.md aus einem Verzeichnis
+ */
+function readCLAUDEmd(directoryPath) {
+ if (!directoryPath) {
+ logger.info('readCLAUDEmd: No directoryPath provided');
+ return null;
+ }
+
+ try {
+ const claudePath = path.join(directoryPath, 'CLAUDE.md');
+ logger.info(`readCLAUDEmd: Checking path ${claudePath}`);
+
+ if (fs.existsSync(claudePath)) {
+ const content = fs.readFileSync(claudePath, 'utf8');
+ logger.info(`readCLAUDEmd: Successfully read ${content.length} characters from ${claudePath}`);
+ return content;
+ } else {
+ logger.info(`readCLAUDEmd: File does not exist: ${claudePath}`);
+ }
+ } catch (e) {
+ logger.error('CLAUDE.md lesen fehlgeschlagen:', e);
+ }
+ return null;
+}
+
+/**
+ * GET /api/coding/directories
+ * Alle Coding-Verzeichnisse abrufen
+ */
+router.get('/directories', (req, res) => {
+ try {
+ const db = getDb();
+ const directories = db.prepare(`
+ SELECT cd.*, u.display_name as creator_name
+ FROM coding_directories cd
+ LEFT JOIN users u ON cd.created_by = u.id
+ ORDER BY cd.position ASC, cd.name ASC
+ `).all();
+
+ res.json(directories.map(dir => {
+ // CLAUDE.md aus dem Dateisystem lesen falls vorhanden
+ let claudeMdFromDisk = null;
+ if (isServerPath(dir.local_path)) {
+ claudeMdFromDisk = readCLAUDEmd(dir.local_path);
+ // Fallback: Wenn Pfad /home/claude-dev/TaskMate ist, versuche /app/taskmate-source
+ if (!claudeMdFromDisk && dir.local_path === '/home/claude-dev/TaskMate') {
+ logger.info('Trying fallback path for TaskMate: /app/taskmate-source');
+ claudeMdFromDisk = readCLAUDEmd('/app/taskmate-source');
+ }
+ }
+
+ return {
+ id: dir.id,
+ name: dir.name,
+ localPath: dir.local_path,
+ description: dir.description,
+ color: dir.color,
+ claudeInstructions: dir.claude_instructions,
+ claudeMdFromDisk: claudeMdFromDisk,
+ hasCLAUDEmd: !!claudeMdFromDisk,
+ giteaRepoUrl: dir.gitea_repo_url,
+ giteaRepoOwner: dir.gitea_repo_owner,
+ giteaRepoName: dir.gitea_repo_name,
+ defaultBranch: dir.default_branch,
+ lastSync: dir.last_sync,
+ position: dir.position,
+ createdAt: dir.created_at,
+ createdBy: dir.created_by,
+ creatorName: dir.creator_name
+ };
+ }));
+ } catch (error) {
+ logger.error('Fehler beim Abrufen der Coding-Verzeichnisse:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/directories
+ * Neues Coding-Verzeichnis erstellen
+ */
+router.post('/directories', (req, res) => {
+ try {
+ const { name, localPath, description, color, claudeInstructions, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch } = req.body;
+
+ if (!name || !localPath) {
+ return res.status(400).json({ error: 'Name und Server-Pfad sind erforderlich' });
+ }
+
+ const db = getDb();
+
+ // Prüfe ob Pfad bereits existiert
+ const existing = db.prepare('SELECT id FROM coding_directories WHERE local_path = ?').get(localPath);
+ if (existing) {
+ return res.status(400).json({ error: 'Diese Anwendung ist bereits registriert' });
+ }
+
+ // Höchste Position ermitteln
+ const maxPos = db.prepare('SELECT COALESCE(MAX(position), -1) as max FROM coding_directories').get().max;
+
+ const result = db.prepare(`
+ INSERT INTO coding_directories (name, local_path, description, color, claude_instructions, gitea_repo_url, gitea_repo_owner, gitea_repo_name, default_branch, position, created_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ name,
+ localPath,
+ description || null,
+ color || '#4F46E5',
+ null, // claudeInstructions wird nicht mehr gespeichert
+ giteaRepoUrl || null,
+ giteaRepoOwner || null,
+ giteaRepoName || null,
+ defaultBranch || 'main',
+ maxPos + 1,
+ req.user.id
+ );
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(result.lastInsertRowid);
+
+ // Ordner erstellen falls nicht vorhanden
+ let directoryCreated = false;
+ if (isServerPath(localPath) && !fs.existsSync(localPath)) {
+ try {
+ fs.mkdirSync(localPath, { recursive: true });
+ directoryCreated = true;
+ logger.info(`Anwendungsordner erstellt: ${localPath}`);
+ } catch (e) {
+ logger.error('Ordner erstellen fehlgeschlagen:', e);
+ }
+ }
+
+ // CLAUDE.md wird nicht mehr geschrieben - nur readonly
+ let claudeMdWritten = false;
+
+ logger.info(`Coding-Anwendung erstellt: ${name} (${localPath})`);
+
+ // CLAUDE.md aus dem Dateisystem lesen für aktuelle Anzeige
+ const claudeMdFromDisk = isServerPath(directory.local_path) ? readCLAUDEmd(directory.local_path) : null;
+
+ res.status(201).json({
+ id: directory.id,
+ name: directory.name,
+ localPath: directory.local_path,
+ description: directory.description,
+ color: directory.color,
+ claudeInstructions: directory.claude_instructions,
+ claudeMdFromDisk: claudeMdFromDisk,
+ hasCLAUDEmd: !!claudeMdFromDisk,
+ giteaRepoUrl: directory.gitea_repo_url,
+ giteaRepoOwner: directory.gitea_repo_owner,
+ giteaRepoName: directory.gitea_repo_name,
+ defaultBranch: directory.default_branch,
+ position: directory.position,
+ createdAt: directory.created_at,
+ directoryCreated,
+ claudeMdWritten
+ });
+ } catch (error) {
+ logger.error('Fehler beim Erstellen der Coding-Anwendung:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * PUT /api/coding/directories/:id
+ * Coding-Anwendung aktualisieren
+ */
+router.put('/directories/:id', (req, res) => {
+ try {
+ const { id } = req.params;
+ const { name, localPath, description, color, claudeInstructions, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch, position } = req.body;
+
+ const db = getDb();
+
+ const existing = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!existing) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ // Prüfe ob neuer Pfad bereits von anderem Eintrag verwendet wird
+ if (localPath && localPath !== existing.local_path) {
+ const pathExists = db.prepare('SELECT id FROM coding_directories WHERE local_path = ? AND id != ?').get(localPath, id);
+ if (pathExists) {
+ return res.status(400).json({ error: 'Dieser Server-Pfad ist bereits registriert' });
+ }
+ }
+
+ db.prepare(`
+ UPDATE coding_directories SET
+ name = COALESCE(?, name),
+ local_path = COALESCE(?, local_path),
+ description = ?,
+ color = COALESCE(?, color),
+ claude_instructions = ?,
+ gitea_repo_url = ?,
+ gitea_repo_owner = ?,
+ gitea_repo_name = ?,
+ default_branch = COALESCE(?, default_branch),
+ position = COALESCE(?, position)
+ WHERE id = ?
+ `).run(
+ name || null,
+ localPath || null,
+ description !== undefined ? description : existing.description,
+ color || null,
+ null, // claudeInstructions wird nicht mehr aktualisiert
+ giteaRepoUrl !== undefined ? giteaRepoUrl : existing.gitea_repo_url,
+ giteaRepoOwner !== undefined ? giteaRepoOwner : existing.gitea_repo_owner,
+ giteaRepoName !== undefined ? giteaRepoName : existing.gitea_repo_name,
+ defaultBranch || null,
+ position !== undefined ? position : null,
+ id
+ );
+
+ const updated = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ const finalPath = updated.local_path;
+
+ // Ordner erstellen falls nicht vorhanden
+ let directoryCreated = false;
+ if (isServerPath(finalPath) && !fs.existsSync(finalPath)) {
+ try {
+ fs.mkdirSync(finalPath, { recursive: true });
+ directoryCreated = true;
+ logger.info(`Anwendungsordner erstellt: ${finalPath}`);
+ } catch (e) {
+ logger.error('Ordner erstellen fehlgeschlagen:', e);
+ }
+ }
+
+ // CLAUDE.md wird nicht mehr geschrieben - nur readonly
+ let claudeMdWritten = false;
+
+ logger.info(`Coding-Anwendung aktualisiert: ${updated.name}`);
+
+ // CLAUDE.md aus dem Dateisystem lesen für aktuelle Anzeige
+ const claudeMdFromDisk = isServerPath(updated.local_path) ? readCLAUDEmd(updated.local_path) : null;
+
+ res.json({
+ id: updated.id,
+ name: updated.name,
+ localPath: updated.local_path,
+ description: updated.description,
+ color: updated.color,
+ claudeInstructions: updated.claude_instructions,
+ claudeMdFromDisk: claudeMdFromDisk,
+ hasCLAUDEmd: !!claudeMdFromDisk,
+ giteaRepoUrl: updated.gitea_repo_url,
+ giteaRepoOwner: updated.gitea_repo_owner,
+ giteaRepoName: updated.gitea_repo_name,
+ defaultBranch: updated.default_branch,
+ position: updated.position,
+ createdAt: updated.created_at,
+ directoryCreated,
+ claudeMdWritten
+ });
+ } catch (error) {
+ logger.error('Fehler beim Aktualisieren der Coding-Anwendung:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * DELETE /api/coding/directories/:id
+ * Coding-Anwendung löschen
+ */
+router.delete('/directories/:id', (req, res) => {
+ try {
+ const { id } = req.params;
+ const db = getDb();
+
+ const existing = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!existing) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ db.prepare('DELETE FROM coding_directories WHERE id = ?').run(id);
+
+ logger.info(`Coding-Anwendung gelöscht: ${existing.name}`);
+
+ res.json({ message: 'Anwendung gelöscht' });
+ } catch (error) {
+ logger.error('Fehler beim Löschen der Coding-Anwendung:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * GET /api/coding/directories/:id/status
+ * Git-Status eines Verzeichnisses abrufen
+ */
+router.get('/directories/:id/status', (req, res) => {
+ try {
+ const { id } = req.params;
+ const db = getDb();
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const localPath = directory.local_path;
+
+ // Prüfe ob es ein Git-Repository ist
+ if (!gitService.isGitRepository(localPath)) {
+ return res.json({
+ isGitRepo: false,
+ message: 'Kein Git-Repository'
+ });
+ }
+
+ const status = gitService.getStatus(localPath);
+
+ if (!status.success) {
+ return res.status(500).json({ error: status.error });
+ }
+
+ res.json({
+ isGitRepo: true,
+ branch: status.branch,
+ hasChanges: status.hasChanges,
+ changes: status.changes,
+ ahead: status.ahead,
+ behind: status.behind,
+ isClean: status.isClean
+ });
+ } catch (error) {
+ logger.error('Fehler beim Abrufen des Git-Status:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/directories/:id/fetch
+ * Git Fetch ausführen
+ */
+router.post('/directories/:id/fetch', (req, res) => {
+ try {
+ const { id } = req.params;
+ const db = getDb();
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const result = gitService.fetchRemote(directory.local_path);
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ // Last sync aktualisieren
+ db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id);
+
+ logger.info(`Git fetch ausgeführt für: ${directory.name}`);
+
+ res.json({ success: true, message: 'Fetch erfolgreich' });
+ } catch (error) {
+ logger.error('Fehler beim Git Fetch:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/directories/:id/pull
+ * Git Pull ausführen
+ */
+router.post('/directories/:id/pull', (req, res) => {
+ try {
+ const { id } = req.params;
+ const db = getDb();
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const result = gitService.pullChanges(directory.local_path, { branch: directory.default_branch });
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ // Last sync aktualisieren
+ db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id);
+
+ logger.info(`Git pull ausgeführt für: ${directory.name}`);
+
+ res.json({ success: true, message: 'Pull erfolgreich', output: result.output });
+ } catch (error) {
+ logger.error('Fehler beim Git Pull:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/directories/:id/push
+ * Git Push ausführen
+ */
+router.post('/directories/:id/push', (req, res) => {
+ try {
+ const { id } = req.params;
+ const { force } = req.body;
+ const db = getDb();
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const result = gitService.pushWithUpstream(directory.local_path, directory.default_branch, 'origin', force);
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ // Last sync aktualisieren
+ db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id);
+
+ logger.info(`Git push ausgeführt für: ${directory.name}`);
+
+ res.json({ success: true, message: 'Push erfolgreich', output: result.output });
+ } catch (error) {
+ logger.error('Fehler beim Git Push:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/directories/:id/commit
+ * Git Commit ausführen
+ */
+router.post('/directories/:id/commit', (req, res) => {
+ try {
+ const { id } = req.params;
+ const { message } = req.body;
+ const db = getDb();
+
+ if (!message || message.trim() === '') {
+ return res.status(400).json({ error: 'Commit-Nachricht erforderlich' });
+ }
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ // Stage all changes
+ const stageResult = gitService.stageAll(directory.local_path);
+ if (!stageResult.success) {
+ return res.status(500).json({ error: stageResult.error });
+ }
+
+ // Commit with author info
+ const author = {
+ name: req.user.display_name || req.user.username,
+ email: req.user.email || `${req.user.username}@taskmate.local`
+ };
+
+ const result = gitService.commit(directory.local_path, message, author);
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ logger.info(`Git commit ausgeführt für: ${directory.name} - "${message}"`);
+
+ res.json({ success: true, message: 'Commit erfolgreich', output: result.output });
+ } catch (error) {
+ logger.error('Fehler beim Git Commit:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * GET /api/coding/directories/:id/branches
+ * Branches abrufen
+ */
+router.get('/directories/:id/branches', (req, res) => {
+ try {
+ const { id } = req.params;
+ const db = getDb();
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const result = gitService.getBranches(directory.local_path);
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ res.json({ branches: result.branches });
+ } catch (error) {
+ logger.error('Fehler beim Abrufen der Branches:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/directories/:id/checkout
+ * Branch wechseln
+ */
+router.post('/directories/:id/checkout', (req, res) => {
+ try {
+ const { id } = req.params;
+ const { branch } = req.body;
+ const db = getDb();
+
+ if (!branch) {
+ return res.status(400).json({ error: 'Branch erforderlich' });
+ }
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const result = gitService.checkoutBranch(directory.local_path, branch);
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ logger.info(`Branch gewechselt für ${directory.name}: ${branch}`);
+
+ res.json({ success: true, message: `Gewechselt zu Branch: ${branch}` });
+ } catch (error) {
+ logger.error('Fehler beim Branch-Wechsel:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * POST /api/coding/validate-path
+ * Pfad validieren
+ */
+router.post('/validate-path', (req, res) => {
+ try {
+ const { path: localPath } = req.body;
+
+ if (!localPath) {
+ return res.status(400).json({ error: 'Pfad erforderlich' });
+ }
+
+ // Nur Server-Pfade können validiert werden
+ if (isServerPath(localPath)) {
+ const containerPath = gitService.windowsToContainerPath(localPath);
+ const exists = fs.existsSync(containerPath);
+ const isGitRepo = exists && gitService.isGitRepository(localPath);
+
+ res.json({
+ valid: true,
+ exists,
+ isGitRepo,
+ isServerPath: true
+ });
+ } else {
+ // Windows-Pfad kann nicht serverseitig validiert werden
+ res.json({
+ valid: true,
+ exists: null,
+ isGitRepo: null,
+ isServerPath: false,
+ message: 'Windows-Pfade können nicht serverseitig validiert werden'
+ });
+ }
+ } catch (error) {
+ logger.error('Fehler bei der Pfad-Validierung:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+/**
+ * GET /api/coding/directories/:id/commits
+ * Commit-Historie abrufen
+ */
+router.get('/directories/:id/commits', (req, res) => {
+ try {
+ const { id } = req.params;
+ const limit = parseInt(req.query.limit) || 20;
+ const db = getDb();
+
+ const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
+ if (!directory) {
+ return res.status(404).json({ error: 'Anwendung nicht gefunden' });
+ }
+
+ const result = gitService.getCommitHistory(directory.local_path, limit);
+
+ if (!result.success) {
+ return res.status(500).json({ error: result.error });
+ }
+
+ res.json({ commits: result.commits });
+ } catch (error) {
+ logger.error('Fehler beim Abrufen der Commit-Historie:', error);
+ res.status(500).json({ error: 'Interner Serverfehler' });
+ }
+});
+
+module.exports = router;
diff --git a/backend/server.js b/backend/server.js
index 11e77fb..5e06352 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -4,6 +4,9 @@
* Node.js/Express Backend mit Socket.io für Echtzeit-Sync
*/
+// Umgebungsvariablen laden (muss ganz oben stehen!)
+require('dotenv').config();
+
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
@@ -42,6 +45,7 @@ const gitRoutes = require('./routes/git');
const applicationsRoutes = require('./routes/applications');
const giteaRoutes = require('./routes/gitea');
const knowledgeRoutes = require('./routes/knowledge');
+const codingRoutes = require('./routes/coding');
// Express App erstellen
const app = express();
@@ -59,17 +63,18 @@ const io = new Server(server, {
// MIDDLEWARE
// =============================================================================
-// Sicherheits-Header
+// Erweiterte Sicherheits-Header (CSP temporär deaktiviert für Login-Fix)
app.use(helmet({
- contentSecurityPolicy: {
- directives: {
- defaultSrc: ["'self'"],
- styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
- fontSrc: ["'self'", "https://fonts.gstatic.com"],
- imgSrc: ["'self'", "data:", "blob:"],
- scriptSrc: ["'self'"],
- connectSrc: ["'self'", "ws:", "wss:"]
- }
+ contentSecurityPolicy: false, // Temporär deaktiviert
+ hsts: {
+ maxAge: 31536000, // 1 Jahr
+ includeSubDomains: true,
+ preload: true
+ },
+ noSniff: true,
+ xssFilter: true,
+ referrerPolicy: {
+ policy: "strict-origin-when-cross-origin"
}
}));
@@ -86,6 +91,10 @@ app.use(express.urlencoded({ extended: true, limit: '1mb' }));
// Cookie Parser
app.use(cookieParser());
+// Input Sanitization (vor allen anderen Middlewares)
+const { sanitizeMiddleware } = require('./middleware/validation');
+app.use(sanitizeMiddleware);
+
// Request Logging
app.use((req, res, next) => {
const start = Date.now();
@@ -148,6 +157,9 @@ app.use('/api/gitea', authenticateToken, csrfProtection, giteaRoutes);
// Knowledge-Routes (Wissensmanagement)
app.use('/api/knowledge', authenticateToken, csrfProtection, knowledgeRoutes);
+// Coding-Routes (Entwicklungsverzeichnisse mit Claude/Codex)
+app.use('/api/coding', authenticateToken, csrfProtection, codingRoutes);
+
// =============================================================================
// SOCKET.IO
// =============================================================================
diff --git a/backend/services/gitService.js b/backend/services/gitService.js
index c00caeb..b6a0832 100644
--- a/backend/services/gitService.js
+++ b/backend/services/gitService.js
@@ -21,6 +21,11 @@ function windowsToContainerPath(windowsPath) {
return windowsPath;
}
+ // Spezialfall: TaskMate-Verzeichnis ist als /app/taskmate-source gemountet
+ if (windowsPath === '/home/claude-dev/TaskMate') {
+ return '/app/taskmate-source';
+ }
+
// Windows-Pfad konvertieren (z.B. "C:\foo" oder "C:/foo")
const normalized = windowsPath.replace(/\\/g, '/');
const match = normalized.match(/^([a-zA-Z]):[\/](.*)$/);
@@ -73,8 +78,12 @@ function isGitRepository(localPath) {
const containerPath = windowsToContainerPath(localPath);
try {
const gitDir = path.join(containerPath, '.git');
- return fs.existsSync(gitDir);
+ logger.info(`Git-Repository Check: ${localPath} -> ${containerPath} -> ${gitDir}`);
+ const exists = fs.existsSync(gitDir);
+ logger.info(`Git directory exists: ${exists}`);
+ return exists;
} catch (error) {
+ logger.error(`Git-Repository Check failed: ${error.message}`);
return false;
}
}
diff --git a/backend/utils/backup.js b/backend/utils/backup.js
index a905fc1..cb3241a 100644
--- a/backend/utils/backup.js
+++ b/backend/utils/backup.js
@@ -7,6 +7,7 @@
const fs = require('fs');
const path = require('path');
const logger = require('./logger');
+const { encryptFile, decryptFile, secureDelete } = require('./encryption');
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');
const BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups');
@@ -17,8 +18,9 @@ if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}
+
/**
- * Backup erstellen
+ * Backup erstellen (mit einfacher Verschlüsselung)
*/
function createBackup() {
try {
@@ -29,12 +31,27 @@ function createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `backup_${timestamp}.db`;
+ const encryptedName = `backup_${timestamp}.db.enc`;
const backupPath = path.join(BACKUP_DIR, backupName);
+ const encryptedPath = path.join(BACKUP_DIR, encryptedName);
- // Datenbank kopieren
+ // 1. Normales Backup erstellen (für Kompatibilität)
fs.copyFileSync(DB_FILE, backupPath);
- // WAL-Datei auch sichern falls vorhanden
+ // 2. Verschlüsseltes Backup erstellen (zusätzlich)
+ if (process.env.ENCRYPTION_KEY) {
+ try {
+ if (encryptFile(DB_FILE, encryptedPath)) {
+ logger.info(`Verschlüsseltes Backup erstellt: ${encryptedName}`);
+ } else {
+ logger.warn('Verschlüsselung fehlgeschlagen, nur normales Backup erstellt');
+ }
+ } catch (encError) {
+ logger.warn('Verschlüsselung fehlgeschlagen, nur normales Backup erstellt');
+ }
+ }
+
+ // 3. WAL-Datei sichern falls vorhanden
const walFile = DB_FILE + '-wal';
if (fs.existsSync(walFile)) {
fs.copyFileSync(walFile, backupPath + '-wal');
@@ -53,12 +70,12 @@ function createBackup() {
}
/**
- * Alte Backups löschen
+ * Alte Backups löschen (verschlüsselte)
*/
function cleanupOldBackups(keepCount = 30) {
try {
const files = fs.readdirSync(BACKUP_DIR)
- .filter(f => f.startsWith('backup_') && f.endsWith('.db'))
+ .filter(f => f.startsWith('backup_') && f.endsWith('.db.enc'))
.sort()
.reverse();
@@ -66,15 +83,15 @@ function cleanupOldBackups(keepCount = 30) {
toDelete.forEach(file => {
const filePath = path.join(BACKUP_DIR, file);
- fs.unlinkSync(filePath);
+ secureDelete(filePath);
- // WAL-Datei auch löschen falls vorhanden
+ // Verschlüsselte WAL-Datei auch löschen falls vorhanden
const walPath = filePath + '-wal';
if (fs.existsSync(walPath)) {
- fs.unlinkSync(walPath);
+ secureDelete(walPath);
}
- logger.info(`Altes Backup gelöscht: ${file}`);
+ logger.info(`Altes Backup sicher gelöscht: ${file}`);
});
} catch (error) {
logger.error('Fehler beim Aufräumen alter Backups:', { error: error.message });
@@ -82,32 +99,50 @@ function cleanupOldBackups(keepCount = 30) {
}
/**
- * Backup wiederherstellen
+ * Backup wiederherstellen (entschlüsselt)
*/
function restoreBackup(backupName) {
try {
- const backupPath = path.join(BACKUP_DIR, backupName);
+ const encryptedBackupPath = path.join(BACKUP_DIR, backupName);
- if (!fs.existsSync(backupPath)) {
+ if (!fs.existsSync(encryptedBackupPath)) {
throw new Error(`Backup nicht gefunden: ${backupName}`);
}
- // Aktuelles DB sichern bevor überschrieben wird
+ // Aktuelles DB verschlüsselt sichern bevor überschrieben wird
if (fs.existsSync(DB_FILE)) {
- const safetyBackup = DB_FILE + '.before-restore';
- fs.copyFileSync(DB_FILE, safetyBackup);
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+ const safetyBackupPath = DB_FILE + `.before-restore-${timestamp}.enc`;
+ if (!encryptFile(DB_FILE, safetyBackupPath)) {
+ logger.warn('Sicherheitsbackup vor Wiederherstellung fehlgeschlagen');
+ }
}
- // Backup wiederherstellen
- fs.copyFileSync(backupPath, DB_FILE);
+ // Temporäre entschlüsselte Datei
+ const tempRestorePath = path.join(BACKUP_DIR, `temp_restore_${Date.now()}.db`);
+
+ // Backup entschlüsseln
+ if (!decryptFile(encryptedBackupPath, tempRestorePath)) {
+ throw new Error('Backup-Entschlüsselung fehlgeschlagen');
+ }
+
+ // Entschlüsselte DB kopieren
+ fs.copyFileSync(tempRestorePath, DB_FILE);
// WAL-Datei auch wiederherstellen falls vorhanden
- const walBackup = backupPath + '-wal';
- if (fs.existsSync(walBackup)) {
- fs.copyFileSync(walBackup, DB_FILE + '-wal');
+ const encryptedWalBackup = encryptedBackupPath + '-wal';
+ if (fs.existsSync(encryptedWalBackup)) {
+ const tempWalPath = tempRestorePath + '-wal';
+ if (decryptFile(encryptedWalBackup, tempWalPath)) {
+ fs.copyFileSync(tempWalPath, DB_FILE + '-wal');
+ secureDelete(tempWalPath);
+ }
}
- logger.info(`Backup wiederhergestellt: ${backupName}`);
+ // Temporäre entschlüsselte Dateien sicher löschen
+ secureDelete(tempRestorePath);
+
+ logger.info(`Verschlüsseltes Backup wiederhergestellt: ${backupName}`);
return true;
} catch (error) {
logger.error('Restore-Fehler:', { error: error.message });
@@ -116,12 +151,12 @@ function restoreBackup(backupName) {
}
/**
- * Liste aller Backups
+ * Liste aller verschlüsselten Backups
*/
function listBackups() {
try {
const files = fs.readdirSync(BACKUP_DIR)
- .filter(f => f.startsWith('backup_') && f.endsWith('.db'))
+ .filter(f => f.startsWith('backup_') && f.endsWith('.db.enc'))
.map(f => {
const filePath = path.join(BACKUP_DIR, f);
const stats = fs.statSync(filePath);
diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js
new file mode 100644
index 0000000..800c09b
--- /dev/null
+++ b/backend/utils/encryption.js
@@ -0,0 +1,237 @@
+/**
+ * TASKMATE - Encryption Utilities
+ * ================================
+ * Verschlüsselung für Backups und sensitive Daten
+ */
+
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const { promisify } = require('util');
+const logger = require('./logger');
+
+const ALGORITHM = 'aes-256-cbc';
+const KEY_LENGTH = 32; // 256 bits
+const IV_LENGTH = 16; // 128 bits
+const SALT_LENGTH = 32;
+const TAG_LENGTH = 16;
+
+/**
+ * Encryption Key aus Umgebung oder generiert
+ */
+function getEncryptionKey() {
+ let key = process.env.ENCRYPTION_KEY;
+
+ if (!key) {
+ // Generiere neuen Key falls nicht vorhanden
+ key = crypto.randomBytes(KEY_LENGTH).toString('hex');
+ logger.warn('Encryption Key wurde automatisch generiert. Speichere ihn in der .env: ENCRYPTION_KEY=' + key);
+ return Buffer.from(key, 'hex');
+ }
+
+ // Validiere Key-Length
+ if (key.length !== KEY_LENGTH * 2) { // Hex-String ist doppelt so lang
+ throw new Error(`Encryption Key muss ${KEY_LENGTH * 2} Hex-Zeichen haben`);
+ }
+
+ return Buffer.from(key, 'hex');
+}
+
+/**
+ * Key aus Passwort ableiten (PBKDF2)
+ */
+function deriveKeyFromPassword(password, salt) {
+ return crypto.pbkdf2Sync(password, salt, 100000, KEY_LENGTH, 'sha256');
+}
+
+/**
+ * Datei verschlüsseln
+ */
+function encryptFile(inputPath, outputPath, password = null) {
+ try {
+ const data = fs.readFileSync(inputPath);
+
+ // Salt und IV generieren
+ const salt = crypto.randomBytes(SALT_LENGTH);
+ const iv = crypto.randomBytes(IV_LENGTH);
+
+ // Key ableiten
+ const key = password
+ ? deriveKeyFromPassword(password, salt)
+ : getEncryptionKey();
+
+ // Verschlüsselung
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
+
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
+
+ // Header + Salt + IV + verschlüsselte Daten
+ const header = Buffer.from('TMENC001', 'ascii'); // TaskMate Encryption v1
+ const result = Buffer.concat([
+ header,
+ salt,
+ iv,
+ encrypted
+ ]);
+
+ fs.writeFileSync(outputPath, result);
+ logger.info(`Datei verschlüsselt: ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
+ return true;
+
+ } catch (error) {
+ logger.error(`Verschlüsselung fehlgeschlagen: ${error.message}`);
+ return false;
+ }
+}
+
+/**
+ * Datei entschlüsseln
+ */
+function decryptFile(inputPath, outputPath, password = null) {
+ try {
+ const encryptedData = fs.readFileSync(inputPath);
+
+ // Header prüfen
+ const header = encryptedData.subarray(0, 8);
+ if (header.toString('ascii') !== 'TMENC001') {
+ throw new Error('Ungültiges verschlüsseltes Datei-Format');
+ }
+
+ // Komponenten extrahieren
+ let offset = 8;
+ const salt = encryptedData.subarray(offset, offset + SALT_LENGTH);
+ offset += SALT_LENGTH;
+
+ const iv = encryptedData.subarray(offset, offset + IV_LENGTH);
+ offset += IV_LENGTH;
+
+ const encrypted = encryptedData.subarray(offset);
+
+ // Key ableiten
+ const key = password
+ ? deriveKeyFromPassword(password, salt)
+ : getEncryptionKey();
+
+ // Entschlüsselung
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
+
+ const decrypted = Buffer.concat([
+ decipher.update(encrypted),
+ decipher.final()
+ ]);
+
+ fs.writeFileSync(outputPath, decrypted);
+ logger.info(`Datei entschlüsselt: ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
+ return true;
+
+ } catch (error) {
+ logger.error(`Entschlüsselung fehlgeschlagen: ${error.message}`);
+ return false;
+ }
+}
+
+/**
+ * String verschlüsseln (für Passwörter etc.)
+ */
+function encryptString(plaintext, password = null) {
+ try {
+ const salt = crypto.randomBytes(SALT_LENGTH);
+ const iv = crypto.randomBytes(IV_LENGTH);
+
+ const key = password
+ ? deriveKeyFromPassword(password, salt)
+ : getEncryptionKey();
+
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
+
+ const encrypted = Buffer.concat([
+ cipher.update(Buffer.from(plaintext, 'utf8')),
+ cipher.final()
+ ]);
+
+ // Base64 kodiert zurückgeben
+ const result = Buffer.concat([salt, iv, encrypted]);
+ return result.toString('base64');
+
+ } catch (error) {
+ logger.error(`String-Verschlüsselung fehlgeschlagen: ${error.message}`);
+ return null;
+ }
+}
+
+/**
+ * String entschlüsseln
+ */
+function decryptString(encryptedString, password = null) {
+ try {
+ const data = Buffer.from(encryptedString, 'base64');
+
+ let offset = 0;
+ const salt = data.subarray(offset, offset + SALT_LENGTH);
+ offset += SALT_LENGTH;
+
+ const iv = data.subarray(offset, offset + IV_LENGTH);
+ offset += IV_LENGTH;
+
+ const encrypted = data.subarray(offset);
+
+ const key = password
+ ? deriveKeyFromPassword(password, salt)
+ : getEncryptionKey();
+
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
+
+ const decrypted = Buffer.concat([
+ decipher.update(encrypted),
+ decipher.final()
+ ]);
+
+ return decrypted.toString('utf8');
+
+ } catch (error) {
+ logger.error(`String-Entschlüsselung fehlgeschlagen: ${error.message}`);
+ return null;
+ }
+}
+
+/**
+ * Sicheres Löschen einer Datei (Überschreiben)
+ */
+function secureDelete(filePath) {
+ try {
+ if (!fs.existsSync(filePath)) {
+ return true;
+ }
+
+ const stats = fs.statSync(filePath);
+ const fileSize = stats.size;
+
+ // Datei mehrfach mit Zufallsdaten überschreiben
+ const fd = fs.openSync(filePath, 'r+');
+
+ for (let pass = 0; pass < 3; pass++) {
+ const randomData = crypto.randomBytes(fileSize);
+ fs.writeSync(fd, randomData, 0, fileSize, 0);
+ fs.fsyncSync(fd);
+ }
+
+ fs.closeSync(fd);
+ fs.unlinkSync(filePath);
+
+ logger.info(`Datei sicher gelöscht: ${path.basename(filePath)}`);
+ return true;
+
+ } catch (error) {
+ logger.error(`Sicheres Löschen fehlgeschlagen: ${error.message}`);
+ return false;
+ }
+}
+
+module.exports = {
+ encryptFile,
+ decryptFile,
+ encryptString,
+ decryptString,
+ secureDelete,
+ getEncryptionKey
+};
\ No newline at end of file
diff --git a/data/taskmate.db b/data/taskmate.db
index 9a05d3e..91dd894 100644
Binary files a/data/taskmate.db and b/data/taskmate.db differ
diff --git a/data/taskmate.db-shm b/data/taskmate.db-shm
index 7cc860a..2c6b23b 100644
Binary files a/data/taskmate.db-shm and b/data/taskmate.db-shm differ
diff --git a/data/taskmate.db-wal b/data/taskmate.db-wal
index 185ffd4..bf97269 100644
Binary files a/data/taskmate.db-wal and b/data/taskmate.db-wal differ
diff --git a/docker-compose.yml b/docker-compose.yml
index c3977c6..cba6265 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -31,6 +31,7 @@ services:
- USER2_PASSWORD=${USER2_PASSWORD:-changeme456}
- USER2_DISPLAYNAME=${USER2_DISPLAYNAME:-Benutzer 2}
- USER2_COLOR=${USER2_COLOR:-#FF9500}
+ - ENCRYPTION_KEY=${ENCRYPTION_KEY}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
diff --git a/frontend/css/admin.css b/frontend/css/admin.css
index f27bf6e..dedde1c 100644
--- a/frontend/css/admin.css
+++ b/frontend/css/admin.css
@@ -431,81 +431,163 @@
font-weight: var(--font-medium);
}
-/* Upload Types */
-.admin-upload-types {
+/* =========================
+ Extension Settings (New)
+ ========================= */
+
+.admin-upload-extensions {
margin-bottom: 1.5rem;
}
-.admin-upload-types h3 {
+.admin-upload-extensions h3 {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
margin: 0 0 1rem 0;
}
-/* Upload Category */
-.upload-category {
+/* Extension Tags Container */
+.extension-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ padding: 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
- margin-bottom: 0.75rem;
- overflow: hidden;
- transition: all var(--transition-fast);
+ min-height: 50px;
+ margin-bottom: 1rem;
}
-.upload-category:hover {
- border-color: var(--border-default);
+.extension-empty {
+ color: var(--text-muted);
+ font-size: var(--text-sm);
+ font-style: italic;
}
-.upload-category.disabled {
- opacity: 0.5;
+/* Single Extension Tag */
+.extension-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 6px 10px;
+ background: var(--primary);
+ color: white;
+ border-radius: var(--radius-full);
+ font-size: var(--text-sm);
+ font-weight: var(--font-medium);
+ font-family: var(--font-mono, monospace);
}
-.upload-category.disabled .upload-category-types {
- display: none;
-}
-
-.upload-category-header {
- padding: 0.75rem 1rem;
- background: var(--bg-card);
-}
-
-.upload-category-toggle {
+.extension-tag-remove {
display: flex;
align-items: center;
- gap: 0.75rem;
- cursor: pointer;
- user-select: none;
-}
-
-.upload-category-toggle input[type="checkbox"] {
+ justify-content: center;
width: 18px;
height: 18px;
- accent-color: var(--primary);
+ padding: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ border-radius: 50%;
cursor: pointer;
+ transition: background var(--transition-fast);
}
-.upload-category-title {
+.extension-tag-remove:hover {
+ background: rgba(255, 255, 255, 0.4);
+}
+
+.extension-tag-remove svg {
+ stroke: white;
+}
+
+/* Add Extension Group */
+.extension-add-group {
+ margin-bottom: 1rem;
+}
+
+.extension-add-group label {
+ display: block;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
+ margin-bottom: 0.5rem;
}
-.upload-category-types {
- padding: 0.75rem 1rem;
+.extension-input-row {
+ display: flex;
+ gap: 0.5rem;
+ max-width: 400px;
+}
+
+.extension-input {
+ flex: 1;
+ padding: 8px 12px;
+ background: var(--bg-input);
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-lg);
+ color: var(--text-primary);
+ font-size: var(--text-sm);
+ font-family: var(--font-mono, monospace);
+}
+
+.extension-input:focus {
+ border-color: var(--primary);
+ outline: none;
+ box-shadow: var(--shadow-focus);
+}
+
+.extension-input::placeholder {
+ color: var(--text-muted);
+ font-family: var(--font-primary);
+}
+
+/* Suggestions */
+.extension-suggestions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 1rem;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-lg);
+}
+
+.extension-suggestions-label {
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+ font-weight: var(--font-medium);
+ margin-right: 0.5rem;
+}
+
+.extension-suggestions-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
-.upload-type-tag {
- display: inline-block;
+.extension-suggestion {
padding: 4px 10px;
- background: var(--primary-light);
- color: var(--primary);
+ background: var(--bg-tertiary);
+ border: 1px dashed var(--border-default);
border-radius: var(--radius-full);
+ color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: var(--font-medium);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.extension-suggestion:hover {
+ background: var(--primary-light);
+ border-color: var(--primary);
+ color: var(--primary);
+}
+
+.extension-no-suggestions {
+ font-size: var(--text-xs);
+ color: var(--text-muted);
+ font-style: italic;
}
/* Upload Actions */
@@ -515,6 +597,30 @@
}
/* Responsive */
+/* Passwort-Input Gruppe */
+.password-input-group {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.password-input-group input {
+ flex: 1;
+}
+
+.password-input-group .btn {
+ padding: 0.5rem;
+ min-width: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.password-input-group .btn svg {
+ width: 16px;
+ height: 16px;
+}
+
@media (max-width: 768px) {
.admin-header {
padding: 1rem;
diff --git a/frontend/css/coding.css b/frontend/css/coding.css
new file mode 100644
index 0000000..abcef9d
--- /dev/null
+++ b/frontend/css/coding.css
@@ -0,0 +1,555 @@
+/**
+ * TASKMATE - Coding Tab Styles
+ * ============================
+ * Styling für die Coding-Verzeichnis-Verwaltung
+ */
+
+/* View Container */
+.view-coding {
+ padding: 1.5rem;
+ max-width: 1600px;
+ margin: 0 auto;
+}
+
+/* Header */
+.coding-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.coding-header-centered {
+ justify-content: center;
+}
+
+.coding-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+/* Grid Layout */
+.coding-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 1.5rem;
+}
+
+/* Kachel */
+.coding-tile {
+ background: var(--bg-secondary);
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ transition: transform 0.2s, box-shadow 0.2s;
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+}
+
+.coding-tile:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+/* Farbbalken oben */
+.coding-tile-color {
+ height: 4px;
+ flex-shrink: 0;
+}
+
+/* Tile Header */
+.coding-tile-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 1rem 1rem 0.5rem;
+}
+
+.coding-tile-icon {
+ font-size: 2rem;
+ line-height: 1;
+}
+
+.coding-tile-menu {
+ background: none;
+ border: none;
+ padding: 0.25rem;
+ cursor: pointer;
+ color: var(--text-muted);
+ border-radius: 4px;
+ transition: background 0.2s, color 0.2s;
+}
+
+.coding-tile-menu:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+/* Tile Content */
+.coding-tile-content {
+ padding: 0 1rem;
+ flex-grow: 1;
+}
+
+.coding-tile-name {
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin-bottom: 0.25rem;
+ color: var(--text-primary);
+}
+
+.coding-tile-path {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-family: var(--font-mono, monospace);
+ word-break: break-all;
+ line-height: 1.4;
+}
+
+.coding-tile-description {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ margin-top: 0.5rem;
+ line-height: 1.4;
+}
+
+/* CLAUDE.md Badge */
+.coding-tile-badge {
+ display: inline-block;
+ background: linear-gradient(135deg, #F59E0B, #D97706);
+ color: white;
+ font-size: 0.65rem;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ margin-top: 0.5rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
+/* Git Status */
+.coding-tile-status {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.git-branch-badge {
+ background: var(--bg-tertiary);
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-family: var(--font-mono, monospace);
+ color: var(--text-secondary);
+}
+
+.git-status-badge {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.git-status-badge.loading {
+ background: var(--bg-tertiary);
+ color: var(--text-muted);
+}
+
+.git-status-badge.clean {
+ background: rgba(16, 185, 129, 0.15);
+ color: #10B981;
+}
+
+.git-status-badge.dirty {
+ background: rgba(245, 158, 11, 0.15);
+ color: #F59E0B;
+}
+
+.git-status-badge.error {
+ background: rgba(239, 68, 68, 0.15);
+ color: #EF4444;
+}
+
+.git-status-badge.ahead {
+ background: rgba(59, 130, 246, 0.15);
+ color: #3B82F6;
+}
+
+.git-status-badge.behind {
+ background: rgba(139, 92, 246, 0.15);
+ color: #8B5CF6;
+}
+
+/* Action Buttons */
+.coding-tile-actions {
+ display: flex;
+ gap: 0.75rem;
+ padding: 1rem;
+}
+
+.btn-claude {
+ flex: 1;
+ background: linear-gradient(135deg, #F59E0B, #D97706);
+ color: white;
+ border: none;
+ padding: 0.75rem;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: opacity 0.2s, transform 0.2s;
+}
+
+.btn-claude:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+}
+
+.btn-claude:active {
+ transform: translateY(0);
+}
+
+.btn-codex {
+ flex: 1;
+ background: linear-gradient(135deg, #10B981, #059669);
+ color: white;
+ border: none;
+ padding: 0.75rem;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: opacity 0.2s, transform 0.2s;
+}
+
+.btn-codex:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+}
+
+.btn-codex:active {
+ transform: translateY(0);
+}
+
+/* Git Actions */
+.coding-tile-git {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0 1rem 1rem;
+ border-top: 1px solid var(--border-color);
+ padding-top: 0.75rem;
+}
+
+.coding-tile-git .btn {
+ flex: 1;
+ font-size: 0.75rem;
+ padding: 0.5rem;
+}
+
+/* Empty State */
+.coding-empty {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: var(--text-muted);
+}
+
+.coding-empty .empty-icon {
+ margin-bottom: 1rem;
+ color: var(--text-muted);
+ opacity: 0.5;
+}
+
+.coding-empty .empty-icon svg {
+ width: 64px;
+ height: 64px;
+}
+
+.coding-empty h3 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ color: var(--text-secondary);
+}
+
+.coding-empty p {
+ font-size: 0.875rem;
+ max-width: 400px;
+ margin: 0 auto;
+}
+
+/* Command Modal */
+.command-box {
+ background: var(--bg-tertiary);
+ padding: 1rem;
+ border-radius: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 0.75rem;
+}
+
+.command-box code {
+ font-family: var(--font-mono, monospace);
+ font-size: 0.875rem;
+ word-break: break-all;
+ flex: 1;
+ color: var(--text-primary);
+}
+
+/* Color Presets */
+.color-presets {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.color-preset {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: transform 0.2s, border-color 0.2s;
+}
+
+.color-preset:hover {
+ transform: scale(1.1);
+}
+
+.color-preset.selected {
+ border-color: var(--text-primary);
+ box-shadow: 0 0 0 2px var(--bg-secondary);
+}
+
+.color-picker-custom {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ overflow: hidden;
+}
+
+.color-picker-custom::-webkit-color-swatch-wrapper {
+ padding: 0;
+}
+
+.color-picker-custom::-webkit-color-swatch {
+ border: none;
+ border-radius: 50%;
+}
+
+/* Gitea Section in Modal */
+.coding-gitea-section {
+ margin-top: 1rem;
+ padding: 1rem;
+ background: var(--bg-tertiary);
+ border-radius: 8px;
+}
+
+.coding-gitea-section summary {
+ cursor: pointer;
+ font-weight: 500;
+ color: var(--text-secondary);
+ user-select: none;
+}
+
+.coding-gitea-section summary:hover {
+ color: var(--text-primary);
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .view-coding {
+ padding: 1rem;
+ }
+
+ .coding-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .coding-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: stretch;
+ }
+
+ .coding-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .coding-header .btn {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .coding-tile-actions {
+ flex-direction: column;
+ }
+
+ .coding-tile-git {
+ flex-wrap: wrap;
+ }
+
+ .coding-tile-git .btn {
+ flex: 1 1 calc(50% - 0.25rem);
+ }
+
+ .command-box {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .command-box code {
+ text-align: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .coding-empty {
+ padding: 2rem 1rem;
+ }
+
+ .coding-tile-git .btn {
+ flex: 1 1 100%;
+ }
+}
+
+/* CLAUDE.md Textarea im Modal */
+#coding-claude-instructions {
+ font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
+ font-size: 0.85rem;
+ line-height: 1.5;
+ resize: vertical;
+ min-height: 200px;
+}
+
+/* Hint unter Labels */
+.label-hint {
+ font-weight: normal;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-left: 0.5rem;
+}
+
+.form-hint {
+ display: block;
+ margin-top: 0.25rem;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+}
+
+/* CLAUDE.md Tabs */
+.claude-tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.claude-tab {
+ padding: 0.5rem 1rem;
+ border: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+ border-radius: 6px 6px 0 0;
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 500;
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
+}
+
+.claude-tab:hover {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.claude-tab.active {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border-bottom-color: var(--bg-secondary);
+}
+
+.claude-content {
+ display: block;
+}
+
+.claude-content.active {
+ display: block;
+}
+
+/* CLAUDE.md Link */
+.claude-link-container {
+ margin-top: 0.5rem;
+}
+
+.claude-link {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 0.75rem 1rem;
+ width: 100%;
+ text-align: left;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+}
+
+.claude-link:hover:not(:disabled) {
+ background: var(--bg-secondary);
+ border-color: var(--primary-color);
+}
+
+.claude-link:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.claude-icon {
+ font-size: 1.1rem;
+}
+
+.claude-text {
+ color: var(--text-primary);
+}
+
+.claude-link:disabled .claude-text {
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+/* CLAUDE.md Modal */
+.claude-md-viewer {
+ width: 100%;
+ height: 70vh;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.claude-md-display {
+ width: 100%;
+ height: 100%;
+ background: var(--bg-tertiary);
+ padding: 1.5rem;
+ font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
+ font-size: 0.85rem;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-y: auto;
+ color: var(--text-primary);
+ margin: 0;
+ border: none;
+ outline: none;
+ resize: none;
+}
diff --git a/frontend/css/list.css b/frontend/css/list.css
index 97eb6b1..d0b75a2 100644
--- a/frontend/css/list.css
+++ b/frontend/css/list.css
@@ -413,6 +413,14 @@
gap: var(--spacing-2);
}
+/* Avatar Container für mehrere Avatare */
+.list-cell-assignee .avatar-container {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ flex-shrink: 0;
+}
+
.list-cell-assignee .avatar {
width: 24px;
height: 24px;
@@ -424,6 +432,12 @@
font-weight: var(--font-semibold);
color: white;
flex-shrink: 0;
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.list-cell-assignee .avatar:hover {
+ transform: scale(1.1);
}
.list-cell-assignee select {
@@ -440,6 +454,28 @@
outline: none;
}
+/* Hide assignee dropdown - show only avatars */
+.list-cell-assignee .assignee-select {
+ display: none;
+}
+
+/* Empty avatar placeholder */
+.list-cell-assignee .avatar-empty {
+ background: var(--border-color) !important;
+ color: var(--text-muted);
+ border: 1px solid var(--border-light);
+}
+
+/* Show dropdown when editing */
+.list-cell-assignee.editing .assignee-select {
+ display: block;
+ flex: 1;
+}
+
+.list-cell-assignee.editing .avatar-container {
+ display: none;
+}
+
/* Empty State */
.list-empty {
display: flex;
diff --git a/frontend/css/mobile.css b/frontend/css/mobile.css
new file mode 100644
index 0000000..73ab1b8
--- /dev/null
+++ b/frontend/css/mobile.css
@@ -0,0 +1,472 @@
+/**
+ * TASKMATE - Mobile Styles
+ * ========================
+ * Touch-optimierte Mobile-Erfahrung
+ * Nur auf mobilen Breakpoints angewendet
+ */
+
+/* ========================================
+ DESKTOP: Mobile-Elemente verstecken
+ ======================================== */
+
+@media (min-width: 769px) {
+ .hamburger-btn,
+ .mobile-menu,
+ .mobile-menu-overlay,
+ .swipe-indicator {
+ display: none !important;
+ }
+}
+
+/* ========================================
+ MOBILE STYLES (max-width: 768px)
+ ======================================== */
+
+@media (max-width: 768px) {
+
+ /* ========================================
+ HAMBURGER BUTTON
+ ======================================== */
+
+ .hamburger-btn {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 44px;
+ height: 44px;
+ padding: 10px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ z-index: calc(var(--z-modal) + 10);
+ position: relative;
+ flex-shrink: 0;
+ }
+
+ .hamburger-line {
+ display: block;
+ width: 24px;
+ height: 2px;
+ background: var(--text-primary);
+ border-radius: 2px;
+ transition: all 0.3s ease;
+ }
+
+ .hamburger-line + .hamburger-line {
+ margin-top: 6px;
+ }
+
+ /* Hamburger zu X Animation */
+ .hamburger-btn.active .hamburger-line:nth-child(1) {
+ transform: translateY(8px) rotate(45deg);
+ }
+
+ .hamburger-btn.active .hamburger-line:nth-child(2) {
+ opacity: 0;
+ transform: scaleX(0);
+ }
+
+ .hamburger-btn.active .hamburger-line:nth-child(3) {
+ transform: translateY(-8px) rotate(-45deg);
+ }
+
+ /* ========================================
+ MOBILE SLIDE-IN MENU
+ ======================================== */
+
+ .mobile-menu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 280px;
+ max-width: 85vw;
+ height: 100vh;
+ height: 100dvh;
+ background: var(--bg-card);
+ box-shadow: var(--shadow-xl);
+ z-index: var(--z-modal);
+ transform: translateX(-100%);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .mobile-menu.open {
+ transform: translateX(0);
+ }
+
+ /* Overlay */
+ .mobile-menu-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--overlay-bg);
+ opacity: 0;
+ visibility: hidden;
+ z-index: calc(var(--z-modal) - 1);
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+ }
+
+ .mobile-menu-overlay.visible {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ /* Menu Header */
+ .mobile-menu-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--spacing-4);
+ border-bottom: 1px solid var(--border-light);
+ flex-shrink: 0;
+ }
+
+ .mobile-menu-title {
+ font-size: var(--text-lg);
+ font-weight: var(--font-bold);
+ color: var(--primary);
+ margin: 0;
+ }
+
+ .mobile-menu-close {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-radius: var(--radius-md);
+ transition: background 0.2s;
+ font-size: 1.5rem;
+ }
+
+ .mobile-menu-close:hover,
+ .mobile-menu-close:active {
+ background: var(--bg-hover);
+ }
+
+ /* Menu Sections */
+ .mobile-menu-section {
+ padding: var(--spacing-4);
+ border-bottom: 1px solid var(--border-light);
+ }
+
+ .mobile-menu-label {
+ display: block;
+ font-size: var(--text-xs);
+ font-weight: var(--font-semibold);
+ color: var(--text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: var(--spacing-2);
+ }
+
+ /* Project Select */
+ .mobile-project-select {
+ width: 100%;
+ padding: var(--spacing-3);
+ font-size: var(--text-base);
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-md);
+ background: var(--bg-input);
+ color: var(--text-primary);
+ font-family: var(--font-primary);
+ }
+
+ /* Navigation */
+ .mobile-menu-nav {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-1);
+ }
+
+ .mobile-nav-item {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-3);
+ padding: var(--spacing-3) var(--spacing-4);
+ font-size: var(--text-base);
+ font-weight: var(--font-medium);
+ color: var(--text-secondary);
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all 0.2s;
+ text-align: left;
+ width: 100%;
+ }
+
+ .mobile-nav-item:hover,
+ .mobile-nav-item:active {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ }
+
+ .mobile-nav-item.active {
+ background: var(--primary-light);
+ color: var(--primary);
+ }
+
+ .mobile-nav-item svg {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ }
+
+ /* User Section */
+ .mobile-menu-user {
+ margin-top: auto;
+ padding: var(--spacing-4);
+ border-top: 1px solid var(--border-light);
+ }
+
+ .mobile-user-info {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-3);
+ margin-bottom: var(--spacing-4);
+ }
+
+ .mobile-user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: var(--radius-full);
+ background: var(--primary);
+ color: var(--text-inverse);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: var(--font-semibold);
+ flex-shrink: 0;
+ }
+
+ .mobile-user-details {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .mobile-user-name {
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .mobile-user-role {
+ font-size: var(--text-sm);
+ color: var(--text-tertiary);
+ }
+
+ .mobile-menu-btn {
+ width: 100%;
+ padding: var(--spacing-3);
+ font-size: var(--text-sm);
+ font-weight: var(--font-medium);
+ background: var(--bg-tertiary);
+ border: none;
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ cursor: pointer;
+ margin-bottom: var(--spacing-2);
+ transition: background 0.2s;
+ }
+
+ .mobile-menu-btn:hover,
+ .mobile-menu-btn:active {
+ background: var(--bg-hover);
+ }
+
+ .mobile-menu-btn-danger {
+ color: var(--error);
+ }
+
+ .mobile-menu-btn-danger:hover,
+ .mobile-menu-btn-danger:active {
+ background: var(--error-bg);
+ }
+
+ /* ========================================
+ HEADER ANPASSUNGEN
+ ======================================== */
+
+ /* Desktop-Navigation verstecken */
+ .header-center .view-tabs {
+ display: none !important;
+ }
+
+ .header-left .project-selector {
+ display: none !important;
+ }
+
+ /* Header Layout anpassen */
+ .header-left {
+ gap: var(--spacing-2);
+ }
+
+ /* ========================================
+ TOUCH DRAG & DROP FEEDBACK
+ ======================================== */
+
+ .task-card.touch-dragging {
+ transform: scale(1.03);
+ box-shadow: var(--shadow-xl);
+ opacity: 0.95;
+ z-index: 1000;
+ transition: none;
+ pointer-events: none;
+ }
+
+ .task-card.touch-drag-placeholder {
+ opacity: 0.3;
+ border: 2px dashed var(--border-default);
+ }
+
+ .column-body.touch-drag-over {
+ background: var(--primary-light);
+ border: 2px dashed var(--primary);
+ border-radius: var(--radius-md);
+ }
+
+ /* ========================================
+ SWIPE INDIKATOREN
+ ======================================== */
+
+ .swipe-indicator {
+ position: fixed;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 40px;
+ height: 80px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--overlay-bg);
+ color: var(--text-inverse);
+ z-index: var(--z-tooltip);
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ pointer-events: none;
+ }
+
+ .swipe-indicator.left {
+ left: 0;
+ border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
+ }
+
+ .swipe-indicator.right {
+ right: 0;
+ border-radius: var(--radius-lg) 0 0 var(--radius-lg);
+ }
+
+ .swipe-indicator.visible {
+ opacity: 0.7;
+ }
+
+ .swipe-indicator svg {
+ width: 24px;
+ height: 24px;
+ }
+
+ /* ========================================
+ BOARD VIEW - HORIZONTAL SCROLLING
+ ======================================== */
+
+ .view-board .board {
+ scroll-snap-type: x mandatory;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .view-board .column {
+ scroll-snap-align: start;
+ flex-shrink: 0;
+ }
+
+ /* ========================================
+ PREVENT TEXT SELECTION DURING GESTURES
+ ======================================== */
+
+ .is-swiping,
+ .is-swiping *,
+ .is-touch-dragging,
+ .is-touch-dragging * {
+ user-select: none !important;
+ -webkit-user-select: none !important;
+ -webkit-touch-callout: none !important;
+ }
+
+ /* ========================================
+ BODY SCROLL LOCK
+ ======================================== */
+
+ body.mobile-menu-open {
+ overflow: hidden;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ }
+
+ /* ========================================
+ TOUCH-FREUNDLICHE ELEMENTE
+ ======================================== */
+
+ /* Groessere Touch-Targets */
+ .calendar-day {
+ min-height: 70px;
+ touch-action: manipulation;
+ }
+
+ /* Task-Karten */
+ .task-card {
+ touch-action: pan-y;
+ }
+
+ /* Buttons */
+ .btn {
+ min-height: 44px;
+ min-width: 44px;
+ }
+}
+
+/* ========================================
+ EXTRA SMALL MOBILE (max 480px)
+ ======================================== */
+
+@media (max-width: 480px) {
+ .mobile-menu {
+ width: 100%;
+ max-width: 100%;
+ }
+
+ .hamburger-btn {
+ width: 40px;
+ height: 40px;
+ padding: 8px;
+ }
+
+ .hamburger-line {
+ width: 20px;
+ }
+
+ .hamburger-line + .hamburger-line {
+ margin-top: 5px;
+ }
+
+ .hamburger-btn.active .hamburger-line:nth-child(1) {
+ transform: translateY(7px) rotate(45deg);
+ }
+
+ .hamburger-btn.active .hamburger-line:nth-child(3) {
+ transform: translateY(-7px) rotate(-45deg);
+ }
+}
diff --git a/frontend/index.html b/frontend/index.html
index 794209b..f633285 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -25,8 +25,10 @@
+
+
@@ -102,98 +104,29 @@
-
-
-
Erlaubte Dateiformate
+
+
+
Erlaubte Dateiendungen
-
-
-
-
-
JPEG
-
PNG
-
GIF
-
WebP
-
SVG
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- DOC
- DOCX
- XLS
- XLSX
- PPT
- PPTX
-
-
-
-
-
-
-
- TXT
- CSV
- Markdown
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-