@logboard/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.env.example +37 -0
  2. package/README.md +200 -0
  3. package/bin/logboard +536 -0
  4. package/client/logger.js +309 -0
  5. package/config/index.js +142 -0
  6. package/config.js +2 -0
  7. package/controllers/AnalyticsController.js +46 -0
  8. package/controllers/ApiAnalyticsController.js +129 -0
  9. package/controllers/ApiKeyController.js +58 -0
  10. package/controllers/AuthController.js +131 -0
  11. package/controllers/HealthController.js +56 -0
  12. package/controllers/LogController.js +197 -0
  13. package/controllers/OrgController.js +152 -0
  14. package/controllers/RoleConfigController.js +20 -0
  15. package/controllers/SettingsController.js +39 -0
  16. package/controllers/StreamController.js +55 -0
  17. package/controllers/UiController.js +789 -0
  18. package/controllers/UserController.js +79 -0
  19. package/lib/batchWriter.js +57 -0
  20. package/lib/cleanup.js +67 -0
  21. package/lib/ejs.js +103 -0
  22. package/lib/emitter.js +5 -0
  23. package/lib/healthMonitor.js +245 -0
  24. package/lib/logger.js +21 -0
  25. package/lib/streams.js +32 -0
  26. package/lib/theme.js +77 -0
  27. package/lib/userStore.js +13 -0
  28. package/lib/utils.js +44 -0
  29. package/middleware/apiKey.js +82 -0
  30. package/middleware/auth.js +55 -0
  31. package/middleware/ipWhitelist.js +59 -0
  32. package/middleware/org.js +85 -0
  33. package/middleware/pageAccess.js +20 -0
  34. package/middleware/rateLimit.js +29 -0
  35. package/middleware/roles.js +11 -0
  36. package/package.json +77 -0
  37. package/routes/alerts.js +18 -0
  38. package/routes/analytics.js +26 -0
  39. package/routes/api-analytics.js +30 -0
  40. package/routes/api-keys.js +12 -0
  41. package/routes/archive.js +91 -0
  42. package/routes/audit.js +50 -0
  43. package/routes/auth.js +22 -0
  44. package/routes/bookmarks.js +13 -0
  45. package/routes/health.js +11 -0
  46. package/routes/logs.js +88 -0
  47. package/routes/metrics.js +66 -0
  48. package/routes/notifications.js +14 -0
  49. package/routes/orgs.js +98 -0
  50. package/routes/registration.js +202 -0
  51. package/routes/role-config.js +97 -0
  52. package/routes/saved-searches.js +12 -0
  53. package/routes/server.js +151 -0
  54. package/routes/settings.js +28 -0
  55. package/routes/status.js +21 -0
  56. package/routes/stream.js +11 -0
  57. package/routes/super.js +129 -0
  58. package/routes/ui.js +120 -0
  59. package/routes/users.js +13 -0
  60. package/server.js +172 -0
  61. package/services/AlertRulesService.js +323 -0
  62. package/services/AnalyticsService.js +665 -0
  63. package/services/ApiAnalyticsService.js +471 -0
  64. package/services/ApiKeyService.js +166 -0
  65. package/services/AuditService.js +249 -0
  66. package/services/AuthService.js +234 -0
  67. package/services/BookmarkService.js +49 -0
  68. package/services/GlobalSettingsService.js +44 -0
  69. package/services/LogService.js +1066 -0
  70. package/services/MetricsService.js +116 -0
  71. package/services/NotificationService.js +70 -0
  72. package/services/OrgService.js +217 -0
  73. package/services/ReportService.js +247 -0
  74. package/services/RoleConfigService.js +201 -0
  75. package/services/SavedSearchService.js +63 -0
  76. package/services/SettingsService.js +220 -0
  77. package/services/UserService.js +121 -0
  78. package/setup.js +132 -0
  79. package/views/404.ejs +8 -0
  80. package/views/alerts.ejs +190 -0
  81. package/views/analytics.ejs +209 -0
  82. package/views/api-analytics.ejs +660 -0
  83. package/views/api-keys.ejs +150 -0
  84. package/views/archive.ejs +123 -0
  85. package/views/audit.ejs +314 -0
  86. package/views/bookmarks.ejs +54 -0
  87. package/views/custom-dashboard.ejs +162 -0
  88. package/views/dashboard.ejs +186 -0
  89. package/views/diff.ejs +98 -0
  90. package/views/health.ejs +269 -0
  91. package/views/heatmap.ejs +126 -0
  92. package/views/insights.ejs +334 -0
  93. package/views/invite.ejs +74 -0
  94. package/views/live.ejs +299 -0
  95. package/views/login.ejs +64 -0
  96. package/views/logo.png +0 -0
  97. package/views/logs.ejs +754 -0
  98. package/views/notifications.ejs +58 -0
  99. package/views/partials/head.ejs +282 -0
  100. package/views/partials/sidebar.ejs +168 -0
  101. package/views/register.ejs +100 -0
  102. package/views/roles.ejs +279 -0
  103. package/views/saved-searches.ejs +51 -0
  104. package/views/service-map.ejs +142 -0
  105. package/views/settings.ejs +1159 -0
  106. package/views/sidebar.ejs +129 -0
  107. package/views/status.ejs +100 -0
  108. package/views/super-admin-admins.ejs +58 -0
  109. package/views/super-admin-analytics.ejs +49 -0
  110. package/views/super-admin-orgs.ejs +310 -0
  111. package/views/super-admin-profile.ejs +77 -0
  112. package/views/super-admin-settings.ejs +108 -0
  113. package/views/super-admin-system.ejs +46 -0
  114. package/views/users.ejs +153 -0
package/lib/utils.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const SAFE_APP = /^[a-zA-Z0-9._-]{1,64}$/;
6
+ const SAFE_DATE = /^\d{4}-\d{2}-\d{2}$/;
7
+
8
+ function ensureDir (dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }
9
+
10
+ function getToday () { return new Date().toISOString().slice(0, 10); }
11
+
12
+ function sanitizeAppName (v) {
13
+ if (typeof v !== 'string' || !v.trim()) { throw new RangeError(`Invalid appName: "${v}"`); }
14
+ // Normalise: replace spaces with hyphens, strip unsafe chars, collapse repeats
15
+ v = v.trim().replace(/\s+/g, '-').replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-{2,}/g, '-').slice(0, 64);
16
+ if (!SAFE_APP.test(v)) { throw new RangeError(`Invalid appName after normalisation: "${v}"`); }
17
+ const RESERVED = ['app', 'unknown', '_archive', '_tmp', 'logboard', 'system'];
18
+ if (RESERVED.includes(v.toLowerCase())) { throw new RangeError(`Reserved appName: "${v}" — use a specific service name`); }
19
+ return v;
20
+ }
21
+
22
+ function sanitizeDate (v) {
23
+ if (typeof v !== 'string' || !SAFE_DATE.test(v)) { throw new RangeError(`Invalid date: "${v}"`); }
24
+ return v;
25
+ }
26
+
27
+ function safeLogPath (baseDir, appName, date) {
28
+ const app = sanitizeAppName(appName);
29
+ const d = sanitizeDate(date);
30
+ const resolved = path.resolve(baseDir, app, `${d}.log`);
31
+ if (!resolved.startsWith(path.resolve(baseDir) + path.sep)) { throw new RangeError('Path traversal detected'); }
32
+ return resolved;
33
+ }
34
+
35
+ /** Format bytes to human-readable string */
36
+ function formatBytes (bytes) {
37
+ if (bytes === 0) { return '0 B'; }
38
+ const k = 1024;
39
+ const sizes = ['B', 'KB', 'MB', 'GB'];
40
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
41
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
42
+ }
43
+
44
+ module.exports = { ensureDir, getToday, sanitizeAppName, sanitizeDate, safeLogPath, formatBytes };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+ const ApiKeyService = require('../services/ApiKeyService');
3
+ const config = require('../config');
4
+ const svc = new ApiKeyService();
5
+
6
+ /**
7
+ * validateApiKey(requiredScope?)
8
+ * Supports: blq_... multi-keys (from api-keys.json, SHA-256 hashed)
9
+ * Falls back to single legacy API_KEY env var for backward compat.
10
+ * Attaches req.apiKey = { id, name, scopes[] } on success.
11
+ */
12
+ function validateApiKey (requiredScope) {
13
+ return async function (req, res, next) {
14
+ const raw = req.headers['x-api-key'];
15
+ if (!raw) { return res.status(403).json({ error: 'X-Api-Key header required' }); }
16
+
17
+ // Legacy fallback — also attach default org so logs go to correct path
18
+ if (!raw.startsWith('blq_')) {
19
+ if (raw !== config.API_KEY) { return res.status(403).json({ error: 'Invalid API key' }); }
20
+ req.apiKey = { id: 'legacy', name: 'legacy', scopes: ['logs:write', 'logs:read'], orgSlug: 'default' };
21
+ // Attach default org
22
+ try {
23
+ const OrgService = require('../services/OrgService');
24
+ const defOrg = await OrgService.getOrg('default');
25
+ if (defOrg) req.org = defOrg;
26
+ else {
27
+ // Pre-migration fallback: build org paths from flat config
28
+ const path = require('path');
29
+ // logsDir must point to logs/default/ (org-scoped), not logs/
30
+ const _orgDataDir = require('path').join(config.DATA_DIR, 'orgs', 'default');
31
+ req.org = {
32
+ slug: 'default', name: 'Default',
33
+ dataDir: _orgDataDir,
34
+ logsDir: require('path').join(config.LOG_BASE_DIR, 'default'),
35
+ usersFile: require('path').join(_orgDataDir, 'users.json'),
36
+ settingsFile: require('path').join(_orgDataDir, 'settings.json'),
37
+ rolesFile: require('path').join(_orgDataDir, 'role-config.json'),
38
+ apiKeysFile: require('path').join(_orgDataDir, 'api-keys.json'),
39
+ auditFile: require('path').join(_orgDataDir, 'audit.ndjson'),
40
+ alertsFile: require('path').join(_orgDataDir, 'alerts.json'),
41
+ bookmarksFile: require('path').join(_orgDataDir, 'bookmarks.json'),
42
+ savedSearchesFile: require('path').join(_orgDataDir, 'saved-searches.json'),
43
+ notificationsFile: require('path').join(_orgDataDir, 'notifications.json'),
44
+ invitesFile: require('path').join(_orgDataDir, 'invites.json'),
45
+ };
46
+ }
47
+ } catch {}
48
+ return next();
49
+ }
50
+
51
+ try {
52
+ // Search all orgs for this API key
53
+ const OrgService = require('../services/OrgService');
54
+ const ApiKeyService = require('../services/ApiKeyService');
55
+ let key = null, orgSlug = 'default';
56
+ const orgs = await OrgService.getOrgs().catch(()=>({}));
57
+ for (const slug of Object.keys(orgs)) {
58
+ const orgPaths = OrgService.orgPaths(slug);
59
+ const keySvc = new ApiKeyService(orgPaths);
60
+ const found = await keySvc.validate(raw).catch(()=>null);
61
+ if (found) { key = found; orgSlug = slug; break; }
62
+ }
63
+ if (!key) {
64
+ // Fallback: try flat file
65
+ key = await svc.validate(raw);
66
+ }
67
+ if (!key) { return res.status(403).json({ error: 'Invalid or expired API key' }); }
68
+ if (requiredScope && !key.scopes.includes(requiredScope)) { return res.status(403).json({ error: `Key missing scope: ${requiredScope}` }); }
69
+ req.apiKey = { ...key, orgSlug };
70
+ // Attach org to request
71
+ if (!req.org) {
72
+ try {
73
+ const org = await OrgService.getOrg(orgSlug);
74
+ if (org) req.org = org;
75
+ } catch {}
76
+ }
77
+ next();
78
+ } catch (err) { res.status(500).json({ error: 'Key validation error: ' + err.message }); }
79
+ };
80
+ }
81
+
82
+ module.exports = validateApiKey;
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ const jwt = require('jsonwebtoken');
3
+ const config = require('../config');
4
+
5
+ async function _attachOrg (req) {
6
+ if (!req.user) { return; }
7
+ // Super-admins have no org — they cross all orgs
8
+ if (req.user.role === 'super-admin') { req.org = null; return; }
9
+ const slug = req.user.orgSlug || 'default';
10
+ try {
11
+ const OrgService = require('../services/OrgService');
12
+ let org = await OrgService.getOrg(slug);
13
+ if (!org && slug === 'default') {
14
+ const path = require('path');
15
+ org = {
16
+ slug: 'default', name: 'Default',
17
+ dataDir: config.DATA_DIR, logsDir: config.LOG_BASE_DIR,
18
+ usersFile: config.USERS_FILE, settingsFile: config.SETTINGS_FILE,
19
+ rolesFile: config.ROLES_FILE, apiKeysFile: config.API_KEYS_FILE,
20
+ auditFile: config.AUDIT_FILE, alertsFile: config.ALERTS_FILE,
21
+ bookmarksFile: config.BOOKMARKS_FILE,
22
+ savedSearchesFile: config.SAVED_SEARCHES_FILE,
23
+ notificationsFile: config.NOTIFICATIONS_FILE,
24
+ invitesFile: path.join(config.DATA_DIR, 'invites.json'),
25
+ };
26
+ }
27
+ req.org = org;
28
+ } catch { req.org = null; }
29
+ }
30
+
31
+ function authenticate (req, res, next) {
32
+ const token = req.cookies[config.SESSION_NAME] || (req.headers.authorization || '').replace('Bearer ', '');
33
+ if (!token) { return res.status(401).json({ error: 'Unauthorized — no session' }); }
34
+ try {
35
+ req.user = jwt.verify(token, config.JWT_SECRET);
36
+ if (!req.user.orgSlug) { req.user.orgSlug = 'default'; }
37
+ _attachOrg(req).then(() => next()).catch(() => next());
38
+ } catch {
39
+ res.status(401).json({ error: 'Unauthorized — invalid or expired token' });
40
+ }
41
+ }
42
+
43
+ function authenticateUI (req, res, next) {
44
+ const token = req.cookies[config.SESSION_NAME];
45
+ if (!token) { return res.redirect('/login'); }
46
+ try {
47
+ req.user = jwt.verify(token, config.JWT_SECRET);
48
+ if (!req.user.orgSlug) { req.user.orgSlug = 'default'; }
49
+ _attachOrg(req).then(() => next()).catch(() => next());
50
+ } catch {
51
+ res.redirect('/login');
52
+ }
53
+ }
54
+
55
+ module.exports = { authenticate, authenticateUI };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+ const fsP = require('fs').promises;
3
+ const config = require('../config');
4
+ const logger = require('../lib/logger');
5
+
6
+ let _cache = null,
7
+ _cacheTime = 0;
8
+ const TTL = 30000;
9
+
10
+ async function getList (settingsFile) {
11
+ const file = settingsFile || config.SETTINGS_FILE;
12
+ // Use a cache key per file path
13
+ const cacheKey = file;
14
+ if (_cache && _cache._key === cacheKey && Date.now() - _cacheTime < TTL) {
15
+ return _cache._list;
16
+ }
17
+ try {
18
+ const s = JSON.parse(await fsP.readFile(file, 'utf8'));
19
+ const list = Array.isArray(s.ipWhitelist)
20
+ ? s.ipWhitelist.map((x) => x.trim()).filter(Boolean)
21
+ : [];
22
+ _cache = { _key: cacheKey, _list: list };
23
+ _cacheTime = Date.now();
24
+ return list;
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function bustCache () {
31
+ _cache = null;
32
+ }
33
+
34
+ function ipWhitelist () {
35
+ return async function ipWhitelistMiddleware (req, res, next) {
36
+ // Use org-aware settings file if available
37
+ const settingsFile = req.org ? req.org.settingsFile : config.SETTINGS_FILE;
38
+ const list = await getList(settingsFile);
39
+ if (!list.length) {
40
+ return next();
41
+ }
42
+ const raw = (req.headers['x-forwarded-for'] || req.ip || '')
43
+ .split(',')[0]
44
+ .trim()
45
+ .replace('::ffff:', '');
46
+ const ok = list.some((e) =>
47
+ e.endsWith('/24')
48
+ ? raw.startsWith(`${e.slice(0, e.lastIndexOf('.'))}`)
49
+ : raw === e,
50
+ );
51
+ if (!ok) {
52
+ logger.warn(`[IPWhitelist] Blocked ${raw} ${req.method} ${req.path}`);
53
+ return res.status(403).json({ error: 'IP not whitelisted' });
54
+ }
55
+ next();
56
+ };
57
+ }
58
+
59
+ module.exports = { ipWhitelist, bustCache };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+ const OrgService = require('../services/OrgService');
3
+ const config = require('../config');
4
+
5
+ /**
6
+ * Attach req.org to every authenticated request.
7
+ * If the org doesn't exist yet (pre-migration), auto-bootstraps 'default' org
8
+ * using flat config paths so existing installs keep working without migration.
9
+ */
10
+ async function orgMiddleware (req, res, next) {
11
+ if (!req.user) { return next(); }
12
+ const slug = req.user.orgSlug || 'default';
13
+ try {
14
+ let org = await OrgService.getOrg(slug);
15
+
16
+ // ── Auto-bootstrap for pre-migration installs ──────────────────────────
17
+ if (!org) {
18
+ if (slug === 'default') {
19
+ // Use flat config paths — no migration needed, works out of the box
20
+ org = {
21
+ slug: 'default',
22
+ name: 'Default',
23
+ dataDir: config.DATA_DIR,
24
+ logsDir: config.LOG_BASE_DIR,
25
+ usersFile: config.USERS_FILE,
26
+ settingsFile: config.SETTINGS_FILE,
27
+ rolesFile: config.ROLES_FILE,
28
+ apiKeysFile: config.API_KEYS_FILE,
29
+ auditFile: config.AUDIT_FILE,
30
+ alertsFile: config.ALERTS_FILE,
31
+ bookmarksFile: config.BOOKMARKS_FILE,
32
+ savedSearchesFile: config.SAVED_SEARCHES_FILE,
33
+ notificationsFile: config.NOTIFICATIONS_FILE,
34
+ invitesFile: require('path').join(config.DATA_DIR, 'invites.json'),
35
+ };
36
+ } else {
37
+ // Unknown org — clear session
38
+ res.clearCookie(config.SESSION_NAME);
39
+ return res.status(401).json({ error: 'Org not found — please log in again' });
40
+ }
41
+ }
42
+ req.org = org;
43
+ next();
44
+ } catch (e) {
45
+ // Don't block the request on org middleware failure — degrade gracefully
46
+ req.org = null;
47
+ next();
48
+ }
49
+ }
50
+
51
+ async function orgMiddlewareUI (req, res, next) {
52
+ if (!req.user) { return next(); }
53
+ const slug = req.user.orgSlug || 'default';
54
+ try {
55
+ let org = await OrgService.getOrg(slug);
56
+ if (!org && slug === 'default') {
57
+ org = {
58
+ slug: 'default', name: 'Default',
59
+ dataDir: config.DATA_DIR, logsDir: config.LOG_BASE_DIR,
60
+ usersFile: config.USERS_FILE, settingsFile: config.SETTINGS_FILE,
61
+ rolesFile: config.ROLES_FILE, apiKeysFile: config.API_KEYS_FILE,
62
+ auditFile: config.AUDIT_FILE, alertsFile: config.ALERTS_FILE,
63
+ bookmarksFile: config.BOOKMARKS_FILE,
64
+ savedSearchesFile: config.SAVED_SEARCHES_FILE,
65
+ notificationsFile: config.NOTIFICATIONS_FILE,
66
+ invitesFile: require('path').join(config.DATA_DIR, 'invites.json'),
67
+ };
68
+ } else if (!org) {
69
+ res.clearCookie(config.SESSION_NAME);
70
+ return res.redirect('/login?error=org_not_found');
71
+ }
72
+ req.org = org;
73
+ next();
74
+ } catch (e) {
75
+ req.org = null;
76
+ next();
77
+ }
78
+ }
79
+
80
+ function requireSuperAdmin (req, res, next) {
81
+ if (!req.user || req.user.role !== 'super-admin') { return res.status(403).json({ error: 'Super-admin access required' }); }
82
+ next();
83
+ }
84
+
85
+ module.exports = { orgMiddleware, orgMiddlewareUI, requireSuperAdmin };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+ const { RoleConfigService } = require('../services/RoleConfigService');
3
+ const svc = new RoleConfigService();
4
+
5
+ function requirePage (pageId) {
6
+ return async function pageAccessMiddleware (req, res, next) {
7
+ if (!req.user) { return res.redirect('/login'); }
8
+ try {
9
+ const ok = await svc.canAccessPage(req.user.role, pageId);
10
+ if (!ok) {
11
+ // Don't redirect to /dashboard if that's the page being denied (loop!)
12
+ if (pageId === 'dashboard') { return res.redirect('/login'); }
13
+ return res.redirect('/dashboard?denied=1');
14
+ }
15
+ next();
16
+ } catch { next(); }
17
+ };
18
+ }
19
+
20
+ module.exports = { requirePage };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ function rateLimit ({ windowMs = 60_000, max = 60, message = 'Too many requests' } = {}) {
4
+ const store = new Map();
5
+ setInterval(() => {
6
+ const now = Date.now();
7
+ for (const [ip, times] of store) {
8
+ const fresh = times.filter((t) => now - t < windowMs);
9
+ if (!fresh.length) { store.delete(ip); } else { store.set(ip, fresh); }
10
+ }
11
+ }, windowMs).unref();
12
+
13
+ return function rateLimitMiddleware (req, res, next) {
14
+ const ip = req.ip || 'unknown';
15
+ const now = Date.now();
16
+ const hits = (store.get(ip) || []).filter((t) => now - t < windowMs);
17
+ hits.push(now);
18
+ store.set(ip, hits);
19
+ res.setHeader('X-RateLimit-Limit', max);
20
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, max - hits.length));
21
+ if (hits.length > max) {
22
+ res.setHeader('Retry-After', Math.ceil(windowMs / 1000));
23
+ return res.status(429).json({ error: message });
24
+ }
25
+ next();
26
+ };
27
+ }
28
+
29
+ module.exports = rateLimit;
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ function requireRole (...roles) {
4
+ return function roleMiddleware (req, res, next) {
5
+ if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); }
6
+ if (!roles.includes(req.user.role)) { return res.status(403).json({ error: `Forbidden — requires role: ${ roles.join(' or ')}` }); }
7
+ next();
8
+ };
9
+ }
10
+
11
+ module.exports = requireRole;
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@logboard/cli",
3
+ "version": "1.0.0",
4
+ "description": "LogBoard - Production-grade open-source log aggregator. One-command install on macOS, Linux & Windows.",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "logboard": "./bin/logboard"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.js",
11
+ "dev": "nodemon server.js",
12
+ "setup": "node setup.js",
13
+ "postinstall": "node setup.js",
14
+ "lint:fix": "eslint --fix ."
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "client/",
19
+ "config/",
20
+ "controllers/",
21
+ "lib/",
22
+ "middleware/",
23
+ "routes/",
24
+ "services/",
25
+ "views/",
26
+ "server.js",
27
+ "config.js",
28
+ "setup.js",
29
+ ".env.example",
30
+ "README.md"
31
+ ],
32
+ "keywords": [
33
+ "logboard",
34
+ "logging",
35
+ "log-aggregator",
36
+ "rbac",
37
+ "observability",
38
+ "monitoring",
39
+ "saas",
40
+ "open-source",
41
+ "log-management",
42
+ "devops",
43
+ "dashboard",
44
+ "real-time",
45
+ "alerting"
46
+ ],
47
+ "license": "MIT",
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "preferGlobal": true,
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "dependencies": {
56
+ "archiver": "^7.0.1",
57
+ "axios": "^1.14.0",
58
+ "bcryptjs": "^2.4.3",
59
+ "cookie-parser": "^1.4.6",
60
+ "dotenv": "^16.3.1",
61
+ "express": "^4.18.2",
62
+ "helmet": "^7.0.0",
63
+ "jsonwebtoken": "^9.0.2",
64
+ "node-cron": "^4.2.1",
65
+ "qrcode": "^1.5.3",
66
+ "speakeasy": "^2.0.0",
67
+ "nodemailer": "^6.9.13",
68
+ "passport": "^0.7.0",
69
+ "passport-github2": "^0.1.12",
70
+ "passport-google-oauth20": "^2.0.0"
71
+ },
72
+ "devDependencies": {
73
+ "nodemon": "^3.0.1",
74
+ "@eslint/js": "^9.7.0",
75
+ "eslint": "^9.7.0"
76
+ }
77
+ }
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const svc = require('../services/AlertRulesService');
4
+ const audit = require('../services/AuditService');
5
+ const { authenticate } = require('../middleware/auth');
6
+ const requireRole = require('../middleware/roles');
7
+
8
+ const router = Router();
9
+ const adminOnly = [authenticate, requireRole('admin')];
10
+ const authAny = [authenticate];
11
+
12
+ router.get('/rules', ...authAny, async (req, res) => { try { res.json(await svc.listRules()); } catch (e) { res.status(500).json({ error: e.message }); } });
13
+ router.post('/rules', ...adminOnly, async (req, res) => { try { const r=await svc.upsertRule(req.body); await new audit().log(req.user.username, 'alert_rule_create', r.name, {}, req.ip); res.status(201).json(r); } catch (e) { res.status(e.status||500).json({ error: e.message }); } });
14
+ router.put('/rules/:id', ...adminOnly, async (req, res) => { try { const r=await svc.upsertRule({ ...req.body, id: req.params.id }); await new audit().log(req.user.username, 'alert_rule_update', r.name, {}, req.ip); res.json(r); } catch (e) { res.status(e.status||500).json({ error: e.message }); } });
15
+ router.delete('/rules/:id', ...adminOnly, async (req, res) => { try { await svc.deleteRule(req.params.id); await new audit().log(req.user.username, 'alert_rule_delete', req.params.id, {}, req.ip); res.json({ success: true }); } catch (e) { res.status(e.status||500).json({ error: e.message }); } });
16
+ router.get('/history', ...authAny, async (req, res) => { try { res.json(await svc.getHistory(100)); } catch (e) { res.status(500).json({ error: e.message }); } });
17
+
18
+ module.exports = router;
@@ -0,0 +1,26 @@
1
+
2
+ // RBAC: get allowed apps for requesting user
3
+ async function getAllowedApps (req) {
4
+ try {
5
+ const { RoleConfigService } = require('../services/RoleConfigService');
6
+ return await new RoleConfigService().getAllowedApps(req.user?.role || 'viewer');
7
+ } catch { return []; }
8
+ }
9
+
10
+ 'use strict';
11
+ const { Router } = require('express');
12
+ const AnalyticsService = require('../services/AnalyticsService');
13
+ const AnalyticsController = require('../controllers/AnalyticsController');
14
+ const { authenticate } = require('../middleware/auth');
15
+
16
+ const router = Router();
17
+ const ctrl = new AnalyticsController(new AnalyticsService());
18
+
19
+ router.get('/overview', authenticate, (req, res) => ctrl.overview(req, res));
20
+ router.get('/hourly', authenticate, (req, res) => ctrl.hourly(req, res));
21
+ router.get('/levels', authenticate, (req, res) => ctrl.levels(req, res));
22
+ router.get('/top-services', authenticate, (req, res) => ctrl.topServices(req, res));
23
+ router.get('/recent-errors', authenticate, (req, res) => ctrl.recentErrors(req, res));
24
+ router.get('/trend', authenticate, (req, res) => ctrl.trend(req, res));
25
+
26
+ module.exports = router;
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const ApiAnalyticsService = require('../services/ApiAnalyticsService');
4
+ const ApiAnalyticsController = require('../controllers/ApiAnalyticsController');
5
+ const { authenticate } = require('../middleware/auth');
6
+
7
+ const router = Router();
8
+
9
+ // Per-request org-scoped controller
10
+ function ctrl(req) {
11
+ return new ApiAnalyticsController(new ApiAnalyticsService(req.org));
12
+ }
13
+
14
+ router.get('/services', authenticate, (req, res) => ctrl(req).services(req, res));
15
+ router.get('/overview', authenticate, (req, res) => ctrl(req).overview(req, res));
16
+ router.get('/hourly', authenticate, (req, res) => ctrl(req).hourly(req, res));
17
+ router.get('/endpoints', authenticate, (req, res) => ctrl(req).endpoints(req, res));
18
+ router.get('/slowest', authenticate, (req, res) => ctrl(req).slowest(req, res));
19
+ router.get('/errors', authenticate, (req, res) => ctrl(req).topErrors(req, res));
20
+ router.get('/status', authenticate, (req, res) => ctrl(req).statusDist(req, res));
21
+ router.get('/apdex', authenticate, (req, res) => ctrl(req).apdex(req, res));
22
+ router.get('/trend', authenticate, (req, res) => ctrl(req).trend(req, res));
23
+ router.get('/heaviest', authenticate, (req, res) => ctrl(req).heaviest(req, res));
24
+ router.get('/individual-slowest', authenticate, (req, res) => ctrl(req).individualSlowest(req, res));
25
+ router.get('/peak-rpm', authenticate, (req, res) => ctrl(req).peakRpm(req, res));
26
+ router.get('/slow-trend', authenticate, (req, res) => ctrl(req).slowTrend(req, res));
27
+ router.get('/error-trend', authenticate, (req, res) => ctrl(req).errorTrend(req, res));
28
+ router.get('/hourly-pattern', authenticate, (req, res) => ctrl(req).hourlyPattern(req, res));
29
+
30
+ module.exports = router;
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+ const { Router }=require('express');
3
+ const ApiKeyController=require('../controllers/ApiKeyController');
4
+ const { authenticate }=require('../middleware/auth');
5
+ const requireRole=require('../middleware/roles');
6
+ const router=Router(), ctrl=new ApiKeyController(), admin=[authenticate, requireRole('admin')];
7
+ router.get('/', ...admin, (req, res) => ctrl.list(req, res));
8
+ router.post('/', ...admin, (req, res) => ctrl.create(req, res));
9
+ router.patch('/:id', ...admin, (req, res) => ctrl.update(req, res));
10
+ router.post('/:id/revoke', ...admin, (req, res) => ctrl.revoke(req, res));
11
+ router.delete('/:id', ...admin, (req, res) => ctrl.remove(req, res));
12
+ module.exports=router;
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const router = Router();
4
+ const { authenticate } = require('../middleware/auth');
5
+ const requireRole = require('../middleware/roles');
6
+ const LogService = require('../services/LogService');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const config = require('../config');
10
+
11
+ const admin = [authenticate, requireRole('admin')];
12
+ const auth = [authenticate];
13
+
14
+ let _svc;
15
+ module.exports.setWriter = (bw) => { _svc = new LogService(bw); };
16
+
17
+ router.get('/list', ...auth, async (req, res) => {
18
+ try {
19
+ const dir = config.ARCHIVE_DIR;
20
+ if (!fs.existsSync(dir)) { return res.json({ archives: [] }); }
21
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.tar.gz') || f.endsWith('.zip'));
22
+ const list = files.map((f) => {
23
+ const fp = path.join(dir, f);
24
+ const st = fs.statSync(fp);
25
+ return { name: f, size: st.size, created: st.birthtime };
26
+ }).sort((a, b) => new Date(b.created) - new Date(a.created));
27
+ res.json({ archives: list });
28
+ } catch (e) { res.status(500).json({ error: e.message }); }
29
+ });
30
+
31
+ router.get('/download/:filename', ...auth, async (req, res) => {
32
+ const name = path.basename(req.params.filename);
33
+ const fp = path.join(config.ARCHIVE_DIR, name);
34
+ if (!fs.existsSync(fp)) { return res.status(404).json({ error: 'Not found' }); }
35
+ res.download(fp);
36
+ });
37
+
38
+ router.post('/create', ...admin, async (req, res) => {
39
+ try {
40
+ const { startDate, endDate, services } = req.body;
41
+ if (!startDate || !endDate) { return res.status(400).json({ error: 'startDate and endDate required' }); }
42
+ const archiver = require('archiver');
43
+ const fsP = require('fs').promises;
44
+ const { sanitizeDate, sanitizeAppName } = require('../lib/utils');
45
+
46
+ sanitizeDate(startDate); sanitizeDate(endDate);
47
+
48
+ const dir = config.ARCHIVE_DIR;
49
+ if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
50
+
51
+ const filename = `logboard-${ startDate }-to-${ endDate }-${ Date.now() }.tar.gz`;
52
+ const outPath = path.join(dir, filename);
53
+ const output = fs.createWriteStream(outPath);
54
+ const archive = archiver('tar', { gzip: true });
55
+
56
+ archive.pipe(output);
57
+
58
+ // Collect files in date range
59
+ const allDirs = fs.existsSync(config.LOG_BASE_DIR) ? fs.readdirSync(config.LOG_BASE_DIR) : [];
60
+ const svcs = services?.length ? services : allDirs;
61
+
62
+ for (const svc of svcs) {
63
+ try { sanitizeAppName(svc); } catch { continue; }
64
+ const svcDir = path.join(config.LOG_BASE_DIR, svc);
65
+ if (!fs.existsSync(svcDir)) { continue; }
66
+ const files = fs.readdirSync(svcDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.log$/.test(f));
67
+ for (const f of files) {
68
+ const date = f.slice(0, 10);
69
+ if (date >= startDate && date <= endDate) {
70
+ archive.file(path.join(svcDir, f), { name: `${svc }/${ f}` });
71
+ }
72
+ }
73
+ }
74
+
75
+ await new Promise((resolve, reject) => {
76
+ output.on('close', resolve);
77
+ archive.on('error', reject);
78
+ archive.finalize();
79
+ });
80
+
81
+ res.json({ filename, size: fs.statSync(outPath).size });
82
+ } catch (e) { res.status(500).json({ error: e.message }); }
83
+ });
84
+
85
+ router.delete('/:filename', ...admin, async (req, res) => {
86
+ const name = path.basename(req.params.filename);
87
+ const fp = path.join(config.ARCHIVE_DIR, name);
88
+ try { fs.unlinkSync(fp); res.json({ success: true }); } catch (e) { res.status(404).json({ error: 'Not found' }); }
89
+ });
90
+
91
+ module.exports.router = router;