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
115 lines
4.0 KiB
JavaScript
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;
|