mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
Compare commits
1 Commits
0473a3b5bf
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| a8dc11b024 |
@@ -5,11 +5,11 @@ import { Request } from 'express';
|
|||||||
const getClientIP = (req: Request): string => {
|
const getClientIP = (req: Request): string => {
|
||||||
// Read from environment which header to trust
|
// Read from environment which header to trust
|
||||||
const trustedHeader = process.env.TRUSTED_PROXY_HEADER || 'x-forwarded-for';
|
const trustedHeader = process.env.TRUSTED_PROXY_HEADER || 'x-forwarded-for';
|
||||||
|
|
||||||
const forwarded = req.headers[trustedHeader];
|
const forwarded = req.headers[trustedHeader];
|
||||||
const realIp = req.headers['x-real-ip'];
|
const realIp = req.headers['x-real-ip'];
|
||||||
const cfConnectingIp = req.headers['cf-connecting-ip']; // Cloudflare
|
const cfConnectingIp = req.headers['cf-connecting-ip']; // Cloudflare
|
||||||
|
|
||||||
// If we have a forwarded header and trust proxy is configured
|
// If we have a forwarded header and trust proxy is configured
|
||||||
if (forwarded) {
|
if (forwarded) {
|
||||||
if (Array.isArray(forwarded)) {
|
if (Array.isArray(forwarded)) {
|
||||||
@@ -22,66 +22,95 @@ const getClientIP = (req: Request): string => {
|
|||||||
return firstIP;
|
return firstIP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloudflare support
|
// Cloudflare support
|
||||||
if (cfConnectingIp) {
|
if (cfConnectingIp) {
|
||||||
console.log(`🔍 Using Cloudflare IP: ${cfConnectingIp}`);
|
console.log(`🔍 Using Cloudflare IP: ${cfConnectingIp}`);
|
||||||
return cfConnectingIp.toString();
|
return cfConnectingIp.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to x-real-ip
|
// Fallback to x-real-ip
|
||||||
if (realIp) {
|
if (realIp) {
|
||||||
console.log(`🔍 Using x-real-ip: ${realIp}`);
|
console.log(`🔍 Using x-real-ip: ${realIp}`);
|
||||||
return realIp.toString();
|
return realIp.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final fallback to connection remote address
|
// Final fallback to connection remote address
|
||||||
const remoteAddress = req.socket.remoteAddress || req.ip || 'unknown';
|
const remoteAddress = req.socket.remoteAddress || req.ip || 'unknown';
|
||||||
console.log(`🔍 Using remote address: ${remoteAddress}`);
|
console.log(`🔍 Using remote address: ${remoteAddress}`);
|
||||||
return remoteAddress;
|
return remoteAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to check if an IP is a loopback address (IPv4 or IPv6)
|
||||||
|
const isLoopbackAddress = (ip: string): boolean => {
|
||||||
|
// IPv4 loopback: 127.0.0.0/8
|
||||||
|
if (ip.startsWith('127.') || ip === 'localhost') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6 loopback: ::1
|
||||||
|
// Also handle IPv4-mapped IPv6 addresses like ::ffff:127.0.0.1
|
||||||
|
if (ip === '::1' || ip === '::ffff:127.0.0.1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle full IPv6 loopback notation
|
||||||
|
if (ip.toLowerCase().startsWith('0000:0000:0000:0000:0000:0000:0000:0001') ||
|
||||||
|
ip.toLowerCase() === '0:0:0:0:0:0:0:1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
// Helper to check if request should be limited
|
// Helper to check if request should be limited
|
||||||
const shouldSkipLimit = (req: Request): boolean => {
|
const shouldSkipLimit = (req: Request): boolean => {
|
||||||
const skipPaths = [
|
const skipPaths = [
|
||||||
'/api/health',
|
'/api/health',
|
||||||
'/api/setup/status',
|
'/api/setup/status',
|
||||||
'/api/auth/validate'
|
'/api/auth/validate'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Skip for successful GET requests (data fetching)
|
// Skip for successful GET requests (data fetching)
|
||||||
if (req.method === 'GET' && req.path.startsWith('/api/')) {
|
if (req.method === 'GET' && req.path.startsWith('/api/')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientIP = getClientIP(req);
|
||||||
|
|
||||||
|
// Skip for loopback addresses (local development)
|
||||||
|
if (isLoopbackAddress(clientIP)) {
|
||||||
|
console.log(`✅ Loopback address skipped: ${clientIP}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip for whitelisted IPs from environment
|
// Skip for whitelisted IPs from environment
|
||||||
const whitelist = process.env.RATE_LIMIT_WHITELIST?.split(',') || [];
|
const whitelist = process.env.RATE_LIMIT_WHITELIST?.split(',') || [];
|
||||||
const clientIP = getClientIP(req);
|
|
||||||
if (whitelist.includes(clientIP)) {
|
if (whitelist.includes(clientIP)) {
|
||||||
console.log(`✅ IP whitelisted: ${clientIP}`);
|
console.log(`✅ IP whitelisted: ${clientIP}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return skipPaths.includes(req.path);
|
return skipPaths.includes(req.path);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Environment-based configuration
|
// Environment-based configuration
|
||||||
const getRateLimitConfig = () => {
|
const getRateLimitConfig = () => {
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes default
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes default
|
||||||
max: isProduction
|
max: isProduction
|
||||||
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '1000') // Stricter in production
|
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '1000') // Stricter in production
|
||||||
: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '5000'), // More lenient in development
|
: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '5000'), // More lenient in development
|
||||||
|
|
||||||
// Development-specific relaxations
|
// Development-specific relaxations
|
||||||
skip: (req: Request) => {
|
skip: (req: Request) => {
|
||||||
// Skip all GET requests in development for easier testing
|
// Skip all GET requests in development for easier testing
|
||||||
if (!isProduction && req.method === 'GET') {
|
if (!isProduction && req.method === 'GET') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldSkipLimit(req);
|
return shouldSkipLimit(req);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -90,8 +119,8 @@ const getRateLimitConfig = () => {
|
|||||||
// Main API limiter - nur für POST/PUT/DELETE
|
// Main API limiter - nur für POST/PUT/DELETE
|
||||||
export const apiLimiter = rateLimit({
|
export const apiLimiter = rateLimit({
|
||||||
...getRateLimitConfig(),
|
...getRateLimitConfig(),
|
||||||
message: {
|
message: {
|
||||||
error: 'Zu viele Anfragen, bitte verlangsamen Sie Ihre Aktionen'
|
error: 'Zu viele Anfragen, bitte verlangsamen Sie Ihre Aktionen'
|
||||||
},
|
},
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
@@ -99,8 +128,8 @@ export const apiLimiter = rateLimit({
|
|||||||
handler: (req, res) => {
|
handler: (req, res) => {
|
||||||
const clientIP = getClientIP(req);
|
const clientIP = getClientIP(req);
|
||||||
console.warn(`🚨 Rate limit exceeded for IP: ${clientIP}, Path: ${req.path}, Method: ${req.method}`);
|
console.warn(`🚨 Rate limit exceeded for IP: ${clientIP}, Path: ${req.path}, Method: ${req.method}`);
|
||||||
|
|
||||||
res.status(429).json({
|
res.status(429).json({
|
||||||
error: 'Zu viele Anfragen',
|
error: 'Zu viele Anfragen',
|
||||||
message: 'Bitte versuchen Sie es später erneut',
|
message: 'Bitte versuchen Sie es später erneut',
|
||||||
retryAfter: '15 Minuten',
|
retryAfter: '15 Minuten',
|
||||||
@@ -113,8 +142,8 @@ export const apiLimiter = rateLimit({
|
|||||||
export const authLimiter = rateLimit({
|
export const authLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX_REQUESTS || '100'),
|
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX_REQUESTS || '100'),
|
||||||
message: {
|
message: {
|
||||||
error: 'Zu viele Login-Versuche, bitte versuchen Sie es später erneut'
|
error: 'Zu viele Login-Versuche, bitte versuchen Sie es später erneut'
|
||||||
},
|
},
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
@@ -123,8 +152,8 @@ export const authLimiter = rateLimit({
|
|||||||
handler: (req, res) => {
|
handler: (req, res) => {
|
||||||
const clientIP = getClientIP(req);
|
const clientIP = getClientIP(req);
|
||||||
console.warn(`🚨 Auth rate limit exceeded for IP: ${clientIP}`);
|
console.warn(`🚨 Auth rate limit exceeded for IP: ${clientIP}`);
|
||||||
|
|
||||||
res.status(429).json({
|
res.status(429).json({
|
||||||
error: 'Zu viele Login-Versuche',
|
error: 'Zu viele Login-Versuche',
|
||||||
message: 'Aus Sicherheitsgründen wurde Ihr Konto temporär gesperrt',
|
message: 'Aus Sicherheitsgründen wurde Ihr Konto temporär gesperrt',
|
||||||
retryAfter: '15 Minuten'
|
retryAfter: '15 Minuten'
|
||||||
|
|||||||
Reference in New Issue
Block a user