From 164e62c234a32797ee753f0bff21e88390e8fa4b Mon Sep 17 00:00:00 2001 From: TengkuAchmad Date: Fri, 5 Dec 2025 15:28:14 +0700 Subject: [PATCH] feat: add endpoints to list and read log files; enhance log retrieval functionality --- server/index.cjs | 66 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/server/index.cjs b/server/index.cjs index bb9f783..7925525 100644 --- a/server/index.cjs +++ b/server/index.cjs @@ -338,7 +338,7 @@ app.get('/api/config', (_req, res) => { }) /** - * Get recent logs (dev/debug only) + * Get recent logs from memory (dev/debug only) * GET /api/logs?limit=100&level=debug|info|warn|error&q=keyword */ app.get('/api/logs', (req, res) => { @@ -360,6 +360,70 @@ app.get('/api/logs', (req, res) => { res.json({ count: sliced.length, items: sliced }) }) +/** + * List all log files in logs directory + * GET /api/logs/files + */ +app.get('/api/logs/files', (req, res) => { + if (!LOG_EXPOSE_API) { + return res.status(403).json({ error: 'FORBIDDEN', message: 'Log API disabled. Set LOG_EXPOSE_API=true to enable.' }) + } + try { + const files = fs.readdirSync(LOG_DIR) + .filter(f => f.startsWith('LOGS_') && f.endsWith('.log')) + .map(f => { + const stats = fs.statSync(path.join(LOG_DIR, f)) + return { + filename: f, + size: stats.size, + modified: stats.mtime, + path: `/api/logs/files/${f}` + } + }) + .sort((a, b) => b.modified - a.modified) + res.json({ count: files.length, files }) + } catch (e) { + logError('logs.files.error', { message: e?.message }) + res.status(500).json({ error: 'READ_ERROR', message: e?.message || 'Failed to read log files' }) + } +}) + +/** + * Read specific log file + * GET /api/logs/files/:filename + */ +app.get('/api/logs/files/:filename', (req, res) => { + if (!LOG_EXPOSE_API) { + return res.status(403).json({ error: 'FORBIDDEN', message: 'Log API disabled. Set LOG_EXPOSE_API=true to enable.' }) + } + try { + const { filename } = req.params + + // SECURITY: VALIDATE FILENAME TO PREVENT DIRECTORY TRAVERSAL + if (!filename.match(/^LOGS_\d{8}\.log$/)) { + return res.status(400).json({ error: 'INVALID_FILENAME', message: 'Invalid log filename format' }) + } + + const filePath = path.join(LOG_DIR, filename) + + if (!fs.existsSync(filePath)) { + return res.status(404).json({ error: 'NOT_FOUND', message: 'Log file not found' }) + } + + const content = fs.readFileSync(filePath, 'utf8') + const lines = content.split('\n').filter(Boolean) + + res.json({ + filename, + lines: lines.length, + content: lines + }) + } catch (e) { + logError('logs.file.read.error', { message: e?.message, filename: req.params.filename }) + res.status(500).json({ error: 'READ_ERROR', message: e?.message || 'Failed to read log file' }) + } +}) + /** * Update payment toggles at runtime (dev only) * POST /api/config