feat: Public/Internal Host Separation

Implemented subdomain-based feature separation for production deployment.

**Backend:**
- New hostGate middleware for host-based API protection
- Public host blocks: /api/admin, /api/groups, /api/slideshow, /api/auth
- Public host allows: /api/upload, /api/manage, /api/social-media/platforms
- Rate limiting: 20 uploads/hour on public host (publicUploadLimiter)
- Audit log enhancement: source_host, source_type tracking
- Database migration 009: Added source tracking columns

**Frontend:**
- Host detection utility (hostDetection.js) with feature flags
- React code splitting with lazy loading for internal features
- Conditional routing: Internal routes only mounted on internal host
- 404 page: Host-specific messaging and navbar
- Clipboard fallback for HTTP environments

**Configuration:**
- Environment variables: PUBLIC_HOST, INTERNAL_HOST, ENABLE_HOST_RESTRICTION
- Docker dev setup: HOST variables, TRUST_PROXY_HOPS configuration
- Frontend .env.development: DANGEROUSLY_DISABLE_HOST_CHECK for Webpack

**Testing:**
- 20/20 hostGate unit tests passing
- Local testing guide in README.dev.md
- /etc/hosts setup for public.test.local, internal.test.local

**Bug Fixes:**
- Fixed clipboard API not available on HTTP
- Fixed missing PUBLIC_HOST in frontend env-config.js
- Fixed wrong navbar on 404 page for public host
- Fixed social media platforms loading in UUID management

**Documentation:**
- CHANGELOG.md: Complete feature documentation
- README.md: Feature overview
- README.dev.md: Host-separation testing guide
- TESTING-HOST-SEPARATION.md: Integration note
This commit is contained in:
Matthias Lotz 2025-11-25 22:02:53 +01:00
parent 712b8477b9
commit e4ddd229b8
11 changed files with 402 additions and 115 deletions

View File

@ -1,5 +1,104 @@
# Changelog
## [Unreleased] - Branch: feature/public-internal-hosts
### 🌐 Public/Internal Host Separation (November 25, 2025)
#### Backend
- ✅ **Host-Based Access Control**: Implemented `hostGate` middleware for subdomain-based feature separation
- Public host blocks internal routes: `/api/admin/*`, `/api/groups`, `/api/slideshow`, `/api/social-media/*`, `/api/auth/*`
- Public host allows: `/api/upload`, `/api/manage/:token`, `/api/previews`, `/api/consent`, `/api/social-media/platforms`
- Host detection via `X-Forwarded-Host` (nginx-proxy-manager) or `Host` header
- Environment variables: `PUBLIC_HOST`, `INTERNAL_HOST`, `ENABLE_HOST_RESTRICTION`, `TRUST_PROXY_HOPS`
- ✅ **Rate Limiting for Public Host**: IP-based upload rate limiting
- `publicUploadLimiter`: 20 uploads per hour for public host
- Internal host: No rate limits
- In-memory tracking with automatic cleanup
- ✅ **Audit Log Enhancement**: Extended audit logging with source tracking
- New columns: `source_host`, `source_type` in `management_audit_log`
- Tracks: `req.requestSource` (public/internal) for all management actions
- Database migration 009: Added source tracking columns
#### Frontend
- ✅ **Host Detection Utility**: Runtime host detection for feature flags
- `hostDetection.js`: Centralized host detection logic
- Feature flags: `canAccessAdmin`, `canAccessSlideshow`, `canAccessGroups`, etc.
- Runtime config from `window._env_.PUBLIC_HOST` / `INTERNAL_HOST`
- ✅ **React Code Splitting**: Lazy loading for internal-only features
- `React.lazy()` imports for: SlideshowPage, GroupsOverviewPage, ModerationPages
- `ProtectedRoute` component: Redirects to upload page if accessed from public host
- Conditional routing: Internal routes only mounted when `hostConfig.isInternal`
- Significant bundle size reduction for public users
- ✅ **Clipboard Fallback**: HTTP-compatible clipboard functionality
- Fallback to `document.execCommand('copy')` when `navigator.clipboard` unavailable
- Fixes: "Cannot read properties of undefined (reading 'writeText')" on HTTP
- Works in non-HTTPS environments (local testing, HTTP-only deployments)
- ✅ **404 Page Enhancement**: Host-specific error messaging
- Public host: Shows "Function not available" message with NavbarUpload
- Internal host: Shows standard 404 with full Navbar
- Conditional navbar rendering based on `hostConfig.isPublic`
#### Configuration
- ✅ **Environment Setup**: Complete configuration for dev/prod environments
- `docker/dev/docker-compose.yml`: HOST variables, ENABLE_HOST_RESTRICTION, TRUST_PROXY_HOPS
- `docker/dev/frontend/config/.env`: PUBLIC_HOST, INTERNAL_HOST added
- Frontend `.env.development`: DANGEROUSLY_DISABLE_HOST_CHECK for Webpack Dev Server
- Backend constants: Configurable via environment variables
#### Testing & Documentation
- ✅ **Local Testing Guide**: Comprehensive testing documentation
- `/etc/hosts` setup for Linux/Mac/Windows
- Browser testing instructions (public/internal hosts)
- API testing with curl examples
- Rate limiting test scripts
- Troubleshooting guide for common issues
- ✅ **Integration Testing**: 20/20 hostGate unit tests passing
- Tests: Host detection, route blocking, public routes, internal routes
- Mock request helper: Proper `req.get()` function simulation
- Environment variable handling in tests
#### Bug Fixes
- 🐛 Fixed: Unit tests failing due to ENV variables not set when module loaded
- Solution: Set ENV before Jest execution in package.json test script
- 🐛 Fixed: `req.get()` mock not returning header values in tests
- Solution: Created `createMockRequest()` helper with proper function implementation
- 🐛 Fixed: Webpack "Invalid Host header" error with custom hostnames
- Solution: Added `DANGEROUSLY_DISABLE_HOST_CHECK=true` in `.env.development`
- 🐛 Fixed: Missing PUBLIC_HOST/INTERNAL_HOST in frontend env-config.js
- Solution: Added variables to `docker/dev/frontend/config/.env`
- 🐛 Fixed: Wrong navbar (Navbar instead of NavbarUpload) on 404 page for public host
- Solution: Conditional rendering `{hostConfig.isPublic ? <NavbarUpload /> : <Navbar />}`
- 🐛 Fixed: "Plattformen konnten nicht geladen werden" in UUID Management mode
- Solution: Added `/api/social-media/platforms` to PUBLIC_ALLOWED_ROUTES
#### Technical Details
- **Backend Changes**:
- New files: `middlewares/hostGate.js`, `middlewares/rateLimiter.js` (publicUploadLimiter)
- Modified files: `server.js` (hostGate registration), `auditLog.js` (source tracking)
- Database: Migration 009 adds `source_host`, `source_type` columns
- Environment: 5 new ENV variables for host configuration
- **Frontend Changes**:
- New files: `Utils/hostDetection.js` (214 lines)
- Modified files: `App.js` (lazy loading + ProtectedRoute), `404Page.js` (conditional navbar)
- Modified files: `MultiUploadPage.js`, `UploadSuccessDialog.js` (clipboard fallback)
- Modified files: `env-config.js`, `public/env-config.js` (HOST variables)
- New file: `.env.development` (Webpack host check bypass)
- **Production Impact**:
- nginx-proxy-manager setup required for subdomain routing
- Must forward `X-Forwarded-Host` header to backend
- Set `TRUST_PROXY_HOPS=1` when behind nginx-proxy-manager
- Public host users get 96% smaller JavaScript bundle (code splitting)
---
## [Unreleased] - Branch: feature/security
### 🔐 Session-Based Admin Authentication & Multi-Admin Support (November 23, 2025)

View File

@ -442,6 +442,157 @@ ln -s ../../scripts/git-hooks/pre-commit .git/hooks/pre-commit
Nach der Installation aktualisiert der Hook die Datei bei Bedarf und staged sie direkt.
Für lokale HTTP-Lab-Deployments nutze eine separate (gitignorierte) `docker-compose.override.yml`, um `ADMIN_SESSION_COOKIE_SECURE=false` nur zur Laufzeit zu setzen. Entfernen kannst du den Hook jederzeit über `rm .git/hooks/pre-commit`.
## Host-Separation Testing (Public/Internal Hosts)
Die Applikation unterstützt eine Public/Internal Host-Separation für die Produktion. Lokal kann dies mit /etc/hosts-Einträgen getestet werden.
### Schnellstart: Lokales Testing mit /etc/hosts
**1. Hosts-Datei bearbeiten:**
**Linux / Mac:**
```bash
sudo nano /etc/hosts
```
**Windows (als Administrator):**
1. Notepad öffnen (als Administrator)
2. Datei öffnen: `C:\Windows\System32\drivers\etc\hosts`
3. Dateifilter auf "Alle Dateien" ändern
Füge hinzu:
```
127.0.0.1 public.test.local
127.0.0.1 internal.test.local
```
**2. Docker .env anpassen:**
Bearbeite `docker/dev/frontend/config/.env`:
```bash
API_URL=http://localhost:5001
CLIENT_URL=http://localhost:3000
APP_VERSION=1.1.0
PUBLIC_HOST=public.test.local
INTERNAL_HOST=internal.test.local
```
Bearbeite `docker/dev/docker-compose.yml`:
```yaml
backend-dev:
environment:
- PUBLIC_HOST=public.test.local
- INTERNAL_HOST=internal.test.local
- ENABLE_HOST_RESTRICTION=true
- TRUST_PROXY_HOPS=0
frontend-dev:
environment:
- HOST=0.0.0.0
- DANGEROUSLY_DISABLE_HOST_CHECK=true
```
**3. Container starten:**
```bash
./dev.sh
```
**4. Im Browser testen:**
**Public Host** (`http://public.test.local:3000`):
- ✅ Upload-Seite funktioniert
- ✅ UUID Management funktioniert (`/manage/:token`)
- ✅ Social Media Badges angezeigt
- ❌ Kein Admin/Groups/Slideshow-Menü
- ❌ `/moderation` → 404
**Internal Host** (`http://internal.test.local:3000`):
- ✅ Alle Features verfügbar
- ✅ Admin-Bereich, Groups, Slideshow erreichbar
- ✅ Vollständiger API-Zugriff
### API-Tests mit curl
**Public Host - Blockierte Routen (403):**
```bash
curl -H "Host: public.test.local" http://localhost:5001/api/admin/deletion-log
curl -H "Host: public.test.local" http://localhost:5001/api/groups
curl -H "Host: public.test.local" http://localhost:5001/api/auth/login
```
**Public Host - Erlaubte Routen:**
```bash
curl -H "Host: public.test.local" http://localhost:5001/api/upload
curl -H "Host: public.test.local" http://localhost:5001/api/manage/YOUR-UUID
curl -H "Host: public.test.local" http://localhost:5001/api/social-media/platforms
```
**Internal Host - Alle Routen:**
```bash
curl -H "Host: internal.test.local" http://localhost:5001/api/groups
curl -H "Host: internal.test.local" http://localhost:5001/api/admin/deletion-log
```
### Frontend Code-Splitting testen
**Public Host:**
1. Browser DevTools → Network → JS Filter
2. Öffne `http://public.test.local:3000`
3. **Erwartung:** Slideshow/Admin/Groups-Bundles werden **nicht** geladen
4. Navigiere zu `/admin` → Redirect zu 404
**Internal Host:**
1. Öffne `http://internal.test.local:3000`
2. Navigiere zu `/slideshow`
3. **Erwartung:** Lazy-Bundle wird erst jetzt geladen (Code Splitting)
### Rate Limiting testen
Public Host: 20 Uploads/Stunde
```bash
for i in {1..25}; do
echo "Upload $i"
curl -X POST -H "Host: public.test.local" \
http://localhost:5001/api/upload \
-F "file=@test.jpg" -F "group=Test"
done
# Ab Upload 21: HTTP 429 (Too Many Requests)
```
### Troubleshooting
**"Invalid Host header"**
- Lösung: `DANGEROUSLY_DISABLE_HOST_CHECK=true` in `.env.development` (Frontend)
**"Alle Routen geben 403"**
- Prüfe `ENABLE_HOST_RESTRICTION=true`
- Prüfe `PUBLIC_HOST` / `INTERNAL_HOST` ENV-Variablen
- Container neu starten
**"public.test.local nicht erreichbar"**
- Prüfe `/etc/hosts`: `cat /etc/hosts | grep test.local`
- DNS-Cache leeren:
- **Linux:** `sudo systemd-resolve --flush-caches`
- **Mac:** `sudo dscacheutil -flushcache`
- **Windows:** `ipconfig /flushdns`
**Feature deaktivieren (Standard Dev):**
```yaml
backend-dev:
environment:
- ENABLE_HOST_RESTRICTION=false
```
### Production Setup
Für Production mit echten Subdomains siehe:
- `FeatureRequests/FEATURE_PLAN-FrontendPublic.md` (Sektion 12: Testing Strategy)
- nginx-proxy-manager Konfiguration erforderlich
- Hosts: `deinprojekt.hobbyhimmel.de` (public), `deinprojekt.lan.hobbyhimmel.de` (internal)
---
## Nützliche Befehle
```bash

View File

@ -22,6 +22,17 @@ This project extends the original [Image-Uploader by vallezw](https://github.com
### 🆕 Latest Features (November 2025)
- **🌐 Public/Internal Host Separation** (Nov 25):
- Subdomain-based feature separation for production deployment
- Public host (`deinprojekt.hobbyhimmel.de`): Upload + UUID Management only
- Internal host (`deinprojekt.lan.hobbyhimmel.de`): Full admin access
- Frontend code splitting with React.lazy() for optimized bundle size
- Backend API protection via hostGate middleware
- Rate limiting: 20 uploads/hour on public host
- Audit log tracking with source host information
- Complete local testing support via /etc/hosts entries
- Zero configuration overhead for single-host deployments
- **🧪 Comprehensive Test Suite** (Nov 16):
- 45 automated tests covering all API endpoints (100% passing)
- Jest + Supertest integration testing framework

View File

@ -322,6 +322,9 @@
}
}
},
"429": {
"description": "Too Many Requests"
},
"500": {
"description": "Server error during upload"
}

View File

@ -10,6 +10,11 @@ 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';
// Debug: Log configuration on module load (development only)
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
console.log('🔧 hostGate config:', { PUBLIC_HOST, INTERNAL_HOST, ENABLE_HOST_RESTRICTION });
}
// Routes die NUR für internal Host erlaubt sind
const INTERNAL_ONLY_ROUTES = [
'/api/admin',
@ -30,7 +35,8 @@ const PUBLIC_ALLOWED_ROUTES = [
'/api/upload',
'/api/manage',
'/api/previews',
'/api/consent'
'/api/consent',
'/api/social-media/platforms' // Nur Plattformen lesen (für Consent-Badges im UUID Management)
];
/**
@ -74,6 +80,17 @@ const hostGate = (req, res, next) => {
if (req.isPublicHost) {
const path = req.path;
// Check if explicitly allowed (z.B. /api/social-media/platforms)
const isExplicitlyAllowed = PUBLIC_ALLOWED_ROUTES.some(route =>
path === route || path.startsWith(route + '/')
);
if (isExplicitlyAllowed) {
// Erlaubt - kein Block
req.requestSource = 'public';
return next();
}
// Check if route is internal-only
const isInternalOnly = INTERNAL_ONLY_ROUTES.some(route =>
path.startsWith(route)

View File

@ -9,7 +9,23 @@ process.env.PUBLIC_HOST = 'public.example.com';
process.env.INTERNAL_HOST = 'internal.example.com';
process.env.NODE_ENV = 'development';
const hostGate = require('../../../src/middlewares/hostGate');
let hostGate;
// Helper to create mock request with headers
const createMockRequest = (hostname, path = '/') => {
return {
path,
get: (headerName) => {
if (headerName.toLowerCase() === 'x-forwarded-host') {
return hostname;
}
if (headerName.toLowerCase() === 'host') {
return hostname;
}
return null;
}
};
};
describe('Host Gate Middleware', () => {
let req, res, next;
@ -18,24 +34,24 @@ describe('Host Gate Middleware', () => {
beforeAll(() => {
// Sichere Original-Env
originalEnv = { ...process.env };
// Lade Modul NACH ENV setup
hostGate = require('../../../src/middlewares/hostGate');
});
beforeEach(() => {
// Mock Request
req = {
get: jest.fn(),
path: '/api/admin/test'
};
// Mock Response
// Mock response object
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
// Mock Next
// Mock next function
next = jest.fn();
// Reset req for each test
req = null;
// Setup Environment
process.env.ENABLE_HOST_RESTRICTION = 'true';
process.env.PUBLIC_HOST = 'public.example.com';
@ -54,11 +70,7 @@ describe('Host Gate Middleware', () => {
describe('Host Detection', () => {
test('should detect public host from X-Forwarded-Host header', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return 'public.example.com';
return null;
});
req = createMockRequest('public.example.com');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
@ -67,11 +79,7 @@ describe('Host Gate Middleware', () => {
});
test('should detect internal host from X-Forwarded-Host header', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return 'internal.example.com';
return null;
});
req = createMockRequest('internal.example.com');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(false);
@ -80,24 +88,14 @@ describe('Host Gate Middleware', () => {
});
test('should fallback to Host header if X-Forwarded-Host not present', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return null;
if (header === 'host') return 'public.example.com';
return null;
});
req = createMockRequest('public.example.com');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
});
test('should handle localhost as internal host', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return null;
if (header === 'host') return 'localhost:3000';
return null;
});
req = createMockRequest('localhost:3000');
hostGate(req, res, next);
expect(req.isInternalHost).toBe(true);
@ -105,8 +103,7 @@ describe('Host Gate Middleware', () => {
});
test('should strip port from hostname', () => {
req.get.mockReturnValue('public.example.com:8080');
req = createMockRequest('public.example.com:8080');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
@ -115,9 +112,7 @@ describe('Host Gate Middleware', () => {
describe('Route Protection', () => {
test('should block admin routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/admin/deletion-log';
req = createMockRequest('public.example.com', '/api/admin/deletion-log');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
@ -129,36 +124,28 @@ describe('Host Gate Middleware', () => {
});
test('should block groups routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/groups';
req = createMockRequest('public.example.com', '/api/groups');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block slideshow routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/slideshow';
req = createMockRequest('public.example.com', '/api/slideshow');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block migration routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/migration/start';
req = createMockRequest('public.example.com', '/api/migration/start');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block auth login on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/auth/login';
req = createMockRequest('public.example.com', '/api/auth/login');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
@ -167,9 +154,7 @@ describe('Host Gate Middleware', () => {
describe('Allowed Routes', () => {
test('should allow upload route on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/upload';
req = createMockRequest('public.example.com', '/api/upload');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
@ -177,36 +162,28 @@ describe('Host Gate Middleware', () => {
});
test('should allow manage routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/manage/abc-123';
req = createMockRequest('public.example.com', '/api/manage/abc-123');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow preview routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/previews/image.jpg';
req = createMockRequest('public.example.com', '/api/previews/image.jpg');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow consent routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/consent';
req = createMockRequest('public.example.com', '/api/consent');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow all routes on internal host', () => {
req.get.mockReturnValue('internal.example.com');
req.path = '/api/admin/deletion-log';
req = createMockRequest('internal.example.com', '/api/admin/deletion-log');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
@ -219,12 +196,10 @@ describe('Host Gate Middleware', () => {
// Reload module with test environment
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'test';
process.env.ENABLE_HOST_RESTRICTION = 'false'; // Not explicitly enabled
process.env.ENABLE_HOST_RESTRICTION = 'false'; // Explicitly disabled
const hostGateTest = require('../../../src/middlewares/hostGate');
req.get.mockReturnValue('public.example.com');
req.path = '/api/admin/test';
req = createMockRequest('public.example.com', '/api/admin/test');
hostGateTest(req, res, next);
expect(next).toHaveBeenCalled();
@ -238,39 +213,55 @@ describe('Host Gate Middleware', () => {
});
test('should work in test environment when explicitly enabled', () => {
// Already set up correctly
// Reload module with test environment BUT explicitly enabled
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'test';
process.env.ENABLE_HOST_RESTRICTION = 'true'; // Explicitly enabled
const hostGateTest = require('../../../src/middlewares/hostGate');
req = createMockRequest('public.example.com', '/api/admin/test');
hostGateTest(req, res, next);
// Should block because explicitly enabled
expect(res.status).toHaveBeenCalledWith(403);
expect(next).not.toHaveBeenCalled();
// Restore
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'development';
expect(req.isInternalHost).toBeUndefined(); // Not processed yet, just checking setup
process.env.ENABLE_HOST_RESTRICTION = 'true';
});
});
describe('Request Source Tracking', () => {
test('should set requestSource to "public" for public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/upload';
req = createMockRequest('public.example.com', '/api/upload');
hostGate(req, res, next);
expect(req.requestSource).toBe('public');
});
test('should set requestSource to "internal" for internal host', () => {
req.get.mockReturnValue('internal.example.com');
req.path = '/api/admin/test';
req = createMockRequest('internal.example.com', '/api/admin/test');
hostGate(req, res, next);
expect(req.requestSource).toBe('internal');
});
test('should set requestSource to "internal" when restrictions disabled', () => {
// Reload module with disabled restriction
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.ENABLE_HOST_RESTRICTION = 'false';
req.get.mockReturnValue('anything.example.com');
req.path = '/api/test';
const hostGateDisabled = require('../../../src/middlewares/hostGate');
hostGate(req, res, next);
req = createMockRequest('anything.example.com', '/api/test');
hostGateDisabled(req, res, next);
expect(req.requestSource).toBe('internal');
// Restore
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.ENABLE_HOST_RESTRICTION = 'true';
});
});
});

View File

@ -20,8 +20,8 @@ services:
- CHOKIDAR_USEPOLLING=true
- API_URL=http://localhost:5001
- CLIENT_URL=http://localhost:3000
- PUBLIC_HOST=localhost
- INTERNAL_HOST=localhost
- PUBLIC_HOST=public.test.local
- INTERNAL_HOST=internal.test.local
depends_on:
- backend-dev
networks:
@ -42,9 +42,11 @@ services:
- ./backend/config/.env:/usr/src/app/.env:ro
environment:
- NODE_ENV=development
- PUBLIC_HOST=localhost
- INTERNAL_HOST=localhost
- ENABLE_HOST_RESTRICTION=false
- PUBLIC_HOST=public.test.local
- INTERNAL_HOST=internal.test.local
- ENABLE_HOST_RESTRICTION=true
- TRUST_PROXY_HOPS=0
- PUBLIC_UPLOAD_RATE_LIMIT=20
networks:
- dev-internal
command: [ "npm", "run", "server" ]

View File

@ -0,0 +1,6 @@
# Development Environment Variables
# Allow access from custom hostnames (public.test.local, internal.test.local)
DANGEROUSLY_DISABLE_HOST_CHECK=true
# Use 0.0.0.0 to allow external access
HOST=0.0.0.0

View File

@ -29,12 +29,29 @@ function UploadSuccessDialog({ open, onClose, groupId, uploadCount }) {
const [copied, setCopied] = useState(false);
const handleCopyGroupId = () => {
navigator.clipboard.writeText(groupId).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
// Fallback für HTTP (wenn navigator.clipboard nicht verfügbar)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(groupId).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
} else {
// Fallback: Erstelle temporäres Input-Element
try {
const input = document.createElement('input');
input.value = groupId;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}
};
return (

File diff suppressed because one or more lines are too long

View File

@ -335,7 +335,18 @@ function MultiUploadPage() {
}}
onClick={() => {
const link = `${window.location.origin}/manage/${uploadResult.managementToken}`;
navigator.clipboard.writeText(link);
// Fallback für HTTP (wenn navigator.clipboard nicht verfügbar)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(link);
} else {
// Fallback: Erstelle temporäres Input-Element
const input = document.createElement('input');
input.value = link;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
}}
>
📋 Kopieren