Project-Image-Uploader/backend/src/middlewares/hostGate.js
matthias.lotz e4ddd229b8 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
2025-11-25 22:02:53 +01:00

115 lines
4.0 KiB
JavaScript

/**
* Host Gate Middleware
* Blockiert geschützte API-Routen für public Host
* Erlaubt nur Upload + Management für public Host
*
* Erkennt Host via X-Forwarded-Host (nginx-proxy-manager) oder Host Header
*/
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',
'/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',
'/api/social-media/platforms' // Nur Plattformen lesen (für Consent-Badges im UUID Management)
];
/**
* Middleware: Host-basierte Zugriffskontrolle
* @param {Object} req - Express Request
* @param {Object} res - Express Response
* @param {Function} next - Next Middleware
*/
const hostGate = (req, res, next) => {
// Feature disabled only when explicitly set to false OR in test environment without explicit enable
const isTestEnv = process.env.NODE_ENV === 'test';
const explicitlyEnabled = process.env.ENABLE_HOST_RESTRICTION === 'true';
const explicitlyDisabled = process.env.ENABLE_HOST_RESTRICTION === 'false';
// Skip restriction if:
// - Explicitly disabled, OR
// - Test environment AND not explicitly enabled
if (explicitlyDisabled || (isTestEnv && !explicitlyEnabled)) {
req.isPublicHost = false;
req.isInternalHost = true;
req.requestSource = 'internal';
return next();
}
// Get host from X-Forwarded-Host (nginx-proxy-manager) or Host header
const forwardedHost = req.get('x-forwarded-host');
const hostHeader = req.get('host');
const host = forwardedHost || hostHeader || '';
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' || hostname === '127.0.0.1';
// Log host detection for debugging
if (process.env.NODE_ENV !== 'production') {
console.log(`🔍 Host Detection: ${hostname}${req.isPublicHost ? 'PUBLIC' : 'INTERNAL'}`);
}
// If public host, check if route is allowed
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)
);
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 request source context for audit logging
req.requestSource = req.isPublicHost ? 'public' : 'internal';
next();
};
module.exports = hostGate;