mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
updated network for proxy use
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run build && npx tsx src/server.ts",
|
||||
"dev:single": "cross-env NODE_ENV=development npx tsx src/server.ts",
|
||||
"dev:single": "cross-env NODE_ENV=development TRUST_PROXY_ENABLED=false npx tsx src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"prestart": "npm run build",
|
||||
|
||||
@@ -51,4 +51,44 @@ export const requireRole = (roles: string[]) => {
|
||||
console.log(`✅ Role check passed for user: ${req.user.email}, role: ${req.user.role}`);
|
||||
next();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Add this function to your existing auth.ts
|
||||
export const getClientIP = (req: Request): string => {
|
||||
const trustedHeader = process.env.TRUSTED_PROXY_HEADER || 'x-forwarded-for';
|
||||
const forwarded = req.headers[trustedHeader];
|
||||
const realIp = req.headers['x-real-ip'];
|
||||
|
||||
if (forwarded) {
|
||||
if (Array.isArray(forwarded)) {
|
||||
return forwarded[0].split(',')[0].trim();
|
||||
} else if (typeof forwarded === 'string') {
|
||||
return forwarded.split(',')[0].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (realIp) {
|
||||
return realIp.toString();
|
||||
}
|
||||
|
||||
return req.socket.remoteAddress || req.ip || 'unknown';
|
||||
};
|
||||
|
||||
// Add IP-based security checks
|
||||
export const ipSecurityCheck = (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
const clientIP = getClientIP(req);
|
||||
|
||||
// Log suspicious activity
|
||||
const suspiciousPaths = ['/api/auth/login', '/api/auth/register'];
|
||||
if (suspiciousPaths.includes(req.path)) {
|
||||
console.log(`🔐 Auth attempt from IP: ${clientIP}, Path: ${req.path}`);
|
||||
}
|
||||
|
||||
// Block known malicious IPs (you can expand this)
|
||||
const blockedIPs = process.env.BLOCKED_IPS?.split(',') || [];
|
||||
if (blockedIPs.includes(clientIP)) {
|
||||
console.warn(`🚨 Blocked request from banned IP: ${clientIP}`);
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,46 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { Request } from 'express';
|
||||
|
||||
// Secure IP extraction that works with proxy settings
|
||||
const getClientIP = (req: Request): string => {
|
||||
// Read from environment which header to trust
|
||||
const trustedHeader = process.env.TRUSTED_PROXY_HEADER || 'x-forwarded-for';
|
||||
|
||||
const forwarded = req.headers[trustedHeader];
|
||||
const realIp = req.headers['x-real-ip'];
|
||||
const cfConnectingIp = req.headers['cf-connecting-ip']; // Cloudflare
|
||||
|
||||
// If we have a forwarded header and trust proxy is configured
|
||||
if (forwarded) {
|
||||
if (Array.isArray(forwarded)) {
|
||||
const firstIP = forwarded[0].split(',')[0].trim();
|
||||
console.log(`🔍 Extracted IP from ${trustedHeader}: ${firstIP} (from: ${forwarded[0]})`);
|
||||
return firstIP;
|
||||
} else if (typeof forwarded === 'string') {
|
||||
const firstIP = forwarded.split(',')[0].trim();
|
||||
console.log(`🔍 Extracted IP from ${trustedHeader}: ${firstIP} (from: ${forwarded})`);
|
||||
return firstIP;
|
||||
}
|
||||
}
|
||||
|
||||
// Cloudflare support
|
||||
if (cfConnectingIp) {
|
||||
console.log(`🔍 Using Cloudflare IP: ${cfConnectingIp}`);
|
||||
return cfConnectingIp.toString();
|
||||
}
|
||||
|
||||
// Fallback to x-real-ip
|
||||
if (realIp) {
|
||||
console.log(`🔍 Using x-real-ip: ${realIp}`);
|
||||
return realIp.toString();
|
||||
}
|
||||
|
||||
// Final fallback to connection remote address
|
||||
const remoteAddress = req.socket.remoteAddress || req.ip || 'unknown';
|
||||
console.log(`🔍 Using remote address: ${remoteAddress}`);
|
||||
return remoteAddress;
|
||||
};
|
||||
|
||||
// Helper to check if request should be limited
|
||||
const shouldSkipLimit = (req: Request): boolean => {
|
||||
const skipPaths = [
|
||||
@@ -14,35 +54,92 @@ const shouldSkipLimit = (req: Request): boolean => {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip for whitelisted IPs from environment
|
||||
const whitelist = process.env.RATE_LIMIT_WHITELIST?.split(',') || [];
|
||||
const clientIP = getClientIP(req);
|
||||
if (whitelist.includes(clientIP)) {
|
||||
console.log(`✅ IP whitelisted: ${clientIP}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return skipPaths.includes(req.path);
|
||||
};
|
||||
|
||||
// Environment-based configuration
|
||||
const getRateLimitConfig = () => {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
return {
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes default
|
||||
max: isProduction
|
||||
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100') // Stricter in production
|
||||
: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '1000'), // More lenient in development
|
||||
|
||||
// Development-specific relaxations
|
||||
skip: (req: Request) => {
|
||||
// Skip all GET requests in development for easier testing
|
||||
if (!isProduction && req.method === 'GET') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return shouldSkipLimit(req);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Main API limiter - nur für POST/PUT/DELETE
|
||||
export const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 200, // 200 non-GET requests per 15 minutes
|
||||
...getRateLimitConfig(),
|
||||
message: {
|
||||
error: 'Zu viele Anfragen, bitte verlangsamen Sie Ihre Aktionen'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
// ✅ Skip für GET requests (Data Fetching)
|
||||
if (req.method === 'GET') return true;
|
||||
keyGenerator: (req) => getClientIP(req),
|
||||
handler: (req, res) => {
|
||||
const clientIP = getClientIP(req);
|
||||
console.warn(`🚨 Rate limit exceeded for IP: ${clientIP}, Path: ${req.path}, Method: ${req.method}`);
|
||||
|
||||
// ✅ Skip für Health/Status Checks
|
||||
return shouldSkipLimit(req);
|
||||
res.status(429).json({
|
||||
error: 'Zu viele Anfragen',
|
||||
message: 'Bitte versuchen Sie es später erneut',
|
||||
retryAfter: '15 Minuten',
|
||||
clientIP: process.env.NODE_ENV === 'development' ? clientIP : undefined // Only expose IP in dev
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Strict limiter for auth endpoints
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX_REQUESTS || '5'),
|
||||
message: {
|
||||
error: 'Zu viele Login-Versuche, bitte versuchen Sie es später erneut'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true,
|
||||
keyGenerator: (req) => getClientIP(req),
|
||||
handler: (req, res) => {
|
||||
const clientIP = getClientIP(req);
|
||||
console.warn(`🚨 Auth rate limit exceeded for IP: ${clientIP}`);
|
||||
|
||||
res.status(429).json({
|
||||
error: 'Zu viele Login-Versuche',
|
||||
message: 'Aus Sicherheitsgründen wurde Ihr Konto temporär gesperrt',
|
||||
retryAfter: '15 Minuten'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Separate limiter for expensive endpoints
|
||||
export const expensiveEndpointLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: parseInt(process.env.EXPENSIVE_ENDPOINT_LIMIT || '10'),
|
||||
message: {
|
||||
error: 'Zu viele Anfragen für diese Ressource'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req) => getClientIP(req)
|
||||
});
|
||||
@@ -14,7 +14,12 @@ import shiftPlanRoutes from './routes/shiftPlans.js';
|
||||
import setupRoutes from './routes/setup.js';
|
||||
import scheduledShifts from './routes/scheduledShifts.js';
|
||||
import schedulingRoutes from './routes/scheduling.js';
|
||||
import { authLimiter, apiLimiter } from './middleware/rateLimit.js';
|
||||
import {
|
||||
apiLimiter,
|
||||
authLimiter,
|
||||
expensiveEndpointLimiter
|
||||
} from './middleware/rateLimit.js';
|
||||
import { ipSecurityCheck as authIpCheck } from './middleware/auth.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -23,6 +28,8 @@ const app = express();
|
||||
const PORT = 3002;
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
app.use(authIpCheck);
|
||||
|
||||
let vite: ViteDevServer | undefined;
|
||||
|
||||
if (isDevelopment) {
|
||||
@@ -79,8 +86,6 @@ const configureStaticFiles = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Security configuration
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.info('Checking for JWT_SECRET');
|
||||
@@ -91,6 +96,48 @@ if (process.env.NODE_ENV === 'production') {
|
||||
}
|
||||
}
|
||||
|
||||
const configureTrustProxy = (): string | string[] | boolean | number => {
|
||||
const trustedProxyIps = process.env.TRUSTED_PROXY_IPS;
|
||||
const trustProxyEnabled = process.env.TRUST_PROXY_ENABLED !== 'false'; // Default true for production
|
||||
|
||||
// If explicitly disabled
|
||||
if (!trustProxyEnabled) {
|
||||
console.log('🔒 Trust proxy: Disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
// If specific IPs are provided via environment variable
|
||||
if (trustedProxyIps) {
|
||||
console.log('🔒 Trust proxy: Using configured IPs:', trustedProxyIps);
|
||||
|
||||
// Handle comma-separated list of IPs/CIDR ranges
|
||||
if (trustedProxyIps.includes(',')) {
|
||||
return trustedProxyIps.split(',').map(ip => ip.trim());
|
||||
}
|
||||
|
||||
// Handle single IP/CIDR
|
||||
return trustedProxyIps.trim();
|
||||
}
|
||||
|
||||
// Default behavior based on environment
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log('🔒 Trust proxy: Using production defaults (private networks)');
|
||||
return [
|
||||
'loopback',
|
||||
'linklocal',
|
||||
'uniquelocal',
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16'
|
||||
];
|
||||
} else {
|
||||
console.log('🔒 Trust proxy: Development mode (disabled)');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
app.set('trust proxy', configureTrustProxy());
|
||||
|
||||
// Security headers
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
@@ -123,9 +170,12 @@ app.use(express.json());
|
||||
|
||||
// Rate limiting - weniger restriktiv in Development
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log('🔒 Applying production rate limiting');
|
||||
app.use('/api/', apiLimiter);
|
||||
} else {
|
||||
console.log('🔧 Development: Rate limiting relaxed');
|
||||
console.log('🔧 Development: Relaxed rate limiting applied');
|
||||
// In development, you might want to be more permissive
|
||||
app.use('/api/', apiLimiter);
|
||||
}
|
||||
|
||||
// API Routes
|
||||
@@ -134,7 +184,7 @@ app.use('/api/auth', authLimiter, authRoutes);
|
||||
app.use('/api/employees', employeeRoutes);
|
||||
app.use('/api/shift-plans', shiftPlanRoutes);
|
||||
app.use('/api/scheduled-shifts', scheduledShifts);
|
||||
app.use('/api/scheduling', schedulingRoutes);
|
||||
app.use('/api/scheduling', expensiveEndpointLimiter, schedulingRoutes);
|
||||
|
||||
// Health route
|
||||
app.get('/api/health', (req: express.Request, res: express.Response) => {
|
||||
@@ -279,7 +329,7 @@ const initializeApp = async () => {
|
||||
if (frontendBuildPath) {
|
||||
console.log(`📍 Frontend: http://localhost:${PORT}`);
|
||||
} else if (isDevelopment) {
|
||||
console.log(`📍 Frontend (Vite): http://localhost:3002`);
|
||||
console.log(`📍 Frontend (Vite): http://localhost:3003`);
|
||||
}
|
||||
console.log(`📍 API: http://localhost:${PORT}/api`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user