backend changes
This commit is contained in:
parent
86e68a2e6b
commit
f927bd3d3b
1937
dashboard-service/package-lock.json
generated
Normal file
1937
dashboard-service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -545,7 +545,7 @@ services:
|
||||
- PORT=8000
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- REDIS_PASSWORD=redis123
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=dev_pipeline
|
||||
@ -555,6 +555,17 @@ services:
|
||||
- RABBITMQ_PORT=5672
|
||||
- RABBITMQ_USER=pipeline_admin
|
||||
- RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}
|
||||
# Internal service URLs for proxying (Docker network names)
|
||||
- USER_AUTH_URL=http://user-auth:8011
|
||||
- TEMPLATE_MANAGER_URL=http://template-manager:8009
|
||||
- REQUIREMENT_PROCESSOR_URL=http://requirement-processor:8001
|
||||
- TECH_STACK_SELECTOR_URL=http://tech-stack-selector:8002
|
||||
- ARCHITECTURE_DESIGNER_URL=http://architecture-designer:8003
|
||||
- CODE_GENERATOR_URL=http://code-generator:8004
|
||||
- TEST_GENERATOR_URL=http://test-generator:8005
|
||||
- DEPLOYMENT_MANAGER_URL=http://deployment-manager:8006
|
||||
- DASHBOARD_URL=http://dashboard:8008
|
||||
- SELF_IMPROVING_GENERATOR_URL=http://self-improving-generator:8007
|
||||
networks:
|
||||
- pipeline_network
|
||||
depends_on:
|
||||
@ -578,7 +589,7 @@ services:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- REDIS_PASSWORD=redis123
|
||||
- MONGODB_HOST=mongodb
|
||||
- MONGODB_PORT=27017
|
||||
- NEO4J_URI=bolt://neo4j:7687
|
||||
@ -586,7 +597,7 @@ services:
|
||||
- NEO4J_PASSWORD=password
|
||||
- CHROMA_HOST=chromadb
|
||||
- CHROMA_PORT=8000
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- REDIS_URL=redis://:redis123@redis:6379
|
||||
networks:
|
||||
- pipeline_network
|
||||
depends_on:
|
||||
@ -610,7 +621,7 @@ services:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- REDIS_PASSWORD=redis123
|
||||
networks:
|
||||
- pipeline_network
|
||||
depends_on:
|
||||
@ -666,7 +677,7 @@ services:
|
||||
- MONGODB_PORT=27017
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- REDIS_PASSWORD=redis123
|
||||
- CLAUDE_API_KEY=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
|
||||
- OPENAI_API_KEY=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
|
||||
# - NEO4J_URI=bolt://neo4j:7687
|
||||
@ -711,7 +722,7 @@ services:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- REDIS_PASSWORD=redis123
|
||||
networks:
|
||||
- pipeline_network
|
||||
depends_on:
|
||||
@ -763,7 +774,7 @@ services:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- REDIS_PASSWORD=redis123
|
||||
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
|
||||
- JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
|
||||
- JWT_ACCESS_EXPIRY=15m
|
||||
@ -878,7 +889,15 @@ services:
|
||||
- NODE_ENV=production
|
||||
- PORT=8008
|
||||
- DATABASE_URL=postgresql://pipeline_admin:${POSTGRES_PASSWORD}@postgres:5432/dev_pipeline
|
||||
- REDIS_URL=redis://pipeline_redis:6379
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=dev_pipeline
|
||||
- POSTGRES_USER=pipeline_admin
|
||||
- POSTGRES_PASSWORD=secure_pipeline_2024
|
||||
- REDIS_URL=redis://:redis123@redis:6379
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=redis123
|
||||
- API_GATEWAY_URL=http://pipeline_api_gateway:8000
|
||||
- CODE_GENERATOR_URL=http://pipeline_code_generator:8004
|
||||
- SELF_IMPROVING_URL=http://pipeline_self_improving_generator:8007
|
||||
|
||||
93
services/api-gateway/package-lock.json
generated
93
services/api-gateway/package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"express-rate-limit": "^6.8.1",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.0.0",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.11.1",
|
||||
@ -1117,6 +1118,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
|
||||
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
@ -1493,7 +1503,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@ -2260,6 +2269,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
@ -2408,7 +2423,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@ -2774,6 +2788,60 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz",
|
||||
"integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.15",
|
||||
"debug": "^4.3.6",
|
||||
"http-proxy": "^1.18.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@ -2900,7 +2968,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -2930,7 +2997,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@ -2943,12 +3009,20 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@ -4001,7 +4075,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@ -4547,7 +4620,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@ -4803,6 +4875,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@ -5442,7 +5520,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
||||
@ -9,22 +9,23 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.0.0",
|
||||
"express-rate-limit": "^6.8.1",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"redis": "^4.6.7",
|
||||
"pg": "^8.11.1",
|
||||
"axios": "^1.4.0",
|
||||
"winston": "^3.10.0",
|
||||
"socket.io": "^4.7.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.8.1",
|
||||
"express-validator": "^7.0.1",
|
||||
"morgan": "^1.10.0"
|
||||
"helmet": "^7.0.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.11.1",
|
||||
"redis": "^4.6.7",
|
||||
"socket.io": "^4.7.2",
|
||||
"winston": "^3.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1",
|
||||
"jest": "^29.6.1"
|
||||
"jest": "^29.6.1",
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
122
services/api-gateway/src/middleware/authentication.js
Normal file
122
services/api-gateway/src/middleware/authentication.js
Normal file
@ -0,0 +1,122 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const axios = require('axios');
|
||||
|
||||
// JWT token verification middleware
|
||||
const verifyToken = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Access token required',
|
||||
error: 'No token provided'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
|
||||
// Add user context to headers for downstream services
|
||||
req.headers['x-user-id'] = decoded.id || decoded.userId;
|
||||
req.headers['x-user-email'] = decoded.email;
|
||||
req.headers['x-user-role'] = decoded.role || 'user';
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Token verification failed:', error.message);
|
||||
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Token expired',
|
||||
error: 'Please login again'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid token',
|
||||
error: 'Token verification failed'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication failed',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Forward user context to downstream services
|
||||
const forwardUserContext = (req, res, next) => {
|
||||
if (req.user) {
|
||||
// Add gateway headers for service identification
|
||||
req.headers['x-gateway-request-id'] = req.requestId;
|
||||
req.headers['x-gateway-timestamp'] = new Date().toISOString();
|
||||
req.headers['x-forwarded-by'] = 'api-gateway';
|
||||
req.headers['x-forwarded-for'] = req.ip;
|
||||
req.headers['x-forwarded-proto'] = req.protocol;
|
||||
req.headers['x-forwarded-host'] = req.get('host');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Optional token verification (doesn't fail if no token)
|
||||
const verifyTokenOptional = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
|
||||
// Add user context to headers
|
||||
req.headers['x-user-id'] = decoded.id || decoded.userId;
|
||||
req.headers['x-user-email'] = decoded.email;
|
||||
req.headers['x-user-role'] = decoded.role || 'user';
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// Continue without authentication for optional routes
|
||||
console.log('Optional token verification failed:', error.message);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
// Role-based authorization middleware
|
||||
const requireRole = (roles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
const userRole = req.user.role || 'user';
|
||||
const allowedRoles = Array.isArray(roles) ? roles : [roles];
|
||||
|
||||
if (!allowedRoles.includes(userRole)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Insufficient permissions',
|
||||
required_roles: allowedRoles,
|
||||
user_role: userRole
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
verifyToken,
|
||||
forwardUserContext,
|
||||
verifyTokenOptional,
|
||||
requireRole
|
||||
};
|
||||
126
services/api-gateway/src/middleware/requestLogger.js
Normal file
126
services/api-gateway/src/middleware/requestLogger.js
Normal file
@ -0,0 +1,126 @@
|
||||
const winston = require('winston');
|
||||
|
||||
// Configure Winston logger
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: 'api-gateway' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' }),
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Request logging middleware
|
||||
const logRequest = (req, res, next) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Log incoming request
|
||||
logger.info('Incoming Request', {
|
||||
requestId: req.requestId,
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
timestamp: new Date().toISOString(),
|
||||
headers: {
|
||||
authorization: req.headers.authorization ? '[REDACTED]' : undefined,
|
||||
'content-type': req.headers['content-type'],
|
||||
'x-forwarded-for': req.headers['x-forwarded-for']
|
||||
}
|
||||
});
|
||||
|
||||
// Override res.end to log response
|
||||
const originalEnd = res.end;
|
||||
res.end = function(chunk, encoding) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Log response
|
||||
logger.info('Response Sent', {
|
||||
requestId: req.requestId,
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
statusCode: res.statusCode,
|
||||
responseTime: `${responseTime}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Call original end method
|
||||
originalEnd.call(this, chunk, encoding);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Error logging middleware
|
||||
const logError = (error, req, res, next) => {
|
||||
logger.error('Request Error', {
|
||||
requestId: req.requestId,
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
next(error);
|
||||
};
|
||||
|
||||
// Service proxy logging
|
||||
const logProxyRequest = (serviceName, targetUrl) => {
|
||||
return (proxyReq, req, res) => {
|
||||
logger.info('Proxy Request', {
|
||||
requestId: req.requestId,
|
||||
service: serviceName,
|
||||
method: req.method,
|
||||
originalUrl: req.originalUrl,
|
||||
targetUrl: targetUrl,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const logProxyResponse = (serviceName) => {
|
||||
return (proxyRes, req, res) => {
|
||||
logger.info('Proxy Response', {
|
||||
requestId: req.requestId,
|
||||
service: serviceName,
|
||||
method: req.method,
|
||||
originalUrl: req.originalUrl,
|
||||
statusCode: proxyRes.statusCode,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const logProxyError = (serviceName) => {
|
||||
return (err, req, res) => {
|
||||
logger.error('Proxy Error', {
|
||||
requestId: req.requestId,
|
||||
service: serviceName,
|
||||
method: req.method,
|
||||
originalUrl: req.originalUrl,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
logger,
|
||||
logRequest,
|
||||
logError,
|
||||
logProxyRequest,
|
||||
logProxyResponse,
|
||||
logProxyError
|
||||
};
|
||||
140
services/api-gateway/src/middleware/serviceHealth.js
Normal file
140
services/api-gateway/src/middleware/serviceHealth.js
Normal file
@ -0,0 +1,140 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Service health monitoring
|
||||
class ServiceHealthMonitor {
|
||||
constructor() {
|
||||
this.serviceStatus = {};
|
||||
this.healthCheckInterval = null;
|
||||
this.healthCheckFrequency = parseInt(process.env.HEALTH_CHECK_FREQUENCY) || 30000; // 30 seconds
|
||||
}
|
||||
|
||||
// Initialize health monitoring for all services
|
||||
async initializeHealthMonitoring() {
|
||||
const serviceTargets = {
|
||||
USER_AUTH_URL: process.env.USER_AUTH_URL || 'http://localhost:8011',
|
||||
TEMPLATE_MANAGER_URL: process.env.TEMPLATE_MANAGER_URL || 'http://localhost:8009',
|
||||
REQUIREMENT_PROCESSOR_URL: process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001',
|
||||
TECH_STACK_SELECTOR_URL: process.env.TECH_STACK_SELECTOR_URL || 'http://localhost:8002',
|
||||
ARCHITECTURE_DESIGNER_URL: process.env.ARCHITECTURE_DESIGNER_URL || 'http://localhost:8003',
|
||||
CODE_GENERATOR_URL: process.env.CODE_GENERATOR_URL || 'http://localhost:8004',
|
||||
TEST_GENERATOR_URL: process.env.TEST_GENERATOR_URL || 'http://localhost:8005',
|
||||
DEPLOYMENT_MANAGER_URL: process.env.DEPLOYMENT_MANAGER_URL || 'http://localhost:8006',
|
||||
DASHBOARD_URL: process.env.DASHBOARD_URL || 'http://localhost:8008',
|
||||
SELF_IMPROVING_GENERATOR_URL: process.env.SELF_IMPROVING_GENERATOR_URL || 'http://localhost:8007',
|
||||
};
|
||||
|
||||
// Initial health check
|
||||
await this.checkAllServices(serviceTargets);
|
||||
|
||||
// Start periodic health checks
|
||||
this.startPeriodicHealthChecks(serviceTargets);
|
||||
|
||||
console.log('✅ Service health monitoring initialized');
|
||||
}
|
||||
|
||||
// Check health of all services
|
||||
async checkAllServices(serviceTargets) {
|
||||
const healthPromises = Object.entries(serviceTargets).map(async ([serviceName, serviceUrl]) => {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await axios.get(`${serviceUrl}/health`, {
|
||||
timeout: 5000,
|
||||
headers: { 'User-Agent': 'API-Gateway-Health-Check' }
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
this.serviceStatus[serviceName] = {
|
||||
status: response.status === 200 ? 'healthy' : 'unhealthy',
|
||||
url: serviceUrl,
|
||||
responseTime: responseTime,
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
this.serviceStatus[serviceName] = {
|
||||
status: 'unhealthy',
|
||||
url: serviceUrl,
|
||||
responseTime: null,
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(healthPromises);
|
||||
}
|
||||
|
||||
// Start periodic health checks
|
||||
startPeriodicHealthChecks(serviceTargets) {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.checkAllServices(serviceTargets);
|
||||
}, this.healthCheckFrequency);
|
||||
}
|
||||
|
||||
// Stop health monitoring
|
||||
stopHealthMonitoring() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current service status
|
||||
getServiceStatus() {
|
||||
const totalServices = Object.keys(this.serviceStatus).length;
|
||||
const healthyServices = Object.values(this.serviceStatus).filter(s => s.status === 'healthy').length;
|
||||
|
||||
return {
|
||||
summary: {
|
||||
total_services: totalServices,
|
||||
healthy_services: healthyServices,
|
||||
unhealthy_services: totalServices - healthyServices,
|
||||
overall_health: healthyServices === totalServices ? 'healthy' : 'degraded'
|
||||
},
|
||||
services: this.serviceStatus,
|
||||
last_updated: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const healthMonitor = new ServiceHealthMonitor();
|
||||
|
||||
// Middleware to get service status
|
||||
const getServiceStatus = (req, res) => {
|
||||
const status = healthMonitor.getServiceStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
...status
|
||||
});
|
||||
};
|
||||
|
||||
// Middleware to check if a specific service is healthy
|
||||
const checkServiceHealth = (serviceName) => {
|
||||
return (req, res, next) => {
|
||||
const serviceStatus = healthMonitor.serviceStatus[serviceName];
|
||||
|
||||
if (!serviceStatus || serviceStatus.status !== 'healthy') {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: `Service ${serviceName} is currently unavailable`,
|
||||
service_status: serviceStatus || { status: 'unknown' },
|
||||
retry_after: 30
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
healthMonitor,
|
||||
initializeHealthMonitoring: () => healthMonitor.initializeHealthMonitoring(),
|
||||
getServiceStatus,
|
||||
checkServiceHealth
|
||||
};
|
||||
136
services/api-gateway/src/middleware/webSocket.js
Normal file
136
services/api-gateway/src/middleware/webSocket.js
Normal file
@ -0,0 +1,136 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// WebSocket authentication middleware
|
||||
const authenticateSocket = (socket, next) => {
|
||||
try {
|
||||
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return next(new Error('Authentication token required'));
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
socket.user = decoded;
|
||||
socket.userId = decoded.id || decoded.userId;
|
||||
|
||||
console.log(`✅ WebSocket authenticated: ${socket.user.email || socket.userId}`);
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('WebSocket authentication failed:', error.message);
|
||||
next(new Error('Authentication failed'));
|
||||
}
|
||||
};
|
||||
|
||||
// WebSocket connection handler with authentication
|
||||
const handleWebSocketConnections = (io) => {
|
||||
// Authentication middleware for all connections
|
||||
io.use(authenticateSocket);
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`🔌 Client connected: ${socket.id} (User: ${socket.user.email || socket.userId})`);
|
||||
|
||||
// Join user-specific room
|
||||
const userRoom = `user_${socket.userId}`;
|
||||
socket.join(userRoom);
|
||||
|
||||
// Send connection confirmation
|
||||
socket.emit('connected', {
|
||||
message: 'Connected to CodeNuk API Gateway',
|
||||
timestamp: new Date().toISOString(),
|
||||
socketId: socket.id,
|
||||
user: {
|
||||
id: socket.userId,
|
||||
email: socket.user.email,
|
||||
role: socket.user.role
|
||||
}
|
||||
});
|
||||
|
||||
// Handle service-specific events
|
||||
socket.on('subscribe_to_service', (data) => {
|
||||
const { service } = data;
|
||||
const serviceRoom = `service_${service}`;
|
||||
socket.join(serviceRoom);
|
||||
|
||||
console.log(`📡 User ${socket.userId} subscribed to ${service} updates`);
|
||||
socket.emit('subscribed', { service, room: serviceRoom });
|
||||
});
|
||||
|
||||
socket.on('unsubscribe_from_service', (data) => {
|
||||
const { service } = data;
|
||||
const serviceRoom = `service_${service}`;
|
||||
socket.leave(serviceRoom);
|
||||
|
||||
console.log(`📡 User ${socket.userId} unsubscribed from ${service} updates`);
|
||||
socket.emit('unsubscribed', { service, room: serviceRoom });
|
||||
});
|
||||
|
||||
// Handle project-specific events
|
||||
socket.on('join_project', (data) => {
|
||||
const { projectId } = data;
|
||||
const projectRoom = `project_${projectId}`;
|
||||
socket.join(projectRoom);
|
||||
|
||||
console.log(`📁 User ${socket.userId} joined project ${projectId}`);
|
||||
socket.emit('joined_project', { projectId, room: projectRoom });
|
||||
});
|
||||
|
||||
socket.on('leave_project', (data) => {
|
||||
const { projectId } = data;
|
||||
const projectRoom = `project_${projectId}`;
|
||||
socket.leave(projectRoom);
|
||||
|
||||
console.log(`📁 User ${socket.userId} left project ${projectId}`);
|
||||
socket.emit('left_project', { projectId, room: projectRoom });
|
||||
});
|
||||
|
||||
// Handle real-time notifications
|
||||
socket.on('send_notification', (data) => {
|
||||
const { targetUserId, message, type } = data;
|
||||
const targetRoom = `user_${targetUserId}`;
|
||||
|
||||
io.to(targetRoom).emit('notification', {
|
||||
from: socket.userId,
|
||||
message,
|
||||
type,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`📢 Notification sent from ${socket.userId} to ${targetUserId}`);
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`🔌 Client disconnected: ${socket.id} (User: ${socket.user.email || socket.userId}) - Reason: ${reason}`);
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
socket.on('error', (error) => {
|
||||
console.error(`❌ Socket error for ${socket.id}:`, error);
|
||||
});
|
||||
});
|
||||
|
||||
// Broadcast service status updates
|
||||
const broadcastServiceUpdate = (serviceName, status) => {
|
||||
io.to(`service_${serviceName}`).emit('service_status_update', {
|
||||
service: serviceName,
|
||||
status,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
// Broadcast project updates
|
||||
const broadcastProjectUpdate = (projectId, update) => {
|
||||
io.to(`project_${projectId}`).emit('project_update', {
|
||||
projectId,
|
||||
update,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
broadcastServiceUpdate,
|
||||
broadcastProjectUpdate
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = handleWebSocketConnections;
|
||||
68
services/api-gateway/src/routes/healthRouter.js
Normal file
68
services/api-gateway/src/routes/healthRouter.js
Normal file
@ -0,0 +1,68 @@
|
||||
const express = require('express');
|
||||
const { healthMonitor } = require('../middleware/serviceHealth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get comprehensive service health status
|
||||
router.get('/services', (req, res) => {
|
||||
const status = healthMonitor.getServiceStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
...status
|
||||
});
|
||||
});
|
||||
|
||||
// Get health status for a specific service
|
||||
router.get('/service/:serviceName', (req, res) => {
|
||||
const { serviceName } = req.params;
|
||||
const serviceStatus = healthMonitor.serviceStatus[serviceName];
|
||||
|
||||
if (!serviceStatus) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Service ${serviceName} not found`,
|
||||
available_services: Object.keys(healthMonitor.serviceStatus)
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
service: serviceName,
|
||||
...serviceStatus
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger manual health check for all services
|
||||
router.post('/check', async (req, res) => {
|
||||
try {
|
||||
const serviceTargets = {
|
||||
USER_AUTH_URL: process.env.USER_AUTH_URL || 'http://localhost:8011',
|
||||
TEMPLATE_MANAGER_URL: process.env.TEMPLATE_MANAGER_URL || 'http://localhost:8009',
|
||||
REQUIREMENT_PROCESSOR_URL: process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001',
|
||||
TECH_STACK_SELECTOR_URL: process.env.TECH_STACK_SELECTOR_URL || 'http://localhost:8002',
|
||||
ARCHITECTURE_DESIGNER_URL: process.env.ARCHITECTURE_DESIGNER_URL || 'http://localhost:8003',
|
||||
CODE_GENERATOR_URL: process.env.CODE_GENERATOR_URL || 'http://localhost:8004',
|
||||
TEST_GENERATOR_URL: process.env.TEST_GENERATOR_URL || 'http://localhost:8005',
|
||||
DEPLOYMENT_MANAGER_URL: process.env.DEPLOYMENT_MANAGER_URL || 'http://localhost:8006',
|
||||
DASHBOARD_URL: process.env.DASHBOARD_URL || 'http://localhost:8008',
|
||||
SELF_IMPROVING_GENERATOR_URL: process.env.SELF_IMPROVING_GENERATOR_URL || 'http://localhost:8007',
|
||||
};
|
||||
|
||||
await healthMonitor.checkAllServices(serviceTargets);
|
||||
const status = healthMonitor.getServiceStatus();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Health check completed',
|
||||
...status
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Health check failed',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { router };
|
||||
130
services/api-gateway/src/routes/serviceRouter.js
Normal file
130
services/api-gateway/src/routes/serviceRouter.js
Normal file
@ -0,0 +1,130 @@
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const { logProxyRequest, logProxyResponse, logProxyError } = require('../middleware/requestLogger');
|
||||
|
||||
// Create service proxy with enhanced logging and error handling
|
||||
const createServiceProxy = (targetUrl, serviceName, options = {}) => {
|
||||
return createProxyMiddleware({
|
||||
target: targetUrl,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
timeout: parseInt(process.env.PROXY_TIMEOUT) || 30000,
|
||||
proxyTimeout: parseInt(process.env.PROXY_TIMEOUT) || 30000,
|
||||
pathRewrite: options.pathRewrite || {},
|
||||
onProxyReq: logProxyRequest(serviceName, targetUrl),
|
||||
onProxyRes: logProxyResponse(serviceName),
|
||||
onError: (err, req, res) => {
|
||||
logProxyError(serviceName)(err, req, res);
|
||||
|
||||
if (!res.headersSent) {
|
||||
// Handle different types of proxy errors
|
||||
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: 'Service temporarily unavailable',
|
||||
service: serviceName,
|
||||
error: 'The requested service is currently unavailable',
|
||||
request_id: req.requestId,
|
||||
retry_after: 30
|
||||
});
|
||||
}
|
||||
|
||||
if (err.code === 'ETIMEDOUT' || err.message.includes('timeout')) {
|
||||
return res.status(504).json({
|
||||
success: false,
|
||||
message: 'Service timeout',
|
||||
service: serviceName,
|
||||
error: 'The service took too long to respond',
|
||||
request_id: req.requestId,
|
||||
retry_after: 60
|
||||
});
|
||||
}
|
||||
|
||||
// Generic proxy error
|
||||
res.status(502).json({
|
||||
success: false,
|
||||
message: 'Bad Gateway',
|
||||
service: serviceName,
|
||||
error: 'Unable to connect to the service',
|
||||
request_id: req.requestId
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Create auth-specific proxy (no authentication required for login/register)
|
||||
const createAuthProxy = () => {
|
||||
const targetUrl = process.env.USER_AUTH_URL || 'http://localhost:8011';
|
||||
|
||||
console.log(`🔧 [PROXY CREATION] Creating auth proxy targeting: ${targetUrl}`);
|
||||
|
||||
return createProxyMiddleware({
|
||||
target: targetUrl,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
timeout: 30000,
|
||||
proxyTimeout: 30000,
|
||||
// Don't parse body - let it pass through
|
||||
parseReqBody: false,
|
||||
// Strip /api/auth prefix before forwarding to service
|
||||
pathRewrite: {
|
||||
'^/api/auth': ''
|
||||
},
|
||||
onProxyReq: (proxyReq, req, res) => {
|
||||
console.log(`🔥 [PROXY MIDDLEWARE TRIGGERED!] This means the proxy is working!`);
|
||||
|
||||
// Calculate the rewritten path
|
||||
const rewrittenPath = req.url.replace(/^\/api\/auth/, '');
|
||||
const finalUrl = `${targetUrl}${rewrittenPath}`;
|
||||
|
||||
console.log(`🔐 [AUTH PROXY DEBUG]`);
|
||||
console.log(` Original URL: ${req.originalUrl}`);
|
||||
console.log(` Request URL: ${req.url}`);
|
||||
console.log(` Target URL: ${targetUrl}`);
|
||||
console.log(` Rewritten Path: ${rewrittenPath}`);
|
||||
console.log(` FINAL URL HITTING: ${finalUrl}`);
|
||||
console.log(` Method: ${req.method}`);
|
||||
console.log(` Headers: ${JSON.stringify(req.headers, null, 2)}`);
|
||||
|
||||
// Add gateway headers
|
||||
proxyReq.setHeader('X-Gateway-Request-ID', req.requestId);
|
||||
proxyReq.setHeader('X-Gateway-Timestamp', new Date().toISOString());
|
||||
proxyReq.setHeader('X-Forwarded-By', 'api-gateway');
|
||||
proxyReq.setHeader('X-Forwarded-For', req.ip);
|
||||
proxyReq.setHeader('X-Forwarded-Proto', req.protocol);
|
||||
proxyReq.setHeader('X-Forwarded-Host', req.get('host'));
|
||||
|
||||
// Ensure content-type and content-length are preserved
|
||||
if (req.headers['content-type']) {
|
||||
proxyReq.setHeader('Content-Type', req.headers['content-type']);
|
||||
}
|
||||
if (req.headers['content-length']) {
|
||||
proxyReq.setHeader('Content-Length', req.headers['content-length']);
|
||||
}
|
||||
},
|
||||
onProxyRes: (proxyRes, req, res) => {
|
||||
console.log(`🔐 [AUTH RESPONSE] ${proxyRes.statusCode} for ${req.method} ${req.originalUrl}`);
|
||||
|
||||
// Add CORS headers
|
||||
proxyRes.headers['Access-Control-Allow-Origin'] = req.headers.origin || '*';
|
||||
proxyRes.headers['Access-Control-Allow-Credentials'] = 'true';
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
console.error(`❌ [AUTH PROXY ERROR] ${req.method} ${req.originalUrl}:`, err.message);
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(502).json({
|
||||
success: false,
|
||||
message: 'Authentication service unavailable',
|
||||
error: 'Unable to connect to authentication service',
|
||||
request_id: req.requestId
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createServiceProxy,
|
||||
createAuthProxy
|
||||
};
|
||||
123
services/api-gateway/src/routes/websocketRouter.js
Normal file
123
services/api-gateway/src/routes/websocketRouter.js
Normal file
@ -0,0 +1,123 @@
|
||||
const express = require('express');
|
||||
const { verifyToken } = require('../middleware/authentication');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get WebSocket connection info
|
||||
router.get('/info', verifyToken, (req, res) => {
|
||||
const io = global.io;
|
||||
|
||||
if (!io) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'WebSocket server not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
websocket: {
|
||||
connected_clients: io.engine.clientsCount,
|
||||
transport_types: ['websocket', 'polling'],
|
||||
endpoint: '/socket.io/',
|
||||
authentication_required: true
|
||||
},
|
||||
rooms: {
|
||||
user_rooms: `user_${req.user.id || req.user.userId}`,
|
||||
available_services: [
|
||||
'user-auth', 'template-manager', 'requirement-processor',
|
||||
'tech-stack-selector', 'architecture-designer', 'code-generator',
|
||||
'test-generator', 'deployment-manager', 'dashboard', 'self-improving-generator'
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Broadcast message to specific service subscribers
|
||||
router.post('/broadcast/service/:serviceName', verifyToken, (req, res) => {
|
||||
const { serviceName } = req.params;
|
||||
const { message, type = 'info' } = req.body;
|
||||
const io = global.io;
|
||||
|
||||
if (!io) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'WebSocket server not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
const serviceRoom = `service_${serviceName}`;
|
||||
io.to(serviceRoom).emit('service_broadcast', {
|
||||
service: serviceName,
|
||||
message,
|
||||
type,
|
||||
from: req.user.email || req.user.id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Broadcast sent to ${serviceName} subscribers`,
|
||||
room: serviceRoom
|
||||
});
|
||||
});
|
||||
|
||||
// Broadcast message to specific project
|
||||
router.post('/broadcast/project/:projectId', verifyToken, (req, res) => {
|
||||
const { projectId } = req.params;
|
||||
const { message, type = 'info' } = req.body;
|
||||
const io = global.io;
|
||||
|
||||
if (!io) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'WebSocket server not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
const projectRoom = `project_${projectId}`;
|
||||
io.to(projectRoom).emit('project_broadcast', {
|
||||
projectId,
|
||||
message,
|
||||
type,
|
||||
from: req.user.email || req.user.id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Broadcast sent to project ${projectId}`,
|
||||
room: projectRoom
|
||||
});
|
||||
});
|
||||
|
||||
// Send notification to specific user
|
||||
router.post('/notify/:userId', verifyToken, (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const { message, type = 'info', title } = req.body;
|
||||
const io = global.io;
|
||||
|
||||
if (!io) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'WebSocket server not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
const userRoom = `user_${userId}`;
|
||||
io.to(userRoom).emit('notification', {
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
from: req.user.email || req.user.id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Notification sent to user ${userId}`,
|
||||
room: userRoom
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -1,113 +1,531 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { createServer } = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const axios = require('axios');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
// Import middleware
|
||||
const authMiddleware = require('./middleware/authentication');
|
||||
const serviceHealthMiddleware = require('./middleware/serviceHealth');
|
||||
const requestLogger = require('./middleware/requestLogger');
|
||||
const websocketAuth = require('./middleware/webSocket');
|
||||
|
||||
// Import route handlers
|
||||
const serviceRouter = require('./routes/serviceRouter');
|
||||
const healthRouter = require('./routes/healthRouter');
|
||||
const websocketRouter = require('./routes/websocketRouter');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const PORT = process.env.PORT || 8000;
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(morgan('combined'));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
// Initialize Socket.IO with CORS
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL || "*",
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST']
|
||||
},
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000, // limit each IP to 1000 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
// Make io available globally for other modules
|
||||
global.io = io;
|
||||
|
||||
// Service targets configuration
|
||||
const serviceTargets = {
|
||||
USER_AUTH_URL: process.env.USER_AUTH_URL || 'http://localhost:8011',
|
||||
TEMPLATE_MANAGER_URL: process.env.TEMPLATE_MANAGER_URL || 'http://localhost:8009',
|
||||
REQUIREMENT_PROCESSOR_URL: process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001',
|
||||
TECH_STACK_SELECTOR_URL: process.env.TECH_STACK_SELECTOR_URL || 'http://localhost:8002',
|
||||
ARCHITECTURE_DESIGNER_URL: process.env.ARCHITECTURE_DESIGNER_URL || 'http://localhost:8003',
|
||||
CODE_GENERATOR_URL: process.env.CODE_GENERATOR_URL || 'http://localhost:8004',
|
||||
TEST_GENERATOR_URL: process.env.TEST_GENERATOR_URL || 'http://localhost:8005',
|
||||
DEPLOYMENT_MANAGER_URL: process.env.DEPLOYMENT_MANAGER_URL || 'http://localhost:8006',
|
||||
DASHBOARD_URL: process.env.DASHBOARD_URL || 'http://localhost:8008',
|
||||
SELF_IMPROVING_GENERATOR_URL: process.env.SELF_IMPROVING_GENERATOR_URL || 'http://localhost:8007',
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// MIDDLEWARE SETUP
|
||||
// ========================================
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'", "ws:", "wss:"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// CORS configuration
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || "*",
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: [
|
||||
'Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization',
|
||||
'X-Gateway-Request-ID', 'X-Gateway-Timestamp', 'X-Forwarded-By',
|
||||
'X-Forwarded-For', 'X-Forwarded-Proto', 'X-Forwarded-Host',
|
||||
'X-Session-Token', 'X-Platform', 'X-App-Version'
|
||||
],
|
||||
exposedHeaders: [
|
||||
'X-Gateway-Request-ID', 'X-Gateway-Timestamp', 'X-Forwarded-By',
|
||||
'X-Forwarded-For', 'X-Forwarded-Proto', 'X-Forwarded-Host'
|
||||
]
|
||||
}));
|
||||
|
||||
// Request parsing middleware - only for non-proxy routes
|
||||
app.use('/api/websocket', express.json({ limit: '10mb' }));
|
||||
app.use('/api/gateway', express.json({ limit: '10mb' }));
|
||||
app.use('/api/auth', express.json({ limit: '10mb' }));
|
||||
app.use('/api/templates', express.json({ limit: '10mb' }));
|
||||
app.use('/health', express.json({ limit: '10mb' }));
|
||||
|
||||
// Trust proxy for accurate IP addresses
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// Request ID middleware for tracing
|
||||
app.use((req, res, next) => {
|
||||
req.requestId = `gw-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
res.setHeader('X-Request-ID', req.requestId);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
||||
|
||||
// Custom request logger for service tracking
|
||||
app.use(requestLogger.logRequest);
|
||||
|
||||
// Rate limiting configuration
|
||||
const createServiceLimiter = (maxRequests = 1000) => rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||
max: maxRequests,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many requests, please try again later.',
|
||||
retry_after: 900
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
legacyHeaders: false
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Health check endpoint
|
||||
// Health check endpoint (before rate limiting and authentication)
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
res.json({
|
||||
success: true,
|
||||
service: 'api-gateway',
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage()
|
||||
services: {
|
||||
user_auth: process.env.USER_AUTH_URL ? 'configured' : 'not configured',
|
||||
template_manager: process.env.TEMPLATE_MANAGER_URL ? 'configured' : 'not configured',
|
||||
requirement_processor: process.env.REQUIREMENT_PROCESSOR_URL ? 'configured' : 'not configured',
|
||||
tech_stack_selector: process.env.TECH_STACK_SELECTOR_URL ? 'configured' : 'not configured',
|
||||
architecture_designer: process.env.ARCHITECTURE_DESIGNER_URL ? 'configured' : 'not configured',
|
||||
code_generator: process.env.CODE_GENERATOR_URL ? 'configured' : 'not configured',
|
||||
test_generator: process.env.TEST_GENERATOR_URL ? 'configured' : 'not configured',
|
||||
deployment_manager: process.env.DEPLOYMENT_MANAGER_URL ? 'configured' : 'not configured',
|
||||
dashboard: process.env.DASHBOARD_URL ? 'configured' : 'not configured',
|
||||
self_improving_generator: process.env.SELF_IMPROVING_GENERATOR_URL ? 'configured' : 'not configured'
|
||||
},
|
||||
websocket: 'enabled'
|
||||
});
|
||||
});
|
||||
|
||||
// Basic routes
|
||||
app.get('/api/v1/status', (req, res) => {
|
||||
res.json({
|
||||
message: 'API Gateway is running',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
services: {
|
||||
'requirement-processor': 'pending',
|
||||
'tech-stack-selector': 'pending',
|
||||
'architecture-designer': 'pending',
|
||||
'code-generator': 'pending',
|
||||
'test-generator': 'pending',
|
||||
'deployment-manager': 'pending'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
// Service health monitoring routes
|
||||
app.use('/health', healthRouter.router);
|
||||
|
||||
// WebSocket connection handling
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected:', socket.id);
|
||||
const websocketHandlers = websocketAuth(io);
|
||||
|
||||
// Auth Service - Fixed proxy with proper connection handling
|
||||
console.log('🔧 Registering /api/auth proxy route...');
|
||||
app.use('/api/auth', (req, res, next) => {
|
||||
console.log(`🔥 [AUTH PROXY] ${req.method} ${req.originalUrl} → http://localhost:8011${req.originalUrl}`);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected:', socket.id);
|
||||
// Set response timeout to prevent hanging
|
||||
res.setTimeout(15000, () => {
|
||||
console.error('❌ [AUTH PROXY] Response timeout');
|
||||
if (!res.headersSent) {
|
||||
res.status(504).json({ error: 'Gateway timeout', service: 'user-auth' });
|
||||
}
|
||||
});
|
||||
|
||||
// Send initial connection confirmation
|
||||
socket.emit('connected', {
|
||||
message: 'Connected to API Gateway',
|
||||
timestamp: new Date().toISOString()
|
||||
|
||||
const options = {
|
||||
method: req.method,
|
||||
url: `http://localhost:8011${req.originalUrl}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'API-Gateway/1.0',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
timeout: 8000,
|
||||
validateStatus: () => true,
|
||||
maxRedirects: 0
|
||||
};
|
||||
|
||||
// Always include request body for POST/PUT/PATCH requests
|
||||
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
||||
options.data = req.body || {};
|
||||
console.log(`📦 [AUTH PROXY] Request body:`, JSON.stringify(req.body));
|
||||
}
|
||||
|
||||
axios(options)
|
||||
.then(response => {
|
||||
console.log(`✅ [AUTH PROXY] Response: ${response.status} for ${req.method} ${req.originalUrl}`);
|
||||
if (!res.headersSent) {
|
||||
res.status(response.status).json(response.data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`❌ [AUTH PROXY ERROR]:`, error.message);
|
||||
if (!res.headersSent) {
|
||||
if (error.response) {
|
||||
res.status(error.response.status).json(error.response.data);
|
||||
} else {
|
||||
res.status(502).json({
|
||||
error: 'Auth service unavailable',
|
||||
message: error.code || error.message,
|
||||
service: 'user-auth'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// WebSocket API routes for managing connections
|
||||
app.use('/api/websocket', websocketRouter);
|
||||
|
||||
// Apply rate limiting to other API routes
|
||||
app.use('/api', createServiceLimiter(1000));
|
||||
|
||||
// Template Manager Service - Direct HTTP forwarding
|
||||
console.log('🔧 Registering /api/templates proxy route...');
|
||||
app.use('/api/templates',
|
||||
createServiceLimiter(200),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
(req, res, next) => {
|
||||
console.log(`🔥 [TEMPLATE PROXY] ${req.method} ${req.originalUrl} → http://localhost:8009${req.originalUrl}`);
|
||||
|
||||
// Set response timeout to prevent hanging
|
||||
res.setTimeout(15000, () => {
|
||||
console.error('❌ [TEMPLATE PROXY] Response timeout');
|
||||
if (!res.headersSent) {
|
||||
res.status(504).json({ error: 'Gateway timeout', service: 'template-manager' });
|
||||
}
|
||||
});
|
||||
|
||||
const options = {
|
||||
method: req.method,
|
||||
url: `http://localhost:8009${req.originalUrl}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'API-Gateway/1.0',
|
||||
'Connection': 'keep-alive',
|
||||
// Forward user context from auth middleware
|
||||
'X-User-ID': req.user?.id || req.user?.userId,
|
||||
'X-User-Role': req.user?.role,
|
||||
'Authorization': req.headers.authorization
|
||||
},
|
||||
timeout: 8000,
|
||||
validateStatus: () => true,
|
||||
maxRedirects: 0
|
||||
};
|
||||
|
||||
// Always include request body for POST/PUT/PATCH requests
|
||||
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
||||
options.data = req.body || {};
|
||||
console.log(`📦 [TEMPLATE PROXY] Request body:`, JSON.stringify(req.body));
|
||||
}
|
||||
|
||||
axios(options)
|
||||
.then(response => {
|
||||
console.log(`✅ [TEMPLATE PROXY] Response: ${response.status} for ${req.method} ${req.originalUrl}`);
|
||||
if (!res.headersSent) {
|
||||
res.status(response.status).json(response.data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`❌ [TEMPLATE PROXY ERROR]:`, error.message);
|
||||
if (!res.headersSent) {
|
||||
if (error.response) {
|
||||
res.status(error.response.status).json(error.response.data);
|
||||
} else {
|
||||
res.status(502).json({
|
||||
error: 'Template service unavailable',
|
||||
message: error.code || error.message,
|
||||
service: 'template-manager'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Requirement Processor Service
|
||||
app.use('/api/requirements',
|
||||
createServiceLimiter(300),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.REQUIREMENT_PROCESSOR_URL, 'requirement-processor')
|
||||
);
|
||||
|
||||
// Tech Stack Selector Service
|
||||
app.use('/api/tech-stack',
|
||||
createServiceLimiter(200),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.TECH_STACK_SELECTOR_URL, 'tech-stack-selector')
|
||||
);
|
||||
|
||||
// Architecture Designer Service
|
||||
app.use('/api/architecture',
|
||||
createServiceLimiter(150),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.ARCHITECTURE_DESIGNER_URL, 'architecture-designer')
|
||||
);
|
||||
|
||||
// Code Generator Service
|
||||
app.use('/api/codegen',
|
||||
createServiceLimiter(100),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.CODE_GENERATOR_URL, 'code-generator')
|
||||
);
|
||||
|
||||
// Test Generator Service
|
||||
app.use('/api/tests',
|
||||
createServiceLimiter(150),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.TEST_GENERATOR_URL, 'test-generator')
|
||||
);
|
||||
|
||||
// Deployment Manager Service
|
||||
app.use('/api/deploy',
|
||||
createServiceLimiter(100),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.DEPLOYMENT_MANAGER_URL, 'deployment-manager')
|
||||
);
|
||||
|
||||
// Dashboard Service
|
||||
app.use('/api/dashboard',
|
||||
createServiceLimiter(300),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.DASHBOARD_URL, 'dashboard')
|
||||
);
|
||||
|
||||
// Self-Improving Generator Service
|
||||
app.use('/api/self-improving',
|
||||
createServiceLimiter(50),
|
||||
authMiddleware.verifyToken,
|
||||
authMiddleware.forwardUserContext,
|
||||
serviceRouter.createServiceProxy(serviceTargets.SELF_IMPROVING_GENERATOR_URL, 'self-improving-generator')
|
||||
);
|
||||
|
||||
// Gateway management endpoints
|
||||
app.get('/api/gateway/info', authMiddleware.verifyToken, (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
gateway: {
|
||||
name: 'CodeNuk API Gateway',
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
uptime: process.uptime()
|
||||
},
|
||||
services: {
|
||||
total_services: Object.keys(serviceTargets).length,
|
||||
operational_services: Object.keys(serviceTargets).length,
|
||||
service_urls: serviceTargets
|
||||
},
|
||||
features: {
|
||||
websocket_enabled: true,
|
||||
authentication: true,
|
||||
rate_limiting: true,
|
||||
health_monitoring: true,
|
||||
request_logging: true,
|
||||
cors_enabled: true
|
||||
},
|
||||
websocket: {
|
||||
connected_clients: io.engine.clientsCount,
|
||||
transport_types: ['websocket', 'polling']
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err.stack);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong!',
|
||||
timestamp: new Date().toISOString()
|
||||
// Service status endpoint
|
||||
app.get('/api/gateway/services', authMiddleware.verifyToken, serviceHealthMiddleware.getServiceStatus);
|
||||
|
||||
// Root endpoint
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'CodeNuk API Gateway',
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
description: 'Central gateway for all CodeNuk microservices',
|
||||
documentation: {
|
||||
health: '/health',
|
||||
gateway_info: '/api/gateway/info',
|
||||
service_status: '/api/gateway/services'
|
||||
},
|
||||
services: {
|
||||
auth: '/api/auth',
|
||||
templates: '/api/templates',
|
||||
requirements: '/api/requirements',
|
||||
tech_stack: '/api/tech-stack',
|
||||
architecture: '/api/architecture',
|
||||
codegen: '/api/codegen',
|
||||
tests: '/api/tests',
|
||||
deploy: '/api/deploy',
|
||||
dashboard: '/api/dashboard',
|
||||
self_improving: '/api/self-improving'
|
||||
},
|
||||
websocket: {
|
||||
endpoint: '/socket.io/',
|
||||
authentication_required: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
path: req.originalUrl,
|
||||
timestamp: new Date().toISOString()
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Endpoint not found',
|
||||
available_services: {
|
||||
auth: '/api/auth',
|
||||
templates: '/api/templates',
|
||||
requirements: '/api/requirements',
|
||||
tech_stack: '/api/tech-stack',
|
||||
architecture: '/api/architecture',
|
||||
codegen: '/api/codegen',
|
||||
tests: '/api/tests',
|
||||
deploy: '/api/deploy',
|
||||
dashboard: '/api/dashboard',
|
||||
self_improving: '/api/self-improving'
|
||||
},
|
||||
documentation: '/api/gateway/info'
|
||||
});
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
app.use((error, req, res, next) => {
|
||||
console.error(`[${req.requestId}] Gateway Error:`, error);
|
||||
|
||||
// Handle proxy errors
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: 'Service temporarily unavailable',
|
||||
error: 'The requested service is currently unavailable',
|
||||
request_id: req.requestId
|
||||
});
|
||||
}
|
||||
|
||||
// Handle timeout errors
|
||||
if (error.code === 'ETIMEDOUT' || error.message.includes('timeout')) {
|
||||
return res.status(504).json({
|
||||
success: false,
|
||||
message: 'Service timeout',
|
||||
error: 'The service took too long to respond',
|
||||
request_id: req.requestId
|
||||
});
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication failed',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
|
||||
// Generic error handler
|
||||
res.status(error.status || 500).json({
|
||||
success: false,
|
||||
message: error.message || 'Internal gateway error',
|
||||
error: process.env.NODE_ENV === 'development' ? {
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
} : undefined,
|
||||
request_id: req.requestId
|
||||
});
|
||||
});
|
||||
|
||||
// Start server and initialize health monitoring
|
||||
const startServer = async () => {
|
||||
try {
|
||||
console.log('🚀 Starting CodeNuk API Gateway...');
|
||||
|
||||
// Initialize service health monitoring
|
||||
await serviceHealthMiddleware.initializeHealthMonitoring();
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`✅ API Gateway running on port ${PORT}`);
|
||||
console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`📋 Health check: http://localhost:${PORT}/health`);
|
||||
console.log(`📖 Gateway info: http://localhost:${PORT}/api/gateway/info`);
|
||||
console.log(`🔗 WebSocket enabled on: ws://localhost:${PORT}`);
|
||||
|
||||
// Log service configuration
|
||||
console.log('⚙️ Configured Services:');
|
||||
Object.entries(serviceTargets).forEach(([name, url]) => {
|
||||
console.log(` - ${name}: ${url}`);
|
||||
});
|
||||
|
||||
console.log('🔧 Features:');
|
||||
console.log(` - Rate Limiting: Enabled`);
|
||||
console.log(` - Authentication: JWT with Auth Service`);
|
||||
console.log(` - WebSocket: Real-time notifications`);
|
||||
console.log(` - Health Monitoring: All services`);
|
||||
console.log(` - Request Logging: Enabled`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start API Gateway:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
console.log('📴 SIGTERM received, shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('Process terminated');
|
||||
console.log('✅ API Gateway shut down successfully');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`🚀 API Gateway running on port ${PORT}`);
|
||||
console.log(`📊 Health check: http://localhost:${PORT}/health`);
|
||||
console.log(`📡 WebSocket server started`);
|
||||
process.on('SIGINT', () => {
|
||||
console.log('📴 SIGINT received, shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('✅ API Gateway shut down successfully');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Start the server
|
||||
startServer();
|
||||
|
||||
module.exports = app;
|
||||
@ -283,6 +283,13 @@ class CustomTemplate {
|
||||
admin_reviewed_by
|
||||
};
|
||||
|
||||
// Maintain the legacy boolean flag alongside the status for easier filtering
|
||||
if (status === 'approved') {
|
||||
updates.approved = true;
|
||||
} else if (status === 'rejected' || status === 'duplicate') {
|
||||
updates.approved = false;
|
||||
}
|
||||
|
||||
if (canonical_template_id) {
|
||||
updates.canonical_template_id = canonical_template_id;
|
||||
}
|
||||
|
||||
@ -485,13 +485,51 @@ router.post('/templates/:id/review', async (req, res) => {
|
||||
|
||||
console.log(`🔍 Admin: Reviewing template ${id} with status: ${status}`);
|
||||
|
||||
// If approving, also create a main template entry and link it, atomically
|
||||
if (status === 'approved') {
|
||||
const existingCustom = await CustomTemplate.getById(id);
|
||||
if (!existingCustom) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template not found',
|
||||
message: 'The specified template does not exist'
|
||||
});
|
||||
}
|
||||
// Create in main templates
|
||||
const Template = require('../models/template');
|
||||
const payload = {
|
||||
type: existingCustom.type,
|
||||
title: existingCustom.title,
|
||||
description: existingCustom.description,
|
||||
icon: existingCustom.icon,
|
||||
category: existingCustom.category,
|
||||
gradient: existingCustom.gradient,
|
||||
border: existingCustom.border,
|
||||
text: existingCustom.text,
|
||||
subtext: existingCustom.subtext,
|
||||
};
|
||||
const created = await Template.create(payload);
|
||||
// Update custom template flags and link canonical_template_id
|
||||
const updatedCustom = await CustomTemplate.reviewTemplate(id, {
|
||||
status: 'approved',
|
||||
admin_notes,
|
||||
canonical_template_id: created.id,
|
||||
admin_reviewed_by: req.user.username || req.user.email
|
||||
});
|
||||
return res.json({
|
||||
success: true,
|
||||
data: { custom_template: updatedCustom, template: created },
|
||||
message: `Template '${created.title}' created and custom template approved`
|
||||
});
|
||||
}
|
||||
|
||||
const template = await CustomTemplate.reviewTemplate(id, {
|
||||
status,
|
||||
admin_notes,
|
||||
canonical_template_id,
|
||||
admin_reviewed_by: req.user.username || req.user.email
|
||||
});
|
||||
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
|
||||
@ -737,7 +737,28 @@ router.post('/', async (req, res) => {
|
||||
}
|
||||
|
||||
const template = await Template.create(templateData);
|
||||
|
||||
|
||||
// Link back to custom_templates when approving from a custom
|
||||
if (templateData.approved_from_custom) {
|
||||
try {
|
||||
const customId = templateData.approved_from_custom;
|
||||
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
if (uuidV4Regex.test(customId)) {
|
||||
await CustomTemplate.update(customId, {
|
||||
approved: true,
|
||||
status: 'approved',
|
||||
canonical_template_id: template.id,
|
||||
admin_reviewed_at: new Date(),
|
||||
admin_reviewed_by: 'system_auto'
|
||||
});
|
||||
} else {
|
||||
console.warn('[POST /api/templates] approved_from_custom is not a valid UUID v4');
|
||||
}
|
||||
} catch (linkErr) {
|
||||
console.error('⚠️ Failed to set approved=true on custom_templates:', linkErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: template,
|
||||
@ -763,6 +784,59 @@ router.post('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/templates/approve-custom - Create main template and approve a custom template in one atomic flow
|
||||
router.post('/approve-custom', async (req, res) => {
|
||||
try {
|
||||
const { custom_template_id, template } = req.body || {};
|
||||
|
||||
const customId = custom_template_id || req.body?.customTemplateId || req.body?.id;
|
||||
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
if (!customId || !uuidV4Regex.test(customId)) {
|
||||
return res.status(400).json({ success: false, error: 'Invalid custom_template_id', message: 'Provide a valid UUID v4 for custom_template_id' });
|
||||
}
|
||||
|
||||
// Load custom template to mirror missing fields if needed
|
||||
const existingCustom = await CustomTemplate.getById(customId);
|
||||
if (!existingCustom) {
|
||||
return res.status(404).json({ success: false, error: 'Custom template not found', message: `No custom template with id ${customId}` });
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: template?.type || existingCustom.type,
|
||||
title: template?.title || existingCustom.title,
|
||||
description: template?.description ?? existingCustom.description,
|
||||
icon: template?.icon ?? existingCustom.icon,
|
||||
category: template?.category || existingCustom.category,
|
||||
gradient: template?.gradient ?? existingCustom.gradient,
|
||||
border: template?.border ?? existingCustom.border,
|
||||
text: template?.text ?? existingCustom.text,
|
||||
subtext: template?.subtext ?? existingCustom.subtext,
|
||||
approved_from_custom: customId
|
||||
};
|
||||
|
||||
// Create in main templates
|
||||
const created = await Template.create(payload);
|
||||
|
||||
// Mark custom template as approved and link canonical_template_id
|
||||
await CustomTemplate.update(customId, {
|
||||
approved: true,
|
||||
status: 'approved',
|
||||
canonical_template_id: created.id,
|
||||
admin_reviewed_at: new Date(),
|
||||
admin_reviewed_by: (req.user && (req.user.username || req.user.email)) || 'admin'
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: { template: created, custom_template_id: customId },
|
||||
message: `Template '${created.title}' created and custom template approved`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error approving custom template:', error.message);
|
||||
return res.status(500).json({ success: false, error: 'Failed to approve custom template', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/templates/:id - Update template or custom template based on isCustom flag
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
|
||||
@ -182,6 +182,42 @@ const TEMPLATE_DATA = {
|
||||
display_order: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'ai_chatbot_platform',
|
||||
title: 'AI Chatbot Platform',
|
||||
description: 'Deploy multi-channel AI chatbots with analytics and workflows',
|
||||
icon: '🤖',
|
||||
gradient: 'from-teal-50 to-teal-100',
|
||||
border: 'border-teal-200',
|
||||
text: 'text-teal-900',
|
||||
subtext: 'text-teal-700',
|
||||
features: [
|
||||
{
|
||||
feature_id: 'nlp_engine',
|
||||
name: 'NLP Engine',
|
||||
description: 'Natural language processing for intent and entity extraction',
|
||||
feature_type: 'essential',
|
||||
complexity: 'high',
|
||||
display_order: 1
|
||||
},
|
||||
{
|
||||
feature_id: 'channel_integrations',
|
||||
name: 'Channel Integrations',
|
||||
description: 'Integrate with web, WhatsApp, and Slack',
|
||||
feature_type: 'essential',
|
||||
complexity: 'medium',
|
||||
display_order: 2
|
||||
},
|
||||
{
|
||||
feature_id: 'conversation_analytics',
|
||||
name: 'Conversation Analytics',
|
||||
description: 'Track KPIs: containment rate, CSAT, and response time',
|
||||
feature_type: 'suggested',
|
||||
complexity: 'medium',
|
||||
display_order: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'Custom & Others': [
|
||||
@ -195,6 +231,42 @@ const TEMPLATE_DATA = {
|
||||
text: 'text-gray-900',
|
||||
subtext: 'text-gray-700',
|
||||
features: []
|
||||
},
|
||||
{
|
||||
type: 'project_management_tool',
|
||||
title: 'Project Management Tool',
|
||||
description: 'Tasks, Kanban boards, sprints, and team collaboration',
|
||||
icon: '🗂️',
|
||||
gradient: 'from-amber-50 to-amber-100',
|
||||
border: 'border-amber-200',
|
||||
text: 'text-amber-900',
|
||||
subtext: 'text-amber-700',
|
||||
features: [
|
||||
{
|
||||
feature_id: 'kanban_boards',
|
||||
name: 'Kanban Boards',
|
||||
description: 'Organize tasks into customizable columns',
|
||||
feature_type: 'essential',
|
||||
complexity: 'medium',
|
||||
display_order: 1
|
||||
},
|
||||
{
|
||||
feature_id: 'sprint_planning',
|
||||
name: 'Sprint Planning',
|
||||
description: 'Plan sprints, track velocity, and manage backlogs',
|
||||
feature_type: 'essential',
|
||||
complexity: 'high',
|
||||
display_order: 2
|
||||
},
|
||||
{
|
||||
feature_id: 'collaboration_tools',
|
||||
name: 'Collaboration Tools',
|
||||
description: 'Comments, mentions, and notifications',
|
||||
feature_type: 'suggested',
|
||||
complexity: 'medium',
|
||||
display_order: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user