diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md index e2983cf..0100911 100644 --- a/AUTHENTICATION.md +++ b/AUTHENTICATION.md @@ -24,6 +24,7 @@ Die API verwendet **zwei verschiedene Authentifizierungs-Mechanismen** für unte ```env ADMIN_SESSION_SECRET=$(openssl rand -hex 32) ``` + > ℹ️ Standardmäßig setzt der Server in Production HTTPS-Only Cookies (`Secure`). Falls deine Installation **ohne HTTPS** hinter einem internen Netzwerk läuft, kannst du das Verhalten über `ADMIN_SESSION_COOKIE_SECURE=false` explizit deaktivieren. Verwende dies nur in vertrauenswürdigen Umgebungen! 2. **Backend starten** – Migration legt Tabelle `admin_users` an. 3. **Setup-Status prüfen**: ```bash @@ -189,7 +190,7 @@ npm test - [ ] `ADMIN_SESSION_SECRET` sicher generieren (>= 32 Bytes random) - [ ] `.env` nicht in Git committen (bereits in `.gitignore`) -- [ ] HTTPS verwenden (TLS/SSL) damit Cookies `Secure` gesetzt werden können +- [ ] HTTPS verwenden (TLS/SSL) damit Cookies `Secure` gesetzt werden können (falls nicht möglich: `ADMIN_SESSION_COOKIE_SECURE=false` setzen – nur in vertrauenswürdigen Netzen) - [ ] Session-Store auf persistentem Volume ablegen - [ ] Rate Limiting & Audit Logs überwachen - [ ] Admin-Benutzerverwaltung (On-/Offboarding) dokumentieren diff --git a/README.dev.md b/README.dev.md index 5ac4a55..7c7c15d 100644 --- a/README.dev.md +++ b/README.dev.md @@ -33,7 +33,7 @@ docker compose -f docker/dev/docker-compose.yml up -d ### Zugriff - **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv) - **Backend**: http://localhost:5001 (API) -- **API Documentation**: http://localhost:5001/api/docs/ (Swagger UI) +- **API Documentation**: http://localhost:5001/api/docs/ (Swagger UI, nur in Development verfügbar) - **Slideshow**: http://localhost:3000/slideshow - **Moderation**: http://localhost:3000/moderation (Login über Admin Session) @@ -430,6 +430,17 @@ git commit -m "feat: Add new feature" git push origin feature/my-feature ``` +### Git Hook (optional Absicherung) + +Für Deployments ohne HTTPS muss `docker/prod/docker-compose.yml` die Zeile `- ADMIN_SESSION_COOKIE_SECURE=false` enthalten. +Ein vorgefertigtes Pre-Commit-Hook stellt sicher, dass diese Zeile vorhanden ist bzw. automatisch korrigiert wird: + +```bash +ln -s ../../scripts/git-hooks/pre-commit .git/hooks/pre-commit +``` + +Nach der Installation aktualisiert der Hook die Datei bei Bedarf und staged sie direkt. Entfernen kannst du ihn jederzeit über `rm .git/hooks/pre-commit`. + ## Nützliche Befehle ```bash diff --git a/README.md b/README.md index e5ff297..d80c4e4 100644 --- a/README.md +++ b/README.md @@ -584,7 +584,7 @@ For detailed testing instructions, see: [`tests/TESTING-CLEANUP.md`](tests/TESTI | Variable | Default | Description | |----------|---------|-------------| -| `API_URL` | `http://localhost:5000` | Backend API endpoint | +| `API_URL` | `http://localhost:5001` | Backend API endpoint | | `CLIENT_URL` | `http://localhost` | Frontend application URL | ### Volume Configuration diff --git a/backend/docs/openapi.json b/backend/docs/openapi.json index c0cdce5..3bb9353 100644 --- a/backend/docs/openapi.json +++ b/backend/docs/openapi.json @@ -7,8 +7,8 @@ }, "servers": [ { - "url": "http://localhost:5000", - "description": "Development server" + "url": "http://localhost:5001", + "description": "Development server (dev compose backend)" } ], "tags": [ diff --git a/backend/src/generate-openapi.js b/backend/src/generate-openapi.js index 290dc21..4403362 100644 --- a/backend/src/generate-openapi.js +++ b/backend/src/generate-openapi.js @@ -19,7 +19,7 @@ const doc = { version: '1.0.0', description: 'Auto-generated OpenAPI spec with correct mount prefixes' }, - host: 'localhost:5000', + host: 'localhost:5001', schemes: ['http'], // Add base path hints per router (swagger-autogen doesn't natively support per-file prefixes, // so we'll post-process or use @swagger annotations in route files) @@ -71,7 +71,7 @@ async function generateWithPrefixes() { openapi: '3.0.0', info: doc.info, servers: [ - { url: 'http://localhost:5000', description: 'Development server' } + { url: 'http://localhost:5001', description: 'Development server (dev compose backend)' } ], tags: Array.from(allTags).map(name => ({ name })), paths: allPaths diff --git a/backend/src/middlewares/session.js b/backend/src/middlewares/session.js index 8d1b652..5407c60 100644 --- a/backend/src/middlewares/session.js +++ b/backend/src/middlewares/session.js @@ -9,6 +9,35 @@ const SESSION_DIR = process.env.ADMIN_SESSION_DIR : path.join(__dirname, '..', 'data'); const SESSION_SECRET = process.env.ADMIN_SESSION_SECRET; const IS_PRODUCTION = process.env.NODE_ENV === 'production'; +const ADMIN_SESSION_COOKIE_SECURE = process.env.ADMIN_SESSION_COOKIE_SECURE; + +const parseBooleanEnv = (value) => { + if (typeof value !== 'string') { + return undefined; + } + + switch (value.toLowerCase().trim()) { + case 'true': + case '1': + case 'yes': + case 'on': + return true; + case 'false': + case '0': + case 'no': + case 'off': + return false; + default: + return undefined; + } +}; + +const secureOverride = parseBooleanEnv(ADMIN_SESSION_COOKIE_SECURE); +const cookieSecure = secureOverride ?? IS_PRODUCTION; + +if (IS_PRODUCTION && secureOverride === false) { + console.warn('[Session] ADMIN_SESSION_COOKIE_SECURE=false detected – secure cookies disabled in production. Only do this on trusted HTTP deployments.'); +} if (!SESSION_SECRET) { throw new Error('ADMIN_SESSION_SECRET is required for session management'); @@ -33,7 +62,7 @@ const sessionMiddleware = session({ saveUninitialized: false, cookie: { httpOnly: true, - secure: IS_PRODUCTION, + secure: cookieSecure, sameSite: 'strict', maxAge: 8 * 60 * 60 * 1000 // 8 hours } diff --git a/backend/src/server.js b/backend/src/server.js index 91ea314..19face3 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -4,7 +4,6 @@ const path = require('path'); const initiateResources = require('./utils/initiate-resources'); const dbManager = require('./database/DatabaseManager'); const SchedulerService = require('./services/SchedulerService'); -const generateOpenApi = require('./generate-openapi'); // Dev: Swagger UI (mount only in non-production) — require lazily let swaggerUi = null; @@ -29,6 +28,7 @@ class Server { } try { + const generateOpenApi = require('./generate-openapi'); console.log('🔄 Generating OpenAPI specification...'); await generateOpenApi(); console.log('✓ OpenAPI spec generated'); diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 147f911..7170a54 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -38,6 +38,8 @@ services: - NODE_ENV=production - ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions + - ADMIN_SESSION_COOKIE_SECURE=false + networks: npm-nw: diff --git a/docker/prod/frontend/nginx.conf b/docker/prod/frontend/nginx.conf index f7022ae..42f6862 100644 --- a/docker/prod/frontend/nginx.conf +++ b/docker/prod/frontend/nginx.conf @@ -50,6 +50,15 @@ http { # Allow large uploads for batch upload endpoints client_max_body_size 100M; } + + # Admin auth/session endpoints (login/logout/setup/csrf) + location /auth/ { + proxy_pass http://image-uploader-backend:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } # API - Groups API routes (NO PASSWORD PROTECTION) location ~ ^/groups/[a-zA-Z0-9_-]+(/.*)?$ { diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit new file mode 100755 index 0000000..8023dc7 --- /dev/null +++ b/scripts/git-hooks/pre-commit @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TARGET_FILE="$ROOT_DIR/docker/prod/docker-compose.yml" +ANCHOR_LINE=" - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions" +EXPECTED_LINE=" - ADMIN_SESSION_COOKIE_SECURE=false" + +if [[ ! -f "$TARGET_FILE" ]]; then + exit 0 +fi + +export TARGET_FILE +export ANCHOR_LINE +export EXPECTED_LINE + +result=$(python3 <<'PY' +import os +import pathlib +import re +import sys + +path = pathlib.Path(os.environ['TARGET_FILE']) +anchor = os.environ['ANCHOR_LINE'] +expected = os.environ['EXPECTED_LINE'] +text = path.read_text() +changed = False + +if 'ADMIN_SESSION_COOKIE_SECURE' in text: + pattern = re.compile(r'(\-\s*ADMIN_SESSION_COOKIE_SECURE\s*=\s*)([^\n\r]+)') + new_text, count = pattern.subn(r'\1false', text, count=1) + if count: + changed = new_text != text +else: + if anchor not in text: + print('ERROR: Anchor line not found for ADMIN_SESSION_COOKIE_SECURE insertion', file=sys.stderr) + sys.exit(2) + new_text = text.replace(anchor, anchor + '\n' + expected, 1) + changed = True + +if expected not in new_text: + print('ERROR: Failed to ensure ADMIN_SESSION_COOKIE_SECURE=false in docker-compose.yml', file=sys.stderr) + sys.exit(3) + +if changed: + path.write_text(new_text) + print('UPDATED') +else: + print('UNCHANGED') +PY +) +status=$? + +if [[ $status -ne 0 ]]; then + echo "$result" + echo "[pre-commit] Failed to normalize ADMIN_SESSION_COOKIE_SECURE" >&2 + exit $status +fi + +if [[ $result == "UPDATED" ]]; then + echo "[pre-commit] Normalized ADMIN_SESSION_COOKIE_SECURE in docker/prod/docker-compose.yml" + git -C "$ROOT_DIR" add "$TARGET_FILE" +fi + +exit 0