From 4a6b4a0ae81e95bf30e510fd72476b674b9c1d5c Mon Sep 17 00:00:00 2001 From: Christian Schindler Date: Mon, 1 Dec 2025 08:37:35 +0100 Subject: [PATCH] feat: Enhance security and validation in backend - Added helmet for security headers and configured content security policy - Implemented CORS with a whitelist for allowed origins - Introduced express-validator for input validation in API endpoints - Set request size limits to prevent DoS attacks - Added global error handling and 404 response - Updated TypeScript configuration to use node16 module resolution - Improved Docker Compose configuration for security and resource limits - Created a comprehensive .env.example for environment configuration - Implemented automated security scans in CI/CD with Trivy - Added cleanup script for debugging ports - Established a detailed security policy document --- .devcontainer/Dockerfile | 25 --- .devcontainer/devcontainer.json | 42 ---- .devcontainer/docker-compose.yml | 16 -- .env.example | 118 +++++++++++ .gittea/workflows/build.yaml | 301 ++++++++++++++++++++++++++ .vscode/cleanup-ports.sh | 59 ++++++ .vscode/launch.json | 86 +++----- .vscode/settings.json | 35 +--- .vscode/tasks.json | 350 +++++-------------------------- .woodpecker.yml | 164 --------------- Dockerfile | 79 +++++-- README.md | 195 +++++++++++++++-- SECURITY.md | 208 ++++++++++++++++++ backend/package.json | 4 +- backend/src/index.ts | 149 ++++++++++++- backend/tsconfig.json | 4 +- docker-compose.yml | 155 +++++++++++--- frontend/Dockerfile.dev | 11 +- frontend/tsconfig.json | 2 +- scripts/kill-dev-processes.sh | 57 ----- 20 files changed, 1296 insertions(+), 764 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/docker-compose.yml create mode 100644 .env.example create mode 100644 .gittea/workflows/build.yaml create mode 100644 .vscode/cleanup-ports.sh delete mode 100644 .woodpecker.yml create mode 100644 SECURITY.md delete mode 100755 scripts/kill-dev-processes.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 1396f86..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM node:18 - -# Install git and other useful tools -RUN apt-get update && apt-get install -y \ - git \ - curl \ - vim \ - && rm -rf /var/lib/apt/lists/* - -# Set working directory -WORKDIR /workspace - -# Install global packages -RUN npm install -g typescript ts-node nodemon - -# Create user with same UID as host (for file permissions) -ARG USERNAME=node -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -RUN groupmod --gid $USER_GID $USERNAME \ - && usermod --uid $USER_UID --gid $USER_GID $USERNAME \ - && chown -R $USER_UID:$USER_GID /home/$USERNAME - -USER $USERNAME diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 0ea467d..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "Fotodrucker Full Stack", - "build": { - "dockerfile": "Dockerfile" - }, - "workspaceFolder": "/workspace", - "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", - "customizations": { - "vscode": { - "extensions": [ - "ms-vscode.vscode-typescript-next", - "esbenp.prettier-vscode", - "ms-vscode.vscode-json", - "bradlc.vscode-tailwindcss", - "ms-vscode.vscode-eslint", - "ms-vscode-remote.remote-containers" - ], - "settings": { - "typescript.preferences.includePackageJsonAutoImports": "auto", - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" - } - } - }, - "postCreateCommand": "cd backend && npm install && cd ../frontend && npm install", - "forwardPorts": [3000, 3001, 9229, 9230], - "portsAttributes": { - "3000": { - "label": "Frontend (React)", - "onAutoForward": "openBrowser" - }, - "3001": { - "label": "Backend (Express)" - }, - "9229": { - "label": "Backend Debug Port" - }, - "9230": { - "label": "Frontend Debug Port" - } - } -} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index 6c53dc8..0000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.8' - -services: - app: - build: - context: . - dockerfile: Dockerfile - volumes: - - ..:/workspace:cached - ports: - - "3000:3000" - - "3001:3001" - - "9229:9229" - - "9230:9230" - command: sleep infinity - working_dir: /workspace diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eaa472e --- /dev/null +++ b/.env.example @@ -0,0 +1,118 @@ +# ============================================================================= +# Full Stack TypeScript Template - Environment Configuration +# ============================================================================= +# +# SICHERHEITSHINWEISE: +# 1. Kopiere diese Datei zu '.env' und fülle die Werte aus +# 2. NIEMALS .env in Git committen! (bereits in .gitignore) +# 3. Verwende STARKE, ZUFÄLLIGE Passwörter und Secrets! +# 4. Unterschiedliche Werte für Development, Staging und Production! +# +# Generiere sichere Secrets mit: +# openssl rand -base64 32 +# oder: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Application Settings +# ----------------------------------------------------------------------------- +NODE_ENV=production +APP_PORT=8080 + +# Docker Container Namen (optional) +APP_CONTAINER_NAME=fullstack-app +POSTGRES_CONTAINER_NAME=postgres-db + +# Docker Image (für CI/CD) +APP_IMAGE=fullstack-app:latest + +# ----------------------------------------------------------------------------- +# Database Configuration (OPTIONAL) +# ----------------------------------------------------------------------------- +# Dieses Template kann mit verschiedenen Datenbanken verwendet werden: +# - SQLite (Standard für einfache Projekte, keine Konfiguration nötig) +# - PostgreSQL (für Production, siehe unten) +# - MySQL, MongoDB, etc. (eigene Konfiguration) +# +# Für SQLite: Keine weiteren Einstellungen nötig! +# Die Datenbank wird automatisch im /data Volume erstellt. +# ----------------------------------------------------------------------------- + +# === PostgreSQL Configuration (Optional) === +# Nur notwendig wenn PostgreSQL statt SQLite verwendet wird. +# Aktivierung: +# 1. Entkommentiere den postgres-Service in docker-compose.yml +# 2. Setze die Werte unten +# 3. Aktiviere DATABASE_URL + +# POSTGRES_DB=appdb +# POSTGRES_USER=postgres + +# ⚠️ KRITISCH: MUSS geändert werden! Niemals Default-Passwort verwenden! +# Beispiel für sicheres Passwort generieren: +# openssl rand -base64 32 +# POSTGRES_PASSWORD= + +# Optional: Für lokales Debugging (default: Port nicht exponiert) +# POSTGRES_PORT=5432 + +# Vollständige Database URL (nur für PostgreSQL) +# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE +# DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@postgres:5432/appdb + +# ----------------------------------------------------------------------------- +# Security & Authentication +# ----------------------------------------------------------------------------- + +# ⚠️ KRITISCH: JWT Secret für Token-Signierung +# MUSS ein starker, zufälliger String sein (min. 32 Zeichen) +# Beispiel generieren: +# openssl rand -base64 32 +JWT_SECRET= + +# Optional: JWT Token Gültigkeit +JWT_EXPIRY=7d + +# Optional: Session Secret (falls Sessions verwendet werden) +# SESSION_SECRET= + +# ----------------------------------------------------------------------------- +# Application Secrets & API Keys +# ----------------------------------------------------------------------------- + +# Füge hier weitere Secrets hinzu, z.B.: +# STRIPE_API_KEY= +# SENDGRID_API_KEY= +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# REDIS_URL= + +# ----------------------------------------------------------------------------- +# Development Settings (nur für lokale Entwicklung) +# ----------------------------------------------------------------------------- + +# Für Development-Modus setze NODE_ENV=development +# NODE_ENV=development +# APP_PORT=3000 + +# ----------------------------------------------------------------------------- +# Beispiel für verschiedene Umgebungen: +# ----------------------------------------------------------------------------- + +# === DEVELOPMENT (mit SQLite) === +# NODE_ENV=development +# APP_PORT=3000 +# JWT_SECRET=dev_jwt_secret_12345 # NUR für lokales Development! +# # Keine DB-Config nötig für SQLite! + +# === STAGING (mit PostgreSQL) === +# NODE_ENV=staging +# APP_PORT=8080 +# JWT_SECRET=[SICHERES_RANDOM_SECRET] +# DATABASE_URL=postgresql://postgres:[PASSWORD]@postgres:5432/appdb + +# === PRODUCTION (mit PostgreSQL) === +# NODE_ENV=production +# APP_PORT=8080 +# JWT_SECRET=[SEHR_SICHERES_RANDOM_SECRET] +# DATABASE_URL=postgresql://postgres:[PASSWORD]@postgres:5432/appdb diff --git a/.gittea/workflows/build.yaml b/.gittea/workflows/build.yaml new file mode 100644 index 0000000..12eade2 --- /dev/null +++ b/.gittea/workflows/build.yaml @@ -0,0 +1,301 @@ +name: 🚀 Docker Image Build & Security Scan + +on: + push: + branches: + - main + - beta + - alpha + - Nachweise + workflow_dispatch: # Manueller Trigger + schedule: + # Täglicher Security-Scan um 6:00 UTC + - cron: '0 6 * * *' + +env: + # ⚠️ ANPASSEN: Nur die Registry-URL anpassen + REGISTRY: git.csnetworkx.dev + # Image-Name wird automatisch aus Repository generiert: REGISTRY/owner/repo + # Beispiel: git.csnetworkx.dev/user/project + TRIVY_VERSION: 0.58.1 + +jobs: + # 🔍 Security-Scan des Quellcodes und der Abhängigkeiten + security-scan: + name: 🔍 Sicherheitsüberprüfung + runs-on: ubuntu-latest + steps: + - name: 📥 Repository auschecken + uses: actions/checkout@v4 + + - name: 🔧 Trivy installieren + run: | + echo "📦 Installiere Trivy Security Scanner..." + # Moderne Installation ohne deprecated apt-key + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/trivy-archive-keyring.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + trivy --version + + - name: 🔍 Quellcode & Abhängigkeiten scannen + run: | + echo "🔍 Scanne Quellcode und npm-Abhängigkeiten..." + trivy fs --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed . + + - name: 🔍 Dockerfile & Konfiguration scannen + run: | + echo "🔍 Scanne Dockerfile und IaC-Konfigurationen..." + # Erzwinge auch kleingeschriebene Dockerfiles (z.B. ./dockerfile) + trivy config --severity CRITICAL,HIGH --exit-code 1 \ + --file-patterns "dockerfile:.*[Dd]ockerfile.*" . + + # 🔄 Täglicher Scan und automatische Fixes (nur bei scheduled runs) + daily-security-scan: + name: 🔄 Täglicher Security-Scan & Auto-Fix + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + steps: + - name: 📥 Repository auschecken (main Branch) + uses: actions/checkout@v4 + with: + ref: main + + - name: 🏷️ Image-Namen generieren + id: image_name + run: | + # Generiere Image-Name: REGISTRY/repository-lowercase + # Gitea: nutze GITHUB_REPOSITORY oder GITEA_REPOSITORY + REPO="${GITHUB_REPOSITORY:-$GITEA_REPOSITORY}" + IMAGE="${{ env.REGISTRY }}/${REPO}" + IMAGE_LOWER=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]') + echo "image=${IMAGE_LOWER}" >> "$GITHUB_OUTPUT" + echo "✅ Image-Name: ${IMAGE_LOWER}" + echo "📦 Repository: ${REPO}" + + - name: 🟢 Node.js einrichten + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 🔧 Trivy installieren + run: | + echo "📦 Installiere Trivy Security Scanner..." + # Moderne Installation ohne deprecated apt-key + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/trivy-archive-keyring.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + trivy --version + + - name: 🔐 In Docker Registry einloggen + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: 🔍 Docker Image scannen (latest) + id: image_scan + continue-on-error: true + run: | + echo "🐳 Scanne Docker Image: ${{ steps.image_name.outputs.image }}:latest" + trivy image --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed ${{ steps.image_name.outputs.image }}:latest || echo "scan_failed=true" >> "$GITHUB_OUTPUT" + + - name: 🔍 Dateisystem scannen + id: fs_scan + continue-on-error: true + run: | + echo "📁 Scanne Dateisystem und Abhängigkeiten..." + trivy fs --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed . || echo "scan_failed=true" >> "$GITHUB_OUTPUT" + + - name: 🔧 npm audit fix ausführen (Frontend) + if: steps.fs_scan.outcome == 'failure' + working-directory: ./frontend + run: | + echo "🔧 Führe npm audit fix aus (Frontend)..." + npm audit fix --force || true + npm update || true + + - name: 🔧 npm audit fix ausführen (Backend) + if: steps.fs_scan.outcome == 'failure' + working-directory: ./backend + run: | + echo "🔧 Führe npm audit fix aus (Backend)..." + npm audit fix --force || true + npm update || true + + - name: 🔎 Änderungen prüfen + id: changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "✅ Keine Änderungen durch npm audit fix" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "📝 Änderungen wurden vorgenommen" + fi + + - name: 💾 Änderungen committen & pushen + if: steps.changes.outputs.has_changes == 'true' + run: | + echo "💾 Committe automatische Security-Fixes..." + git config user.name "Security Bot" + git config user.email "security-bot@csnetworkx.dev" + git add -A + git commit -m "🔒 Security: Automatische Dependency-Updates [skip ci] + + - npm audit fix ausgeführt + - Schwachstellen in Abhängigkeiten behoben + + 🤖 Automatisch generiert durch den täglichen Security-Scan + + Der Build wird beim nächsten manuellen Push ausgeführt." + git push + + - name: 🔨 Docker Buildx einrichten + if: steps.image_scan.outcome == 'failure' || steps.changes.outputs.has_changes == 'true' + uses: docker/setup-buildx-action@v3 + + - name: 🏗️ Docker Image neu bauen & pushen + if: steps.image_scan.outcome == 'failure' || steps.changes.outputs.has_changes == 'true' + uses: docker/build-push-action@v5 + with: + context: . + push: true + pull: true # Zieht immer das neueste Base-Image + build-args: | + VERSION_TAG=latest + VERSION_SHA=${{ github.sha }} + tags: ${{ steps.image_name.outputs.image }}:latest + + - name: 📊 Security-Report erstellen + if: always() + run: | + echo "╔════════════════════════════════════════╗" + echo "║ 🔒 SECURITY SCAN REPORT 🔒 ║" + echo "╚════════════════════════════════════════╝" + echo "" + echo "📋 Scan-Ergebnisse:" + echo " • Docker Image: ${{ steps.image_scan.outcome }}" + echo " • Dateisystem: ${{ steps.fs_scan.outcome }}" + echo " • Änderungen: ${{ steps.changes.outputs.has_changes }}" + echo "" + if [[ "${{ steps.image_scan.outcome }}" == "failure" ]] || [[ "${{ steps.fs_scan.outcome }}" == "failure" ]]; then + echo "⚠️ SCHWACHSTELLEN GEFUNDEN!" + if [[ "${{ steps.changes.outputs.has_changes }}" == "true" ]]; then + echo "✅ Automatische Fixes wurden angewendet und committed." + else + echo "❌ Keine automatischen Fixes möglich - manuelle Überprüfung erforderlich!" + fi + else + echo "✅ Keine kritischen Schwachstellen gefunden." + fi + echo "" + + # 🐳 Docker Image bauen und pushen + docker-build-and-push: + name: 🐳 Docker Image Build & Push + runs-on: ubuntu-latest + needs: security-scan + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.security-scan.result == 'success' + steps: + - name: 📥 Repository auschecken + uses: actions/checkout@v4 + + - name: 🏷️ Image-Namen & Tags berechnen + id: vars + shell: bash + run: | + # Generiere Image-Name aus Repository (lowercase) + # Gitea: nutze GITHUB_REPOSITORY oder GITEA_REPOSITORY + REPO="${GITHUB_REPOSITORY:-$GITEA_REPOSITORY}" + IMAGE="${{ env.REGISTRY }}/${REPO}" + IMAGE_LOWER=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]') + echo "image_name=${IMAGE_LOWER}" >> "$GITHUB_OUTPUT" + echo "✅ Image-Name: ${IMAGE_LOWER}" + echo "📦 Repository: ${REPO}" + + # Berechne Branch-Tag (Gitea kompatibel) + BRANCH="${GITHUB_REF_NAME:-$GITEA_REF_NAME}" + echo "🏷️ Berechne Image-Tags für Branch: ${BRANCH}" + case "${BRANCH}" in + main) echo "branch_tag=latest" >> "$GITHUB_OUTPUT" ;; + beta) echo "branch_tag=beta" >> "$GITHUB_OUTPUT" ;; + alpha) echo "branch_tag=alpha" >> "$GITHUB_OUTPUT" ;; + Nachweise) echo "branch_tag=Nachweise" >> "$GITHUB_OUTPUT" ;; + *) echo "branch_tag=${BRANCH}" >> "$GITHUB_OUTPUT" ;; + esac + + # SHA (Gitea kompatibel) + SHA="${GITHUB_SHA:-$GITEA_SHA}" + echo "short_sha=${SHA::8}" >> "$GITHUB_OUTPUT" + echo "✅ Branch-Tag: $(cat $GITHUB_OUTPUT | grep branch_tag | cut -d'=' -f2)" + echo "✅ SHA-Tag: ${SHA::8}" + + - name: 🖥️ QEMU einrichten (Multi-Arch Support) + uses: docker/setup-qemu-action@v3 + + - name: 🔨 Docker Buildx einrichten + uses: docker/setup-buildx-action@v3 + + - name: 🔌 Registry-Verbindung testen + run: | + echo "🔌 Teste Verbindung zur Registry: ${{ env.REGISTRY }}" + echo "" + echo "1️⃣ HTTP-Verbindungstest..." + curl -v --connect-timeout 10 https://${{ env.REGISTRY }}/v2/ || echo "⚠️ Curl fehlgeschlagen" + echo "" + echo "2️⃣ DNS-Auflösung..." + nslookup ${{ env.REGISTRY }} || echo "⚠️ DNS-Lookup fehlgeschlagen" + echo "" + echo "3️⃣ Netzwerk-Ping..." + ping -c 3 ${{ env.REGISTRY }} || echo "⚠️ Ping fehlgeschlagen" + + - name: 🔐 Manueller Login-Test + run: | + echo "🔐 Teste manuellen Docker-Login..." + echo ${{ secrets.DOCKER_PASSWORD }} | docker login --username ${{ secrets.DOCKER_USERNAME }} --password-stdin ${{ env.REGISTRY }} + echo "✅ Login erfolgreich!" + + - name: 🔐 In Docker Registry einloggen + uses: docker/login-action@v3 + env: + DOCKER_CLI_DEBUG: 1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: 🏗️ Docker Image bauen & pushen + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + build-args: | + VERSION_TAG=${{ steps.vars.outputs.branch_tag }} + VERSION_SHA=${{ steps.vars.outputs.short_sha }} + tags: | + ${{ steps.vars.outputs.image_name }}:${{ steps.vars.outputs.branch_tag }} + ${{ steps.vars.outputs.image_name }}:${{ steps.vars.outputs.short_sha }} + + - name: ✅ Build-Zusammenfassung + if: success() + run: | + echo "╔════════════════════════════════════════╗" + echo "║ ✅ BUILD ERFOLGREICH ✅ ║" + echo "╚════════════════════════════════════════╝" + echo "" + echo "📦 Gebaute Images:" + echo " • ${{ steps.vars.outputs.image_name }}:${{ steps.vars.outputs.branch_tag }}" + echo " • ${{ steps.vars.outputs.image_name }}:${{ steps.vars.outputs.short_sha }}" + echo "" + echo "🎯 Plattformen: linux/amd64, linux/arm64" + echo "📤 Erfolgreich in Registry gepusht!" + echo "" + echo "🏷️ Image-Name: ${{ steps.vars.outputs.image_name }}" + echo "📍 Registry: ${{ env.REGISTRY }}" + echo "🔗 Repository: ${GITHUB_REPOSITORY:-$GITEA_REPOSITORY}" + echo "" + diff --git a/.vscode/cleanup-ports.sh b/.vscode/cleanup-ports.sh new file mode 100644 index 0000000..d0b1bbd --- /dev/null +++ b/.vscode/cleanup-ports.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Debug Port Cleanup Script +# Stellt sicher, dass die Debug-Ports frei sind und beendet laufende Prozesse + +set -euo pipefail + +PORTS=("3000" "3001" "9229" "5173" "9222") + +echo "🧹 Prüfe und bereinige Debug-Ports (${PORTS[*]})..." + +have_lsof=false +have_fuser=false +have_ss=false + +command -v lsof >/dev/null 2>&1 && have_lsof=true +command -v fuser >/dev/null 2>&1 && have_fuser=true +command -v ss >/dev/null 2>&1 && have_ss=true + +if [ "$have_lsof" = false ] && [ "$have_fuser" = false ] && [ "$have_ss" = false ]; then + echo "⚠️ Weder lsof noch fuser/ss verfügbar. Ports können nicht geprüft/gekilled werden." + exit 0 +fi + +for PORT in "${PORTS[@]}"; do + PIDS="" + + if [ "$have_lsof" = true ]; then + PIDS=$({ lsof -ti :"$PORT" 2>/dev/null || true; } | tr '\n' ' ') + elif [ "$have_fuser" = true ]; then + # fuser listet PIDs, -n tcp beschränkt auf TCP + PIDS=$({ fuser -n tcp "$PORT" 2>/dev/null || true; } | tr '\n' ' ') + elif [ "$have_ss" = true ]; then + # ss fallback, extrahiere pid=... aus der letzten Spalte + PIDS=$({ ss -ltnp "sport = :$PORT" 2>/dev/null || true; } | awk 'NR>1 {split($NF,pid,"pid="); split(pid[2],p,","); if(p[1]!=""){print p[1]}}' | tr '\n' ' ') + fi + + if [ -n "$PIDS" ]; then + echo " ✓ Beende Prozesse auf Port $PORT (PID: $PIDS)" + if [ "$have_fuser" = true ]; then + fuser -k -n tcp "$PORT" 2>/dev/null || true + else + kill -9 $PIDS 2>/dev/null || true + fi + else + echo " ○ Port $PORT ist frei" + fi +done + +echo "✅ Port-Bereinigung abgeschlossen." +echo "" +echo "Belegte Debug-Ports aktuell:" +if [ "$have_lsof" = true ]; then + lsof -i :3000 -i :3001 -i :9229 -i :5173 -i :9222 2>/dev/null || echo " Alle Debug-Ports sind frei ✓" +elif [ "$have_ss" = true ]; then + ss -ltnp "( sport = :3000 or sport = :3001 or sport = :9229 or sport = :5173 or sport = :9222 )" 2>/dev/null || echo " Alle Debug-Ports sind frei ✓" +else + echo " Port-Status nicht prüfbar (lsof/ss fehlen), Cleanup wurde dennoch ausgeführt." +fi diff --git a/.vscode/launch.json b/.vscode/launch.json index 9fbe170..03a27ce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,71 +2,49 @@ "version": "0.2.0", "configurations": [ { - "name": "🚀 Debug Backend", - "type": "node", - "request": "launch", - "runtimeExecutable": "npx", - "runtimeArgs": [ - "ts-node-dev", - "--respawn", - "--transpile-only", - "--no-notify" - ], - "args": ["src/index.ts"], - "cwd": "${workspaceFolder}/backend", - "env": { - "NODE_ENV": "development" - }, - "console": "integratedTerminal", - "skipFiles": ["/**"], - "sourceMaps": true, - "restart": true, - "outputCapture": "std", - "presentation": { - "group": "fullstack", - "panel": "new" - }, - "postDebugTask": "🛑 Terminate All Development Processes" - }, - { - "name": "🌐 Debug Frontend (Chrome)", + "name": "Debug Vite", "type": "chrome", "request": "launch", "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/frontend/src", - "skipFiles": ["/**"] + "webRoot": "${workspaceFolder}/Client", + "skipFiles": ["/**"], + "preLaunchTask": "Start Vite (after backend ready)" }, { - "name": "🔧 Debug Frontend (Edge)", - "type": "msedge", + "name": "Debug Backend", + "type": "node", "request": "launch", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/frontend/src", + "runtimeExecutable": "npx", + "args": ["tsx", "watch", "--inspect=9229", "./src/index.ts"], + "cwd": "${workspaceFolder}/Server", + "console": "integratedTerminal", + "skipFiles": ["/**"], + "preLaunchTask": "Clean Debug Ports", + "envFile": "${workspaceFolder}/Server/.env", + "env": { + "PORT": "3001" + } + }, + + + { + "name": "Debug Jest Tests", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/Server/node_modules/jest/bin/jest.js", + "args": [ + "--config=Server/tests/jest.config.cjs", + "--runInBand" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", "skipFiles": ["/**"] } ], "compounds": [ { - "name": "🚀🌐 Debug Full Stack (Chrome)", - "preLaunchTask": "🌐 Frontend", - "configurations": ["🚀 Debug Backend", "🌐 Debug Frontend (Chrome)"], - "stopAll": true, - "presentation": { - "hidden": false, - "group": "fullstack", - "order": 1 - } - }, - { - "name": "🚀🔧 Debug Full Stack (Edge)", - "preLaunchTask": "🌐 Frontend", - "configurations": ["🚀 Debug Backend", "🔧 Debug Frontend (Edge)"], - "stopAll": true, - "presentation": { - "hidden": false, - "group": "fullstack", - "order": 2 - } + "name": "Compound Debug", + "configurations": ["Debug Vite", "Debug Backend"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a8f3df..e744838 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,28 +1,9 @@ { - "typescript.preferences.includePackageJsonAutoImports": "auto", - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "files.exclude": { - "**/node_modules": true, - "**/dist": true, - "**/.git": true, - "**/.DS_Store": true - }, - "search.exclude": { - "**/node_modules": true, - "**/dist": true - }, - "typescript.updateImportsOnFileMove.enabled": "always", - "emmet.includeLanguages": { - "typescript": "typescriptreact" - }, - "debug.allowBreakpointsEverywhere": true, - "debug.node.autoAttach": "on", - "terminal.integrated.enablePersistentSessions": false, - "terminal.integrated.confirmOnKill": "editor", - "task.autoDetect": "off", - "task.showDecorations": true -} + "github.copilot.chat.languageContext.inline.typescript.enabled": true, + "github.copilot.chat.languageContext.fix.typescript.enabled": true, + "github.copilot.chat.edits.temporalContext.enabled": true, + "github.copilot.chat.completionContext.typescript.mode": "on", + "github.copilot.chat.agent.thinkingTool": true, + "github.copilot.chat.followUps": "always", + "debug.onTaskErrors": "abort" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 449efe6..bd627a3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,320 +2,80 @@ "version": "2.0.0", "tasks": [ { - "label": "Install Backend Dependencies", + "label": "Clean Debug Ports", "type": "shell", - "command": "npm", - "args": ["install"], - "options": { - "cwd": "${workspaceFolder}/backend" - }, - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - } + "command": "bash ${workspaceFolder}/.vscode/cleanup-ports.sh", + "problemMatcher": [] }, { - "label": "Install Frontend Dependencies", + "label": "Wait for Backend Ready", "type": "shell", - "command": "npm", - "args": ["install"], - "options": { - "cwd": "${workspaceFolder}/frontend" - }, - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - } + "command": "bash -lc 'for i in {1..120}; do if (echo > /dev/tcp/127.0.0.1/3001) >/dev/null 2>&1; then exit 0; fi; sleep 1; done; echo \"Backend not ready on :3001\"; exit 1'", + "problemMatcher": [] }, { - "label": "🖥️ Backend", + "label": "Start Backend (watch + inspect)", "type": "shell", - "command": "npm", - "args": ["run", "dev"], - "options": { - "cwd": "${workspaceFolder}/backend" - }, - "group": "build", + "command": "npx tsx watch --inspect=9229 ./src/index.ts", "isBackground": true, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "group": "dev", - "showReuseMessage": false - }, - "problemMatcher": { - "pattern": { - "regexp": "^.*$", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "^.*ts-node-dev.*$", - "endsPattern": "^🚀 Backend Server läuft auf Port \\d+$" - } - } - }, - { - "label": "🌐 Frontend", - "type": "shell", - "command": "npm", - "args": ["start"], "options": { - "cwd": "${workspaceFolder}/frontend", + "cwd": "${workspaceFolder}/Server", "env": { - "BROWSER": "none" + "PORT": "3001" } }, - "group": "build", + "dependsOn": "Clean Debug Ports", + "problemMatcher": [ + { + "owner": "backend-ready", + "fileLocation": ["absolute"], + "pattern": { + "regexp": "^(.*)$", + "message": 1 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "Starte Datenbankinitialisierung|Server läuft unter http://localhost:3001", + "endsPattern": "NotificationScheduler erfolgreich initialisiert" + } + } + ], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": false + } + }, + { + "label": "Start Vite (after backend ready)", + "type": "npm", + "script": "frontend", "isBackground": true, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "group": "dev", - "showReuseMessage": false + "options": { + "cwd": "${workspaceFolder}/Client" }, - "problemMatcher": { - "owner": "custom", - "pattern": { - "regexp": "^.*$", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "^.*Starting the development server.*$", - "endsPattern": "^.*webpack compiled.*$" + "dependsOn": "Wait for Backend Ready", + "dependsOrder": "sequence", + "problemMatcher": [ + { + "owner": "vite", + "fileLocation": ["relative", "${workspaceFolder}"], + "pattern": { + "regexp": "^(.*)$", + "file": 1, + "message": 1 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "VITE|ready in", + "endsPattern": "localhost:3000" + } } - } - }, - { - "label": "🛑 Kill Frontend Process", - "type": "shell", - "command": "pkill", - "args": ["-f", "npm start"], - "options": { - "cwd": "${workspaceFolder}/frontend" - }, - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - }, - "problemMatcher": [] - }, - { - "label": "🛑 Kill Backend Process", - "type": "shell", - "command": "pkill", - "args": ["-f", "ts-node-dev"], - "options": { - "cwd": "${workspaceFolder}/backend" - }, - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - }, - "problemMatcher": [] - }, - { - "label": "📊 Show Development Processes Status", - "type": "shell", - "command": "bash", - "args": [ - "-c", - "echo '📊 Aktuelle Development-Prozesse:'; echo ''; ps aux | grep -E 'npm.*start|react-scripts|ts-node-dev' | grep -v grep | awk '{print \"PID: \" $2 \" - \" $11 \" \" $12 \" \" $13}' || echo 'Keine Development-Prozesse gefunden'" ], - "group": "build", "presentation": { - "echo": true, "reveal": "always", - "focus": false, - "panel": "shared" - }, - "problemMatcher": [] - }, - { - "label": "🛑 Cleanup Development Processes", - "type": "shell", - "command": "bash", - "args": [ - "-c", - "pkill -f 'ts-node-dev' > /dev/null 2>&1 || true; pkill -f 'npm.*start' > /dev/null 2>&1 || true; pkill -f 'react-scripts' > /dev/null 2>&1 || true" - ], - "group": "build", - "presentation": { - "echo": false, - "reveal": "never", - "focus": false, - "panel": "shared", - "clear": false, - "showReuseMessage": false, - "close": true - }, - "problemMatcher": [], - "isBackground": false, - "runOptions": { - "reevaluateOnRerun": true - } - }, - { - "label": "🧹 Post Debug Cleanup", - "type": "shell", - "command": "bash", - "args": ["-c", "/workspace/scripts/post-debug-cleanup.sh"], - "group": "build", - "presentation": { - "echo": false, - "reveal": "never", - "focus": false, - "panel": "new", - "clear": false, - "showReuseMessage": false, - "close": true - }, - "problemMatcher": [], - "isBackground": false, - "runOptions": { - "reevaluateOnRerun": true, - "runOn": "default" - } - }, - { - "label": "🔄 Force Kill All Dev Processes", - "type": "shell", - "command": "bash", - "args": [ - "-c", - "RANDOM_ID=$RANDOM; echo \"Cleanup ID: $RANDOM_ID\" > /dev/null; pkill -9 -f 'ts-node-dev' > /dev/null 2>&1 || true; pkill -9 -f 'npm.*start' > /dev/null 2>&1 || true; pkill -9 -f 'react-scripts' > /dev/null 2>&1 || true" - ], - "group": "build", - "presentation": { - "echo": false, - "reveal": "never", - "focus": false, - "panel": "shared", - "clear": false, - "showReuseMessage": false, - "close": true - }, - "problemMatcher": [] - }, - { - "label": "🛑 Terminate All Development Processes", - "type": "shell", - "command": "/workspace/scripts/kill-dev-processes.sh", - "args": [], - "group": "build", - "presentation": { - "echo": false, - "reveal": "never", - "focus": false, - "panel": "shared", - "clear": false, - "showReuseMessage": false, - "close": true - }, - "problemMatcher": [], - "runOptions": { - "runOn": "default" - } - }, - { - "label": "🛑 Terminate All Development Processes (Verbose)", - "type": "shell", - "command": "/workspace/scripts/kill-dev-processes.sh", - "args": [], - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, "panel": "dedicated", - "clear": true, - "showReuseMessage": false - }, - "problemMatcher": [] - }, - { - "label": "🚀 Start Full Stack (Split Terminal)", - "dependsOrder": "parallel", - "dependsOn": ["🖥️ Backend", "🌐 Frontend"], - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "🔄 Restart Full Stack", - "type": "shell", - "command": "bash", - "args": [ - "-c", - "/workspace/scripts/kill-dev-processes.sh && sleep 2 && echo '🚀 Starte Full Stack...' && code --command 'workbench.action.tasks.runTask' 'shell: 🚀 Start Full Stack (Split Terminal)'" - ], - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared", - "clear": true - }, - "problemMatcher": [] - }, - { - "label": "Build Backend", - "type": "shell", - "command": "npm", - "args": ["run", "build"], - "options": { - "cwd": "${workspaceFolder}/backend" - }, - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - }, - "problemMatcher": ["$tsc"] - }, - { - "label": "Build Frontend", - "type": "shell", - "command": "npm", - "args": ["run", "build"], - "options": { - "cwd": "${workspaceFolder}/frontend" - }, - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" + "clear": false } } ] diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index 416862d..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,164 +0,0 @@ -# Woodpecker CI Pipeline für Full Stack TypeScript Template -# Automatisiert Docker Builds für main, beta und alpha branches - -when: - event: [push, pull_request] - -steps: - # ============================================================================= - # Dependencies installieren und Tests ausführen - # ============================================================================= - - dependencies: - image: node:18-alpine - commands: - - echo "📦 Installing dependencies..." - - npm ci - - cd backend && npm ci && cd .. - - cd frontend && npm ci && cd .. - when: - event: [push, pull_request] - - lint_and_test: - image: node:18-alpine - commands: - - echo "🔍 Running linting and tests..." - - cd backend && npm run test && cd .. - - cd frontend && npm run test -- --coverage --watchAll=false && cd .. - when: - event: [push, pull_request] - depends_on: [dependencies] - - build_check: - image: node:18-alpine - commands: - - echo "🏗️ Testing production builds..." - - cd backend && npm run build && cd .. - - cd frontend && npm run build && cd .. - when: - event: [push, pull_request] - depends_on: [dependencies] - - # ============================================================================= - # Docker Build & Push für MAIN Branch (Production) - # ============================================================================= - - docker_build_push_main: - image: woodpeckerci/plugin-docker-buildx - settings: - repo: YOUR_REGISTRY/YOUR_ORG/fullstack-typescript-template - registry: YOUR_REGISTRY - platforms: linux/amd64,linux/arm64 - tag: latest - dockerfile: Dockerfile - context: . - username: - from_secret: docker_username - password: - from_secret: docker_password - build_args: - - NODE_ENV=production - - BUILD_VERSION=${CI_COMMIT_SHA:0:8} - when: - branch: main - event: push - depends_on: [lint_and_test, build_check] - - # ============================================================================= - # Docker Build & Push für BETA Branch (Staging) - # ============================================================================= - - docker_build_push_beta: - image: woodpeckerci/plugin-docker-buildx - settings: - repo: YOUR_REGISTRY/YOUR_ORG/fullstack-typescript-template - registry: YOUR_REGISTRY - platforms: linux/amd64,linux/arm64 - tag: beta - dockerfile: Dockerfile - context: . - username: - from_secret: docker_username - password: - from_secret: docker_password - build_args: - - NODE_ENV=staging - - BUILD_VERSION=${CI_COMMIT_SHA:0:8} - when: - branch: beta - event: push - depends_on: [lint_and_test, build_check] - - # ============================================================================= - # Docker Build & Push für ALPHA Branch (Development) - # ============================================================================= - - docker_build_push_alpha: - image: woodpeckerci/plugin-docker-buildx - settings: - repo: YOUR_REGISTRY/YOUR_ORG/fullstack-typescript-template - registry: YOUR_REGISTRY - platforms: linux/amd64,linux/arm64 - tag: alpha - dockerfile: Dockerfile - context: . - username: - from_secret: docker_username - password: - from_secret: docker_password - build_args: - - NODE_ENV=development - - BUILD_VERSION=${CI_COMMIT_SHA:0:8} - when: - branch: alpha - event: push - depends_on: [lint_and_test, build_check] - - # ============================================================================= - # Notification bei erfolgreichem Build - # ============================================================================= - - notify_success: - image: alpine:latest - commands: - - echo "✅ Pipeline erfolgreich abgeschlossen!" - - echo "🐳 Docker Image gebaut für Branch: ${CI_COMMIT_BRANCH}" - - echo "📦 Tag: ${CI_COMMIT_BRANCH == 'main' ? 'latest' : CI_COMMIT_BRANCH}" - - echo "🔗 Commit: ${CI_COMMIT_SHA:0:8}" - when: - status: success - event: push - branch: [main, beta, alpha] - depends_on: [docker_build_push_main, docker_build_push_beta, docker_build_push_alpha] - - # ============================================================================= - # Security Scan (Optional - kann aktiviert werden) - # ============================================================================= - - # security_scan: - # image: aquasec/trivy:latest - # commands: - # - trivy image --exit-code 0 --severity HIGH,CRITICAL YOUR_REGISTRY/YOUR_ORG/fullstack-typescript-template:${CI_COMMIT_BRANCH == 'main' ? 'latest' : CI_COMMIT_BRANCH} - # when: - # branch: [main, beta, alpha] - # event: push - # depends_on: [docker_build_push_main, docker_build_push_beta, docker_build_push_alpha] - -# ============================================================================= -# Pipeline Services (falls externe Services für Tests benötigt werden) -# ============================================================================= - -# services: -# postgres: -# image: postgres:15-alpine -# environment: -# POSTGRES_DB: testdb -# POSTGRES_USER: test -# POSTGRES_PASSWORD: test -# ports: -# - 5432:5432 -# -# redis: -# image: redis:7-alpine -# ports: -# - 6379:6379 diff --git a/Dockerfile b/Dockerfile index 91ef811..e9d1ee9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,13 +52,21 @@ RUN apk add --no-cache \ curl \ tzdata \ tini \ + su-exec \ + shadow \ && rm -rf /var/cache/apk/* -# Arbeitsverzeichnisse erstellen -RUN mkdir -p /var/www/html \ +# Non-root User erstellen (UID/GID 1000 für Kompatibilität) +RUN addgroup -g 1000 appuser \ + && adduser -D -u 1000 -G appuser appuser \ + && mkdir -p /var/www/html \ && mkdir -p /app/backend/dist \ && mkdir -p /etc/supervisor.d \ - && mkdir -p /data + && mkdir -p /data \ + && mkdir -p /var/log/nginx \ + && mkdir -p /var/lib/nginx \ + && mkdir -p /var/tmp/nginx \ + && mkdir -p /run/nginx # Frontend Build von Builder Stage kopieren COPY --from=frontend-builder /app/frontend/build /var/www/html @@ -67,10 +75,10 @@ COPY --from=frontend-builder /app/frontend/build /var/www/html COPY --from=backend-builder /app/backend/dist /app/backend/dist COPY --from=backend-builder /app/backend/node_modules /app/backend/node_modules -# Nginx Konfiguration erstellen +# Nginx Konfiguration erstellen (non-root optimiert) RUN printf 'worker_processes auto;\n\ error_log /var/log/nginx/error.log warn;\n\ -pid /var/run/nginx.pid;\n\ +pid /run/nginx/nginx.pid;\n\ \n\ events {\n\ worker_connections 1024;\n\ @@ -81,6 +89,13 @@ events {\n\ http {\n\ include /etc/nginx/mime.types;\n\ default_type application/octet-stream;\n\ +\n\ + # Temp-Pfade für non-root\n\ + client_body_temp_path /var/tmp/nginx/client_body;\n\ + proxy_temp_path /var/tmp/nginx/proxy;\n\ + fastcgi_temp_path /var/tmp/nginx/fastcgi;\n\ + uwsgi_temp_path /var/tmp/nginx/uwsgi;\n\ + scgi_temp_path /var/tmp/nginx/scgi;\n\ \n\ # Logging\n\ log_format main '\''$remote_addr - $remote_user [$time_local] "$request" '\''\n\ @@ -96,19 +111,24 @@ http {\n\ gzip on;\n\ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;\n\ \n\ - # Security Headers\n\ + # Moderne Security Headers\n\ add_header X-Frame-Options "SAMEORIGIN" always;\n\ add_header X-Content-Type-Options "nosniff" always;\n\ - add_header X-XSS-Protection "1; mode=block" always;\n\ add_header Referrer-Policy "strict-origin-when-cross-origin" always;\n\ + add_header Content-Security-Policy "default-src '\''self'\''; script-src '\''self'\'' '\''unsafe-inline'\''; style-src '\''self'\'' '\''unsafe-inline'\''; img-src '\''self'\'' data: https:; font-src '\''self'\'' data:; connect-src '\''self'\'' https:; frame-ancestors '\''self'\''; base-uri '\''self'\''; form-action '\''self'\'';" always;\n\ + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;\n\ + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;\n\ +\n\ + # Request-Limits gegen DoS\n\ + client_max_body_size 50M;\n\ + client_body_timeout 60s;\n\ + client_header_timeout 60s;\n\ + client_body_buffer_size 128k;\n\ + large_client_header_buffers 4 16k;\n\ \n\ server {\n\ - listen 80;\n\ + listen 8080;\n\ server_name _;\n\ -\n\ - client_max_body_size 50M;\n\ - client_body_timeout 60s;\n\ - client_header_timeout 60s;\n\ \n\ # Health Check Endpoint\n\ location /health {\n\ @@ -147,16 +167,22 @@ http {\n\ proxy_connect_timeout 60s;\n\ proxy_send_timeout 60s;\n\ proxy_read_timeout 60s;\n\ + \n\ + # Buffer-Limits\n\ + proxy_buffer_size 4k;\n\ + proxy_buffers 8 4k;\n\ + proxy_busy_buffers_size 8k;\n\ }\n\ }\n\ }' > /etc/nginx/nginx.conf -# Supervisor Konfiguration erstellen +# Supervisor Konfiguration erstellen (non-root) RUN printf '[supervisord]\n\ nodaemon=true\n\ loglevel=info\n\ logfile=/dev/stdout\n\ logfile_maxbytes=0\n\ +user=appuser\n\ \n\ [program:nginx]\n\ command=/usr/sbin/nginx -g "daemon off;"\n\ @@ -167,6 +193,7 @@ stdout_logfile_maxbytes=0\n\ stderr_logfile=/dev/stderr\n\ stderr_logfile_maxbytes=0\n\ priority=10\n\ +user=appuser\n\ \n\ [program:backend]\n\ command=node /app/backend/dist/index.js\n\ @@ -179,12 +206,12 @@ stdout_logfile_maxbytes=0\n\ stderr_logfile=/dev/stderr\n\ stderr_logfile_maxbytes=0\n\ priority=20\n\ -user=node\n' > /etc/supervisor.d/supervisord.ini +user=appuser\n' > /etc/supervisor.d/supervisord.ini # Health Check Script erstellen RUN printf '#!/bin/sh\n\ # Health check for both nginx and backend\n\ -nginx_status=$(curl -f -s http://localhost/health > /dev/null && echo "ok" || echo "fail")\n\ +nginx_status=$(curl -f -s http://localhost:8080/health > /dev/null && echo "ok" || echo "fail")\n\ backend_status=$(curl -f -s http://localhost:3001/api/health > /dev/null && echo "ok" || echo "fail")\n\ \n\ if [ "$nginx_status" = "ok" ] && [ "$backend_status" = "ok" ]; then\n\ @@ -194,9 +221,17 @@ else\n\ exit 1\n\ fi' > /usr/local/bin/healthcheck.sh && chmod +x /usr/local/bin/healthcheck.sh -# Berechtigungen setzen -RUN chown -R node:node /data /app/backend -RUN chown nginx:nginx /var/www/html +# Berechtigungen setzen für non-root User +RUN chown -R appuser:appuser \ + /data \ + /app/backend \ + /var/www/html \ + /var/log/nginx \ + /var/lib/nginx \ + /var/tmp/nginx \ + /run/nginx \ + /etc/nginx \ + /usr/local/bin/healthcheck.sh # Health Check konfigurieren HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ @@ -205,6 +240,12 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ # Arbeitsverzeichnis setzen WORKDIR /app +# Exponiere Port 8080 (non-root) statt 80 +EXPOSE 8080 + +# Wechsle zu non-root User +USER appuser + # Startup mit Tini für proper signal handling ENTRYPOINT ["/sbin/tini", "--"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor.d/supervisord.ini"] @@ -216,7 +257,7 @@ LABEL traefik.enable=true LABEL traefik.http.routers.app.rule="Host(\`your-domain.com\`)" LABEL traefik.http.routers.app.entrypoints=websecure LABEL traefik.http.routers.app.tls.certresolver=letsencrypt -LABEL traefik.http.services.app.loadbalancer.server.port=80 +LABEL traefik.http.services.app.loadbalancer.server.port=8080 # ============================================================================= # Metadata Labels diff --git a/README.md b/README.md index f1cb9d7..eb880c9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,29 @@ # 🚀 Full Stack TypeScript Template -Ein modernes Full Stack Template mit React Frontend und Express Backend, komplett containerisiert mit Docker. +Ein modernes, **sicherheitsoptimiertes** Full Stack Template mit React Frontend und Express Backend, komplett containerisiert mit Docker. ## 📋 Übersicht - **Frontend**: React 18 + TypeScript + Create React App -- **Backend**: Express.js + TypeScript + Node.js -- **Database**: PostgreSQL 15 +- **Backend**: Express.js + TypeScript + Node.js + Helmet + CORS +- **Database**: SQLite (Standard) oder PostgreSQL 15 (optional, mit scram-sha-256 Auth) - **Container**: Docker + Docker Compose - **Development**: Hot Reload für Frontend & Backend -- **CI/CD**: Woodpecker CI Ready +- **CI/CD**: Gitea Actions mit automatischen Security-Scans +- **Security**: Non-root Container, Input-Validierung, Security Headers +- **Flexibel**: PostgreSQL optional - auch für SQLite-only Projekte geeignet + +## 🔐 Sicherheitsfeatures + +✅ **Non-root Docker Container** - Alle Container laufen als nicht-privilegierte User +✅ **Security Headers** - Helmet.js & moderne CSP-Policies +✅ **Input-Validierung** - express-validator für alle API-Endpoints +✅ **CORS-Whitelist** - Konfigurierbare Origin-Beschränkung +✅ **Resource Limits** - CPU/Memory-Limits gegen DoS +✅ **Isolierte Datenbank** - PostgreSQL (falls verwendet) nur intern erreichbar +✅ **Automatische Scans** - Trivy Security Scans in CI/CD +✅ **Secrets Management** - Keine Hardcoded Credentials +✅ **Flexible Architektur** - SQLite für einfache Projekte, PostgreSQL für Production ## 🛠️ Voraussetzungen @@ -123,10 +137,14 @@ npm run build:backend # Nur Backend ### Production Build ```bash -# Mit Docker (empfohlen) -npm run docker:prod +# Mit Docker (empfohlen - baut automatisch) +docker-compose up --build -d -# Manuell +# Oder mit npm +npm run docker:build # Baut das Image +npm run docker:up # Startet die Services + +# Manuell (ohne Docker) npm run build NODE_ENV=production npm start ``` @@ -149,29 +167,93 @@ npm run docker:clean # Docker Cleanup ## 🌍 Environment Variables -Erstelle eine `.env` Datei im Root-Verzeichnis: +### ⚠️ WICHTIG: Sicherheitskonfiguration -```bash -# Database -POSTGRES_DB=appdb -POSTGRES_USER=postgres -POSTGRES_PASSWORD=secure-password-change-this -DATABASE_URL=postgresql://postgres:secure-password-change-this@postgres:5432/appdb +1. **Kopiere die Beispiel-Datei**: + ```bash + cp .env.example .env + ``` -# App -NODE_ENV=development -JWT_SECRET=your-jwt-secret-change-this +2. **Generiere sichere Secrets**: + ```bash + # JWT Secret generieren + openssl rand -base64 32 -# Ports -APP_PORT=3000 -POSTGRES_PORT=5432 -``` + # PostgreSQL Passwort generieren + openssl rand -base64 32 + ``` + +3. **Fülle die `.env` Datei mit deinen Werten**: + ```bash + # Database (⚠️ MUSS geändert werden!) + POSTGRES_DB=appdb + POSTGRES_USER=postgres + POSTGRES_PASSWORD=[DEIN_SICHERES_PASSWORT_HIER] + + # Security (⚠️ MUSS geändert werden!) + JWT_SECRET=[DEIN_SICHERES_SECRET_HIER] + + # App + NODE_ENV=production + APP_PORT=8080 + ``` + +### 📋 Vollständige Dokumentation + +Siehe [.env.example](.env.example) für alle verfügbaren Optionen und detaillierte Erklärungen. + +### 🚨 Sicherheitshinweise + +- ❌ **NIEMALS** `.env` in Git committen (bereits in `.gitignore`) +- ✅ Verwende **unterschiedliche Secrets** für Development, Staging und Production +- ✅ Nutze **starke, zufällige Passwörter** (min. 32 Zeichen) +- ✅ Rotiere Secrets regelmäßig in Production-Umgebungen + +## 🗄️ Datenbank-Konfiguration + +Dieses Template ist **flexibel** und unterstützt verschiedene Datenbanken: + +### Option 1: SQLite (Standard - keine Konfiguration nötig) +Perfekt für: +- ✅ Einfache Projekte und Prototypen +- ✅ Low-Traffic Anwendungen +- ✅ Development und Testing +- ✅ Keine separate DB-Server benötigt + +**Keine weiteren Schritte nötig!** Die Datenbank wird automatisch im `/data` Volume erstellt. + +### Option 2: PostgreSQL (Optional - für Production) +Empfohlen für: +- ✅ Production-Umgebungen +- ✅ High-Traffic Anwendungen +- ✅ Komplexe Datenmodelle +- ✅ Mehrere parallele Connections + +**Aktivierung:** +1. Öffne [docker-compose.yml](docker-compose.yml) und entkommentiere den `postgres`-Service (Zeile 82-135) +2. Aktiviere `depends_on` im `app`-Service (Zeile 52-54) +3. Entkommentiere `postgres_data` Volume (Zeile 149-150) +4. Setze in `.env`: `DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@postgres:5432/appdb` +5. Setze ein sicheres `POSTGRES_PASSWORD` + +### Option 3: Andere Datenbanken +Du kannst auch MySQL, MongoDB, etc. verwenden: +- Füge den entsprechenden Service in `docker-compose.yml` hinzu +- Passe `DATABASE_URL` in `.env` an +- Installiere den entsprechenden Client (`mysql2`, `mongoose`, etc.) ## 📡 Ports +### Development (lokal ohne Docker) - **Frontend**: http://localhost:3000 - **Backend**: http://localhost:3001 -- **Database**: localhost:5432 + +### Production (Docker Container) +- **Application**: http://localhost:8080 (Nginx + Backend) +- **Database (PostgreSQL)**: Nur intern erreichbar (kein Port-Expose für Sicherheit) +- **SQLite**: Wird im `/data` Volume gespeichert (kein Port nötig) + +> 💡 **Tipp**: Für lokales PostgreSQL-Debugging den Port in `docker-compose.yml` freigeben (Zeile 127-128) ## 🏗️ Projekt-Struktur @@ -249,6 +331,75 @@ docker system prune -a ./scripts/kill-dev-processes.sh ``` +## 🔒 Sicherheits-Best-Practices + +### Vor dem Deployment + +1. **Environment-Variablen überprüfen** + - [ ] Alle Secrets in `.env` sind gesetzt und stark + - [ ] `NODE_ENV=production` ist gesetzt + - [ ] JWT_SECRET ist ein starker, zufälliger String + - [ ] POSTGRES_PASSWORD ist sicher + +2. **CORS-Konfiguration anpassen** + ```typescript + // backend/src/index.ts - Zeile 36 + const allowedOrigins = [ + 'https://your-domain.com', // ⚠️ Anpassen! + 'https://www.your-domain.com' + ]; + ``` + +3. **Gitea Actions konfigurieren** + ```yaml + # .gittea/workflows/build.yaml - Zeile 17 + REGISTRY: git.csnetworkx.dev # Deine Gitea-Instanz URL + # Image-Name wird automatisch generiert: REGISTRY/owner/repo + ``` + + **Secrets in Gitea setzen** (Repository Settings → Secrets): + - `DOCKER_USERNAME` → Dein Gitea Username + - `DOCKER_PASSWORD` → Gitea Access Token mit Package-Rechten + +### Container Security + +- ✅ Alle Container laufen als **non-root** (UID 1000 / 70) +- ✅ `no-new-privileges` Security Option aktiv +- ✅ Minimale Linux Capabilities (CAP_DROP: ALL) +- ✅ Resource Limits gegen DoS-Angriffe +- ✅ PostgreSQL verwendet scram-sha-256 Authentifizierung + +### API Security + +- ✅ **Helmet.js** für Security Headers +- ✅ **CORS** mit Origin-Whitelist +- ✅ **Input-Validierung** mit express-validator +- ✅ **Request Size Limits** (10MB max) +- ✅ **Error Handling** ohne Stacktrace-Leaks in Production + +### CI/CD Security + +Die Gitea Actions Pipeline führt automatisch aus: +- 🔍 **Trivy Security Scans** (Quellcode, Dependencies, Docker Images) +- 🔍 **Dockerfile Lint** (IaC-Konfiguration) +- 🔄 **Automatische npm audit fix** bei Schwachstellen +- 📊 **Security Reports** nach jedem Scan + +### Empfohlene Zusatz-Maßnahmen + +1. **Secrets Management**: Verwende Vault oder Docker Secrets in Production +2. **TLS/HTTPS**: Nutze Traefik mit Let's Encrypt (Labels bereits im Dockerfile) +3. **Monitoring**: Implementiere Health-Checks und Alerting +4. **Backups**: Automatisiere PostgreSQL-Backups +5. **Updates**: Halte Dependencies aktuell (`npm audit`, `npm outdated`) +6. **Penetration Testing**: Führe regelmäßige Security-Audits durch + +### Weitere Ressourcen + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Docker Security Best Practices](https://docs.docker.com/develop/security-best-practices/) +- [Node.js Security Checklist](https://blog.risingstack.com/node-js-security-checklist/) + ## 📄 License [MIT](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..33e7afe --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,208 @@ +# 🔒 Security Policy + +## Sicherheitsarchitektur + +Dieses Template wurde mit einem **Security-First-Ansatz** entwickelt und ist optimiert für den Einsatz mit Gitea Actions und anderen CI/CD-Systemen. + +## 🛡️ Implementierte Sicherheitsmaßnahmen + +### Container Security + +#### Non-Root Container (✅ Kritisch) +Alle Container laufen als nicht-privilegierte Benutzer: + +- **Application Container**: UID 1000 (`appuser`) +- **PostgreSQL Container**: UID 70 (Standard postgres-User) +- **Frontend Dev Container**: UID 1000 (`node`) + +**Warum wichtig?**: Container die als root laufen, können bei einem Breakout den Host kompromittieren. + +#### Security Options +```yaml +security_opt: + - no-new-privileges:true # Verhindert Privilege Escalation +cap_drop: + - ALL # Entfernt alle Linux Capabilities +cap_add: + - NET_BIND_SERVICE # Nur benötigte Capabilities +``` + +#### Resource Limits +```yaml +resources: + limits: + cpus: '2.0' + memory: 2G +``` +Verhindert DoS-Angriffe durch Resource-Exhaustion. + +### Network Security + +#### Isolierte Datenbank +- PostgreSQL ist **nicht** nach außen exponiert +- Nur über internes Docker-Netzwerk erreichbar +- Für lokales Debugging kann der Port bei Bedarf freigegeben werden + +#### CORS-Whitelist +```typescript +const allowedOrigins = [ + 'https://your-domain.com', + 'https://www.your-domain.com' +]; +``` +Nur explizit erlaubte Origins dürfen API-Requests durchführen. + +### Application Security + +#### Security Headers (Helmet.js) +- `Content-Security-Policy`: Verhindert XSS-Angriffe +- `Strict-Transport-Security`: Erzwingt HTTPS +- `X-Content-Type-Options`: Verhindert MIME-Sniffing +- `X-Frame-Options`: Verhindert Clickjacking +- `Permissions-Policy`: Kontrolliert Browser-Features + +#### Input-Validierung +Alle API-Endpoints validieren ihre Inputs: + +```typescript +app.post('/api/print', [ + body('photoId').isInt({ min: 1 }), + body('copies').isInt({ min: 1, max: 100 }), + validateRequest +], handler); +``` + +#### Request Size Limits +```typescript +app.use(express.json({ limit: '10mb' })); +``` +Verhindert DoS durch große Payloads. + +### Database Security + +#### Authentifizierung +- Verwendet `scram-sha-256` statt MD5 +- Keine Default-Passwörter (müssen gesetzt werden) +- Sichere Passwortgenerierung empfohlen + +#### Secrets Management +- Keine Secrets im Code +- `.env` in `.gitignore` +- `.env.example` als Vorlage + +## 🔍 CI/CD Security Scans + +### Automatische Scans in Gitea Actions + +#### 1. Quellcode-Scan +```bash +trivy fs --severity CRITICAL,HIGH --exit-code 1 . +``` +Scannt auf Schwachstellen in Dependencies. + +#### 2. Docker Image-Scan +```bash +trivy image --severity CRITICAL,HIGH $IMAGE_NAME +``` +Scannt fertige Docker Images. + +#### 3. IaC-Scan +```bash +trivy config --severity CRITICAL,HIGH . +``` +Prüft Dockerfiles und docker-compose.yml. + +#### 4. Automatische Fixes +Bei gefundenen Schwachstellen: +- Führt `npm audit fix` aus +- Erstellt automatischen Commit +- Baut neue Docker Images + +### Tägliche Security-Scans +- Laufen täglich um 6:00 UTC (Cron-Job) +- Prüfen auf neue Schwachstellen +- Erstellen automatische Updates + +## ⚠️ Bekannte Einschränkungen + +### Kein Rate Limiting +Rate Limiting ist **absichtlich nicht** implementiert, da es in Development-Umgebungen Probleme verursachen kann. + +**Für Production**: Implementiere Rate Limiting über: +- Traefik/Nginx Rate Limiting +- express-rate-limit Middleware +- CloudFlare oder ähnliche Services + +### Keine End-to-End Verschlüsselung +Das Template setzt voraus, dass TLS/HTTPS auf Reverse-Proxy-Ebene (z.B. Traefik) implementiert wird. + +## 🚨 Vulnerability Reporting + +### Sicherheitslücken melden + +Wenn du eine Sicherheitslücke in diesem Template findest: + +1. **NICHT** als öffentliches GitHub Issue melden +2. Sende eine E-Mail an: [security@your-domain.com] +3. Beschreibe das Problem detailliert +4. Füge PoC hinzu (falls vorhanden) + +### Response Timeline +- **Initial Response**: Innerhalb von 48 Stunden +- **Fix & Patch**: Abhängig von Severity (Critical: 7 Tage, High: 30 Tage) +- **Public Disclosure**: Nach Patch-Veröffentlichung + +## ✅ Security Checklist vor Deployment + +### Pflicht (vor jedem Deployment) +- [ ] `.env` Datei erstellt und ausgefüllt +- [ ] Starke Secrets generiert (min. 32 Zeichen) +- [ ] `NODE_ENV=production` gesetzt +- [ ] CORS-Origins auf Production-Domains angepasst +- [ ] PostgreSQL-Passwort geändert +- [ ] JWT-Secret geändert + +### Empfohlen +- [ ] TLS/HTTPS konfiguriert (Traefik mit Let's Encrypt) +- [ ] Monitoring & Alerting eingerichtet +- [ ] Backup-Strategie implementiert +- [ ] Secrets in Vault oder Secret Manager +- [ ] Logging konfiguriert (keine Secrets in Logs!) +- [ ] Firewall-Regeln geprüft + +### Optional (Production) +- [ ] Rate Limiting aktiviert +- [ ] WAF (Web Application Firewall) vorgeschaltet +- [ ] DDoS-Protection aktiv +- [ ] Penetration Testing durchgeführt +- [ ] Security Headers im Reverse Proxy verdoppelt + +## 📚 Weitere Ressourcen + +### Standards & Best Practices +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/) +- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker) +- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/) + +### Tools +- [Trivy Scanner](https://github.com/aquasecurity/trivy) +- [npm audit](https://docs.npmjs.com/cli/v8/commands/npm-audit) +- [Snyk](https://snyk.io/) +- [OWASP ZAP](https://www.zaproxy.org/) + +## 📝 Changelog + +### Version 1.0.0 (2024) +- ✅ Non-root Container für alle Services +- ✅ Security Headers mit Helmet.js +- ✅ Input-Validierung mit express-validator +- ✅ CORS-Whitelist +- ✅ Resource Limits +- ✅ Isolierte PostgreSQL-Datenbank +- ✅ Automatische Security-Scans in CI/CD +- ✅ scram-sha-256 Authentifizierung für PostgreSQL + +## 📄 License + +Dieses Security-Dokument ist Teil des Full Stack TypeScript Templates und unterliegt der MIT-Lizenz. diff --git a/backend/package.json b/backend/package.json index 55fbc35..58f4b2a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,8 @@ }, "dependencies": { "cors": "^2.8.5", - "express": "^4.18.3" + "express": "^4.18.3", + "helmet": "^7.1.0", + "express-validator": "^7.0.1" } } diff --git a/backend/src/index.ts b/backend/src/index.ts index 56fd7dc..b6fea63 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,14 +1,91 @@ import express from "express"; import cors from "cors"; +import helmet from "helmet"; +import { body, validationResult } from "express-validator"; const app = express(); const PORT = process.env.PORT || 3001; -// Middleware -app.use(cors()); -app.use(express.json()); +// ============================================================================= +// Security Middleware +// ============================================================================= +// Helmet: Setzt wichtige Security Headers +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], // Für React Development + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'"], + fontSrc: ["'self'", "data:"], + objectSrc: ["'none'"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"], + }, + }, + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true + } +})); + +// CORS: Konfiguriert mit Whitelist (anpassen für Production!) +const allowedOrigins = process.env.NODE_ENV === 'production' + ? [ + 'https://your-domain.com', + 'https://www.your-domain.com' + ] + : [ + 'http://localhost:3000', + 'http://localhost:8080', + 'http://127.0.0.1:3000', + 'http://127.0.0.1:8080' + ]; + +app.use(cors({ + origin: (origin, callback) => { + // Erlaube Requests ohne Origin (z.B. Mobile Apps, Postman) + if (!origin) return callback(null, true); + + if (allowedOrigins.indexOf(origin) !== -1) { + callback(null, true); + } else { + console.warn(`🚫 CORS blocked request from origin: ${origin}`); + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + allowedHeaders: ['Content-Type', 'Authorization'], + maxAge: 86400 // 24 Stunden +})); + +// Body Parser mit Size Limits (gegen DoS) +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// ============================================================================= +// Helper: Validierungsergebnis prüfen +// ============================================================================= +const validateRequest = (req: express.Request, res: express.Response, next: express.NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + next(); +}; + +// ============================================================================= // Routes +// ============================================================================= + +// Health Check Endpoint (ohne Validierung) app.get("/api/health", (req, res) => { // 🐛 Breakpoint-Test: Setze hier einen Breakpoint zum Testen console.log("Health check aufgerufen!"); @@ -17,9 +94,11 @@ app.get("/api/health", (req, res) => { status: "OK", message: "Backend is running", timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV || 'development' }); }); +// Beispiel: GET Endpoint ohne Body app.get("/api/photos", (req, res) => { // Beispiel-Endpunkt für Fotos res.json({ @@ -30,16 +109,64 @@ app.get("/api/photos", (req, res) => { }); }); -app.post("/api/print", (req, res) => { - // Beispiel-Endpunkt für Druckaufträge - const { photoId, copies } = req.body; +// Beispiel: POST Endpoint mit Input-Validierung +app.post( + "/api/print", + [ + // Validierungsregeln + body('photoId') + .isInt({ min: 1 }) + .withMessage('photoId muss eine positive Ganzzahl sein'), + body('copies') + .isInt({ min: 1, max: 100 }) + .withMessage('copies muss zwischen 1 und 100 liegen'), + validateRequest + ], + (req, res) => { + // Input ist validiert - sicher zu verwenden + const { photoId, copies } = req.body; - console.log(`Druckauftrag erhalten: Photo ID ${photoId}, Kopien: ${copies}`); + console.log(`✅ Druckauftrag erhalten: Photo ID ${photoId}, Kopien: ${copies}`); - res.json({ - success: true, - message: `Druckauftrag für Photo ${photoId} mit ${copies} Kopien wurde erstellt`, - jobId: Math.random().toString(36).substr(2, 9), + res.json({ + success: true, + message: `Druckauftrag für Photo ${photoId} mit ${copies} Kopien wurde erstellt`, + jobId: Math.random().toString(36).substr(2, 9), + }); + } +); + +// ============================================================================= +// Error Handler +// ============================================================================= + +// 404 Handler +app.use((req, res) => { + res.status(404).json({ + success: false, + message: 'Endpoint nicht gefunden', + path: req.path + }); +}); + +// Global Error Handler +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('❌ Error:', err); + + // CORS Error + if (err.message === 'Not allowed by CORS') { + return res.status(403).json({ + success: false, + message: 'CORS policy: Origin not allowed' + }); + } + + // Generic Error + res.status(500).json({ + success: false, + message: process.env.NODE_ENV === 'production' + ? 'Interner Serverfehler' + : err.message }); }); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 84124a6..6d9235b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "module": "commonjs", + "module": "node16", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", @@ -20,7 +20,7 @@ "noImplicitThis": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "node16", "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true diff --git a/docker-compose.yml b/docker-compose.yml index 607ed5b..7d03b4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,47 +1,150 @@ # Docker Compose für Full Stack Application +# SICHERHEITS-OPTIMIERT für Gitea Actions & Production version: "3.8" services: app: - image: ${APP_IMAGE:-fullstack-app:latest} + # Automatischer Build aus dem aktuellen Verzeichnis + build: + context: . + dockerfile: Dockerfile + args: + NODE_ENV: ${NODE_ENV:-production} + BUILD_VERSION: ${BUILD_VERSION:-latest} + + # Optional: Image-Name für Registry-Push (für CI/CD) + # image: ${REGISTRY:-localhost}/${IMAGE_NAME:-fullstack-app}:${TAG:-latest} + container_name: ${APP_CONTAINER_NAME:-fullstack-app} restart: unless-stopped + + # Non-root User (UID/GID 1000) + user: "1000:1000" + environment: - NODE_ENV=${NODE_ENV:-production} - - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/appdb} - - JWT_SECRET=${JWT_SECRET:-your-jwt-secret-change-this} - networks: - - app-network - depends_on: - postgres: - condition: service_healthy - ports: - - "${APP_PORT:-3000}:3000" + # Optional: Nur wenn PostgreSQL verwendet wird + - DATABASE_URL=${DATABASE_URL:-} + - JWT_SECRET=${JWT_SECRET} + + # Security Options + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE # Erlaubt Binding an Ports < 1024 (falls nötig) + + # Resource Limits (verhindern DoS) + deploy: + resources: + limits: + cpus: '2.0' + memory: 2G + reservations: + cpus: '0.5' + memory: 512M - postgres: - image: postgres:15-alpine - container_name: ${POSTGRES_CONTAINER_NAME:-postgres-db} - restart: unless-stopped - environment: - POSTGRES_DB: ${POSTGRES_DB:-appdb} - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - volumes: - - postgres_data:/var/lib/postgresql/data networks: - app-network + + # Optional: Nur aktivieren wenn PostgreSQL verwendet wird + # depends_on: + # postgres: + # condition: service_healthy + ports: - - "${POSTGRES_PORT:-5432}:5432" + - "${APP_PORT:-8080}:8080" # Port 8080 für non-root + + # Volumes für persistente Daten + volumes: + - app-data:/data + healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] - interval: 10s - timeout: 5s - retries: 5 + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ============================================================================= + # PostgreSQL Service (OPTIONAL) + # ============================================================================= + # Nur aktivieren wenn PostgreSQL benötigt wird. + # Für SQLite oder andere Datenbanken kann dieser Service entfernt werden. + # + # Um PostgreSQL zu aktivieren: + # 1. Entferne die Kommentare (#) vor dem gesamten postgres-Service + # 2. Aktiviere "depends_on" im app-Service (siehe oben) + # 3. Setze DATABASE_URL in .env + # ============================================================================= + + # postgres: + # image: postgres:15-alpine + # container_name: ${POSTGRES_CONTAINER_NAME:-postgres-db} + # restart: unless-stopped + # + # # Non-root User (postgres verwendet UID 70 standardmäßig) + # user: "70:70" + # + # environment: + # POSTGRES_DB: ${POSTGRES_DB:-appdb} + # POSTGRES_USER: ${POSTGRES_USER:-postgres} + # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # MUSS in .env gesetzt werden! + # POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256" # Sichere Auth + # POSTGRES_HOST_AUTH_METHOD: scram-sha-256 + # + # # Security Options + # security_opt: + # - no-new-privileges:true + # cap_drop: + # - ALL + # cap_add: + # - CHOWN + # - DAC_OVERRIDE + # - FOWNER + # - SETGID + # - SETUID + # + # # Resource Limits + # deploy: + # resources: + # limits: + # cpus: '1.0' + # memory: 1G + # reservations: + # cpus: '0.25' + # memory: 256M + # + # volumes: + # - postgres_data:/var/lib/postgresql/data + # + # networks: + # - app-network + # + # # KEIN Port-Expose nach außen - nur intern erreichbar! + # # Für lokales Debugging kann der Port freigegeben werden: + # # ports: + # # - "${POSTGRES_PORT:-5432}:5432" + # + # healthcheck: + # test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] + # interval: 10s + # timeout: 5s + # retries: 5 + # start_period: 10s networks: app-network: driver: bridge + # Optional: Netzwerk isolieren + internal: false # Auf 'true' setzen wenn kein Internet-Zugriff nötig volumes: - postgres_data: + # Persistente Daten für die App (z.B. SQLite-Datenbank, Uploads, etc.) + app-data: driver: local + + # Optional: Nur wenn PostgreSQL verwendet wird + # postgres_data: + # driver: local diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index 109db7b..96b730e 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -1,14 +1,21 @@ # Frontend Development Dockerfile +# SICHERHEITS-OPTIMIERT: Non-root User FROM node:18-alpine +# Non-root User erstellen (node-User existiert bereits) +RUN mkdir -p /app && chown -R node:node /app + WORKDIR /app +# Wechsle zu non-root User VOR Package-Installation +USER node + # Package files kopieren -COPY package*.json ./ +COPY --chown=node:node package*.json ./ RUN npm ci # Source code kopieren -COPY . . +COPY --chown=node:node . . # Port exposieren EXPOSE 3000 diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a273b0c..b53758b 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -14,7 +14,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/scripts/kill-dev-processes.sh b/scripts/kill-dev-processes.sh deleted file mode 100755 index 8eacf94..0000000 --- a/scripts/kill-dev-processes.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Einfacher Lock-Mechanismus ohne störende Ausgaben -LOCK_FILE="/tmp/kill-dev-processes.lock" - -# Prüfe auf bereits laufende Instanz -if [ -f "$LOCK_FILE" ]; then - # Prüfe ob Prozess tatsächlich noch läuft - if [ -f "$LOCK_FILE" ] && kill -0 $(cat "$LOCK_FILE") 2>/dev/null; then - # Stilles Beenden wenn bereits aktiv - exit 0 - else - # Aufräumen alter Lock-Dateien - rm -f "$LOCK_FILE" 2>/dev/null - fi -fi - -# Erstelle Lock-File mit aktueller PID -echo $$ > "$LOCK_FILE" -trap "rm -f $LOCK_FILE 2>/dev/null" EXIT - -echo "🛑 Beende alle Development-Prozesse..." -echo "" - -# Finde alle Development-Prozesse -FRONTEND_PIDS=$(ps aux | grep -E 'npm start|react-scripts' | grep -v grep | awk '{print $2}') -BACKEND_PIDS=$(ps aux | grep 'ts-node-dev' | grep -v grep | awk '{print $2}') - -echo "Frontend-Prozesse (PIDs): $FRONTEND_PIDS" -echo "Backend-Prozesse (PIDs): $BACKEND_PIDS" -echo "" - -# Beende Frontend-Prozesse -if [ ! -z "$FRONTEND_PIDS" ]; then - echo "Beende Frontend-Prozesse..." - echo $FRONTEND_PIDS | xargs kill -TERM 2>/dev/null - sleep 2 - echo $FRONTEND_PIDS | xargs kill -KILL 2>/dev/null - echo "✅ Frontend-Prozesse beendet" -else - echo "❌ Keine Frontend-Prozesse gefunden" -fi - -# Beende Backend-Prozesse -if [ ! -z "$BACKEND_PIDS" ]; then - echo "Beende Backend-Prozesse..." - echo $BACKEND_PIDS | xargs kill -TERM 2>/dev/null - sleep 1 - echo $BACKEND_PIDS | xargs kill -KILL 2>/dev/null - echo "✅ Backend-Prozesse beendet" -else - echo "❌ Keine Backend-Prozesse gefunden" -fi - -echo "" -echo "Verbleibende Development-Prozesse:" -ps aux | grep -E 'npm start|react-scripts|ts-node-dev' | grep -v grep || echo "✅ Keine Development-Prozesse mehr vorhanden"