diff --git a/.env.template b/.env.template index bab13d6..e9115bf 100644 --- a/.env.template +++ b/.env.template @@ -13,4 +13,15 @@ PORT=${PORT:-3002} # App Configuration APP_TITLE="Shift Planning App" -ENABLE_PRO=${ENABLE_PRO:-false} \ No newline at end of file +ENABLE_PRO=${ENABLE_PRO:-false} + +# Trust Proxy Configuration +TRUST_PROXY_ENABLED=false +TRUSTED_PROXY_IPS= # Your load balancer/reverse proxy IPs +TRUSTED_PROXY_HEADER=x-forwarded-for + +# Rate Limiting Configuration +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +RATE_LIMIT_SKIP_PATHS=/api/health,/api/status +RATE_LIMIT_WHITELIST=127.0.0.1,::1 \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index d274aab..09c7f46 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 36320f5..082ba45 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -51,4 +51,44 @@ export const requireRole = (roles: string[]) => { console.log(`✅ Role check passed for user: ${req.user.email}, role: ${req.user.role}`); next(); }; -}; \ No newline at end of file +}; + +// 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; + } +} \ No newline at end of file diff --git a/backend/src/middleware/rateLimit.ts b/backend/src/middleware/rateLimit.ts index 710f23e..dd4141d 100644 --- a/backend/src/middleware/rateLimit.ts +++ b/backend/src/middleware/rateLimit.ts @@ -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) }); \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 962827a..1cb3c22 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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`); }); diff --git a/frontend/package.json b/frontend/package.json index 786414f..cb904f3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,7 @@ "framer-motion": "12.23.24" }, "scripts": { - "dev": "vite", + "dev": "vite dev", "build": "tsc && vite build", "preview": "vite preview" } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9080687..9ff376d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -14,14 +14,14 @@ export default defineConfig(({ mode }) => { NODE_ENV: mode, ENABLE_PRO: env.ENABLE_PRO || 'false', VITE_APP_TITLE: env.APP_TITLE || 'Shift Planning App', - VITE_API_URL: isProduction ? '/api' : 'http://localhost:3002/api', + VITE_API_URL: isProduction ? '/api' : '/api', } return { plugins: [react()], - server: isDevelopment ? undefined : { - port: 3002, + server: isDevelopment ? { + port: 3003, host: true, open: true, proxy: { @@ -31,7 +31,7 @@ export default defineConfig(({ mode }) => { secure: false, } } - }, + } : undefined, build: { outDir: 'dist', diff --git a/package-lock.json b/package-lock.json index a955e1b..2164bd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -260,7 +260,6 @@ "backend/node_modules/@types/node": { "version": "24.7.0", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -1857,7 +1856,6 @@ "version": "7.28.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2288,6 +2286,10 @@ "win32" ] }, + "node_modules/@schichtenplaner/premium": { + "resolved": "premium", + "link": true + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -2308,6 +2310,7 @@ "integrity": "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" }, @@ -2551,6 +2554,7 @@ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -2561,7 +2565,8 @@ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/estree": { "version": "1.0.8", @@ -2623,7 +2628,6 @@ "version": "19.2.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2704,6 +2708,7 @@ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", @@ -2721,6 +2726,7 @@ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", @@ -2748,6 +2754,7 @@ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tinyrainbow": "^2.0.0" }, @@ -2761,6 +2768,7 @@ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tinyspy": "^4.0.3" }, @@ -2774,6 +2782,7 @@ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", @@ -2857,6 +2866,7 @@ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" } @@ -2867,6 +2877,7 @@ "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.1" }, @@ -2959,7 +2970,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3044,6 +3054,7 @@ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -3078,6 +3089,7 @@ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 16" } @@ -3255,6 +3267,7 @@ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -3416,6 +3429,7 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -3430,6 +3444,7 @@ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -3994,7 +4009,8 @@ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lru-cache": { "version": "5.1.1", @@ -4020,6 +4036,7 @@ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -4237,6 +4254,7 @@ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 14.16" } @@ -4367,7 +4385,6 @@ "node_modules/react": { "version": "19.2.0", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4375,7 +4392,6 @@ "node_modules/react-dom": { "version": "19.2.0", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4432,6 +4448,7 @@ "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", @@ -4770,6 +4787,7 @@ "integrity": "sha512-WQMfNsbyHO8fx5bLatsLATzvFwzG42xHnq8W9s2HzjhGzQ2Pp77k7nRWXMuRNvvv+4ypKhJdqeDl8S9kwWuz2A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^1.6.0", @@ -4805,6 +4823,7 @@ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -4844,7 +4863,6 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -4863,7 +4881,8 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -4886,6 +4905,7 @@ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -4896,6 +4916,7 @@ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -4946,7 +4967,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5029,7 +5049,6 @@ "version": "6.4.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5176,6 +5195,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5200,7 +5220,6 @@ "premium": { "name": "@schichtenplaner/premium", "version": "1.0.0", - "extraneous": true, "workspaces": [ "backendPRO", "frontendPRO"