From 7ac8a7026020043d34857b531725adbc23be98a0 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Tue, 25 Nov 2025 20:05:31 +0100 Subject: [PATCH] docs: Add FEATURE_PLAN for public/internal host separation - Host-based access control (public vs internal subdomain) - Backend middleware for API protection - Frontend code splitting for internal-only features - Rate limiting for public uploads (20/hour/IP) - Comprehensive testing strategy - Security review and deployment plan --- .../FEATURE_PLAN-FrontendPublic.md | 1170 +++++++++++++++++ 1 file changed, 1170 insertions(+) create mode 100644 FeatureRequests/FEATURE_PLAN-FrontendPublic.md diff --git a/FeatureRequests/FEATURE_PLAN-FrontendPublic.md b/FeatureRequests/FEATURE_PLAN-FrontendPublic.md new file mode 100644 index 0000000..00210e8 --- /dev/null +++ b/FeatureRequests/FEATURE_PLAN-FrontendPublic.md @@ -0,0 +1,1170 @@ +# Feature Plan: Public vs. Internal Frontend/API per Subdomain + +**Erstellt:** 25.11.2025 +**Basiert auf:** `FEATURE_REQUEST-FrontendPublic.md` +**Ziel:** Subdomain-abhängige Features und API-Zugriffe (Public Upload-Only vs. Internal Full-Feature) + +--- + +## 1. Übersicht & Architektur-Entscheidungen + +### 1.1 Ziele +- **Public Host** (`deinprojekt.hobbyhimmel.de`): Nur Upload + Management-Portal (UUID-basiert) +- **Internal Host** (`deinprojekt.lan.hobbyhimmel.de`): Vollständige App (Slideshow, Groups, Moderation, Admin) +- **Sicherheit**: Serverseitige Blockierung von Admin/Moderation/Groups APIs auf public Host +- **Performance**: Code Splitting - internal Features werden auf public Host nicht geladen + +### 1.2 Architektur (bestätigt) +- **Ein Docker Container** mit einem Port (80 für Frontend, 5000 für Backend) +- **nginx-proxy-manager** leitet beide Subdomains auf denselben Container weiter + - Setzt automatisch `X-Forwarded-Host` Header + - Public: `deinprojekt.hobbyhimmel.de` → Container:80 + - Internal: `deinprojekt.lan.hobbyhimmel.de` → Container:80 +- **Backend**: Erkennt Host via `X-Forwarded-Host` und blockiert geschützte APIs für public +- **Frontend**: + - Ein Build mit React Code Splitting (lazy loading) + - Runtime-Erkennung der Subdomain + - Internal-only Routes werden auf public Host nicht geladen + +### 1.3 Sicherheitskonzept +1. **Defense in Depth**: + - Backend Middleware blockiert geschützte APIs basierend auf Host + - Frontend lädt internal Features nicht (Code Splitting) + - Rate Limiting für public Uploads (20/Stunde/IP) +2. **Geschützte Ressourcen** (nur internal): + - `/api/admin/*` - Admin-Funktionen + - `/api/groups` - Groups Listing + - `/api/slideshow` - Slideshow Data + - `/api/migration/*` - Migration Tools + - `/api/moderation/*` - Moderation (falls vorhanden) +3. **Public erlaubte Ressourcen**: + - `/api/upload` - Upload Endpoint + - `/api/manage/:token` - Management Portal (UUID-basiert) + - `/api/previews/*` - Preview Images (nur mit validem Token) + +--- + +## 2. Environment Variablen + +### 2.1 Neue Variablen + +**Backend** (`docker/prod/backend/.env` bzw. docker-compose): +```bash +# Host Configuration +PUBLIC_HOST=deinprojekt.hobbyhimmel.de +INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de + +# Rate Limiting (Public Host) +PUBLIC_UPLOAD_RATE_LIMIT=20 +PUBLIC_UPLOAD_RATE_WINDOW=3600000 # 1 Stunde in ms + +# Feature Flags +ENABLE_HOST_RESTRICTION=true +``` + +**Frontend** (runtime `env-config.js`): +```javascript +window._env_ = { + API_URL: process.env.API_URL || 'http://localhost:5000', + PUBLIC_HOST: process.env.PUBLIC_HOST || 'deinprojekt.hobbyhimmel.de', + INTERNAL_HOST: process.env.INTERNAL_HOST || 'deinprojekt.lan.hobbyhimmel.de' +}; +``` + +### 2.2 docker-compose.yml Anpassungen + +**File**: `docker/prod/docker-compose.yml` + +```yaml +backend: + environment: + # ... existing vars ... + - PUBLIC_HOST=deinprojekt.hobbyhimmel.de + - INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de + - PUBLIC_UPLOAD_RATE_LIMIT=20 + - ENABLE_HOST_RESTRICTION=true + +frontend: + environment: + # ... existing vars ... + - PUBLIC_HOST=deinprojekt.hobbyhimmel.de + - INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de +``` + +--- + +## 3. Backend Implementierung + +### 3.1 Host Gate Middleware + +**Neue Datei**: `backend/src/middlewares/hostGate.js` + +**Zweck**: Erkennt public vs. internal Host und blockiert geschützte Routes für public + +**Implementierung**: +```javascript +/** + * Host Gate Middleware + * Blockiert geschützte API-Routen für public Host + * Erlaubt nur Upload + Management für public + */ + +const PUBLIC_HOST = process.env.PUBLIC_HOST || 'deinprojekt.hobbyhimmel.de'; +const INTERNAL_HOST = process.env.INTERNAL_HOST || 'deinprojekt.lan.hobbyhimmel.de'; +const ENABLE_HOST_RESTRICTION = process.env.ENABLE_HOST_RESTRICTION !== 'false'; + +// Routes die NUR für internal Host erlaubt sind +const INTERNAL_ONLY_ROUTES = [ + '/api/admin', + '/api/groups', + '/api/slideshow', + '/api/migration', + '/api/moderation', + '/api/reorder', + '/api/batch-upload', + '/api/social-media', + '/api/auth/login', // Admin Login nur internal + '/api/auth/logout', + '/api/auth/session' +]; + +// Routes die für public Host erlaubt sind +const PUBLIC_ALLOWED_ROUTES = [ + '/api/upload', + '/api/manage', + '/api/previews', + '/api/consent' +]; + +const hostGate = (req, res, next) => { + // Feature disabled in dev/test + if (!ENABLE_HOST_RESTRICTION || process.env.NODE_ENV === 'test') { + req.isPublicHost = false; + req.isInternalHost = true; + return next(); + } + + // Get host from X-Forwarded-Host (nginx-proxy-manager) or Host header + const host = req.get('x-forwarded-host') || req.get('host') || ''; + const hostname = host.split(':')[0]; // Remove port if present + + // Determine if request is from public or internal host + req.isPublicHost = hostname === PUBLIC_HOST; + req.isInternalHost = hostname === INTERNAL_HOST || hostname === 'localhost'; + + // If public host, check if route is allowed + if (req.isPublicHost) { + const path = req.path; + + // Check if route is internal-only + const isInternalOnly = INTERNAL_ONLY_ROUTES.some(route => + path.startsWith(route) + ); + + if (isInternalOnly) { + console.warn(`🚫 Public host blocked access to: ${path} (Host: ${hostname})`); + return res.status(403).json({ + error: 'Not available on public host', + message: 'This endpoint is only available on the internal network' + }); + } + } + + // Add audit log context + req.requestSource = req.isPublicHost ? 'public' : 'internal'; + + next(); +}; + +module.exports = hostGate; +``` + +**Integration in Middleware Stack**: + +**File**: `backend/src/middlewares/index.js` + +```javascript +const hostGate = require('./hostGate'); + +const applyMiddlewares = (app) => { + // ... existing middlewares (CORS, body-parser, etc.) ... + + // Host Gate MUSS VOR den Routes kommen + app.use(hostGate); + + // ... rest of middlewares ... +}; +``` + +### 3.2 Rate Limiter Anpassung + +**File**: `backend/src/middlewares/rateLimiter.js` + +**Anpassung**: Strengere Limits für public Host Uploads + +```javascript +const rateLimit = require('express-rate-limit'); + +const PUBLIC_UPLOAD_RATE_LIMIT = parseInt(process.env.PUBLIC_UPLOAD_RATE_LIMIT || '20', 10); +const PUBLIC_UPLOAD_RATE_WINDOW = parseInt(process.env.PUBLIC_UPLOAD_RATE_WINDOW || '3600000', 10); + +// Bestehende Limiter... + +// Neuer Public Upload Limiter +const publicUploadLimiter = rateLimit({ + windowMs: PUBLIC_UPLOAD_RATE_WINDOW, + max: PUBLIC_UPLOAD_RATE_LIMIT, + message: { + error: 'Too many uploads', + message: `Maximum ${PUBLIC_UPLOAD_RATE_LIMIT} uploads per hour allowed` + }, + standardHeaders: true, + legacyHeaders: false, + // Nur für public Host anwenden + skip: (req) => !req.isPublicHost +}); + +module.exports = { + // ... existing limiters ... + publicUploadLimiter +}; +``` + +**Integration in Upload Route**: + +**File**: `backend/src/routes/upload.js` + +```javascript +const { publicUploadLimiter } = require('../middlewares/rateLimiter'); + +// Apply public upload limiter +router.post('/upload', publicUploadLimiter, uploadController.uploadImages); +``` + +### 3.3 Audit Log Erweiterung + +**File**: `backend/src/middlewares/auditLog.js` + +**Anpassung**: `source_host` in Audit Logs aufnehmen + +```javascript +const logManagementAction = (req, action, details = {}) => { + const auditEntry = { + // ... existing fields ... + source_host: req.get('x-forwarded-host') || req.get('host'), + source_type: req.requestSource || 'unknown', + // ... rest ... + }; + + // Log to DB... +}; +``` + +--- + +## 4. Frontend Implementierung + +### 4.1 Runtime Environment Config + +**File**: `frontend/public/env-config.js` + +**Zweck**: Wird beim Container-Start mit echten Env-Variablen befüllt + +```javascript +// This file is replaced at runtime by env.sh +window._env_ = { + API_URL: "${API_URL}", + PUBLIC_HOST: "${PUBLIC_HOST}", + INTERNAL_HOST: "${INTERNAL_HOST}" +}; +``` + +**File**: `frontend/env.sh` (anpassen) + +```bash +#!/bin/bash +# Inject runtime environment variables + +cat < /usr/share/nginx/html/env-config.js +window._env_ = { + API_URL: "${API_URL:-http://localhost:5000}", + PUBLIC_HOST: "${PUBLIC_HOST:-deinprojekt.hobbyhimmel.de}", + INTERNAL_HOST: "${INTERNAL_HOST:-deinprojekt.lan.hobbyhimmel.de}" +}; +EOF +``` + +### 4.2 Host Detection Utility + +**Neue Datei**: `frontend/src/Utils/hostDetection.js` + +```javascript +/** + * Erkennt, ob App auf public oder internal Host läuft + * Basiert auf window.location.hostname + env-config + */ + +export const getHostConfig = () => { + const hostname = window.location.hostname; + const publicHost = window._env_?.PUBLIC_HOST || 'deinprojekt.hobbyhimmel.de'; + const internalHost = window._env_?.INTERNAL_HOST || 'deinprojekt.lan.hobbyhimmel.de'; + + const isPublic = hostname === publicHost; + const isInternal = hostname === internalHost || hostname === 'localhost'; + + return { + hostname, + publicHost, + internalHost, + isPublic, + isInternal, + // Feature Flags + canAccessAdmin: isInternal, + canAccessSlideshow: isInternal, + canAccessGroups: isInternal, + canAccessModeration: isInternal, + canUpload: true, // Immer erlaubt + canManageByUUID: true // Immer erlaubt + }; +}; + +export const isPublicHost = () => getHostConfig().isPublic; +export const isInternalHost = () => getHostConfig().isInternal; +``` + +### 4.3 App.js mit Code Splitting + +**File**: `frontend/src/App.js` + +**Anpassung**: Lazy Loading für internal-only Pages + +```javascript +import React, { lazy, Suspense } from 'react'; +import './App.css'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { AdminSessionProvider } from './contexts/AdminSessionContext.jsx'; +import { getHostConfig } from './Utils/hostDetection.js'; + +// Always loaded (public + internal) +import MultiUploadPage from './Components/Pages/MultiUploadPage'; +import ManagementPortalPage from './Components/Pages/ManagementPortalPage'; +import NotFoundPage from './Components/Pages/404Page.js'; + +// Lazy loaded (internal only) +const SlideshowPage = lazy(() => import('./Components/Pages/SlideshowPage')); +const GroupsOverviewPage = lazy(() => import('./Components/Pages/GroupsOverviewPage')); +const PublicGroupImagesPage = lazy(() => import('./Components/Pages/PublicGroupImagesPage')); +const ModerationGroupsPage = lazy(() => import('./Components/Pages/ModerationGroupsPage')); +const ModerationGroupImagesPage = lazy(() => import('./Components/Pages/ModerationGroupImagesPage')); + +// Protected Route Component +const ProtectedRoute = ({ children }) => { + const hostConfig = getHostConfig(); + + if (hostConfig.isPublic) { + // Redirect to upload page with message + return ; + } + + return children; +}; + +// Loading Fallback +const LoadingFallback = () => ( +
+

Loading...

+
+); + +function App() { + const hostConfig = getHostConfig(); + + return ( + + + }> + + {/* Public Routes - immer verfügbar */} + } /> + } /> + + {/* Internal Only Routes */} + {hostConfig.isInternal && ( + <> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + )} + + {/* 404 / Not Found */} + } /> + + + + + ); +} + +export default App; +``` + +### 4.4 Navigation / Menu Anpassung + +**Betrifft**: `frontend/src/Components/Pages/MultiUploadPage.js` und andere Pages mit Navigation + +**Anpassung**: Menü-Items nur anzeigen, wenn auf internal Host + +```javascript +import { getHostConfig } from '../../Utils/hostDetection'; + +const MultiUploadPage = () => { + const hostConfig = getHostConfig(); + + return ( +
+ {/* Nur auf internal Host Navigation anzeigen */} + {hostConfig.isInternal && ( + + )} + + {/* Upload Form - immer sichtbar */} + + + {/* Optional: Hinweis für public users */} + {hostConfig.isPublic && ( +
+

Sie nutzen den öffentlichen Upload-Bereich.

+
+ )} +
+ ); +}; +``` + +**Hinweis**: Bestehende Navigation ist bereits so gestaltet, dass auf Upload-Seite keine Menüpunkte sichtbar sind. Diese Logik wird durch hostConfig verstärkt. + +### 4.5 404 Page Anpassung + +**File**: `frontend/src/Components/Pages/404Page.js` + +**Anpassung**: Unterschiedliche Meldung für public vs. internal + +```javascript +import { getHostConfig } from '../../Utils/hostDetection'; + +const NotFoundPage = () => { + const hostConfig = getHostConfig(); + + return ( +
+

404 - Seite nicht gefunden

+ + {hostConfig.isPublic ? ( + <> +

Diese Funktion ist nicht öffentlich verfügbar.

+ Zurück zum Upload + + ) : ( + <> +

Die angeforderte Seite existiert nicht.

+ Zurück zur Startseite + + )} +
+ ); +}; + +export default NotFoundPage; +``` + +--- + +## 5. nginx-proxy-manager Konfiguration + +### 5.1 Wichtige Hinweise + +- **X-Forwarded-Host**: Wird automatisch gesetzt - keine manuelle Konfiguration nötig +- **SSL**: Beide Hosts müssen gültige Zertifikate haben +- **Proxy-Hosts**: Zwei separate Einträge erstellen + +### 5.2 Proxy Host Setup (GUI) + +**Public Host**: +``` +Domain Names: deinprojekt.hobbyhimmel.de +Scheme: http +Forward Hostname/IP: image-uploader-frontend (Docker Container Name) +Forward Port: 80 +Cache Assets: Yes +Block Common Exploits: Yes +Websockets Support: No + +SSL: +- Let's Encrypt Certificate +- Force SSL: Yes +- HTTP/2 Support: Yes +- HSTS Enabled: Yes +``` + +**Internal Host**: +``` +Domain Names: deinprojekt.lan.hobbyhimmel.de +Scheme: http +Forward Hostname/IP: image-uploader-frontend (Docker Container Name) +Forward Port: 80 +Cache Assets: Yes +Block Common Exploits: Yes +Websockets Support: No + +SSL: +- Let's Encrypt DNS Challenge (für *.lan.hobbyhimmel.de) +- Force SSL: Yes +- HTTP/2 Support: Yes +``` + +### 5.3 Optional: Zusätzliche nginx-Sicherheit + +**Advanced Settings** für Public Host (optional, Defense in Depth): + +```nginx +# Block admin routes on nginx level (zusätzlich zu Backend-Middleware) +location ~ ^/api/(admin|groups|slideshow|moderation|migration|reorder|batch-upload|social-media) { + return 403; +} + +# Allow only upload and management +location ~ ^/api/(upload|manage|previews|consent) { + proxy_pass http://image-uploader-frontend:80; + 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; + proxy_set_header X-Forwarded-Host $host; +} +``` + +**Hinweis**: Dies ist **optional** - die Backend-Middleware ist bereits ausreichend. nginx-Blockierung ist zusätzliche Sicherheitsebene. + +--- + +## 6. Docker & Deployment + +### 6.1 docker-compose.yml Finale Anpassungen + +**File**: `docker/prod/docker-compose.yml` + +```yaml +services: + frontend: + container_name: image-uploader-frontend + image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-frontend:latest + ports: + - "80:80" # Beide Hosts auf selben Port + build: + context: ../../ + dockerfile: docker/prod/frontend/Dockerfile + depends_on: + - backend + environment: + - API_URL=http://backend:5000 + - CLIENT_URL=http://localhost + - PUBLIC_HOST=deinprojekt.hobbyhimmel.de + - INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de + networks: + - npm-nw + - prod-internal + + backend: + container_name: image-uploader-backend + image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-backend:latest + build: + context: ../../ + dockerfile: docker/prod/backend/Dockerfile + ports: + - "5000:5000" + volumes: + - image_data:/usr/src/app/src/data + networks: + - prod-internal + environment: + - REMOVE_IMAGES=false + - NODE_ENV=production + - ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET} + - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions + - ADMIN_SESSION_COOKIE_SECURE=true + # Host Configuration + - PUBLIC_HOST=deinprojekt.hobbyhimmel.de + - INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de + - PUBLIC_UPLOAD_RATE_LIMIT=20 + - PUBLIC_UPLOAD_RATE_WINDOW=3600000 + - ENABLE_HOST_RESTRICTION=true + # Trust nginx-proxy-manager + - TRUST_PROXY_HOPS=1 + +volumes: + image_data: + +networks: + npm-nw: + external: true + prod-internal: + driver: bridge +``` + +### 6.2 Frontend Dockerfile Anpassung + +**File**: `docker/prod/frontend/Dockerfile` + +**Sicherstellen**: `env.sh` wird beim Container-Start ausgeführt + +```dockerfile +# ... existing build steps ... + +# Copy env.sh +COPY frontend/env.sh /docker-entrypoint.d/ + +# Make executable +RUN chmod +x /docker-entrypoint.d/env.sh + +# ... rest ... +``` + +### 6.3 Dev Setup Anpassungen + +**File**: `docker/dev/docker-compose.yml` + +```yaml +services: + frontend: + environment: + # ... existing ... + - PUBLIC_HOST=localhost + - INTERNAL_HOST=localhost + + backend: + environment: + # ... existing ... + - PUBLIC_HOST=localhost + - INTERNAL_HOST=localhost + - ENABLE_HOST_RESTRICTION=false # Disabled in dev +``` + +**Hinweis**: In Dev sind beide Hosts auf `localhost`, Feature-Restriction ist deaktiviert für einfaches Testing. + +### 6.4 Testing mit Dev-Setup + +Um beide Modi im Dev zu testen: + +**Option 1 - /etc/hosts Einträge**: +``` +127.0.0.1 deinprojekt.hobbyhimmel.de +127.0.0.1 deinprojekt.lan.hobbyhimmel.de +``` + +**Option 2 - Browser URL Parameter**: +```javascript +// In hostDetection.js +const urlParams = new URLSearchParams(window.location.search); +const forcedMode = urlParams.get('mode'); // ?mode=public oder ?mode=internal + +if (forcedMode === 'public') { + return { ...config, isPublic: true, isInternal: false }; +} +``` + +--- + +## 7. Testing + +### 7.1 Backend Unit Tests + +**Neue Datei**: `backend/tests/unit/middlewares/hostGate.test.js` + +```javascript +const hostGate = require('../../../src/middlewares/hostGate'); + +describe('Host Gate Middleware', () => { + let req, res, next; + + beforeEach(() => { + req = { + get: jest.fn(), + path: '/api/admin/test' + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + next = jest.fn(); + + process.env.ENABLE_HOST_RESTRICTION = 'true'; + process.env.PUBLIC_HOST = 'public.example.com'; + process.env.INTERNAL_HOST = 'internal.example.com'; + }); + + test('blocks admin routes on public host', () => { + req.get.mockReturnValue('public.example.com'); + + hostGate(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + + test('allows admin routes on internal host', () => { + req.get.mockReturnValue('internal.example.com'); + + hostGate(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('allows upload route on public host', () => { + req.path = '/api/upload'; + req.get.mockReturnValue('public.example.com'); + + hostGate(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('respects X-Forwarded-Host header', () => { + req.get.mockImplementation((header) => { + if (header === 'x-forwarded-host') return 'public.example.com'; + return null; + }); + req.path = '/api/admin/test'; + + hostGate(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + }); +}); +``` + +### 7.2 Backend Integration Tests + +**Neue Datei**: `backend/tests/api/hostRestriction.test.js` + +```javascript +const request = require('supertest'); +const { setupTestServer, cleanupTestServer } = require('../testServer'); + +describe('Host Restriction Integration', () => { + let app; + + beforeAll(async () => { + app = await setupTestServer(); + }); + + afterAll(async () => { + await cleanupTestServer(); + }); + + describe('Public Host', () => { + test('POST /api/upload should succeed', async () => { + const response = await request(app) + .post('/api/upload') + .set('X-Forwarded-Host', 'public.example.com') + .attach('images', Buffer.from('fake'), 'test.jpg'); + + expect(response.status).not.toBe(403); + }); + + test('GET /api/admin/deletion-log should be blocked', async () => { + const response = await request(app) + .get('/api/admin/deletion-log') + .set('X-Forwarded-Host', 'public.example.com'); + + expect(response.status).toBe(403); + }); + + test('GET /api/groups should be blocked', async () => { + const response = await request(app) + .get('/api/groups') + .set('X-Forwarded-Host', 'public.example.com'); + + expect(response.status).toBe(403); + }); + }); + + describe('Internal Host', () => { + test('GET /api/admin/deletion-log should succeed', async () => { + const response = await request(app) + .get('/api/admin/deletion-log') + .set('X-Forwarded-Host', 'internal.example.com'); + + expect(response.status).not.toBe(403); + }); + }); +}); +``` + +### 7.3 Frontend Tests + +**Neue Datei**: `frontend/src/Utils/__tests__/hostDetection.test.js` + +```javascript +import { getHostConfig, isPublicHost, isInternalHost } from '../hostDetection'; + +describe('Host Detection', () => { + beforeEach(() => { + delete window._env_; + delete window.location; + }); + + test('detects public host correctly', () => { + window._env_ = { + PUBLIC_HOST: 'public.example.com', + INTERNAL_HOST: 'internal.example.com' + }; + window.location = { hostname: 'public.example.com' }; + + const config = getHostConfig(); + + expect(config.isPublic).toBe(true); + expect(config.isInternal).toBe(false); + expect(config.canAccessAdmin).toBe(false); + }); + + test('detects internal host correctly', () => { + window._env_ = { + PUBLIC_HOST: 'public.example.com', + INTERNAL_HOST: 'internal.example.com' + }; + window.location = { hostname: 'internal.example.com' }; + + const config = getHostConfig(); + + expect(config.isPublic).toBe(false); + expect(config.isInternal).toBe(true); + expect(config.canAccessAdmin).toBe(true); + }); + + test('localhost defaults to internal', () => { + window._env_ = { + PUBLIC_HOST: 'public.example.com', + INTERNAL_HOST: 'internal.example.com' + }; + window.location = { hostname: 'localhost' }; + + const config = getHostConfig(); + + expect(config.isInternal).toBe(true); + }); +}); +``` + +### 7.4 E2E Test Checklist (Manuell) + +- [ ] Upload auf `deinprojekt.hobbyhimmel.de` funktioniert +- [ ] Management-Portal (`/manage/:uuid`) auf public Host funktioniert +- [ ] `/slideshow` auf public Host zeigt 404 / Not Found +- [ ] `/groups` auf public Host zeigt 404 / Not Found +- [ ] `/moderation` auf public Host zeigt 404 / Not Found +- [ ] Admin Login auf public Host blockiert (403) +- [ ] Alle Features auf `deinprojekt.lan.hobbyhimmel.de` funktionieren +- [ ] Navigation auf internal Host zeigt alle Menüpunkte +- [ ] Navigation auf public Host zeigt nur Upload-relevante Items +- [ ] Rate Limiting: 21. Upload/Stunde auf public Host wird blockiert +- [ ] DevTools Network: Internal-only JS-Bundles werden auf public nicht geladen + +--- + +## 8. Sicherheits-Review + +### 8.1 Threat Model + +| Bedrohung | Mitigation | Status | +|-----------|------------|--------| +| Direkter API-Zugriff auf Admin-Endpoints von extern | Backend Middleware blockiert basierend auf Host | ✅ Implementiert | +| Frontend-Code-Analyse zeigt interne Features | Code Splitting verhindert Laden von internal Bundles | ✅ Implementiert | +| Rate Limiting Bypass | IP-basiertes Limiting + nginx-proxy-manager Logs | ✅ Implementiert | +| UUID-Token Leak | Management-Tokens sind per Design shareable; Rate Limit 10 req/h | ⚠️ Akzeptiertes Risiko | +| MITM-Angriffe | SSL/TLS auf beiden Hosts (Let's Encrypt) | ✅ Bestehend | +| CSRF auf Upload | CSRF-Protection in Middleware | ✅ Bestehend | + +### 8.2 Security Checklist + +- [ ] `ENABLE_HOST_RESTRICTION=true` in Production +- [ ] `TRUST_PROXY_HOPS=1` korrekt gesetzt (nginx-proxy-manager) +- [ ] SSL Zertifikate für beide Hosts gültig +- [ ] Rate Limits getestet (20 uploads/h) +- [ ] Admin-Endpoints per `curl` von extern getestet (403 expected) +- [ ] Audit Logs enthalten `source_host` und `source_type` +- [ ] nginx-proxy-manager Block Common Exploits enabled +- [ ] HSTS enabled auf beiden Hosts +- [ ] Firewall: Port 5000 (Backend) nicht direkt von extern erreichbar + +--- + +## 9. Dokumentation Updates + +### 9.1 README.md Ergänzungen + +**Abschnitt**: "Deployment - Production" + +```markdown +## Host-basierte Zugriffskontrolle + +Die App unterstützt unterschiedliche Features abhängig von der Subdomain: + +### Public Host (`deinprojekt.hobbyhimmel.de`) +- **Verfügbar**: Upload, Management-Portal (UUID-basiert) +- **Nicht verfügbar**: Admin, Moderation, Slideshow, Groups +- **Rate Limit**: 20 Uploads pro Stunde pro IP + +### Internal Host (`deinprojekt.lan.hobbyhimmel.de`) +- **Verfügbar**: Alle Features (Full App) +- **Zugriff**: Nur über Intranet / VPN + +### Konfiguration + +Environment Variablen in `docker-compose.yml`: + +```yaml +environment: + - PUBLIC_HOST=deinprojekt.hobbyhimmel.de + - INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de + - ENABLE_HOST_RESTRICTION=true + - PUBLIC_UPLOAD_RATE_LIMIT=20 +``` + +### nginx-proxy-manager Setup + +1. Erstelle zwei Proxy Hosts (public + internal) +2. Beide leiten auf denselben Container (Port 80) +3. SSL/TLS für beide Hosts aktivieren +4. `X-Forwarded-Host` Header wird automatisch gesetzt +``` + +### 9.2 README.dev.md Ergänzungen + +**Abschnitt**: "Development - Testing Host Restrictions" + +```markdown +## Host Restriction Testing + +### Dev Setup +In Development ist Host Restriction standardmäßig deaktiviert: +- `ENABLE_HOST_RESTRICTION=false` in `docker/dev/docker-compose.yml` + +### Testing mit /etc/hosts + +Füge lokale Host-Einträge hinzu: + +```bash +sudo nano /etc/hosts + +127.0.0.1 deinprojekt.hobbyhimmel.de +127.0.0.1 deinprojekt.lan.hobbyhimmel.de +``` + +Dann mit aktivierter Restriction testen: +```bash +ENABLE_HOST_RESTRICTION=true npm start +``` + +### Browser Testing +- Public: `http://deinprojekt.hobbyhimmel.de:3000` +- Internal: `http://deinprojekt.lan.hobbyhimmel.de:3000` +``` + +### 9.3 CHANGELOG.md Eintrag + +```markdown +## [Unreleased] + +### Added +- **Host-basierte Zugriffskontrolle**: Unterschiedliche Features für public vs. internal Subdomain + - Public Host: Nur Upload + Management-Portal + - Internal Host: Vollständige App-Features + - Backend Middleware blockiert geschützte APIs für public Host + - Frontend Code Splitting: Internal Features werden auf public nicht geladen + - Rate Limiting: 20 Uploads/Stunde/IP für public Host + - Environment Variablen: `PUBLIC_HOST`, `INTERNAL_HOST`, `ENABLE_HOST_RESTRICTION` + +### Changed +- Frontend: React Lazy Loading für internal-only Pages +- Backend: `hostGate` Middleware in Middleware-Stack integriert +- Audit Logs: `source_host` und `source_type` Felder hinzugefügt +- docker-compose: Environment Variablen für Host-Konfiguration + +### Security +- Defense in Depth: Serverseitige API-Blockierung + Frontend Code Splitting +- Strengere Rate Limits für public Uploads +- Audit Logging für Public vs. Internal Requests +``` + +--- + +## 10. Rollout Plan + +### Phase 1: Development & Testing (Woche 1) +- [ ] Branch erstellen: `feature/public-internal-hosts` +- [ ] Backend Middleware implementieren (`hostGate.js`) +- [ ] Backend Tests schreiben und ausführen +- [ ] Frontend Utils implementieren (`hostDetection.js`) +- [ ] Frontend App.js mit Code Splitting anpassen +- [ ] Frontend Tests schreiben +- [ ] Dev-Setup testen (localhost mit /etc/hosts) + +### Phase 2: Staging Deployment (Woche 2) +- [ ] docker-compose.yml finalisieren +- [ ] env-config.js Injection testen +- [ ] nginx-proxy-manager Konfiguration vorbereiten +- [ ] DNS-Einträge für Staging erstellen +- [ ] SSL-Zertifikate für Staging-Hosts beantragen +- [ ] Staging Deployment durchführen +- [ ] E2E Tests auf Staging durchführen + +### Phase 3: Production Rollout (Woche 3) +- [ ] Security Review durchführen +- [ ] Production DNS-Einträge erstellen +- [ ] Production SSL-Zertifikate beantragen +- [ ] Production nginx-proxy-manager Hosts konfigurieren +- [ ] Production Deployment durchführen +- [ ] Monitoring & Logs prüfen (erste 24h) +- [ ] Rate Limiting Metriken auswerten + +### Phase 4: Dokumentation & Cleanup (Woche 4) +- [ ] README.md & README.dev.md finalisieren +- [ ] CHANGELOG.md aktualisieren +- [ ] Feature Request als "Done" markieren +- [ ] Branch mergen (nach Review) +- [ ] Deployment-Dokumentation für Wartung erstellen + +--- + +## 11. Known Limitations & Future Improvements + +### Limitations +- **UUID-Token Permanence**: Management-Tokens sind permanent gültig (bis Gruppe gelöscht) + - **Risiko**: Geleakte URLs bleiben gültig + - **Mitigation**: Rate Limiting (10 req/h), Audit Logging + +- **Code Splitting nicht absolut**: Mit genug Aufwand könnte jemand internal Code aus Bundle extrahieren + - **Mitigation**: Serverseitige API-Blockierung ist primäre Defense + +- **Single Container**: Frontend und Backend teilen sich Netzwerk-Namespace + - **Akzeptiert**: Für kleine Deployments ausreichend sicher + +### Future Improvements (Optional) +- [ ] **Captcha für Public Uploads**: reCAPTCHA v3 Integration für Abuse-Protection +- [ ] **JWT-Tokens für Management**: TTL-basierte Tokens statt permanenter UUIDs +- [ ] **Separate Backend für Public**: Microservices-Architektur (upload-only backend) +- [ ] **CDN für Previews**: Presigned URLs mit kurzen TTLs +- [ ] **IP Whitelist für Admin**: Zusätzliche IP-basierte Restriction für Admin-APIs +- [ ] **WAF Integration**: Web Application Firewall vor nginx-proxy-manager + +--- + +## 12. Implementation Checklist + +### Backend +- [ ] `backend/src/middlewares/hostGate.js` erstellen +- [ ] `backend/src/middlewares/index.js` anpassen (hostGate integrieren) +- [ ] `backend/src/middlewares/rateLimiter.js` anpassen (publicUploadLimiter) +- [ ] `backend/src/routes/upload.js` anpassen (publicUploadLimiter verwenden) +- [ ] `backend/src/middlewares/auditLog.js` anpassen (source_host, source_type) +- [ ] `backend/tests/unit/middlewares/hostGate.test.js` erstellen +- [ ] `backend/tests/api/hostRestriction.test.js` erstellen +- [ ] Tests ausführen: `npm test` + +### Frontend +- [ ] `frontend/src/Utils/hostDetection.js` erstellen +- [ ] `frontend/src/App.js` anpassen (Code Splitting, ProtectedRoute) +- [ ] `frontend/src/Components/Pages/404Page.js` anpassen +- [ ] `frontend/public/env-config.js` erstellen +- [ ] `frontend/env.sh` anpassen (PUBLIC_HOST, INTERNAL_HOST) +- [ ] `frontend/src/Utils/__tests__/hostDetection.test.js` erstellen +- [ ] Tests ausführen: `npm test` + +### Docker & Config +- [ ] `docker/prod/docker-compose.yml` anpassen (Environment Variables) +- [ ] `docker/dev/docker-compose.yml` anpassen (ENABLE_HOST_RESTRICTION=false) +- [ ] `docker/prod/frontend/Dockerfile` prüfen (env.sh Ausführung) + +### nginx-proxy-manager +- [ ] Public Host erstellen (`deinprojekt.hobbyhimmel.de`) +- [ ] Internal Host erstellen (`deinprojekt.lan.hobbyhimmel.de`) +- [ ] SSL-Zertifikate konfigurieren +- [ ] Advanced Settings testen (optional Route-Blocking) + +### Dokumentation +- [ ] `README.md` ergänzen (Host-basierte Zugriffskontrolle) +- [ ] `README.dev.md` ergänzen (Testing Host Restrictions) +- [ ] `CHANGELOG.md` aktualisieren +- [ ] `FEATURE_REQUEST-FrontendPublic.md` als "Done" markieren + +### Testing +- [ ] Unit Tests (Backend & Frontend) +- [ ] Integration Tests (Backend API) +- [ ] E2E Tests (Manuell, siehe 7.4) +- [ ] Security Review (siehe 8.2) + +--- + +## 13. Kontakt & Support + +Bei Fragen oder Problemen während der Implementierung: +- GitHub Issues im Repository erstellen +- Feature Branch: `feature/public-internal-hosts` +- Reviewer: [Admin/Maintainer Name] + +--- + +**Status**: Ready for Implementation +**Geschätzte Implementierungszeit**: 2-3 Wochen +**Risiko**: Medium (neue Middleware, Testing erforderlich) +**Priorität**: High (Sicherheitsfeature) + +