# 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)