Project-Image-Uploader/FeatureRequests/done/FEATURE_PLAN-FrontendPublic.md

33 KiB

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

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

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

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:

/**
 * 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

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

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

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

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

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

#!/bin/bash
# Inject runtime environment variables

cat <<EOF > /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

/**
 * 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

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 <Navigate to="/" replace />;
  }
  
  return children;
};

// Loading Fallback
const LoadingFallback = () => (
  <div style={{ 
    display: 'flex', 
    justifyContent: 'center', 
    alignItems: 'center', 
    height: '100vh' 
  }}>
    <p>Loading...</p>
  </div>
);

function App() {
  const hostConfig = getHostConfig();

  return (
    <AdminSessionProvider>
      <Router>
        <Suspense fallback={<LoadingFallback />}>
          <Routes>
            {/* Public Routes - immer verfügbar */}
            <Route path="/" element={<MultiUploadPage />} />
            <Route path="/manage/:token" element={<ManagementPortalPage />} />

            {/* Internal Only Routes */}
            {hostConfig.isInternal && (
              <>
                <Route 
                  path="/slideshow" 
                  element={
                    <ProtectedRoute>
                      <SlideshowPage />
                    </ProtectedRoute>
                  } 
                />
                <Route 
                  path="/groups/:groupId" 
                  element={
                    <ProtectedRoute>
                      <PublicGroupImagesPage />
                    </ProtectedRoute>
                  } 
                />
                <Route 
                  path="/groups" 
                  element={
                    <ProtectedRoute>
                      <GroupsOverviewPage />
                    </ProtectedRoute>
                  } 
                />
                <Route 
                  path="/moderation" 
                  element={
                    <ProtectedRoute>
                      <ModerationGroupsPage />
                    </ProtectedRoute>
                  } 
                />
                <Route 
                  path="/moderation/groups/:groupId" 
                  element={
                    <ProtectedRoute>
                      <ModerationGroupImagesPage />
                    </ProtectedRoute>
                  } 
                />
              </>
            )}

            {/* 404 / Not Found */}
            <Route path="*" element={<NotFoundPage />} />
          </Routes>
        </Suspense>
      </Router>
    </AdminSessionProvider>
  );
}

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

import { getHostConfig } from '../../Utils/hostDetection';

const MultiUploadPage = () => {
  const hostConfig = getHostConfig();

  return (
    <div>
      {/* Nur auf internal Host Navigation anzeigen */}
      {hostConfig.isInternal && (
        <nav>
          <a href="/slideshow">Slideshow</a>
          <a href="/groups">Groups</a>
          <a href="/moderation">Moderation</a>
        </nav>
      )}
      
      {/* Upload Form - immer sichtbar */}
      <UploadForm />
      
      {/* Optional: Hinweis für public users */}
      {hostConfig.isPublic && (
        <div className="public-notice">
          <p>Sie nutzen den öffentlichen Upload-Bereich.</p>
        </div>
      )}
    </div>
  );
};

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

import { getHostConfig } from '../../Utils/hostDetection';

const NotFoundPage = () => {
  const hostConfig = getHostConfig();

  return (
    <div className="not-found">
      <h1>404 - Seite nicht gefunden</h1>
      
      {hostConfig.isPublic ? (
        <>
          <p>Diese Funktion ist nicht öffentlich verfügbar.</p>
          <a href="/">Zurück zum Upload</a>
        </>
      ) : (
        <>
          <p>Die angeforderte Seite existiert nicht.</p>
          <a href="/">Zurück zur Startseite</a>
        </>
      )}
    </div>
  );
};

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

# 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

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

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

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:

// 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

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

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

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"

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

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)