feat: adapt codebase for IP-based HTTP deployment on 172.20.10.59

Switch from hostname+HTTPS (insight-dev.xinion.lan) to IP+HTTP
(172.20.10.59) for alpha/dev deployment without DNS.

Key changes:
- Cookie secure/sameSite flags environment-conditional (fixes HTTP auth)
- docker-compose.yml: remove HTTPS, update host rules, reduce PG memory
- Traefik: disable TLS, update CORS/CSP for HTTP
- Add Prisma init migration (7 tables) and admin seed script
- Generate package-lock.json for npm ci in Docker builds
- Update all documentation for IP-based access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-08 16:21:45 +01:00
parent 10f291cdda
commit 5412ae137a
17 changed files with 14970 additions and 105 deletions

View file

@ -8,8 +8,8 @@
# --- Allgemein --- # --- Allgemein ---
NODE_ENV=development NODE_ENV=development
APP_PORT=3000 APP_PORT=3000
APP_URL=https://insight-dev.xinion.lan APP_URL=http://172.20.10.59
FRONTEND_URL=https://insight-dev.xinion.lan FRONTEND_URL=http://172.20.10.59
LOG_LEVEL=info LOG_LEVEL=info
# --- PostgreSQL --- # --- PostgreSQL ---
@ -41,7 +41,7 @@ JWT_ISSUER=insight-platform
BCRYPT_COST=12 BCRYPT_COST=12
# --- CORS --- # --- CORS ---
CORS_ORIGINS=https://insight-dev.xinion.lan CORS_ORIGINS=http://172.20.10.59
# --- Rate Limiting --- # --- Rate Limiting ---
THROTTLE_TTL=60000 THROTTLE_TTL=60000

View file

@ -63,9 +63,9 @@ cp .env.example .env
### 3. JWT-Schluessel generieren ### 3. JWT-Schluessel generieren
```bash ```bash
# RS256 Schluessel fuer JWT-Signierung # RS256 Schluessel fuer JWT-Signierung
mkdir -p packages/core-service/keys mkdir -p keys
openssl genpkey -algorithm RSA -out packages/core-service/keys/jwt-private.pem -pkeyopt rsa_keygen_bits:2048 openssl genpkey -algorithm RSA -out keys/jwt-private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in packages/core-service/keys/jwt-private.pem -out packages/core-service/keys/jwt-public.pem openssl rsa -pubout -in keys/jwt-private.pem -out keys/jwt-public.pem
``` ```
### 4. Services starten ### 4. Services starten
@ -77,23 +77,24 @@ docker compose up -d
docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d
``` ```
### 5. Datenbank-Migration ### 5. Datenbank-Migration + Seed
```bash ```bash
# Core-Schema # Core-Schema migrieren
docker compose exec core npx prisma migrate deploy --schema=./prisma/core.schema.prisma docker compose run --rm core npx prisma migrate deploy --schema=./prisma/core.schema.prisma
# Tenant-Schema (wird beim Onboarding automatisch ausgefuehrt) # Admin-User anlegen
docker compose run --rm core npx ts-node prisma/seed.ts
``` ```
### 6. Health-Checks pruefen ### 6. Health-Checks pruefen
```bash ```bash
curl http://localhost:3000/health # Core-Service curl http://172.20.10.59/health
curl http://localhost:8080 # Frontend
``` ```
### 7. Erster Login ### 7. Erster Login
- URL: https://insight-dev.xinion.lan (oder http://localhost) - URL: `http://172.20.10.59`
- Initialer Admin-Account wird beim ersten Start via Seed-Script angelegt - Admin: `admin@xinion.de` / `ChangeMe123!`
- Passwort nach erstem Login aendern!
--- ---
@ -101,7 +102,7 @@ curl http://localhost:8080 # Frontend
| Service | Port (intern) | URL (extern via Traefik) | Beschreibung | | Service | Port (intern) | URL (extern via Traefik) | Beschreibung |
|---------------|---------------|----------------------------------|------------------------| |---------------|---------------|----------------------------------|------------------------|
| Traefik | 80/443 | https://insight-dev.xinion.lan | API Gateway | | Traefik | 80 | http://172.20.10.59 | API Gateway |
| Core-Service | 3000 | /api/v1/* | NestJS Backend | | Core-Service | 3000 | /api/v1/* | NestJS Backend |
| Frontend | 8080 | /* | React App | | Frontend | 8080 | /* | React App |
| PostgreSQL | 5432 | - | Datenbank | | PostgreSQL | 5432 | - | Datenbank |

View file

@ -67,11 +67,11 @@
**Erstellte Dateien:** **Erstellte Dateien:**
1. **`docker-compose.yml`** - Alle Basis-Services: 1. **`docker-compose.yml`** - Alle Basis-Services:
- Traefik 3 (API Gateway, Reverse Proxy, TLS, Rate Limiting) - Traefik 3 (API Gateway, Reverse Proxy, Rate Limiting)
- PostgreSQL 16-alpine (mit Performance-Tuning fuer 8GB RAM) - PostgreSQL 16-alpine (Performance-Tuning: 1GB shared_buffers, 4GB cache)
- PgBouncer (Connection Pooling, Transaction Mode) - PgBouncer (Connection Pooling, Transaction Mode)
- Redis 7-alpine (Cache, Sessions, Token-Revocation) - Redis 7-alpine (Cache, Sessions, Token-Revocation)
- step-ca (Interne Certificate Authority fuer mTLS) - step-ca (Interne Certificate Authority fuer mTLS - geplant)
- Core-Service (NestJS) mit Traefik-Labels - Core-Service (NestJS) mit Traefik-Labels
- Frontend (React) mit Traefik-Labels - Frontend (React) mit Traefik-Labels
- 3 isolierte Docker-Netzwerke (insight-web, insight-db, insight-cache) - 3 isolierte Docker-Netzwerke (insight-web, insight-db, insight-cache)
@ -87,7 +87,7 @@
- PostgreSQL Exporter (DB-Metriken) - PostgreSQL Exporter (DB-Metriken)
3. **Konfigurationsdateien:** 3. **Konfigurationsdateien:**
- `config/traefik/dynamic/tls.yml` - TLS-Konfiguration - `config/traefik/dynamic/tls.yml` - TLS deaktiviert (Alpha/Dev)
- `config/traefik/dynamic/middlewares.yml` - Security-Headers, CORS, Compression - `config/traefik/dynamic/middlewares.yml` - Security-Headers, CORS, Compression
- `config/prometheus/prometheus.yml` - Scrape-Konfiguration - `config/prometheus/prometheus.yml` - Scrape-Konfiguration
- `config/loki/loki.yml` - Log-Storage-Konfiguration - `config/loki/loki.yml` - Log-Storage-Konfiguration
@ -113,7 +113,7 @@
- `TotpService`: TOTP 2FA (Google Authenticator kompatibel) - `TotpService`: TOTP 2FA (Google Authenticator kompatibel)
- `LoginDto`: Validierung mit class-validator - `LoginDto`: Validierung mit class-validator
- Account-Lockout nach 5 Fehlversuchen (15 Min Sperre) - Account-Lockout nach 5 Fehlversuchen (15 Min Sperre)
- Refresh-Token als HttpOnly/Secure/SameSite=Strict Cookie - Refresh-Token als HttpOnly Cookie (secure/sameSite umgebungsabhaengig)
- Token-Rotation mit Redis-basierter Familien-Erkennung - Token-Rotation mit Redis-basierter Familien-Erkennung
2. **Users-Modul** (`src/core/users/`) 2. **Users-Modul** (`src/core/users/`)
@ -145,7 +145,7 @@
6. **Config:** 6. **Config:**
- `validateConfig()` mit class-validator fuer Umgebungsvariablen - `validateConfig()` mit class-validator fuer Umgebungsvariablen
#### 6. Prisma-Schemas #### 6. Prisma-Schemas & Migration
1. **`core.schema.prisma`** (platform_core Datenbank): 1. **`core.schema.prisma`** (platform_core Datenbank):
- `User` - Plattform-Benutzer (mit Login-Tracking, 2FA) - `User` - Plattform-Benutzer (mit Login-Tracking, 2FA)
@ -161,6 +161,14 @@
- `Activity` - CRM-Aktivitaeten (Notiz, Anruf, E-Mail, Meeting, Task) - `Activity` - CRM-Aktivitaeten (Notiz, Anruf, E-Mail, Meeting, Task)
- Referenz-Schema fuer Sprint 2 (CRM-Modul) - Referenz-Schema fuer Sprint 2 (CRM-Modul)
3. **Erste Migration erstellt** (`prisma/migrations/20260308000000_init/`)
- 7 Tabellen, alle Indizes und Foreign Keys
- `migration_lock.toml` fuer Prisma
4. **Seed-Script erstellt** (`prisma/seed.ts`)
- Erstellt Platform-Admin: `admin@xinion.de` / `ChangeMe123!`
- Bcrypt Cost 12, Rolle: PLATFORM_ADMIN
#### 7. React Frontend-Shell #### 7. React Frontend-Shell
**Projekt-Setup:** **Projekt-Setup:**
@ -211,6 +219,46 @@
- SSH-Deploy auf insight-dev-01 - SSH-Deploy auf insight-dev-01
- Health-Check Verifizierung - Health-Check Verifizierung
#### 9. IP-basierte Deployment-Anpassung (HTTP statt HTTPS)
**Grund:** Kein DNS-Eintrag vorhanden, Zugriff nur ueber IP 172.20.10.59.
**Geaenderte Dateien:**
1. **`auth.controller.ts`** - Cookie secure/sameSite umgebungsabhaengig
- `secure: true` -> `secure: process.env.NODE_ENV === 'production'`
- `sameSite: 'strict'` -> `isProduction ? 'strict' : 'lax'`
- Betrifft `setRefreshTokenCookie()` und `logout()`
2. **`docker-compose.yml`** - HTTP + IP Umstellung
- HTTPS-Redirect entfernt
- TLS-Entrypoint deaktiviert, Port 443 entfernt
- Alle Host-Rules: `insight-dev.xinion.lan` -> `172.20.10.59`
- Alle Entrypoints: `websecure` -> `web`
- URL-Defaults auf `http://172.20.10.59`
- PostgreSQL Memory reduziert (1GB/4GB/256MB fuer 8GB RAM VM)
- JWT-Keys Volume-Mount hinzugefuegt: `./keys:/app/keys:ro`
3. **`config/traefik/dynamic/tls.yml`** - TLS-Konfiguration deaktiviert
4. **`config/traefik/dynamic/middlewares.yml`**
- HSTS-Headers entfernt
- CSP: `wss://insight-dev.xinion.lan` -> `ws://172.20.10.59`
- CORS: `https://insight-dev.xinion.lan` -> `http://172.20.10.59`
5. **`main.ts`** - CORS-Fallback auf `http://172.20.10.59`
6. **`env.validation.ts`** - APP_URL Default auf `http://172.20.10.59`
7. **`.env.example`** - Alle URLs auf `http://172.20.10.59`
8. **`package-lock.json`** - Generiert fuer core-service und frontend (npm ci braucht diese)
9. **Dokumentation aktualisiert:**
- `docs/INFRASTRUCTURE.md` - HTTP statt HTTPS, IP statt DNS
- `docs/ACCESS.md` - Ports, URLs, Default-Zugangsdaten
- `README.md` - Setup-Anleitung, URLs, Seed-Befehle
--- ---
### Naechste Schritte ### Naechste Schritte
@ -222,15 +270,21 @@
- [x] Prisma-Schemas erstellen (core + tenant) - [x] Prisma-Schemas erstellen (core + tenant)
- [x] React Frontend-Shell implementieren - [x] React Frontend-Shell implementieren
- [x] CI/CD Pipelines (.forgejo/workflows/) definieren - [x] CI/CD Pipelines (.forgejo/workflows/) definieren
- [x] Codebase auf HTTP + IP (172.20.10.59) umstellen
- [x] Seed-Script erstellen (admin@xinion.de)
- [x] Prisma-Migration erstellen (init)
- [x] package-lock.json generieren
- [x] Dokumentation aktualisieren
- [ ] Commit & Push auf develop
- [ ] LVM-Festplatte auf Server erweitern (60GB -> voll nutzbar)
- [ ] Docker + Docker Compose auf insight-dev-01 installieren - [ ] Docker + Docker Compose auf insight-dev-01 installieren
- [ ] .env-Datei auf Server anlegen (echte Passwoerter) - [ ] Repo klonen, .env + JWT-Keys auf Server erstellen
- [ ] JWT RS256 Schluessel generieren (fuer Token-Signierung) - [ ] Services starten, Migration + Seed ausfuehren
- [ ] Erste Prisma-Migration ausfuehren
- [ ] Platform-Admin User anlegen (Seed)
- [ ] Erster End-to-End Test (Login -> Dashboard) - [ ] Erster End-to-End Test (Login -> Dashboard)
--- ---
### Offene Fragen / Abhaengigkeiten ### Offene Fragen / Abhaengigkeiten
- DNS-Eintrag `insight-dev.xinion.lan` muss auf 172.20.10.59 zeigen - DNS-Eintrag `insight-dev.xinion.lan` wird spaeter eingerichtet (dann HTTPS aktivieren)
- LVM auf Server muss erweitert werden (60GB Disk, nur ~56GB sichtbar)

View file

@ -10,9 +10,6 @@ http:
browserXssFilter: true browserXssFilter: true
contentTypeNosniff: true contentTypeNosniff: true
frameDeny: true frameDeny: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN" customFrameOptionsValue: "SAMEORIGIN"
referrerPolicy: "strict-origin-when-cross-origin" referrerPolicy: "strict-origin-when-cross-origin"
contentSecurityPolicy: >- contentSecurityPolicy: >-
@ -21,7 +18,7 @@ http:
style-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:; img-src 'self' data: blob:;
font-src 'self'; font-src 'self';
connect-src 'self' wss://insight-dev.xinion.lan; connect-src 'self' ws://172.20.10.59;
frame-ancestors 'self'; frame-ancestors 'self';
# CORS fuer API # CORS fuer API
@ -40,7 +37,7 @@ http:
- X-Tenant-ID - X-Tenant-ID
- X-Request-ID - X-Request-ID
accessControlAllowOriginList: accessControlAllowOriginList:
- "https://insight-dev.xinion.lan" - "http://172.20.10.59"
accessControlMaxAge: 86400 accessControlMaxAge: 86400
accessControlAllowCredentials: true accessControlAllowCredentials: true
addVaryHeader: true addVaryHeader: true

View file

@ -1,24 +1,2 @@
# ============================================================ # TLS-Konfiguration deaktiviert fuer Alpha/Dev (IP-basierter HTTP-Zugang).
# Traefik - Dynamische TLS-Konfiguration # Wird reaktiviert wenn DNS + HTTPS eingerichtet wird.
# ============================================================
# Fuer die Alpha-Phase verwenden wir ein selbst-signiertes
# Zertifikat. Spaeter wird step-ca als ACME-Provider genutzt.
# ============================================================
tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
sniStrict: false
stores:
default:
defaultGeneratedCert:
resolver: default
domain:
main: "insight-dev.xinion.lan"

View file

@ -45,12 +45,8 @@ services:
# API & Dashboard # API & Dashboard
- "--api.dashboard=true" - "--api.dashboard=true"
- "--api.insecure=true" - "--api.insecure=true"
# Entrypoints # Entrypoints (nur HTTP fuer Alpha/Dev mit IP-Zugang)
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# HTTP -> HTTPS Redirect
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
# Docker Provider # Docker Provider
- "--providers.docker=true" - "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
@ -58,8 +54,6 @@ services:
# File Provider (fuer dynamische Konfiguration) # File Provider (fuer dynamische Konfiguration)
- "--providers.file.directory=/etc/traefik/dynamic" - "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true" - "--providers.file.watch=true"
# TLS (self-signed fuer Dev)
- "--entrypoints.websecure.http.tls=true"
# Logging # Logging
- "--log.level=INFO" - "--log.level=INFO"
- "--accesslog=true" - "--accesslog=true"
@ -70,7 +64,6 @@ services:
- "--entrypoints.metrics.address=:8082" - "--entrypoints.metrics.address=:8082"
ports: ports:
- "80:80" - "80:80"
- "443:443"
- "8080:8080" # Dashboard (nur intern) - "8080:8080" # Dashboard (nur intern)
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
@ -80,10 +73,10 @@ services:
- insight-web - insight-web
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
# Dashboard Route (nur intern) # Dashboard Route (nur intern, via Port 8080)
- "traefik.http.routers.dashboard.rule=Host(`traefik.insight-dev.xinion.lan`)" - "traefik.http.routers.dashboard.rule=Host(`172.20.10.59`) && PathPrefix(`/dashboard`)"
- "traefik.http.routers.dashboard.service=api@internal" - "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure" - "traefik.http.routers.dashboard.entrypoints=web"
healthcheck: healthcheck:
test: ["CMD", "traefik", "healthcheck"] test: ["CMD", "traefik", "healthcheck"]
interval: 30s interval: 30s
@ -118,13 +111,13 @@ services:
command: command:
- "postgres" - "postgres"
- "-c" - "-c"
- "shared_buffers=2GB" - "shared_buffers=1GB"
- "-c" - "-c"
- "effective_cache_size=6GB" - "effective_cache_size=4GB"
- "-c" - "-c"
- "work_mem=16MB" - "work_mem=16MB"
- "-c" - "-c"
- "maintenance_work_mem=512MB" - "maintenance_work_mem=256MB"
- "-c" - "-c"
- "max_connections=200" - "max_connections=200"
- "-c" - "-c"
@ -225,8 +218,8 @@ services:
environment: environment:
NODE_ENV: ${NODE_ENV:-development} NODE_ENV: ${NODE_ENV:-development}
APP_PORT: ${APP_PORT:-3000} APP_PORT: ${APP_PORT:-3000}
APP_URL: ${APP_URL:-https://insight-dev.xinion.lan} APP_URL: ${APP_URL:-http://172.20.10.59}
FRONTEND_URL: ${FRONTEND_URL:-https://insight-dev.xinion.lan} FRONTEND_URL: ${FRONTEND_URL:-http://172.20.10.59}
LOG_LEVEL: ${LOG_LEVEL:-info} LOG_LEVEL: ${LOG_LEVEL:-info}
# Database (via PgBouncer) # Database (via PgBouncer)
DATABASE_URL: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@pgbouncer:6432/${DB_NAME:-platform_core}" DATABASE_URL: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@pgbouncer:6432/${DB_NAME:-platform_core}"
@ -245,10 +238,12 @@ services:
# Bcrypt # Bcrypt
BCRYPT_COST: ${BCRYPT_COST:-12} BCRYPT_COST: ${BCRYPT_COST:-12}
# CORS # CORS
CORS_ORIGINS: ${CORS_ORIGINS:-https://insight-dev.xinion.lan} CORS_ORIGINS: ${CORS_ORIGINS:-http://172.20.10.59}
# Rate Limiting # Rate Limiting
THROTTLE_TTL: ${THROTTLE_TTL:-60000} THROTTLE_TTL: ${THROTTLE_TTL:-60000}
THROTTLE_LIMIT: ${THROTTLE_LIMIT:-200} THROTTLE_LIMIT: ${THROTTLE_LIMIT:-200}
volumes:
- ./keys:/app/keys:ro
networks: networks:
- insight-web - insight-web
- insight-db - insight-db
@ -261,13 +256,13 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
# API Routing # API Routing
- "traefik.http.routers.core-api.rule=Host(`insight-dev.xinion.lan`) && PathPrefix(`/api`)" - "traefik.http.routers.core-api.rule=Host(`172.20.10.59`) && PathPrefix(`/api`)"
- "traefik.http.routers.core-api.entrypoints=websecure" - "traefik.http.routers.core-api.entrypoints=web"
- "traefik.http.routers.core-api.service=core-api" - "traefik.http.routers.core-api.service=core-api"
- "traefik.http.services.core-api.loadbalancer.server.port=3000" - "traefik.http.services.core-api.loadbalancer.server.port=3000"
# Health-Endpunkt (ohne Auth) # Health-Endpunkt (ohne Auth)
- "traefik.http.routers.core-health.rule=Host(`insight-dev.xinion.lan`) && Path(`/health`)" - "traefik.http.routers.core-health.rule=Host(`172.20.10.59`) && Path(`/health`)"
- "traefik.http.routers.core-health.entrypoints=websecure" - "traefik.http.routers.core-health.entrypoints=web"
- "traefik.http.routers.core-health.service=core-api" - "traefik.http.routers.core-health.service=core-api"
# Rate Limiting Middleware # Rate Limiting Middleware
- "traefik.http.middlewares.api-ratelimit.ratelimit.average=100" - "traefik.http.middlewares.api-ratelimit.ratelimit.average=100"
@ -295,8 +290,8 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
# Frontend Routing (Catch-All nach API) # Frontend Routing (Catch-All nach API)
- "traefik.http.routers.frontend.rule=Host(`insight-dev.xinion.lan`)" - "traefik.http.routers.frontend.rule=Host(`172.20.10.59`)"
- "traefik.http.routers.frontend.entrypoints=websecure" - "traefik.http.routers.frontend.entrypoints=web"
- "traefik.http.routers.frontend.service=frontend" - "traefik.http.routers.frontend.service=frontend"
- "traefik.http.routers.frontend.priority=1" - "traefik.http.routers.frontend.priority=1"
- "traefik.http.services.frontend.loadbalancer.server.port=8080" - "traefik.http.services.frontend.loadbalancer.server.port=8080"

View file

@ -118,10 +118,11 @@ docker compose restart core
## 4. Service-Ports (auf der VM) ## 4. Service-Ports (auf der VM)
> **Alpha/Dev:** Kein HTTPS, kein DNS. Zugriff via `http://172.20.10.59`
| Service | Interner Port | Externer Port | URL | | Service | Interner Port | Externer Port | URL |
|-----------------|---------------|---------------|----------------------------------| |-----------------|---------------|---------------|----------------------------------|
| Traefik (HTTP) | 80 | 80 | http://insight-dev.xinion.lan | | Traefik (HTTP) | 80 | 80 | http://172.20.10.59 |
| Traefik (HTTPS) | 443 | 443 | https://insight-dev.xinion.lan |
| Traefik Dashboard | 8080 | - | Nur intern | | Traefik Dashboard | 8080 | - | Nur intern |
| Core-Service | 3000 | - | Via Traefik: /api/v1/* | | Core-Service | 3000 | - | Via Traefik: /api/v1/* |
| Frontend | 8080 | - | Via Traefik: /* | | Frontend | 8080 | - | Via Traefik: /* |
@ -210,7 +211,20 @@ Laufende Anwendung
--- ---
## 9. Wichtige Befehle ## 9. Default-Zugangsdaten (Alpha/Dev)
> **WICHTIG:** Diese Zugangsdaten gelten nur fuer die Ersteinrichtung!
> Passwoerter muessen nach dem ersten Login geaendert werden.
| Service | User / E-Mail | Passwort |
|-------------------|------------------------|--------------------|
| Plattform-Admin | `admin@xinion.de` | `ChangeMe123!` |
| Grafana | `admin` | Siehe `.env` |
| Traefik Dashboard | `admin` | Siehe `.env` |
---
## 10. Wichtige Befehle
### Vom MacBook aus ### Vom MacBook aus
```bash ```bash
@ -220,6 +234,9 @@ git push origin develop
# SSH auf Server # SSH auf Server
ssh -i .keys/deploy_ed25519 deploy@172.20.10.59 ssh -i .keys/deploy_ed25519 deploy@172.20.10.59
# Plattform oeffnen
open http://172.20.10.59
# Grafana oeffnen (SSH-Tunnel) # Grafana oeffnen (SSH-Tunnel)
ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@172.20.10.59 ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@172.20.10.59
# Dann im Browser: http://localhost:3001 # Dann im Browser: http://localhost:3001
@ -234,10 +251,13 @@ docker compose up -d
docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d
# Health-Check # Health-Check
curl http://localhost:3000/health curl http://172.20.10.59/health
# Datenbank-Migration # Datenbank-Migration
docker compose exec core npx prisma migrate deploy docker compose run --rm core npx prisma migrate deploy --schema=./prisma/core.schema.prisma
# Admin-User seeden
docker compose run --rm core npx ts-node prisma/seed.ts
# Logs folgen # Logs folgen
docker compose logs -f --tail=100 docker compose logs -f --tail=100

View file

@ -25,12 +25,14 @@ Alle Services werden als Docker-Container betrieben.
- SSH: nur Key-basiert (`PasswordAuthentication no`) - SSH: nur Key-basiert (`PasswordAuthentication no`)
- Firewall (ufw): - Firewall (ufw):
- Port 22 (SSH) - nur internes Netzwerk - Port 22 (SSH) - nur internes Netzwerk
- Port 80 (HTTP -> Redirect auf HTTPS) - Port 80 (HTTP) - Webzugang (kein HTTPS in Alpha/Dev)
- Port 443 (HTTPS)
- Alle anderen Ports: DENY - Alle anderen Ports: DENY
- Automatische Sicherheitsupdates: `unattended-upgrades` aktiviert - Automatische Sicherheitsupdates: `unattended-upgrades` aktiviert
- Fail2ban fuer SSH-Brute-Force-Schutz - Fail2ban fuer SSH-Brute-Force-Schutz
> **Hinweis:** In der Alpha/Dev-Phase wird kein HTTPS verwendet.
> Zugriff erfolgt ueber `http://172.20.10.59` (IP-basiert, kein DNS).
--- ---
## 3. Software auf der VM ## 3. Software auf der VM
@ -54,11 +56,11 @@ Alles laeuft in Containern.
``` ```
Internet / Internes Netz Internet / Internes Netz
| |
[ Port 80/443 ] [ Port 80 ]
| |
+-------v--------+ +-------v--------+
| Traefik | API Gateway, SSL-Terminierung, | Traefik | API Gateway, Reverse Proxy,
| (Gateway) | Rate Limiting, mTLS-Terminierung | (Gateway) | Rate Limiting
+---+-------+----+ +---+-------+----+
| | | |
+---------+ +---------+ +---------+ +---------+
@ -88,12 +90,12 @@ Alles laeuft in Containern.
| `insight-db` | Core-Service <-> PgBouncer <-> PostgreSQL (intern) | | `insight-db` | Core-Service <-> PgBouncer <-> PostgreSQL (intern) |
| `insight-cache`| Core-Service <-> Redis (intern) | | `insight-cache`| Core-Service <-> Redis (intern) |
### mTLS (step-ca) ### mTLS (step-ca) - geplant fuer Produktion
Alle interne Kommunikation zwischen Containern wird ueber mTLS abgesichert. > **Status:** mTLS ist in der Alpha/Dev-Phase deaktiviert.
step-ca (Smallstep) fungiert als interne Certificate Authority. > step-ca wird spaeter fuer interne Container-Kommunikation eingesetzt.
| Komponente | Zertifikat | | Komponente | Zertifikat (geplant) |
|---------------|-------------------------------| |---------------|-------------------------------|
| Traefik | Wildcard fuer externe Domain | | Traefik | Wildcard fuer externe Domain |
| Core-Service | `core-service.insight.local` | | Core-Service | `core-service.insight.local` |
@ -108,7 +110,7 @@ step-ca (Smallstep) fungiert als interne Certificate Authority.
| Service | Image | Port (intern) | Port (extern) | Beschreibung | | Service | Image | Port (intern) | Port (extern) | Beschreibung |
|---------------|--------------------------------|---------------|---------------|-------------------------------| |---------------|--------------------------------|---------------|---------------|-------------------------------|
| `traefik` | traefik:3 | 80, 443, 8080 | 80, 443 | API Gateway, Reverse Proxy | | `traefik` | traefik:3 | 80, 8080 | 80 | API Gateway, Reverse Proxy |
| `core` | insight-core:latest | 3000 | - | NestJS Backend | | `core` | insight-core:latest | 3000 | - | NestJS Backend |
| `frontend` | insight-frontend:latest | 8080 | - | React App (Nginx served) | | `frontend` | insight-frontend:latest | 8080 | - | React App (Nginx served) |
| `postgres` | postgres:16-alpine | 5432 | - | Datenbank | | `postgres` | postgres:16-alpine | 5432 | - | Datenbank |
@ -149,12 +151,22 @@ PostgreSQL-Server
--- ---
## 8. DNS / Domains ## 8. Netzwerk / Zugriff
> **Alpha/Dev-Phase:** Kein DNS, Zugriff ueber IP-Adresse.
> HTTPS wird spaeter mit DNS-Eintrag aktiviert.
| Zugriff | URL | Zweck |
|----------------------------|--------------------------------|-------------------------------|
| Frontend + API | `http://172.20.10.59` | Entwicklungs-Plattform |
| API-Endpunkte | `http://172.20.10.59/api/v1/*` | REST API |
| Git-Server | `git.xinion.lan` | Git Repository & CI/CD |
### Spaeter (mit DNS):
| Eintrag | Ziel | Zweck | | Eintrag | Ziel | Zweck |
|----------------------------|--------------------|-------------------------------| |----------------------------|--------------------|-------------------------------|
| `insight-dev.xinion.lan` | VM-IP | Entwicklungs-Frontend | | `insight-dev.xinion.lan` | VM-IP | Entwicklungs-Frontend (HTTPS) |
| `api.insight-dev.xinion.lan` | VM-IP | API-Endpunkt |
| `git.xinion.lan` | Forgejo-Server | Git Repository & CI/CD | | `git.xinion.lan` | Forgejo-Server | Git Repository & CI/CD |
--- ---

10925
packages/core-service/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,8 @@
"prisma:generate": "prisma generate --schema=prisma/core.schema.prisma", "prisma:generate": "prisma generate --schema=prisma/core.schema.prisma",
"prisma:migrate": "prisma migrate dev --schema=prisma/core.schema.prisma", "prisma:migrate": "prisma migrate dev --schema=prisma/core.schema.prisma",
"prisma:migrate:deploy": "prisma migrate deploy --schema=prisma/core.schema.prisma", "prisma:migrate:deploy": "prisma migrate deploy --schema=prisma/core.schema.prisma",
"prisma:studio": "prisma studio --schema=prisma/core.schema.prisma" "prisma:studio": "prisma studio --schema=prisma/core.schema.prisma",
"prisma:seed": "ts-node prisma/seed.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.4.0", "@nestjs/common": "^10.4.0",

View file

@ -0,0 +1,146 @@
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"email" VARCHAR(255) NOT NULL,
"first_name" VARCHAR(100) NOT NULL,
"last_name" VARCHAR(100) NOT NULL,
"role" VARCHAR(50) NOT NULL DEFAULT 'USER',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"two_factor_enabled" BOOLEAN NOT NULL DEFAULT false,
"last_login" TIMESTAMP(3),
"failed_login_attempts" INTEGER NOT NULL DEFAULT 0,
"last_failed_login" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "auth_providers" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"provider" VARCHAR(50) NOT NULL,
"provider_id" VARCHAR(255),
"password_hash" VARCHAR(255),
"totp_secret" VARCHAR(255),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "auth_providers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenants" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" VARCHAR(200) NOT NULL,
"slug" VARCHAR(50) NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"settings" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_memberships" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"tenant_role" VARCHAR(50) NOT NULL DEFAULT 'MEMBER',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenant_memberships_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "modules" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"key" VARCHAR(50) NOT NULL,
"name" VARCHAR(100) NOT NULL,
"description" TEXT,
"version" VARCHAR(20) NOT NULL DEFAULT '1.0.0',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "modules_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_modules" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"module_id" UUID NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"config" JSONB NOT NULL DEFAULT '{}',
"activated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_modules_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID,
"action" VARCHAR(100) NOT NULL,
"entity" VARCHAR(100) NOT NULL,
"entity_id" VARCHAR(255),
"details" JSONB,
"ip_address" VARCHAR(45),
"user_agent" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "auth_providers_user_id_provider_key" ON "auth_providers"("user_id", "provider");
-- CreateIndex
CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_memberships_user_id_tenant_id_key" ON "tenant_memberships"("user_id", "tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "modules_key_key" ON "modules"("key");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_modules_tenant_id_module_id_key" ON "tenant_modules"("tenant_id", "module_id");
-- CreateIndex
CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs"("user_id");
-- CreateIndex
CREATE INDEX "audit_logs_action_idx" ON "audit_logs"("action");
-- CreateIndex
CREATE INDEX "audit_logs_entity_entity_id_idx" ON "audit_logs"("entity", "entity_id");
-- CreateIndex
CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs"("created_at");
-- AddForeignKey
ALTER TABLE "auth_providers" ADD CONSTRAINT "auth_providers_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_modules" ADD CONSTRAINT "tenant_modules_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_modules" ADD CONSTRAINT "tenant_modules_module_id_fkey" FOREIGN KEY ("module_id") REFERENCES "modules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View file

@ -0,0 +1,65 @@
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
/**
* Seed-Script: Erstellt den initialen Platform-Admin User.
*
* Ausfuehrung:
* npx ts-node prisma/seed.ts
*
* WICHTIG: Passwort nach erstem Login aendern!
*/
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL_DIRECT || process.env.DATABASE_URL,
},
},
});
async function main(): Promise<void> {
const email = 'admin@xinion.de';
// Pruefen ob Admin bereits existiert
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
console.log(`Admin-User ${email} existiert bereits. Seed uebersprungen.`);
return;
}
// Passwort hashen (Bcrypt Cost 12 gemaess Sicherheitsregel)
const passwordHash = await bcrypt.hash('ChangeMe123!', 12);
// Admin-User anlegen
const user = await prisma.user.create({
data: {
email,
firstName: 'Platform',
lastName: 'Admin',
role: 'PLATFORM_ADMIN',
isActive: true,
authProvider: {
create: {
provider: 'LOCAL',
passwordHash,
},
},
},
});
console.log(`Platform-Admin erstellt: ${user.email} (ID: ${user.id})`);
console.log('');
console.log('Zugangsdaten:');
console.log(` E-Mail: ${email}`);
console.log(' Passwort: ChangeMe123!');
console.log('');
console.log('WICHTIG: Passwort nach erstem Login aendern!');
}
main()
.catch((e: Error) => {
console.error('Seed fehlgeschlagen:', e.message);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View file

@ -27,7 +27,7 @@ class EnvironmentVariables {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
APP_URL = 'https://insight-dev.xinion.lan'; APP_URL = 'http://172.20.10.59';
// Datenbank // Datenbank
@IsString() @IsString()

View file

@ -96,10 +96,11 @@ export class AuthController {
await this.authService.logout(user, refreshToken); await this.authService.logout(user, refreshToken);
// Refresh-Token Cookie loeschen // Refresh-Token Cookie loeschen
const isProduction = process.env.NODE_ENV === 'production';
res.clearCookie('refresh_token', { res.clearCookie('refresh_token', {
httpOnly: true, httpOnly: true,
secure: true, secure: isProduction,
sameSite: 'strict', sameSite: isProduction ? 'strict' : 'lax',
path: '/api/v1/auth', path: '/api/v1/auth',
}); });
@ -107,14 +108,17 @@ export class AuthController {
} }
/** /**
* Setzt das Refresh-Token als HttpOnly, Secure, SameSite=Strict Cookie. * Setzt das Refresh-Token als HttpOnly Cookie.
* Secure + SameSite=Strict nur in Produktion (HTTPS).
* In Development (HTTP) wird Secure deaktiviert und SameSite=Lax gesetzt.
*/ */
private setRefreshTokenCookie(res: Response, refreshToken: string): void { private setRefreshTokenCookie(res: Response, refreshToken: string): void {
const isProduction = process.env.NODE_ENV === 'production';
res.cookie('refresh_token', refreshToken, { res.cookie('refresh_token', refreshToken, {
httpOnly: true, httpOnly: true,
secure: true, // Nur HTTPS secure: isProduction,
sameSite: 'strict', sameSite: isProduction ? 'strict' : 'lax',
path: '/api/v1/auth', // Nur fuer Auth-Endpunkte path: '/api/v1/auth',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 Tage maxAge: 7 * 24 * 60 * 60 * 1000, // 7 Tage
}); });
} }

View file

@ -17,7 +17,7 @@ async function bootstrap(): Promise<void> {
// CORS // CORS
const corsOrigins = process.env.CORS_ORIGINS?.split(',') ?? [ const corsOrigins = process.env.CORS_ORIGINS?.split(',') ?? [
'https://insight-dev.xinion.lan', 'http://172.20.10.59',
]; ];
app.enableCors({ app.enableCors({
origin: corsOrigins, origin: corsOrigins,

3664
packages/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff