backend changes in business rules
This commit is contained in:
parent
c88b3989fc
commit
909d446d77
@ -266,6 +266,7 @@ services:
|
|||||||
- DEPLOYMENT_MANAGER_URL=http://deployment-manager:8006
|
- DEPLOYMENT_MANAGER_URL=http://deployment-manager:8006
|
||||||
- DASHBOARD_URL=http://dashboard:8008
|
- DASHBOARD_URL=http://dashboard:8008
|
||||||
- SELF_IMPROVING_GENERATOR_URL=http://self-improving-generator:8007
|
- SELF_IMPROVING_GENERATOR_URL=http://self-improving-generator:8007
|
||||||
|
- AI_MOCKUP_URL=http://ai-mockup-service:8021
|
||||||
volumes:
|
volumes:
|
||||||
- api_gateway_logs:/app/logs # Add persistent volume for logs
|
- api_gateway_logs:/app/logs # Add persistent volume for logs
|
||||||
user: "node" # Run as node user instead of root
|
user: "node" # Run as node user instead of root
|
||||||
|
|||||||
@ -61,6 +61,7 @@ const serviceTargets = {
|
|||||||
DEPLOYMENT_MANAGER_URL: process.env.DEPLOYMENT_MANAGER_URL || 'http://localhost:8006',
|
DEPLOYMENT_MANAGER_URL: process.env.DEPLOYMENT_MANAGER_URL || 'http://localhost:8006',
|
||||||
DASHBOARD_URL: process.env.DASHBOARD_URL || 'http://localhost:8008',
|
DASHBOARD_URL: process.env.DASHBOARD_URL || 'http://localhost:8008',
|
||||||
SELF_IMPROVING_GENERATOR_URL: process.env.SELF_IMPROVING_GENERATOR_URL || 'http://localhost:8007',
|
SELF_IMPROVING_GENERATOR_URL: process.env.SELF_IMPROVING_GENERATOR_URL || 'http://localhost:8007',
|
||||||
|
AI_MOCKUP_URL: process.env.AI_MOCKUP_URL || 'http://localhost:8021',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@ -89,6 +90,7 @@ app.use('/api/auth', express.json({ limit: '10mb' }));
|
|||||||
app.use('/api/templates', express.json({ limit: '10mb' }));
|
app.use('/api/templates', express.json({ limit: '10mb' }));
|
||||||
app.use('/api/features', express.json({ limit: '10mb' }));
|
app.use('/api/features', express.json({ limit: '10mb' }));
|
||||||
app.use('/api/github', express.json({ limit: '10mb' }));
|
app.use('/api/github', express.json({ limit: '10mb' }));
|
||||||
|
app.use('/api/mockup', express.json({ limit: '10mb' }));
|
||||||
app.use('/health', express.json({ limit: '10mb' }));
|
app.use('/health', express.json({ limit: '10mb' }));
|
||||||
|
|
||||||
// Trust proxy for accurate IP addresses
|
// Trust proxy for accurate IP addresses
|
||||||
@ -106,8 +108,13 @@ app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
|||||||
// Custom request logger for service tracking
|
// Custom request logger for service tracking
|
||||||
app.use(requestLogger.logRequest);
|
app.use(requestLogger.logRequest);
|
||||||
|
|
||||||
// Rate limiting configuration
|
// Rate limiting configuration (disabled by default via env)
|
||||||
const createServiceLimiter = (maxRequests = 1000) => rateLimit({
|
const isRateLimitDisabled = (process.env.GATEWAY_DISABLE_RATE_LIMIT || process.env.DISABLE_RATE_LIMIT || 'true').toLowerCase() === 'true';
|
||||||
|
const createServiceLimiter = (maxRequests = 1000) => {
|
||||||
|
if (isRateLimitDisabled) {
|
||||||
|
return (req, res, next) => next();
|
||||||
|
}
|
||||||
|
return rateLimit({
|
||||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||||
max: maxRequests,
|
max: maxRequests,
|
||||||
message: {
|
message: {
|
||||||
@ -118,6 +125,7 @@ const createServiceLimiter = (maxRequests = 1000) => rateLimit({
|
|||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Health check endpoint (before rate limiting and authentication)
|
// Health check endpoint (before rate limiting and authentication)
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
@ -140,7 +148,8 @@ app.get('/health', (req, res) => {
|
|||||||
test_generator: process.env.TEST_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',
|
deployment_manager: process.env.DEPLOYMENT_MANAGER_URL ? 'configured' : 'not configured',
|
||||||
dashboard: process.env.DASHBOARD_URL ? 'configured' : 'not configured',
|
dashboard: process.env.DASHBOARD_URL ? 'configured' : 'not configured',
|
||||||
self_improving_generator: process.env.SELF_IMPROVING_GENERATOR_URL ? 'configured' : 'not configured'
|
self_improving_generator: process.env.SELF_IMPROVING_GENERATOR_URL ? 'configured' : 'not configured',
|
||||||
|
ai_mockup: process.env.AI_MOCKUP_URL ? 'configured' : 'not configured'
|
||||||
},
|
},
|
||||||
websocket: 'enabled'
|
websocket: 'enabled'
|
||||||
});
|
});
|
||||||
@ -569,6 +578,91 @@ app.use('/api/github',
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// AI Mockup Service - Direct HTTP forwarding
|
||||||
|
console.log('🔧 Registering /api/mockup proxy route...');
|
||||||
|
app.use('/api/mockup',
|
||||||
|
createServiceLimiter(200),
|
||||||
|
// Public proxy: AI mockup endpoints do not require auth for basic generation
|
||||||
|
(req, res, next) => {
|
||||||
|
console.log(`🎨 [AI MOCKUP PROXY] ${req.method} ${req.originalUrl}`);
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
(req, res, next) => {
|
||||||
|
const aiMockupServiceUrl = serviceTargets.AI_MOCKUP_URL;
|
||||||
|
// Strip the /api/mockup prefix so /api/mockup/health -> /health at target
|
||||||
|
const rewrittenPath = (req.originalUrl || '').replace(/^\/api\/mockup/, '');
|
||||||
|
const targetUrl = `${aiMockupServiceUrl}${rewrittenPath}`;
|
||||||
|
console.log(`🔥 [AI MOCKUP PROXY] ${req.method} ${req.originalUrl} → ${targetUrl}`);
|
||||||
|
|
||||||
|
res.setTimeout(30000, () => {
|
||||||
|
console.error('❌ [AI MOCKUP PROXY] Response timeout');
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(504).json({ error: 'Gateway timeout', service: 'ai-mockup' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: req.method,
|
||||||
|
url: targetUrl,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'API-Gateway/1.0',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Authorization': req.headers.authorization,
|
||||||
|
'X-User-ID': req.user?.id || req.user?.userId,
|
||||||
|
'X-User-Role': req.user?.role,
|
||||||
|
},
|
||||||
|
timeout: 25000,
|
||||||
|
validateStatus: () => true,
|
||||||
|
maxRedirects: 0,
|
||||||
|
responseType: 'text'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
||||||
|
options.data = req.body || {};
|
||||||
|
console.log(`📦 [AI MOCKUP PROXY] Request body:`, JSON.stringify(req.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
axios(options)
|
||||||
|
.then(response => {
|
||||||
|
console.log(`✅ [AI MOCKUP PROXY] Response: ${response.status} for ${req.method} ${req.originalUrl}`);
|
||||||
|
if (res.headersSent) return;
|
||||||
|
const contentType = response.headers['content-type'] || '';
|
||||||
|
// Forward key headers
|
||||||
|
if (contentType) res.setHeader('Content-Type', contentType);
|
||||||
|
res.setHeader('X-Gateway-Request-ID', req.requestId);
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
|
|
||||||
|
// If response is SVG or XML or plain text, send as-is; else JSON
|
||||||
|
if (contentType.includes('image/svg') || contentType.includes('xml') || contentType.includes('text/plain') || typeof response.data === 'string') {
|
||||||
|
res.status(response.status).send(response.data);
|
||||||
|
} else {
|
||||||
|
res.status(response.status).json(response.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`❌ [AI MOCKUP PROXY ERROR]:`, error.message);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
if (error.response) {
|
||||||
|
const ct = error.response.headers?.['content-type'] || '';
|
||||||
|
if (ct.includes('image/svg') || ct.includes('xml') || typeof error.response.data === 'string') {
|
||||||
|
res.status(error.response.status).send(error.response.data);
|
||||||
|
} else {
|
||||||
|
res.status(error.response.status).json(error.response.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(502).json({
|
||||||
|
error: 'AI Mockup service unavailable',
|
||||||
|
message: error.code || error.message,
|
||||||
|
service: 'ai-mockup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Gateway management endpoints
|
// Gateway management endpoints
|
||||||
app.get('/api/gateway/info', authMiddleware.verifyToken, (req, res) => {
|
app.get('/api/gateway/info', authMiddleware.verifyToken, (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
@ -625,7 +719,8 @@ app.get('/', (req, res) => {
|
|||||||
tests: '/api/tests',
|
tests: '/api/tests',
|
||||||
deploy: '/api/deploy',
|
deploy: '/api/deploy',
|
||||||
dashboard: '/api/dashboard',
|
dashboard: '/api/dashboard',
|
||||||
self_improving: '/api/self-improving'
|
self_improving: '/api/self-improving',
|
||||||
|
mockup: '/api/mockup'
|
||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
endpoint: '/socket.io/',
|
endpoint: '/socket.io/',
|
||||||
@ -650,7 +745,8 @@ app.use('*', (req, res) => {
|
|||||||
tests: '/api/tests',
|
tests: '/api/tests',
|
||||||
deploy: '/api/deploy',
|
deploy: '/api/deploy',
|
||||||
dashboard: '/api/dashboard',
|
dashboard: '/api/dashboard',
|
||||||
self_improving: '/api/self-improving'
|
self_improving: '/api/self-improving',
|
||||||
|
mockup: '/api/mockup'
|
||||||
},
|
},
|
||||||
documentation: '/api/gateway/info'
|
documentation: '/api/gateway/info'
|
||||||
});
|
});
|
||||||
|
|||||||
@ -229,6 +229,21 @@ router.post('/', async (req, res) => {
|
|||||||
business_rules: featureData.business_rules,
|
business_rules: featureData.business_rules,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also persist into feature_business_rules for defaults/suggested features
|
||||||
|
try {
|
||||||
|
// Prefer structured business_rules when provided; fallback to flat logic_rules
|
||||||
|
const rules = (featureData.business_rules ?? featureData.logic_rules ?? []);
|
||||||
|
if (featureData.template_id && (featureData.id || feature?.feature_id)) {
|
||||||
|
await FeatureBusinessRules.upsert(
|
||||||
|
featureData.template_id,
|
||||||
|
featureData.id || feature.feature_id,
|
||||||
|
rules
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (ruleErr) {
|
||||||
|
console.error('⚠️ Failed to persist feature business rules (default/suggested):', ruleErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, data: feature, message: `Feature '${feature.name}' created successfully in template_features table` });
|
res.status(201).json({ success: true, data: feature, message: `Feature '${feature.name}' created successfully in template_features table` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error creating feature:', error.message);
|
console.error('❌ Error creating feature:', error.message);
|
||||||
@ -477,7 +492,8 @@ router.post('/custom', async (req, res) => {
|
|||||||
|
|
||||||
// Persist aggregated rules
|
// Persist aggregated rules
|
||||||
try {
|
try {
|
||||||
const rules = Array.isArray(data.logic_rules) ? data.logic_rules : data.business_rules ?? [];
|
// Prefer structured business_rules; fallback to flat logic_rules
|
||||||
|
const rules = (data.business_rules ?? data.logic_rules ?? []);
|
||||||
await FeatureBusinessRules.upsert(data.template_id, `custom_${created.id}`, rules);
|
await FeatureBusinessRules.upsert(data.template_id, `custom_${created.id}`, rules);
|
||||||
} catch (ruleErr) {
|
} catch (ruleErr) {
|
||||||
console.error('⚠️ Failed to persist custom feature business rules:', ruleErr.message);
|
console.error('⚠️ Failed to persist custom feature business rules:', ruleErr.message);
|
||||||
@ -553,20 +569,46 @@ router.put('/custom/:id', async (req, res) => {
|
|||||||
router.delete('/custom/:id', async (req, res) => {
|
router.delete('/custom/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const existing = await CustomFeature.getById(id);
|
// const rawId = String(id).replace(/^custom_/, '')
|
||||||
if (!existing) return res.status(404).json({ success: false, error: 'Not found' });
|
|
||||||
|
// Try deleting from custom_features first
|
||||||
|
let existing = await CustomFeature.getById(id);
|
||||||
|
if (existing) {
|
||||||
await CustomFeature.delete(id);
|
await CustomFeature.delete(id);
|
||||||
// Remove mirrored template_features with feature_id = `custom_<id>`
|
// Remove mirrored template_features with feature_id = `custom_<id>`
|
||||||
try {
|
try {
|
||||||
const featureId = `custom_${id}`
|
const featureId = id
|
||||||
const mirroredExisting = await Feature.getByFeatureId(existing.template_id, featureId)
|
const mirroredExisting = await Feature.getByFeatureId(existing.template_id, featureId)
|
||||||
if (mirroredExisting) {
|
if (mirroredExisting) {
|
||||||
await Feature.delete(mirroredExisting.id)
|
await Feature.delete(mirroredExisting.id)
|
||||||
}
|
}
|
||||||
|
// Cleanup business rules if present
|
||||||
|
try {
|
||||||
|
await database.query('DELETE FROM feature_business_rules WHERE template_id = $1 AND feature_id = $2', [existing.template_id, featureId])
|
||||||
|
} catch (cleanupErr) { console.error('Failed to cleanup business rules:', cleanupErr.message) }
|
||||||
} catch (mirrorErr) {
|
} catch (mirrorErr) {
|
||||||
console.error('Failed to mirror custom feature delete:', mirrorErr.message)
|
console.error('Failed to mirror custom feature delete:', mirrorErr.message)
|
||||||
}
|
}
|
||||||
res.json({ success: true, message: `Custom feature '${existing.name}' deleted successfully` });
|
return res.json({ success: true, message: `Custom feature '${existing.name}' deleted successfully` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: handle case where only mirrored template_features exists or client sent prefixed id
|
||||||
|
try {
|
||||||
|
const prefixed = id.startsWith('custom_') ? id : `custom_${rawId}`
|
||||||
|
const tf = await database.query('SELECT id, template_id, name FROM template_features WHERE feature_id = $1', [prefixed])
|
||||||
|
if (tf.rows.length > 0) {
|
||||||
|
const row = tf.rows[0]
|
||||||
|
await Feature.delete(row.id)
|
||||||
|
try {
|
||||||
|
await database.query('DELETE FROM feature_business_rules WHERE template_id = $1 AND feature_id = $2', [row.template_id, prefixed])
|
||||||
|
} catch (cleanupErr) { console.error('Failed to cleanup business rules:', cleanupErr.message) }
|
||||||
|
return res.json({ success: true, message: `Mirrored feature '${row.name}' deleted successfully` })
|
||||||
|
}
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
console.error('Fallback delete check failed:', fallbackErr.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).json({ success: false, error: 'Not found', message: 'Custom feature not found' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ success: false, error: 'Failed to delete custom feature', message: e.message });
|
res.status(500).json({ success: false, error: 'Failed to delete custom feature', message: e.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ const authService = require('./services/authService');
|
|||||||
// Import routes and middleware
|
// Import routes and middleware
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
const {
|
const {
|
||||||
apiRateLimit,
|
|
||||||
securityHeaders,
|
securityHeaders,
|
||||||
authErrorHandler
|
authErrorHandler
|
||||||
} = require('./middleware/auth');
|
} = require('./middleware/auth');
|
||||||
@ -76,8 +75,7 @@ app.use(morgan('combined', {
|
|||||||
format: ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'
|
format: ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting disabled by request
|
||||||
app.use('/api', apiRateLimit);
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// ROUTES
|
// ROUTES
|
||||||
|
|||||||
@ -86,8 +86,12 @@ const requireAdmin = requireRole(['admin']);
|
|||||||
// User or Admin middleware
|
// User or Admin middleware
|
||||||
const requireUserOrAdmin = requireRole(['user', 'admin']);
|
const requireUserOrAdmin = requireRole(['user', 'admin']);
|
||||||
|
|
||||||
// Rate Limiting Middleware
|
// Rate Limiting Middleware (disabled by default via env)
|
||||||
|
const isAuthRateLimitDisabled = (process.env.AUTH_DISABLE_RATE_LIMIT || process.env.DISABLE_RATE_LIMIT || 'true').toLowerCase() === 'true';
|
||||||
const createRateLimit = (windowMs, max, message) => {
|
const createRateLimit = (windowMs, max, message) => {
|
||||||
|
if (isAuthRateLimitDisabled) {
|
||||||
|
return (req, res, next) => next();
|
||||||
|
}
|
||||||
return rateLimit({
|
return rateLimit({
|
||||||
windowMs,
|
windowMs,
|
||||||
max,
|
max,
|
||||||
@ -125,11 +129,11 @@ const passwordChangeRateLimit = createRateLimit(
|
|||||||
'Too many password change attempts. Please try again in 1 hour.'
|
'Too many password change attempts. Please try again in 1 hour.'
|
||||||
);
|
);
|
||||||
|
|
||||||
const apiRateLimit = createRateLimit(
|
// const apiRateLimit = createRateLimit(
|
||||||
15 * 60 * 1000, // 15 minutes
|
// 15 * 60 * 1000, // 15 minutes
|
||||||
10000, // 100 requests
|
// 1000000, // 100 requests
|
||||||
'Too many API requests. Please slow down.'
|
// 'Too many API requests. Please slow down.'
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Session Validation Middleware
|
// Session Validation Middleware
|
||||||
const validateSession = async (req, res, next) => {
|
const validateSession = async (req, res, next) => {
|
||||||
@ -303,7 +307,7 @@ module.exports = {
|
|||||||
loginRateLimit,
|
loginRateLimit,
|
||||||
registerRateLimit,
|
registerRateLimit,
|
||||||
passwordChangeRateLimit,
|
passwordChangeRateLimit,
|
||||||
apiRateLimit,
|
// apiRateLimit,
|
||||||
validateSession,
|
validateSession,
|
||||||
logAuthRequests,
|
logAuthRequests,
|
||||||
validateOwnership,
|
validateOwnership,
|
||||||
|
|||||||
@ -751,4 +751,20 @@ router.put('/admin/custom-templates/:id/review', authenticateToken, requireAdmin
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// TOKEN VERIFICATION (For internal service checks)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// GET /api/auth/verify - Verify access token and return user info
|
||||||
|
router.get('/verify', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = req.user;
|
||||||
|
const payload = user && user.toJSON ? user.toJSON() : user;
|
||||||
|
return res.json({ success: true, data: { user: payload }, message: 'Token verified' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Token verification error:', error.message);
|
||||||
|
return res.status(401).json({ success: false, error: 'Token verification failed', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
Loading…
Reference in New Issue
Block a user