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 + +
+ +
+ + +
+ +
+ +
- -
-
- -
-
- PDF -
-
- - -
-
- -
-
- DOC - DOCX - XLS - XLSX - PPT - PPTX -
-
- - -
-
- -
-
- TXT - CSV - Markdown -
-
- - -
-
- -
-
- ZIP - RAR - 7Z -
-
- - -
-
- -
-
- JSON + +
+ Vorschläge: +
+
@@ -212,6 +145,13 @@
+ + +

TaskMate

@@ -235,7 +175,7 @@ - +
@@ -509,383 +449,29 @@
- -