From e2090d3c700903341d9434beacc54a6e66960689 Mon Sep 17 00:00:00 2001 From: ChaohuiLi0321 Date: Mon, 8 Sep 2025 00:34:53 +1000 Subject: [PATCH 01/10] created service/errorLogService.js and middleware/errorLogger.js --- middleware/errorLogger.js | 102 ++++++++++++++++ services/errorLogService.js | 237 ++++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 middleware/errorLogger.js create mode 100644 services/errorLogService.js diff --git a/middleware/errorLogger.js b/middleware/errorLogger.js new file mode 100644 index 0000000..f3a30e4 --- /dev/null +++ b/middleware/errorLogger.js @@ -0,0 +1,102 @@ +// middleware/errorLogger.js +const errorLogService = require('../services/errorLogService'); + +/** + * Enhanced error logging middleware + */ +const errorLogger = (err, req, res, next) => { + // Automatically categorize errors + const classification = errorLogService.categorizeError(err, { req, res }); + + // Log the error + errorLogService.logError({ + error: err, + req, + res, + category: classification.category, + type: classification.type, + additionalContext: { + route: req.route?.path, + middleware_stack: req.route?.stack?.map(s => s.handle.name), + query_params: req.query, + path_params: req.params + } + }).catch(loggingError => { + console.error('Error in error logging middleware:', loggingError); + }); + + next(err); +}; + +/** + * Request response time tracking middleware + */ +const responseTimeLogger = (req, res, next) => { + const startTime = Date.now(); + + // Capture response end event + res.on('finish', () => { + const responseTime = Date.now() - startTime; + res.responseTime = responseTime; + + // Log slow requests + if (responseTime > 5000) { + errorLogService.logError({ + error: new Error(`Slow request detected: ${responseTime}ms`), + req, + res, + category: 'warning', + type: 'performance', + additionalContext: { + response_time_ms: responseTime, + slow_request: true + } + }); + } + }); + + next(); +}; + +/** + * Uncaught exception handler + */ +const uncaughtExceptionHandler = (error) => { + errorLogService.logError({ + error, + category: 'critical', + type: 'system', + additionalContext: { + uncaught_exception: true, + process_uptime: process.uptime() + } + }); + + console.error('Uncaught Exception:', error); + // Graceful shutdown + process.exit(1); +}; + +/** + * Unhandled Promise Rejection handler + */ +const unhandledRejectionHandler = (reason, promise) => { + errorLogService.logError({ + error: new Error(`Unhandled Promise Rejection: ${reason}`), + category: 'critical', + type: 'system', + additionalContext: { + unhandled_rejection: true, + promise_state: promise + } + }); + + console.error('Unhandled Rejection:', reason); +}; + +module.exports = { + errorLogger, + responseTimeLogger, + uncaughtExceptionHandler, + unhandledRejectionHandler +}; \ No newline at end of file diff --git a/services/errorLogService.js b/services/errorLogService.js new file mode 100644 index 0000000..04dcb3d --- /dev/null +++ b/services/errorLogService.js @@ -0,0 +1,237 @@ +const { createClient } = require('@supabase/supabase-js'); +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); + +class ErrorLogService { + constructor() { + this.severityLevels = { + critical: 4, + warning: 3, + info: 2, + minor: 1 + }; + } + + /** Record error logs + */ + async logError({ + error, + req = null, + res = null, + category = 'warning', + type = 'system', + additionalContext = {} + }) { + try { + const logEntry = { + // Error information + error_category: category, + error_type: type, + error_code: error.code || 'UNKNOWN_ERROR', + error_message: error.message || error.toString(), + error_stack: error.stack, + + // Request context + ...(req && this.extractRequestContext(req)), + + // User session information + ...(req && this.extractUserContext(req)), + + // System context + ...this.getSystemContext(), + + // Response context + ...(res && this.extractResponseContext(res)), + + // Additional context + ...additionalContext + }; + + const { data, error: insertError } = await supabase + .from('error_logs') + .insert([logEntry]) + .select() + .single(); + + if (insertError) { + console.error('Failed to log error:', insertError); + // Fallback logging to file or console + this.fallbackLogging(logEntry); + } + + // Real-time alerting for critical errors + if (category === 'critical') { + await this.triggerCriticalAlert(logEntry); + } + + return data; + } catch (loggingError) { + console.error('Error logging service failed:', loggingError); + this.fallbackLogging({ error, req, res, category, type }); + } + } + + /** + * Extract request context + */ + extractRequestContext(req) { + return { + request_id: req.id || req.headers['x-request-id'], + request_method: req.method, + request_url: req.originalUrl || req.url, + request_origin: req.headers.origin || req.headers.referer, + request_user_agent: req.headers['user-agent'], + request_ip_address: this.getClientIP(req), + request_headers: this.sanitizeHeaders(req.headers), + request_body: this.sanitizeRequestBody(req.body) + }; + } + + /** + * Extract user context + */ + extractUserContext(req) { + const user = req.user || {}; + return { + user_id: user.userId || user.id, + session_id: req.sessionID || req.headers['x-session-id'], + user_role: user.role + }; + } + + /** + * Get system context + */ + getSystemContext() { + const memUsage = process.memoryUsage(); + return { + server_instance: process.env.SERVER_INSTANCE || 'unknown', + node_env: process.env.NODE_ENV, + memory_usage: { + rss: memUsage.rss, + heapTotal: memUsage.heapTotal, + heapUsed: memUsage.heapUsed, + external: memUsage.external + }, + cpu_usage: process.cpuUsage ? this.getCPUUsage() : null + }; + } + + /** + * Extract response context + */ + extractResponseContext(res) { + return { + response_status: res.statusCode, + response_time_ms: res.responseTime || null + }; + } + + /** + * Get client IP + */ + getClientIP(req) { + return req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null); + } + + /** + * Sanitize sensitive request headers + */ + sanitizeHeaders(headers) { + const sanitized = { ...headers }; + const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key']; + + sensitiveHeaders.forEach(header => { + if (sanitized[header]) { + sanitized[header] = '[REDACTED]'; + } + }); + + return sanitized; + } + + /** + * Sanitize sensitive request body + */ + sanitizeRequestBody(body) { + if (!body || typeof body !== 'object') return body; + + const sanitized = { ...body }; + const sensitiveFields = ['password', 'token', 'secret', 'key']; + + sensitiveFields.forEach(field => { + if (sanitized[field]) { + sanitized[field] = '[REDACTED]'; + } + }); + + return sanitized; + } + + /** + * Get CPU usage + */ + getCPUUsage() { + const startUsage = process.cpuUsage(); + setTimeout(() => { + const usage = process.cpuUsage(startUsage); + return (usage.user + usage.system) / 1000000; // Convert to seconds + }, 100); + } + + /** + * Trigger critical error alert + */ + async triggerCriticalAlert(logEntry) { + // Here you can integrate email, Slack, SMS and other alert mechanisms + console.error('๐Ÿšจ CRITICAL ERROR ALERT:', { + message: logEntry.error_message, + type: logEntry.error_type, + timestamp: new Date().toISOString(), + user_id: logEntry.user_id, + url: logEntry.request_url + }); + + // You can add more alerting logic here + // await this.sendSlackAlert(logEntry); + // await this.sendEmailAlert(logEntry); + } + + /** + * Fallback logging + */ + fallbackLogging(logData) { + const timestamp = new Date().toISOString(); + console.error(`[${timestamp}] FALLBACK ERROR LOG:`, JSON.stringify(logData, null, 2)); + } + + /** + * Error classification + */ + categorizeError(error, context = {}) { + // Automatically categorize based on error type and context + if (error.message.includes('ECONNREFUSED') || + error.message.includes('database') || + error.code === 'ENOTFOUND') { + return { category: 'critical', type: 'database' }; + } + + if (error.status === 401 || error.status === 403) { + return { category: 'warning', type: 'authentication' }; + } + + if (error.status >= 400 && error.status < 500) { + return { category: 'info', type: 'validation' }; + } + + if (error.status >= 500) { + return { category: 'critical', type: 'system' }; + } + + return { category: 'warning', type: 'system' }; + } +} + +module.exports = new ErrorLogService(); From 026c5ec0e34177a68a6240f78800f6ec0f231e28 Mon Sep 17 00:00:00 2001 From: ChaohuiLi0321 Date: Sun, 14 Sep 2025 01:24:40 +1000 Subject: [PATCH 02/10] feat: add file upload endpoint and error simulation for testing logging --- index.yaml | 61 +++++++++++++++++++++++++++++++- routes/systemRoutes.js | 4 +++ routes/testError.js | 24 +++++++++++++ server.js | 39 ++++++++++++--------- services/errorLogService.js | 40 +++++++++------------ testErrorLogging.js | 70 +++++++++++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 41 deletions(-) create mode 100644 routes/testError.js create mode 100644 testErrorLogging.js diff --git a/index.yaml b/index.yaml index 1444261..0aefb00 100644 --- a/index.yaml +++ b/index.yaml @@ -200,7 +200,29 @@ paths: /appointments: post: summary: Save appointment data - description: Receives a user ID, date, time, and description, and saves the appointment data + /upload: + post: + summary: Upload a file + description: Upload JPG, PNG, or PDF (max 5MB, limited to 5 uploads per 10 minutes) + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: File uploaded successfully + '400': + description: Upload failed due to size/type restriction + '429': + description: Too many uploads from this IP (rate limit exceeded) requestBody: required: true content: @@ -358,6 +380,43 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + + /api/system/test-error/trigger: + post: + tags: + - System + summary: Trigger a simulated error for testing error logging + description: |- + This endpoint intentionally triggers an error so you can test the error logging middleware and verify entries are written to the Supabase `error_logs` table. + Use the `simulate` field in the request body to choose the behavior: `throw` (synchronous throw), `next` (pass to next), or omit for a delayed async error. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + simulate: + type: string + example: throw + description: 'Options: "throw", "next" or omitted (delayed error)' + responses: + '200': + description: If the request unexpectedly succeeds + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Triggered error (this should not be returned) + '500': + description: Error triggered and handled by error logging middleware + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /fooddata/spicelevels: get: summary: Get spice levels diff --git a/routes/systemRoutes.js b/routes/systemRoutes.js index 9a528e2..2bca4ba 100644 --- a/routes/systemRoutes.js +++ b/routes/systemRoutes.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const { checkFileIntegrity, generateBaseline } = require('../tools/integrity/integrityService'); +const testErrorRouter = require('./testError'); /** * @swagger @@ -65,5 +66,8 @@ router.get('/integrity-check', (req, res) => { } }); +// Mount test error router for triggering errors (used for demo/testing) +router.use('/test-error', testErrorRouter); + module.exports = router; \ No newline at end of file diff --git a/routes/testError.js b/routes/testError.js new file mode 100644 index 0000000..4010cb6 --- /dev/null +++ b/routes/testError.js @@ -0,0 +1,24 @@ +const express = require('express'); +const router = express.Router(); + +// Intentionally trigger an error to test error logging +router.post('/trigger', (req, res, next) => { + const simulate = req.body && req.body.simulate ? req.body.simulate : 'basic'; + + if (simulate === 'throw') { + // throw synchronously + throw new Error('Simulated synchronous error from /api/system/test-error/trigger'); + } + + if (simulate === 'next') { + // pass to next error handler + return next(new Error('Simulated async error via next() from /api/system/test-error/trigger')); + } + + // default: create an error after a tick (simulate async failure) + setTimeout(() => { + next(new Error('Simulated delayed error from /api/system/test-error/trigger')); + }, 10); +}); + +module.exports = router; diff --git a/server.js b/server.js index 86399c0..521f35e 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ require("dotenv").config(); const express = require("express"); - -const FRONTEND_ORIGIN = "http://localhost:3000"; +const { errorLogger, responseTimeLogger } = require('./middleware/errorLogger'); +const FRONTEND_ORIGIN = "http://localhost:3000"; const helmet = require('helmet'); const cors = require("cors"); @@ -94,8 +94,9 @@ app.use(limiter); // Swagger const swaggerDocument = yaml.load("./index.yaml"); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - -// Parsers +// Response time monitoring +app.use(responseTimeLogger); +// JSON & URL parser app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); @@ -110,22 +111,28 @@ app.use("/uploads", express.static("uploads")); // Signup app.use("/api/signup", require("./routes/signup")); -// Login dashboard -app.use('/api/login-dashboard', loginDashboard); - -// โœ… Mount allergy routes HERE (after app exists) -app.use('/api/allergy', require('./routes/allergyRoutes')); +// Error handler +app.use(errorLogger); -// Error handlers +// Final error handler app.use((err, req, res, next) => { - if (err) return res.status(400).json({ error: err.message }); - next(); -}); -app.use((err, req, res, next) => { - console.error("Unhandled error:", err); - res.status(500).json({ error: "Internal server error" }); + const status = err.status || 500; + const message = process.env.NODE_ENV === 'production' + ? 'Internal Server Error' + : err.message; + + res.status(status).json({ + success: false, + error: message, + timestamp: new Date().toISOString() + }); }); +// Global error handler +const { uncaughtExceptionHandler, unhandledRejectionHandler } = require('./middleware/errorLogger'); +process.on('uncaughtException', uncaughtExceptionHandler); +process.on('unhandledRejection', unhandledRejectionHandler); + // Start app.listen(port, async () => { console.log('\n๐ŸŽ‰ NutriHelp API launched successfully!'); diff --git a/services/errorLogService.js b/services/errorLogService.js index 04dcb3d..f1ead2e 100644 --- a/services/errorLogService.js +++ b/services/errorLogService.js @@ -23,27 +23,15 @@ class ErrorLogService { }) { try { const logEntry = { - // Error information - error_category: category, error_type: type, - error_code: error.code || 'UNKNOWN_ERROR', error_message: error.message || error.toString(), - error_stack: error.stack, - - // Request context - ...(req && this.extractRequestContext(req)), - - // User session information - ...(req && this.extractUserContext(req)), - - // System context - ...this.getSystemContext(), - - // Response context - ...(res && this.extractResponseContext(res)), - - // Additional context - ...additionalContext + stack_trace: error.stack, + endpoint: req?.originalUrl || req?.url, + method: req?.method, + request_body: req?.body ? JSON.stringify(this.sanitizeRequestBody(req.body)) : null, + user_id: req?.user?.userId || null, + ip_address: this.getClientIP(req), + created_at: new Date().toISOString() }; const { data, error: insertError } = await supabase @@ -130,22 +118,26 @@ class ErrorLogService { * Get client IP */ getClientIP(req) { + if (!req) return null; return req.ip || - req.connection.remoteAddress || - req.socket.remoteAddress || - (req.connection.socket ? req.connection.socket.remoteAddress : null); + (req.connection && req.connection.remoteAddress) || + (req.socket && req.socket.remoteAddress) || + (req.connection && req.connection.socket ? req.connection.socket.remoteAddress : null) || null; } /** * Sanitize sensitive request headers */ sanitizeHeaders(headers) { + if (!headers || typeof headers !== 'object') return headers; const sanitized = { ...headers }; const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key']; sensitiveHeaders.forEach(header => { - if (sanitized[header]) { - sanitized[header] = '[REDACTED]'; + // header keys may be in different cases + const key = Object.keys(sanitized).find(k => k.toLowerCase() === header); + if (key && sanitized[key]) { + sanitized[key] = '[REDACTED]'; } }); diff --git a/testErrorLogging.js b/testErrorLogging.js new file mode 100644 index 0000000..4af91d9 --- /dev/null +++ b/testErrorLogging.js @@ -0,0 +1,70 @@ +// testErrorLogging.js +// Load .env: try multiple likely locations (script dir, project root, process.cwd()) +const path = require('path'); +const dotenv = require('dotenv'); + +const tryPaths = [ + path.resolve(__dirname, '.env'), + path.resolve(__dirname, '..', '.env'), + path.resolve(process.cwd(), '.env') +]; + +let loaded = false; +for (const p of tryPaths) { + try { + const result = dotenv.config({ path: p }); + if (result.parsed) { + console.log(`Loaded .env from ${p}`); + loaded = true; + break; + } + } catch (e) { + // ignore + } +} + +if (!loaded) { + console.warn('Warning: .env not found in standard locations; relying on process.env'); +} + +// Delay requiring the service until after env is (attempted) loaded to avoid early Supabase client initialization errors +const errorLogService = require('./services/errorLogService'); + +async function testErrorLogging() { + console.log('๐Ÿงช Testing Error Logging...'); + + // Check if environment variables are loaded + if (!process.env.SUPABASE_URL) { + console.error('โŒ SUPABASE_URL not found in environment variables'); + return; + } + + // Testing basic error logging + const testError = new Error('Test error logging'); + testError.code = 'TEST_ERROR'; + + try { + await errorLogService.logError({ + error: testError, + category: 'info', + type: 'system' + }); + + console.log('โœ… Basic error logging test passed'); + + // Testing critical error alerting + const criticalError = new Error('Critical test error'); + await errorLogService.logError({ + error: criticalError, + category: 'critical', + type: 'system' + }); + + console.log('โœ… Critical error logging test passed'); + + } catch (error) { + console.error('โŒ Test failed:', error); + } +} + +testErrorLogging(); \ No newline at end of file From f546267999f9f0bef054628661b6c62aa963346d Mon Sep 17 00:00:00 2001 From: ChaohuiLi0321 Date: Fri, 19 Sep 2025 02:31:47 +1000 Subject: [PATCH 03/10] fix: update path for test error trigger endpoint --- index.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.yaml b/index.yaml index 0aefb00..04800bc 100644 --- a/index.yaml +++ b/index.yaml @@ -381,7 +381,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /api/system/test-error/trigger: + /system/test-error/trigger: post: tags: - System From 9c35c59948e18254c434bd496b9292a33606532f Mon Sep 17 00:00:00 2001 From: kundanr2 Date: Sun, 21 Sep 2025 11:46:58 +1000 Subject: [PATCH 04/10] rbac logging --- middleware/authorizeRoles.js | 41 ++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/middleware/authorizeRoles.js b/middleware/authorizeRoles.js index 0b578ff..c186e6b 100644 --- a/middleware/authorizeRoles.js +++ b/middleware/authorizeRoles.js @@ -1,13 +1,19 @@ /** - * Role-based access control (RBAC) middleware + * Role-based access control (RBAC) middleware with violation logging */ +const { createClient } = require('@supabase/supabase-js'); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY // โœ… still using anon key +); + function authorizeRoles(...allowedRoles) { - return (req, res, next) => { - // Supabase JWTs: "role" is usually "authenticated" or "service_role" - // Custom JWTs: you explicitly set "role" in payload + return async (req, res, next) => { const userRole = req.user?.role || req.user?.user_roles || null; if (!userRole) { + await logViolation(req, userRole, "ROLE_MISSING"); return res.status(403).json({ success: false, error: "Role missing in token", @@ -15,13 +21,11 @@ function authorizeRoles(...allowedRoles) { }); } - // Normalize role value (lowercase string) const roleValue = String(userRole).toLowerCase(); - - // Normalize allowed roles too const normalizedAllowed = allowedRoles.map(r => r.toLowerCase()); if (!normalizedAllowed.includes(roleValue)) { + await logViolation(req, roleValue, "ACCESS_DENIED"); return res.status(403).json({ success: false, error: "Access denied: insufficient role", @@ -29,8 +33,31 @@ function authorizeRoles(...allowedRoles) { }); } + // โœ… If role is allowed, continue next(); }; } +async function logViolation(req, role, status) { + const payload = { + user_id: req.user?.userId || "unknown", + email: req.user?.email || "unknown", // โœ… added email + role: role || "unknown", + endpoint: req.originalUrl, + method: req.method, + status + }; + + try { + const { error } = await supabase.from("rbac_violation_logs").insert([payload]); + if (error) { + console.error("โŒ Supabase insert error:", error.message); + } else { + console.log("โœ… RBAC violation logged:", payload); + } + } catch (err) { + console.error("โŒ RBAC log exception:", err.message); + } +} + module.exports = authorizeRoles; \ No newline at end of file From 1ac3ea1a38ae05bb2a24739f807eb15e39f3fbcb Mon Sep 17 00:00:00 2001 From: ChaohuiLi0321 Date: Tue, 23 Sep 2025 20:46:51 +1000 Subject: [PATCH 05/10] feat: enhance error logging service with dynamic Supabase integration and unified log entry format --- services/errorLogService.js | 369 ++++++++++++++++++++++++++++-------- 1 file changed, 291 insertions(+), 78 deletions(-) diff --git a/services/errorLogService.js b/services/errorLogService.js index f1ead2e..1162594 100644 --- a/services/errorLogService.js +++ b/services/errorLogService.js @@ -1,7 +1,19 @@ -const { createClient } = require('@supabase/supabase-js'); -const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); +const fs = require('fs'); +const path = require('path'); -class ErrorLogService { +// Dynamically import Supabase (if available) +let supabase = null; +try { + const { createClient } = require('@supabase/supabase-js'); + if (process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY) { + supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); + } +} catch (error) { + // Supabase not available, using file-based logging + console.warn('Supabase not available, using file-based logging'); +} + +class UnifiedErrorLogService { constructor() { this.severityLevels = { critical: 4, @@ -9,9 +21,24 @@ class ErrorLogService { info: 2, minor: 1 }; + + // Configuration options + this.config = { + enableDatabaseLogging: !!supabase, + enableFileLogging: true, + enableConsoleLogging: true, + logLevel: process.env.LOG_LEVEL || 'info' + }; + + // Ensure log directory exists (for file logging) + this.logDir = path.join(process.cwd(), 'logs'); + if (this.config.enableFileLogging && !fs.existsSync(this.logDir)) { + fs.mkdirSync(this.logDir, { recursive: true }); + } } - /** Record error logs + /** + * Main error logging method - compatible with both branches' interfaces */ async logError({ error, @@ -22,45 +49,208 @@ class ErrorLogService { additionalContext = {} }) { try { - const logEntry = { - error_type: type, - error_message: error.message || error.toString(), - stack_trace: error.stack, - endpoint: req?.originalUrl || req?.url, - method: req?.method, - request_body: req?.body ? JSON.stringify(this.sanitizeRequestBody(req.body)) : null, - user_id: req?.user?.userId || null, - ip_address: this.getClientIP(req), - created_at: new Date().toISOString() - }; - - const { data, error: insertError } = await supabase - .from('error_logs') - .insert([logEntry]) - .select() - .single(); + // Create unified log entry + const logEntry = this.createUnifiedLogEntry({ + error, + req, + res, + category, + type, + additionalContext + }); - if (insertError) { - console.error('Failed to log error:', insertError); - // Fallback logging to file or console - this.fallbackLogging(logEntry); + // Execute all logging methods in parallel + const logPromises = []; + + if (this.config.enableDatabaseLogging && supabase) { + logPromises.push(this.logToDatabase(logEntry)); + } + + if (this.config.enableFileLogging) { + logPromises.push(this.logToFile(logEntry)); + } + + if (this.config.enableConsoleLogging) { + logPromises.push(this.logToConsole(logEntry)); } - // Real-time alerting for critical errors + // Wait for all logging to complete + const results = await Promise.allSettled(logPromises); + + // Handle real-time alerts for critical errors if (category === 'critical') { await this.triggerCriticalAlert(logEntry); } - return data; + // Return result summary + return { + success: true, + methods: { + database: this.config.enableDatabaseLogging ? + (results[0]?.status === 'fulfilled') : false, + file: this.config.enableFileLogging ? + (results[this.config.enableDatabaseLogging ? 1 : 0]?.status === 'fulfilled') : false, + console: this.config.enableConsoleLogging + }, + timestamp: logEntry.timestamp || logEntry.created_at + }; + } catch (loggingError) { - console.error('Error logging service failed:', loggingError); - this.fallbackLogging({ error, req, res, category, type }); + console.error('Unified error logging service failed:', loggingError); + // Fallback emergency logging + this.emergencyLogging({ error, req, res, category, type, additionalContext }); + return { success: false, error: loggingError }; } } /** - * Extract request context + * Create unified log entry format + */ + createUnifiedLogEntry({ error, req, res, category, type, additionalContext }) { + const baseEntry = { + timestamp: new Date().toISOString(), + message: error?.message || error?.toString() || 'Unknown error', + stack: error?.stack || null, + code: error?.code || null, + category, + type, + additionalContext + }; + + // If request object is available, add detailed context information (feature of Extended_Middleware_Error_Logging branch) + if (req) { + Object.assign(baseEntry, { + // Database format fields + error_type: type, + error_message: baseEntry.message, + stack_trace: baseEntry.stack, + endpoint: req.originalUrl || req.url, + method: req.method, + request_body: req.body ? JSON.stringify(this.sanitizeRequestBody(req.body)) : null, + user_id: req.user?.userId || req.user?.id || null, + ip_address: this.getClientIP(req), + created_at: baseEntry.timestamp, + + // Extended context information + request_context: this.extractRequestContext(req), + user_context: this.extractUserContext(req), + system_context: this.getSystemContext() + }); + } + + // If response object is available, add response context + if (res) { + baseEntry.response_context = this.extractResponseContext(res); + } + + return baseEntry; + } + + /** + * Database logging (feature of Extended_Middleware_Error_Logging branch) */ + async logToDatabase(logEntry) { + if (!supabase) { + throw new Error('Supabase client not available'); + } + + const dbEntry = { + error_type: logEntry.error_type || logEntry.type, + error_message: logEntry.error_message || logEntry.message, + stack_trace: logEntry.stack_trace || logEntry.stack, + endpoint: logEntry.endpoint, + method: logEntry.method, + request_body: logEntry.request_body, + user_id: logEntry.user_id, + ip_address: logEntry.ip_address, + created_at: logEntry.created_at || logEntry.timestamp + }; + + const { data, error: insertError } = await supabase + .from('error_logs') + .insert([dbEntry]) + .select() + .single(); + + if (insertError) { + throw insertError; + } + + return data; + } + + /** + * File logging (feature of Automated-Security-Assessment-Tool branch) + */ + async logToFile(logEntry) { + const fileEntry = { + timestamp: logEntry.timestamp, + message: logEntry.message, + stack: logEntry.stack, + code: logEntry.code, + category: logEntry.category, + type: logEntry.type, + additionalContext: logEntry.additionalContext + }; + + const logFile = path.join(this.logDir, 'error_log.jsonl'); + const logLine = JSON.stringify(fileEntry) + '\n'; + + return new Promise((resolve, reject) => { + fs.appendFile(logFile, logLine, 'utf8', (err) => { + if (err) reject(err); + else resolve({ success: true }); + }); + }); + } + + /** + * Console logging + */ + async logToConsole(logEntry) { + const severity = logEntry.category || 'info'; + const emoji = this.getSeverityEmoji(severity); + + console.log(`${emoji} Error logged: ${logEntry.message}`); + if (logEntry.stack && severity === 'critical') { + console.error('Stack trace:', logEntry.stack); + } + + return { success: true }; + } + + /** + * Get emoji corresponding to severity level + */ + getSeverityEmoji(severity) { + const emojis = { + critical: '๐Ÿšจ', + warning: 'โš ๏ธ', + info: '๐Ÿ“', + minor: '๐Ÿ’ก' + }; + return emojis[severity] || '๐Ÿ“'; + } + + /** + * Emergency logging (last resort when all methods fail) + */ + emergencyLogging(logData) { + const timestamp = new Date().toISOString(); + const emergencyMessage = `[${timestamp}] EMERGENCY ERROR LOG: ${JSON.stringify(logData, null, 2)}`; + + // Try to write to emergency log file + try { + const emergencyFile = path.join(process.cwd(), 'emergency.log'); + fs.appendFileSync(emergencyFile, emergencyMessage + '\n', 'utf8'); + } catch (e) { + // If even file writing fails, fallback to console output + console.error(emergencyMessage); + } + } + + // ========== Extended_Middleware_Error_Logging ========== + extractRequestContext(req) { return { request_id: req.id || req.headers['x-request-id'], @@ -74,9 +264,6 @@ class ErrorLogService { }; } - /** - * Extract user context - */ extractUserContext(req) { const user = req.user || {}; return { @@ -86,9 +273,6 @@ class ErrorLogService { }; } - /** - * Get system context - */ getSystemContext() { const memUsage = process.memoryUsage(); return { @@ -104,9 +288,6 @@ class ErrorLogService { }; } - /** - * Extract response context - */ extractResponseContext(res) { return { response_status: res.statusCode, @@ -114,9 +295,6 @@ class ErrorLogService { }; } - /** - * Get client IP - */ getClientIP(req) { if (!req) return null; return req.ip || @@ -125,16 +303,12 @@ class ErrorLogService { (req.connection && req.connection.socket ? req.connection.socket.remoteAddress : null) || null; } - /** - * Sanitize sensitive request headers - */ sanitizeHeaders(headers) { if (!headers || typeof headers !== 'object') return headers; const sanitized = { ...headers }; const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key']; sensitiveHeaders.forEach(header => { - // header keys may be in different cases const key = Object.keys(sanitized).find(k => k.toLowerCase() === header); if (key && sanitized[key]) { sanitized[key] = '[REDACTED]'; @@ -144,9 +318,6 @@ class ErrorLogService { return sanitized; } - /** - * Sanitize sensitive request body - */ sanitizeRequestBody(body) { if (!body || typeof body !== 'object') return body; @@ -162,48 +333,25 @@ class ErrorLogService { return sanitized; } - /** - * Get CPU usage - */ getCPUUsage() { const startUsage = process.cpuUsage(); setTimeout(() => { const usage = process.cpuUsage(startUsage); - return (usage.user + usage.system) / 1000000; // Convert to seconds + return (usage.user + usage.system) / 1000000; }, 100); } - /** - * Trigger critical error alert - */ async triggerCriticalAlert(logEntry) { - // Here you can integrate email, Slack, SMS and other alert mechanisms console.error('๐Ÿšจ CRITICAL ERROR ALERT:', { - message: logEntry.error_message, - type: logEntry.error_type, - timestamp: new Date().toISOString(), + message: logEntry.error_message || logEntry.message, + type: logEntry.error_type || logEntry.type, + timestamp: logEntry.created_at || logEntry.timestamp, user_id: logEntry.user_id, - url: logEntry.request_url + url: logEntry.endpoint }); - - // You can add more alerting logic here - // await this.sendSlackAlert(logEntry); - // await this.sendEmailAlert(logEntry); - } - - /** - * Fallback logging - */ - fallbackLogging(logData) { - const timestamp = new Date().toISOString(); - console.error(`[${timestamp}] FALLBACK ERROR LOG:`, JSON.stringify(logData, null, 2)); } - /** - * Error classification - */ categorizeError(error, context = {}) { - // Automatically categorize based on error type and context if (error.message.includes('ECONNREFUSED') || error.message.includes('database') || error.code === 'ENOTFOUND') { @@ -224,6 +372,71 @@ class ErrorLogService { return { category: 'warning', type: 'system' }; } + + // ========== Configuration Management Methods ========== + + /** + * Dynamic update configuration + */ + updateConfig(newConfig) { + this.config = { ...this.config, ...newConfig }; + + // If database logging is enabled but Supabase is not available, issue a warning + if (this.config.enableDatabaseLogging && !supabase) { + console.warn('Database logging enabled but Supabase client not available'); + } + } + + /** + * Get current configuration + */ + getConfig() { + return { ...this.config }; + } + + /** + * Health check - Verify availability of various logging methods + */ + async healthCheck() { + const health = { + database: false, + file: false, + console: true, // Console is always available + overall: false + }; + + // Check database connection + if (supabase) { + try { + const { error } = await supabase.from('error_logs').select('id').limit(1); + health.database = !error; + } catch (e) { + health.database = false; + } + } + + // Check file write permissions + try { + const testFile = path.join(this.logDir, '.test'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + health.file = true; + } catch (e) { + health.file = false; + } + + health.overall = health.database || health.file || health.console; + + return health; + } } -module.exports = new ErrorLogService(); +// Creating a singleton instance +const unifiedErrorLogService = new UnifiedErrorLogService(); + +// Backward compatibility - support both branches of the calling method +module.exports = unifiedErrorLogService; + +// Additional exports to support different import methods +module.exports.logError = unifiedErrorLogService.logError.bind(unifiedErrorLogService); +module.exports.UnifiedErrorLogService = UnifiedErrorLogService; \ No newline at end of file From 6c26678c16abb0244c73647d1c1f981894e25e33 Mon Sep 17 00:00:00 2001 From: Tanya2209 Date: Fri, 26 Sep 2025 06:07:29 +1000 Subject: [PATCH 06/10] ci: add parallel lint/test/openapi jobs --- .github/workflows/security.yml | 27 +++++++++++++++------------ package-lock.json | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d823482..78c4f54 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -9,26 +9,29 @@ on: - '**' env: NODE_VERSION: "20" + OPENAPI_FILE: 'index.yaml' + PORT: '3000' jobs: - node-ci: + lint: runs-on: ubuntu-latest steps: - # 1. Checkout repository - - name: Checkout - uses: actions/checkout@v4 - - # 2. Setup Node.js with caching enabled - - name: Setup Node with cache - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version: 20 cache: npm cache-dependency-path: package-lock.json + - run: npm ci --prefer-offline --no-audit --no-fund - # 3. Install dependencies (reproducible and cache-friendly) - - name: Install dependencies - run: npm ci + + - name: Ensure minimal ESLint config + run: | + node -e "const fs=require('fs');const has=['.eslintrc.json','.eslintrc.js','eslint.config.js'].some(f=>fs.existsSync(f));if(!has){fs.writeFileSync('.eslintrc.json',JSON.stringify({root:true,env:{es2021:true,node:true,browser:true},parserOptions:{ecmaVersion:2022,sourceType:'module'},ignorePatterns:['node_modules/','dist/','build/','coverage/'],rules:{curly:'off',eqeqeq:'off','no-undef':'off','no-unused-vars':'off'}},null,2));}" + + - name: Run ESLint (non-blocking) + run: npx -y eslint@8.57.0 . --no-error-on-unmatched-pattern --quiet || true + run-security-scan: runs-on: ubuntu-latest diff --git a/package-lock.json b/package-lock.json index 059759b..c33089a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7858,4 +7858,4 @@ } } } -} \ No newline at end of file +} From 5688f9bba476302e44ca5978c6187ffc477c47ab Mon Sep 17 00:00:00 2001 From: Tanya2209 Date: Fri, 26 Sep 2025 06:11:01 +1000 Subject: [PATCH 07/10] ci: add parallel lint/test/openapi jobs --- .github/workflows/security.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 78c4f54..25c0ae3 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -31,6 +31,31 @@ jobs: - name: Run ESLint (non-blocking) run: npx -y eslint@8.57.0 . --no-error-on-unmatched-pattern --quiet || true + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + - run: npm ci --prefer-offline --no-audit --no-fund + + + - name: Run fast tests (non-blocking) + shell: bash + run: | + set -e + if node -e "const s=(require('./package.json').scripts)||{};process.exit(s['test:unit']?0:1)"; then + npm run test:unit -- --ci || true + elif node -e "const p=require('./package.json');const hasJest=(p.devDependencies&&p.devDependencies.jest)||(p.dependencies&&p.dependencies.jest);process.exit(hasJest?0:1)"; then + npx jest --ci --passWithNoTests || true + else + npm test --silent || true + fi + run-security-scan: runs-on: ubuntu-latest From 9b51213cec36006fac92a4268c1bd2b6ecb1afe6 Mon Sep 17 00:00:00 2001 From: Tanya2209 Date: Fri, 26 Sep 2025 06:11:58 +1000 Subject: [PATCH 08/10] ci: add parallel lint/test/openapi jobs --- .github/workflows/security.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 25c0ae3..10e7da1 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -32,7 +32,7 @@ jobs: - name: Run ESLint (non-blocking) run: npx -y eslint@8.57.0 . --no-error-on-unmatched-pattern --quiet || true - test: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 80999050207f6cbe9a73810489a56e0945efec9a Mon Sep 17 00:00:00 2001 From: Tanya2209 Date: Fri, 26 Sep 2025 06:16:16 +1000 Subject: [PATCH 09/10] ci: add parallel lint/test/openapi jobs --- .github/workflows/security.yml | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 10e7da1..810115a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -55,6 +55,71 @@ jobs: else npm test --silent || true fi + + openapi-validate: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + + with: + + node-version: ${{ env.NODE_VERSION }} + + cache: npm + + cache-dependency-path: package-lock.json + + - run: npm ci + + - name: Install swagger-cli + + run: npx -y swagger-cli@4.0.4 --version + + + + - name: Validate OpenAPI (non-blocking) + + run: | + + REPORT=openapi-validate.log + + if [ ! -f "${{ env.OPENAPI_FILE }}" ]; then + + echo "::warning::OpenAPI file '${{ env.OPENAPI_FILE }}' not found at repo root. Skipping validation." | tee "$REPORT" + + exit 0 + + fi + + # Run validation, capture output; do not fail the step + + npx swagger-cli validate "${{ env.OPENAPI_FILE }}" >"$REPORT" 2>&1 || { + + echo "::warning::OpenAPI validation failed. See artifact '$REPORT' for details." + + } + + # Always succeed + + exit 0 + + - name: Upload OpenAPI validation report + + uses: actions/upload-artifact@v4 + + with: + + name: openapi-validate-report + + path: openapi-validate.log + + if-no-files-found: ignore + run-security-scan: From 22a8c4e9ef3b11cd67997bce4e169e3e24280538 Mon Sep 17 00:00:00 2001 From: Tanya2209 Date: Fri, 26 Sep 2025 06:16:54 +1000 Subject: [PATCH 10/10] ci: add parallel lint/test/openapi jobs --- .github/workflows/security.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 810115a..5ff6613 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -56,7 +56,7 @@ jobs: npm test --silent || true fi - openapi-validate: + openapi-validate: runs-on: ubuntu-latest