diff --git a/.dockerignore b/.dockerignore index accc348c..fbe5c0ed 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,7 @@ backend/dist .vscode .idea *.md -!docker/README.md + # Environment files (use Docker environment instead) .env diff --git a/.env.example b/.env.example index 1e6f6432..ebe328c5 100644 --- a/.env.example +++ b/.env.example @@ -53,6 +53,14 @@ JWT_REFRESH_EXPIRES_IN=7d # Generieren: openssl rand -hex 32 ENCRYPTION_KEY=change-this-to-64-hex-characters-please-rotate-before-production-xx +# ---------- Optionaler Reverse-Proxy (Caddy) ---------- +# Nur nötig, wenn du KEINEN eigenen Reverse-Proxy hast und Caddy das SSL +# übernehmen soll. Starten dann mit: docker-compose --profile caddy up -d +# Ohne dieses Profil bleibt Caddy aus und ändert nichts am Setup. +DOMAIN=crm.example.com +CADDY_DIR=./data/caddy +CADDY_CONFIG_DIR=./data/caddy-config + # ---------- Siegel für das Audit-Log ---------- # OpenCRM protokolliert, wer wann welche Daten gesehen oder geändert hat – die # Beweisspur bei Streit oder Datenschutz-Nachfragen. Dieser Schlüssel versieht diff --git a/docker/Caddyfile b/Caddyfile similarity index 72% rename from docker/Caddyfile rename to Caddyfile index 22e31ce2..6fa0a8e8 100644 --- a/docker/Caddyfile +++ b/Caddyfile @@ -1,11 +1,14 @@ # OpenCRM Caddyfile # =================== -# Replace {$DOMAIN} with your actual domain or use environment variable -# For local development without SSL, use localhost:80 +# Wird NUR gebraucht, wenn du den optionalen Caddy-Dienst startest: +# docker-compose --profile caddy up -d +# Caddy holt und erneuert dann das SSL-Zertifikat automatisch fuer ${DOMAIN}. +# Betreibst du bereits einen eigenen Reverse-Proxy (nginx, NPM, Traefik), +# brauchst du diese Datei nicht - dann laeuft alles ueber deinen Proxy. {$DOMAIN:localhost} { # Reverse proxy to OpenCRM app - reverse_proxy app:3001 + reverse_proxy opencrm:3001 # Logging log { diff --git a/README.md b/README.md index 7e481e35..a840f425 100644 --- a/README.md +++ b/README.md @@ -341,9 +341,8 @@ openssl rand -hex 32 | Betrieb | Datei | |---|---| -| **Docker** (Standard, `docker-compose.yml` im Projektstamm) | **`.env` im Projektstamm** | +| **Docker** (Standard) | **`.env` im Projektstamm** | | Backend direkt, ohne Container (Entwicklung) | `backend/.env` | -| Alternativer Caddy-Stack unter `docker/` | `docker/.env` | Beim Docker-Betrieb reicht der Eintrag in der Stamm-`.env` – `docker-compose.yml` reicht die Variable an den Container weiter. Ein Eintrag in `backend/.env` wirkt @@ -817,48 +816,44 @@ docker-compose down -v # Container stoppen + Daten löschen docker-compose logs -f # Logs anzeigen ``` -### Docker (Produktion) +### Betrieb mit eigenem SSL (optionaler Caddy-Dienst) -Im `docker/` Verzeichnis liegt ein komplettes Produktions-Setup: +Es gibt **eine** `docker-compose.yml` und **eine** `.env` im Projektstamm. +Standardmässig bringt der Stack **keinen** Reverse-Proxy mit – die Annahme ist, +dass du bereits einen hast (nginx, Nginx Proxy Manager, Traefik) und OpenCRM +dahinter hängst. + +Hast du **keinen**, kannst du Caddy als optionalen Dienst dazuschalten. Er holt +und erneuert das SSL-Zertifikat automatisch: ```bash -cd docker +# In der .env die Domain setzen +DOMAIN=crm.example.de +HTTPS_ENABLED=true -# Image bauen -docker-compose build - -# Container starten -docker-compose up -d - -# Logs anzeigen -docker-compose logs -f app +# Mit Caddy starten (Ports 80 + 443 müssen frei sein) +docker-compose --profile caddy up -d ``` -**Komponenten:** -- **MariaDB 10.11**: Datenbank -- **App**: Backend + Frontend in einem Container -- **Caddy**: Reverse-Proxy mit automatischem SSL +Ohne `--profile caddy` bleibt Caddy aus und nichts ändert sich – der Dienst +wird dann gar nicht erst angelegt. -> **Wann dieser Stack?** Wenn du **keinen** eigenen Reverse-Proxy betreibst – -> Caddy holt und erneuert das SSL-Zertifikat hier selbst. Hast du bereits einen -> Proxy davor (nginx, Nginx Proxy Manager, Traefik), nimm den Stack im -> Projektstamm; er bringt bewusst keinen eigenen Proxy mit. +**Was dabei zu beachten ist:** -**Umgebungsvariablen (`docker/.env`):** – vollständige Vorlage mit Erklärungen -in `docker/.env.example` +- Die Zertifikate liegen unter `./data/caddy`. Diesen Ordner **nicht löschen**, + sonst werden bei jedem Start neue Zertifikate angefordert und du läufst in + die Mengenbegrenzung von Let's Encrypt. +- Die Domain muss vorher per DNS auf den Server zeigen, sonst schlägt die + Zertifikatsausstellung fehl. +- `HTTPS_ENABLED=true` setzen – das aktiviert das `Secure`-Attribut am + Refresh-Cookie und die korrekte Proxy-Erkennung. +- Der direkte App-Port (`OPENCRM_PORT`, Standard 3010) bleibt weiterhin offen. + Wenn nur Caddy von aussen erreichbar sein soll, schliesse ihn per Firewall + oder setze ihn in der `.env` auf einen Wert, den du nicht freigibst. -```env -DB_ROOT_PASSWORD=sicheres-root-passwort -DB_NAME=opencrm -DB_USER=opencrm -DB_PASSWORD=sicheres-passwort -JWT_SECRET=sehr-langer-zufaelliger-string -ENCRYPTION_KEY=64-zeichen-hex-string -DOMAIN=crm.example.com -HTTPS_ENABLED=true # Caddy terminiert TLS → Secure-Cookies an -AUDIT_HMAC_KEY= # Siegel fürs Audit-Log, siehe oben -RUN_SEED=true # Nur beim ersten Start -``` +Die Konfiguration liegt in der `Caddyfile` im Projektstamm. Sie liefert +Sicherheits-Header mit und komprimiert bewusst **nicht** die API-Antworten +(BREACH-Schutz, siehe Abschnitt zum Reverse-Proxy oben). ## Projektstruktur @@ -910,7 +905,7 @@ opencrm/ │ │ ├── types/ # TypeScript-Typen │ │ └── App.tsx # Haupt-Komponente │ └── package.json -├── docker/ # Docker-Deployment +├── Caddyfile # Optionaler Reverse-Proxy (nur mit --profile caddy) │ ├── Dockerfile # Multi-Stage Build │ ├── docker-compose.yml # Produktion (MariaDB, App, Caddy) │ ├── Caddyfile # Reverse-Proxy mit SSL diff --git a/docker-compose.yml b/docker-compose.yml index 6f3ce67e..3691098b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -90,6 +90,30 @@ services: # Auto-aktualisierter BLZ-/Bankdaten-Datensatz (Bundesbank-Bankleitzahlen) - ${BANKDATA_DIR:-./data/bankdata}:/app/bankdata + # Optionaler Reverse-Proxy mit automatischem SSL – nur fuer Betreiber OHNE + # eigenen Proxy. Startet NICHT mit `docker-compose up -d`, sondern nur mit: + # docker-compose --profile caddy up -d + # Wer nginx / Nginx Proxy Manager / Traefik davor hat, laesst das weg. + caddy: + image: caddy:2-alpine + container_name: opencrm-caddy + restart: unless-stopped + profiles: ["caddy"] + depends_on: + - opencrm + environment: + DOMAIN: ${DOMAIN:-localhost} + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + # Zertifikate liegen wie alle Daten unter ./data/ – nicht loeschen, + # sonst werden bei jedem Start neue Zertifikate angefordert + # (Let's-Encrypt-Limits!). + - ${CADDY_DIR:-./data/caddy}:/data + - ${CADDY_CONFIG_DIR:-./data/caddy-config}:/config + adminer: image: adminer:latest container_name: opencrm-adminer diff --git a/docker/.env.example b/docker/.env.example deleted file mode 100644 index 77342287..00000000 --- a/docker/.env.example +++ /dev/null @@ -1,61 +0,0 @@ -# OpenCRM Docker Environment -# ============================ -# Copy this file to .env and adjust the values - -# Domain (for Caddy SSL certificate) -# Use your actual domain, e.g., crm.example.com -DOMAIN=localhost - -# Database -# HINWEIS: In diesem Stack wird die Verbindungs-URL direkt aus diesen Werten -# zusammengesetzt. Sonderzeichen im Passwort ($ ! # @ : / ?) koennen die URL -# zerlegen und den Start verhindern. Nutze hier ein langes Passwort aus -# Buchstaben und Ziffern - oder den Stack im Projektstamm, dessen Entrypoint -# die Zeichen korrekt kodiert. -DB_ROOT_PASSWORD=change-this-root-password -DB_NAME=opencrm -DB_USER=opencrm -DB_PASSWORD=change-this-password - -# JWT Authentication -# Generate with: openssl rand -base64 32 -JWT_SECRET=change-this-to-a-secure-random-string -# Access-Token kurz (XSS-Schutz, Token lebt nur im JS-Memory). -# Refresh-Token lang im httpOnly-Cookie. -JWT_EXPIRES_IN=15m -JWT_REFRESH_EXPIRES_IN=7d - -# Encryption Key (for portal credentials) -# Generate with: openssl rand -hex 32 -ENCRYPTION_KEY=change-this-to-a-32-byte-hex-key - -# TLS: In diesem Stack terminiert Caddy das HTTPS. Das Flag setzt das -# Secure-Attribut am Refresh-Cookie, trust proxy = 1 Hop und -# upgrade-insecure-requests in der CSP. -# Nur auf false setzen, wenn der Stack bewusst ohne TLS laeuft (reiner Test). -HTTPS_ENABLED=true - -# Nur noetig, wenn das Frontend unter einer ANDEREN Domain laeuft als das -# Backend. Beim Standard-Setup (alles hinter derselben Caddy-Domain) leer -# lassen. -CORS_ORIGINS= - -# Cloud-Deploy: blockt zusaetzlich private IP-Ranges, damit ein eingeloggter -# Admin ueber die Provider-/SMTP-Testverbindung keine internen Dienste -# anpingen kann. On-Prem meist false, weil Mailserver oft lokal laufen. -SSRF_BLOCK_PRIVATE_IPS=false - -# First Install: Set to "true" to seed database on first startup -# After first successful start, set back to "false" -RUN_SEED=true - -# ---------- Siegel für das Audit-Log ---------- -# Signiert jeden neuen Audit-Eintrag mit einem Schlüssel ausserhalb der -# Datenbank, damit sich die Beweisspur nicht nachtraeglich faelschen laesst. -# Generieren: openssl rand -hex 32 (pro Umgebung ein eigener Wert). -# Ohne Wert laeuft alles normal weiter, nur ungesiegelt. -# Einmal gesetzt nicht wieder entfernen - Details in der README. -AUDIT_HMAC_KEY= - -# Nur beim Schluesselwechsel: frueherer Schluessel, mehrere kommagetrennt. -AUDIT_HMAC_KEY_OLD= diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 3965f539..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,97 +0,0 @@ -# =================================== -# Stage 1: Build Frontend -# =================================== -FROM node:20-alpine AS frontend-builder - -WORKDIR /app/frontend - -# Copy package files -COPY frontend/package*.json ./ - -# Install dependencies -RUN npm ci - -# Copy frontend source -COPY frontend/ ./ - -# Build frontend -RUN npm run build - -# =================================== -# Stage 2: Build Backend -# =================================== -FROM node:20-alpine AS backend-builder - -WORKDIR /app/backend - -# Install OpenSSL for Prisma -RUN apk add --no-cache openssl - -# Copy package files -COPY backend/package*.json ./ - -# Install dependencies (including dev for build) -RUN npm ci - -# Copy backend source -COPY backend/ ./ - -# Generate Prisma client -RUN npx prisma generate - -# Build TypeScript -RUN npm run build - -# =================================== -# Stage 3: Production Runtime -# =================================== -FROM node:20-alpine AS production - -WORKDIR /app - -# Install OpenSSL for Prisma runtime and netcat for DB health check -RUN apk add --no-cache openssl netcat-openbsd - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S opencrm -u 1001 -G nodejs - -# Copy backend package files -COPY backend/package*.json ./ - -# Install production dependencies + tsx for seed script -RUN npm ci --omit=dev && npm install tsx - -# Copy Prisma schema and generate client -COPY backend/prisma ./prisma -RUN npx prisma generate - -# Copy built backend -COPY --from=backend-builder /app/backend/dist ./dist - -# Copy built frontend to public directory -COPY --from=frontend-builder /app/frontend/dist ./public - -# Copy seed file for factory reset -COPY backend/prisma/seed.ts ./prisma/ - -# Copy entrypoint script -COPY docker/entrypoint.sh ./entrypoint.sh - -# Create directories for uploads and backups -RUN mkdir -p uploads backups && \ - chmod +x entrypoint.sh && \ - chown -R opencrm:nodejs /app - -# Switch to non-root user -USER opencrm - -# Expose port -EXPOSE 3001 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1 - -# Start the application with entrypoint -ENTRYPOINT ["./entrypoint.sh"] diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 34f197c9..00000000 --- a/docker/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# OpenCRM Docker Deployment - -## Schnellstart - -1. **Umgebungsvariablen konfigurieren:** - ```bash - cd docker - cp .env.example .env - nano .env # Sichere Werte setzen - ``` - -2. **Container starten (erster Start mit RUN_SEED=true):** - ```bash - docker compose up -d - ``` - - Beim ersten Start wird automatisch: - - Auf die Datenbank gewartet - - Migrationen ausgeführt - - Seed-Daten geladen (wenn `RUN_SEED=true`) - -3. **Nach erfolgreicher Installation:** - ```bash - # RUN_SEED in .env auf false setzen - sed -i 's/RUN_SEED=true/RUN_SEED=false/' .env - ``` - -4. **Anwendung aufrufen:** - - Mit Domain: `https://your-domain.com` - - Lokal: `http://localhost` - -5. **Login:** - - E-Mail: `admin@admin.com` - - Passwort: `admin` - -## Architektur - -``` - ┌─────────────┐ - │ Caddy │ - │ (SSL/TLS) │ - │ :80/:443 │ - └──────┬──────┘ - │ - ┌──────▼──────┐ - │ OpenCRM │ - │ (Node.js) │ - │ :3001 │ - └──────┬──────┘ - │ - ┌──────▼──────┐ - │ MariaDB │ - │ :3306 │ - └─────────────┘ -``` - -## Befehle - -### Container verwalten -```bash -# Starten -docker compose up -d - -# Stoppen -docker compose down - -# Logs anzeigen -docker compose logs -f app - -# Neustart -docker compose restart app -``` - -### Datenbank -```bash -# Migration ausführen -docker compose exec app npx prisma migrate deploy - -# Seed-Daten laden -docker compose exec app npx tsx prisma/seed.ts - -# Prisma Studio (Datenbank-UI) -docker compose exec app npx prisma studio -``` - -### Backup & Restore -```bash -# Backup-Verzeichnis ist unter /app/backups gemountet -# Backups werden über die Anwendung erstellt/wiederhergestellt -``` - -### Update -```bash -# Image neu bauen und Container aktualisieren -docker compose build --no-cache -docker compose up -d -# Migrationen werden automatisch beim Start ausgeführt -``` - -## Volumes - -| Volume | Beschreibung | -|--------|--------------| -| `mariadb_data` | Datenbank-Dateien | -| `uploads_data` | Hochgeladene Dokumente | -| `backups_data` | Backup-Dateien | -| `caddy_data` | SSL-Zertifikate | -| `caddy_config` | Caddy-Konfiguration | - -## SSL-Zertifikat - -Caddy holt automatisch ein Let's Encrypt Zertifikat wenn: -- Die Domain in `.env` korrekt gesetzt ist -- Port 80 und 443 von außen erreichbar sind -- DNS auf den Server zeigt - -Für lokale Entwicklung mit `DOMAIN=localhost` wird ein selbstsigniertes Zertifikat verwendet. - -## Troubleshooting - -### Container startet nicht -```bash -docker compose logs app -``` - -### Datenbank-Verbindung fehlgeschlagen -```bash -# Warten bis MariaDB bereit ist -docker compose logs db -``` - -### SSL-Zertifikat Probleme -```bash -docker compose logs caddy -# Caddy-Daten zurücksetzen -docker compose down -docker volume rm opencrm_caddy_data opencrm_caddy_config -docker compose up -d -``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 7a2a2c30..00000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,107 +0,0 @@ -version: '3.8' - -services: - # =================================== - # MariaDB Database - # =================================== - db: - image: mariadb:10.11 - container_name: opencrm-db - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpassword} - MYSQL_DATABASE: ${DB_NAME:-opencrm} - MYSQL_USER: ${DB_USER:-opencrm} - MYSQL_PASSWORD: ${DB_PASSWORD:-opencrm123} - volumes: - - mariadb_data:/var/lib/mysql - networks: - - opencrm-network - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - start_period: 10s - interval: 10s - timeout: 5s - retries: 3 - - # =================================== - # OpenCRM Application - # =================================== - app: - build: - context: .. - dockerfile: docker/Dockerfile - container_name: opencrm-app - restart: unless-stopped - environment: - NODE_ENV: production - PORT: 3001 - DATABASE_URL: mysql://${DB_USER:-opencrm}:${DB_PASSWORD:-opencrm123}@db:3306/${DB_NAME:-opencrm} - JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required} - # Access-Token kurzlebig (lebt nur im Browser-Speicher → XSS-Fenster - # klein halten), Refresh-Token lang im httpOnly-Cookie. - # Frueher stand hier 7d fuer den ACCESS-Token – das war ein Ueberbleibsel - # aus der Zeit vor dem Access-/Refresh-Pattern. - JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-15m} - JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-7d} - ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY is required} - # Caddy terminiert TLS und steht als EIN Proxy davor. Das Flag setzt das - # Secure-Attribut am Refresh-Cookie, trust proxy = 1 Hop und - # upgrade-insecure-requests in der CSP. Nur auf false setzen, wenn der - # Stack bewusst ohne TLS laeuft (reiner Test ueber http://). - HTTPS_ENABLED: ${HTTPS_ENABLED:-true} - # Nur noetig, wenn das Frontend unter einer anderen Origin laeuft. - CORS_ORIGINS: ${CORS_ORIGINS:-} - # In der Container-Umgebung muss der Dienst fuer Caddy erreichbar sein. - LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} - # Cloud-Deploy: private IP-Ranges fuer Provider-/SMTP-Tests blocken. - SSRF_BLOCK_PRIVATE_IPS: ${SSRF_BLOCK_PRIVATE_IPS:-false} - # Siegel für das Audit-Log (optional, fail-safe ohne Wert) - AUDIT_HMAC_KEY: ${AUDIT_HMAC_KEY:-} - AUDIT_HMAC_KEY_OLD: ${AUDIT_HMAC_KEY_OLD:-} - RUN_SEED: ${RUN_SEED:-false} - volumes: - - uploads_data:/app/uploads - - backups_data:/app/backups - networks: - - opencrm-network - depends_on: - db: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/api/health"] - start_period: 60s - interval: 30s - timeout: 10s - retries: 3 - - # =================================== - # Caddy Reverse Proxy (with auto SSL) - # =================================== - caddy: - image: caddy:2-alpine - container_name: opencrm-caddy - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - networks: - - opencrm-network - depends_on: - app: - condition: service_healthy - -networks: - opencrm-network: - driver: bridge - -volumes: - mariadb_data: - uploads_data: - backups_data: - caddy_data: - caddy_config: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100644 index e8534491..00000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh -set -e - -echo "=== OpenCRM Startup ===" - -# Wait for database to be ready -echo "Waiting for database connection..." -MAX_RETRIES=30 -RETRY_COUNT=0 - -while ! nc -z db 3306 2>/dev/null; do - RETRY_COUNT=$((RETRY_COUNT + 1)) - if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "Error: Database not available after $MAX_RETRIES attempts" - exit 1 - fi - echo " Attempt $RETRY_COUNT/$MAX_RETRIES - waiting..." - sleep 2 -done -echo "Database is ready!" - -# Run migrations -echo "Running database migrations..." -npx prisma migrate deploy - -# Seed database if RUN_SEED is set (first install) -if [ "$RUN_SEED" = "true" ]; then - echo "Seeding database..." - npx tsx prisma/seed.ts -fi - -# Einmal-Bereinigung für Pentest-Reste (Runde 12): XSS-Strings aus -# Customer/User-Feldern entfernen + unbekannte AppSettings löschen. -# Idempotent – läuft bei jedem Container-Start ohne Risiko. -echo "Running data cleanup..." -npx tsx prisma/cleanup-xss-and-mass-assignment.ts || echo " (Cleanup übersprungen, nicht-kritisch)" - -# Start the application -echo "Starting OpenCRM server..." -exec node dist/index.js