@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.
- package/.env.example +37 -0
- package/README.md +200 -0
- package/bin/logboard +536 -0
- package/client/logger.js +309 -0
- package/config/index.js +142 -0
- package/config.js +2 -0
- package/controllers/AnalyticsController.js +46 -0
- package/controllers/ApiAnalyticsController.js +129 -0
- package/controllers/ApiKeyController.js +58 -0
- package/controllers/AuthController.js +131 -0
- package/controllers/HealthController.js +56 -0
- package/controllers/LogController.js +197 -0
- package/controllers/OrgController.js +152 -0
- package/controllers/RoleConfigController.js +20 -0
- package/controllers/SettingsController.js +39 -0
- package/controllers/StreamController.js +55 -0
- package/controllers/UiController.js +789 -0
- package/controllers/UserController.js +79 -0
- package/lib/batchWriter.js +57 -0
- package/lib/cleanup.js +67 -0
- package/lib/ejs.js +103 -0
- package/lib/emitter.js +5 -0
- package/lib/healthMonitor.js +245 -0
- package/lib/logger.js +21 -0
- package/lib/streams.js +32 -0
- package/lib/theme.js +77 -0
- package/lib/userStore.js +13 -0
- package/lib/utils.js +44 -0
- package/middleware/apiKey.js +82 -0
- package/middleware/auth.js +55 -0
- package/middleware/ipWhitelist.js +59 -0
- package/middleware/org.js +85 -0
- package/middleware/pageAccess.js +20 -0
- package/middleware/rateLimit.js +29 -0
- package/middleware/roles.js +11 -0
- package/package.json +77 -0
- package/routes/alerts.js +18 -0
- package/routes/analytics.js +26 -0
- package/routes/api-analytics.js +30 -0
- package/routes/api-keys.js +12 -0
- package/routes/archive.js +91 -0
- package/routes/audit.js +50 -0
- package/routes/auth.js +22 -0
- package/routes/bookmarks.js +13 -0
- package/routes/health.js +11 -0
- package/routes/logs.js +88 -0
- package/routes/metrics.js +66 -0
- package/routes/notifications.js +14 -0
- package/routes/orgs.js +98 -0
- package/routes/registration.js +202 -0
- package/routes/role-config.js +97 -0
- package/routes/saved-searches.js +12 -0
- package/routes/server.js +151 -0
- package/routes/settings.js +28 -0
- package/routes/status.js +21 -0
- package/routes/stream.js +11 -0
- package/routes/super.js +129 -0
- package/routes/ui.js +120 -0
- package/routes/users.js +13 -0
- package/server.js +172 -0
- package/services/AlertRulesService.js +323 -0
- package/services/AnalyticsService.js +665 -0
- package/services/ApiAnalyticsService.js +471 -0
- package/services/ApiKeyService.js +166 -0
- package/services/AuditService.js +249 -0
- package/services/AuthService.js +234 -0
- package/services/BookmarkService.js +49 -0
- package/services/GlobalSettingsService.js +44 -0
- package/services/LogService.js +1066 -0
- package/services/MetricsService.js +116 -0
- package/services/NotificationService.js +70 -0
- package/services/OrgService.js +217 -0
- package/services/ReportService.js +247 -0
- package/services/RoleConfigService.js +201 -0
- package/services/SavedSearchService.js +63 -0
- package/services/SettingsService.js +220 -0
- package/services/UserService.js +121 -0
- package/setup.js +132 -0
- package/views/404.ejs +8 -0
- package/views/alerts.ejs +190 -0
- package/views/analytics.ejs +209 -0
- package/views/api-analytics.ejs +660 -0
- package/views/api-keys.ejs +150 -0
- package/views/archive.ejs +123 -0
- package/views/audit.ejs +314 -0
- package/views/bookmarks.ejs +54 -0
- package/views/custom-dashboard.ejs +162 -0
- package/views/dashboard.ejs +186 -0
- package/views/diff.ejs +98 -0
- package/views/health.ejs +269 -0
- package/views/heatmap.ejs +126 -0
- package/views/insights.ejs +334 -0
- package/views/invite.ejs +74 -0
- package/views/live.ejs +299 -0
- package/views/login.ejs +64 -0
- package/views/logo.png +0 -0
- package/views/logs.ejs +754 -0
- package/views/notifications.ejs +58 -0
- package/views/partials/head.ejs +282 -0
- package/views/partials/sidebar.ejs +168 -0
- package/views/register.ejs +100 -0
- package/views/roles.ejs +279 -0
- package/views/saved-searches.ejs +51 -0
- package/views/service-map.ejs +142 -0
- package/views/settings.ejs +1159 -0
- package/views/sidebar.ejs +129 -0
- package/views/status.ejs +100 -0
- package/views/super-admin-admins.ejs +58 -0
- package/views/super-admin-analytics.ejs +49 -0
- package/views/super-admin-orgs.ejs +310 -0
- package/views/super-admin-profile.ejs +77 -0
- package/views/super-admin-settings.ejs +108 -0
- package/views/super-admin-system.ejs +46 -0
- package/views/users.ejs +153 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const {
|
|
4
|
+
RoleConfigService,
|
|
5
|
+
ALL_PAGES,
|
|
6
|
+
ALL_CARDS,
|
|
7
|
+
} = require('../services/RoleConfigService');
|
|
8
|
+
const { authenticate } = require('../middleware/auth');
|
|
9
|
+
const requireRole = require('../middleware/roles');
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
const svc = new RoleConfigService();
|
|
13
|
+
const admin = [authenticate, requireRole('admin')];
|
|
14
|
+
|
|
15
|
+
// ── List roles + catalogue ─────────────────────────────────────────────────
|
|
16
|
+
router.get('/', authenticate, async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
res.json({
|
|
19
|
+
roles: await svc.getRoles(),
|
|
20
|
+
catalogue: { pages: ALL_PAGES, cards: ALL_CARDS },
|
|
21
|
+
});
|
|
22
|
+
} catch (e) {
|
|
23
|
+
res.status(500).json({ error: e.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── Upsert role (save pages, cards, label, color, allowedApps) ────────────
|
|
28
|
+
router.put('/:name', ...admin, async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const role = await svc.upsertRole(req.params.name, req.body);
|
|
31
|
+
const audit = require('../services/AuditService');
|
|
32
|
+
await new audit()
|
|
33
|
+
.log(
|
|
34
|
+
req.user?.username,
|
|
35
|
+
'role_update',
|
|
36
|
+
req.params.name,
|
|
37
|
+
{
|
|
38
|
+
pages: req.body.pages?.length,
|
|
39
|
+
cards: req.body.cards?.length,
|
|
40
|
+
allowedApps: req.body.allowedApps,
|
|
41
|
+
},
|
|
42
|
+
req.ip || '',
|
|
43
|
+
)
|
|
44
|
+
.catch(() => {});
|
|
45
|
+
res.json(role);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── Delete role ────────────────────────────────────────────────────────────
|
|
52
|
+
router.delete('/:name', ...admin, async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
await svc.deleteRole(req.params.name);
|
|
55
|
+
const audit = require('../services/AuditService');
|
|
56
|
+
await audit
|
|
57
|
+
.log(req.user?.username, 'role_delete', req.params.name, {}, req.ip || '')
|
|
58
|
+
.catch(() => {});
|
|
59
|
+
res.json({ success: true });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── Per-service log access (allowedApps) ──────────────────────────────────
|
|
66
|
+
router.get('/:roleName/allowed-apps', ...admin, async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const apps = await svc.getAllowedApps(req.params.roleName);
|
|
69
|
+
res.json({ roleName: req.params.roleName, allowedApps: apps });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
router.put('/:roleName/allowed-apps', ...admin, async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const role = await svc.setAllowedApps(
|
|
78
|
+
req.params.roleName,
|
|
79
|
+
req.body.allowedApps || [],
|
|
80
|
+
);
|
|
81
|
+
const audit = require('../services/AuditService');
|
|
82
|
+
await audit
|
|
83
|
+
.log(
|
|
84
|
+
req.user?.username,
|
|
85
|
+
'role_apps_update',
|
|
86
|
+
req.params.roleName,
|
|
87
|
+
{ apps: req.body.allowedApps },
|
|
88
|
+
req.ip || '',
|
|
89
|
+
)
|
|
90
|
+
.catch(() => {});
|
|
91
|
+
res.json(role);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
module.exports = router;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const svc = require('../services/SavedSearchService');
|
|
4
|
+
const { authenticate } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
router.get('/', authenticate, async (req, res) => { try { res.json(await svc.getAll(req.user.username)); } catch (e) { res.status(500).json({ error: e.message }); } });
|
|
9
|
+
router.post('/', authenticate, async (req, res) => { try { res.status(201).json(await svc.create(req.user.username, req.body)); } catch (e) { res.status(e.status||500).json({ error: e.message }); } });
|
|
10
|
+
router.delete('/:id', authenticate, async (req, res) => { try { await svc.delete(req.user.username, req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } });
|
|
11
|
+
|
|
12
|
+
module.exports = router;
|
package/routes/server.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const helmet = require('helmet');
|
|
4
|
+
const cookieParser = require('cookie-parser');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
const { scheduleCleanup } = require('./lib/cleanup');
|
|
8
|
+
const { closeAllStreams } = require('./lib/streams');
|
|
9
|
+
const BatchWriter = require('./lib/batchWriter');
|
|
10
|
+
const logger = require('./lib/logger');
|
|
11
|
+
const ejsEngine = require('./lib/ejs');
|
|
12
|
+
const { ipWhitelist } = require('./middleware/ipWhitelist');
|
|
13
|
+
const { orgMiddleware, orgMiddlewareUI } = require('./middleware/org');
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
app.set('trust proxy', true);
|
|
17
|
+
|
|
18
|
+
// ── View engine ────────────────────────────────────────────────────────────
|
|
19
|
+
app.engine('ejs', ejsEngine.renderFile);
|
|
20
|
+
app.set('view engine', 'ejs');
|
|
21
|
+
app.set('views', path.join(__dirname, 'views'));
|
|
22
|
+
|
|
23
|
+
// ── Static assets (logo, future public dir) ────────────────────────────────
|
|
24
|
+
app.use('/public', express.static(path.join(__dirname, 'views')));
|
|
25
|
+
|
|
26
|
+
// ── Security headers ───────────────────────────────────────────────────────
|
|
27
|
+
app.use(helmet({
|
|
28
|
+
contentSecurityPolicy: { directives: {
|
|
29
|
+
defaultSrc: ["'self'"],
|
|
30
|
+
connectSrc: ["'self'", `http://localhost:${config.PORT}`, `ws://localhost:${config.PORT}`, 'https://cdn.jsdelivr.net'],
|
|
31
|
+
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
32
|
+
scriptSrcAttr: ["'unsafe-inline'"],
|
|
33
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
|
34
|
+
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
|
|
35
|
+
imgSrc: ["'self'", 'data:', 'https:', 'http:'], // allow external logo URLs
|
|
36
|
+
}},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// ── CORS ───────────────────────────────────────────────────────────────────
|
|
40
|
+
app.use((req, res, next) => {
|
|
41
|
+
const { origin } = req.headers;
|
|
42
|
+
if (!origin || config.CORS_ORIGINS.includes('*') || config.CORS_ORIGINS.includes(origin)) {
|
|
43
|
+
if (origin) res.setHeader('Access-Control-Allow-Origin', origin);
|
|
44
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
45
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key, Authorization');
|
|
46
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
47
|
+
}
|
|
48
|
+
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
|
49
|
+
next();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ── Body / cookies ─────────────────────────────────────────────────────────
|
|
53
|
+
app.use(express.json({ limit: config.MAX_BODY_SIZE }));
|
|
54
|
+
app.use(cookieParser());
|
|
55
|
+
app.use((req, _res, next) => { logger.debug(req.method+' '+req.path); next(); });
|
|
56
|
+
// req.org is attached by orgMiddleware — called inside each route after authenticate
|
|
57
|
+
|
|
58
|
+
// ── Routes ─────────────────────────────────────────────────────────────────
|
|
59
|
+
const healthRoute = require('./routes/health');
|
|
60
|
+
const authRoute = require('./routes/auth');
|
|
61
|
+
const streamRoute = require('./routes/stream');
|
|
62
|
+
const analyticsRoute = require('./routes/analytics');
|
|
63
|
+
const logsRoute = require('./routes/logs');
|
|
64
|
+
const uiRoute = require('./routes/ui');
|
|
65
|
+
const settingsRoute = require('./routes/settings');
|
|
66
|
+
const apiAnalyticsRoute= require('./routes/api-analytics');
|
|
67
|
+
const roleConfigRoute = require('./routes/role-config');
|
|
68
|
+
const usersRoute = require('./routes/users');
|
|
69
|
+
const apiKeysRoute = require('./routes/api-keys');
|
|
70
|
+
const alertsRoute = require('./routes/alerts');
|
|
71
|
+
const auditRoute = require('./routes/audit');
|
|
72
|
+
const statusRoute = require('./routes/status');
|
|
73
|
+
const archiveRoute = require('./routes/archive');
|
|
74
|
+
const notificationsRoute = require('./routes/notifications')
|
|
75
|
+
const savedSearchesRoute = require('./routes/saved-searches')
|
|
76
|
+
const bookmarksRoute = require('./routes/bookmarks')
|
|
77
|
+
const metricsRoute = require('./routes/metrics')
|
|
78
|
+
const orgsRoute = require('./routes/orgs')
|
|
79
|
+
|
|
80
|
+
const batchWriter = new BatchWriter();
|
|
81
|
+
logsRoute.setWriter(batchWriter);
|
|
82
|
+
uiRoute.setWriter(batchWriter);
|
|
83
|
+
archiveRoute.setWriter(batchWriter);
|
|
84
|
+
|
|
85
|
+
// ── Mount API routes ───────────────────────────────────────────────────────
|
|
86
|
+
app.use('/api/health', healthRoute);
|
|
87
|
+
app.use('/api/auth', authRoute);
|
|
88
|
+
app.use('/api/logs/stream', streamRoute);
|
|
89
|
+
// IP whitelist only on ingest endpoint
|
|
90
|
+
app.use('/api/logs', ipWhitelist(), logsRoute.router);
|
|
91
|
+
app.use('/api/analytics', analyticsRoute);
|
|
92
|
+
app.use('/api/settings', settingsRoute);
|
|
93
|
+
app.use('/api/api-analytics', apiAnalyticsRoute);
|
|
94
|
+
app.use('/api/role-config', roleConfigRoute);
|
|
95
|
+
app.use('/api/users', usersRoute);
|
|
96
|
+
app.use('/api/api-keys', apiKeysRoute);
|
|
97
|
+
app.use('/api/alerts', alertsRoute);
|
|
98
|
+
app.use('/api/audit', auditRoute);
|
|
99
|
+
app.use('/api/archive', archiveRoute.router);
|
|
100
|
+
app.use('/status', statusRoute);
|
|
101
|
+
app.use('/api/orgs', orgsRoute);
|
|
102
|
+
app.use('/api/notifications', notificationsRoute);
|
|
103
|
+
app.use('/api/saved-searches',savedSearchesRoute);
|
|
104
|
+
app.use('/api/bookmarks', bookmarksRoute);
|
|
105
|
+
app.use('/api/metrics', metricsRoute);
|
|
106
|
+
|
|
107
|
+
// ── UI routes ──────────────────────────────────────────────────────────────
|
|
108
|
+
app.use('/', uiRoute.router);
|
|
109
|
+
|
|
110
|
+
// ── 404 / error ────────────────────────────────────────────────────────────
|
|
111
|
+
app.use((req, res) => {
|
|
112
|
+
if (req.path.startsWith('/api/')) return res.status(404).json({ error: 'Not found' });
|
|
113
|
+
res.status(404).render('404', { title: '404', user: req.user||null });
|
|
114
|
+
});
|
|
115
|
+
app.use((err, _req, res, _next) => {
|
|
116
|
+
logger.error('Unhandled: '+err.stack);
|
|
117
|
+
if (res.headersSent) return;
|
|
118
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
scheduleCleanup();
|
|
122
|
+
// Health monitor — alerts for RAM/CPU/disk
|
|
123
|
+
const healthMonitor = require('./lib/healthMonitor');
|
|
124
|
+
const SettingsService = require('./services/SettingsService');
|
|
125
|
+
healthMonitor.start(() => new SettingsService().get());
|
|
126
|
+
// Scheduled reports
|
|
127
|
+
const ReportService = require('./services/ReportService');
|
|
128
|
+
ReportService.startScheduler(() => new SettingsService().get());
|
|
129
|
+
|
|
130
|
+
const server = app.listen(config.PORT, () => {
|
|
131
|
+
logger.info('LogBoard → http://localhost:'+config.PORT);
|
|
132
|
+
logger.info('Env: '+config.NODE_ENV+' | Log dir: '+config.LOG_BASE_DIR);
|
|
133
|
+
logger.info('Data dir: '+config.DATA_DIR);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let shuttingDown = false;
|
|
137
|
+
async function shutdown(sig) {
|
|
138
|
+
if (shuttingDown) return; shuttingDown=true;
|
|
139
|
+
logger.info(sig+' received. Shutting down…');
|
|
140
|
+
server.close(async () => {
|
|
141
|
+
try { await batchWriter.flushAll(); } catch(e) { logger.error('Flush: '+e.message); }
|
|
142
|
+
closeAllStreams();
|
|
143
|
+
logger.info('Shutdown complete.');
|
|
144
|
+
process.exit(0);
|
|
145
|
+
});
|
|
146
|
+
setTimeout(() => { logger.error('Forced exit.'); process.exit(1); }, 10000).unref();
|
|
147
|
+
}
|
|
148
|
+
process.on('SIGTERM', ()=>shutdown('SIGTERM'));
|
|
149
|
+
process.on('SIGINT', ()=>shutdown('SIGINT'));
|
|
150
|
+
process.on('uncaughtException', e=>{ logger.error('Uncaught: '+e.stack); shutdown('uncaughtException'); });
|
|
151
|
+
process.on('unhandledRejection', r=>{ logger.error('UnhandledRejection: '+r); });
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const SettingsService = require('../services/SettingsService');
|
|
4
|
+
const SettingsController = require('../controllers/SettingsController');
|
|
5
|
+
const { authenticate } = require('../middleware/auth');
|
|
6
|
+
const requireRole = require('../middleware/roles');
|
|
7
|
+
const { bustCache } = require('../middleware/ipWhitelist');
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
// ── Org-aware per-request SettingsController ──────────────────────────────
|
|
12
|
+
function getCtrl(req) {
|
|
13
|
+
return new SettingsController(new SettingsService(req.org));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Any authenticated user can read settings
|
|
17
|
+
router.get('/', authenticate, (req, res) => getCtrl(req).getSettings(req, res));
|
|
18
|
+
|
|
19
|
+
// Admin-only mutations
|
|
20
|
+
router.post('/', authenticate, requireRole('admin'), async (req, res) => {
|
|
21
|
+
const ctrl = getCtrl(req);
|
|
22
|
+
await ctrl.saveSettings(req, res);
|
|
23
|
+
bustCache(); // Clear IP whitelist cache after settings update
|
|
24
|
+
});
|
|
25
|
+
router.post('/cleanup', authenticate, requireRole('admin'), (req, res) => getCtrl(req).runCleanup(req, res));
|
|
26
|
+
router.post('/reset', authenticate, requireRole('admin'), (req, res) => getCtrl(req).resetSettings(req, res));
|
|
27
|
+
|
|
28
|
+
module.exports = router;
|
package/routes/status.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const router = Router();
|
|
4
|
+
const AnalyticsService = require('../services/AnalyticsService');
|
|
5
|
+
const svc = new AnalyticsService();
|
|
6
|
+
|
|
7
|
+
// Public — no auth needed
|
|
8
|
+
router.get('/', async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await svc.getStatusPage();
|
|
11
|
+
const accept = req.headers.accept || '';
|
|
12
|
+
if (accept.includes('application/json')) {
|
|
13
|
+
return res.json(data);
|
|
14
|
+
}
|
|
15
|
+
res.render('status', { title: 'Status', data, appName: 'LogBoard', appLogoUrl: '/public/logo.png', user: null, allowedPages: null, allowedCards: [] });
|
|
16
|
+
} catch (e) {
|
|
17
|
+
res.status(500).json({ error: e.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
module.exports = router;
|
package/routes/stream.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const StreamController = require('../controllers/StreamController');
|
|
4
|
+
const { authenticate } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
const ctrl = new StreamController();
|
|
8
|
+
|
|
9
|
+
router.get('/', authenticate, (req, res) => ctrl.stream(req, res));
|
|
10
|
+
|
|
11
|
+
module.exports = router;
|
package/routes/super.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const { authenticate } = require('../middleware/auth');
|
|
4
|
+
const { requireSuperAdmin } = require('../middleware/org');
|
|
5
|
+
const OrgService = require('../services/OrgService');
|
|
6
|
+
const GlobalSettings = require('../services/GlobalSettingsService');
|
|
7
|
+
const config = require('../config');
|
|
8
|
+
const fsP = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
const sa = [authenticate, requireSuperAdmin];
|
|
13
|
+
|
|
14
|
+
// ── Platform stats ─────────────────────────────────────────────────────────
|
|
15
|
+
router.get('/platform-stats', ...sa, async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const orgs = await OrgService.getOrgs();
|
|
18
|
+
let totalUsers = 0, totalServices = 0, totalStorage = 0;
|
|
19
|
+
const orgDetails = [];
|
|
20
|
+
for (const org of Object.values(orgs)) {
|
|
21
|
+
const p = OrgService.orgPaths(org.slug);
|
|
22
|
+
let userCount = 0, serviceCount = 0, storage = 0;
|
|
23
|
+
try { userCount = Object.keys(JSON.parse(await fsP.readFile(p.usersFile,'utf8'))).length; } catch {}
|
|
24
|
+
try {
|
|
25
|
+
const dirs = await fsP.readdir(p.logsDir);
|
|
26
|
+
serviceCount = dirs.filter(d => !['_archive','app','unknown'].includes(d)).length;
|
|
27
|
+
for (const d of dirs) {
|
|
28
|
+
try {
|
|
29
|
+
const st = await fsP.stat(path.join(p.logsDir, d));
|
|
30
|
+
if (st.isDirectory()) {
|
|
31
|
+
const files = await fsP.readdir(path.join(p.logsDir, d));
|
|
32
|
+
for (const f of files) {
|
|
33
|
+
try { const s = await fsP.stat(path.join(p.logsDir, d, f)); storage += s.size; } catch {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
totalUsers += userCount;
|
|
40
|
+
totalServices += serviceCount;
|
|
41
|
+
totalStorage += storage;
|
|
42
|
+
orgDetails.push({ ...org, userCount, serviceCount, storage, storageHuman: formatBytes(storage) });
|
|
43
|
+
}
|
|
44
|
+
res.json({ totalOrgs: Object.keys(orgs).length, totalUsers, totalServices, totalStorage, totalStorageHuman: formatBytes(totalStorage), orgs: orgDetails });
|
|
45
|
+
} catch(e) { res.status(500).json({ error: e.message }); }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── System health ──────────────────────────────────────────────────────────
|
|
49
|
+
router.get('/system-health', ...sa, async (req, res) => {
|
|
50
|
+
const os = require('os');
|
|
51
|
+
const total = os.totalmem(), free = os.freemem();
|
|
52
|
+
res.json({
|
|
53
|
+
node: process.version,
|
|
54
|
+
pid: process.pid,
|
|
55
|
+
uptime: Math.round(process.uptime()),
|
|
56
|
+
platform: process.platform,
|
|
57
|
+
arch: process.arch,
|
|
58
|
+
ramUsed: Math.round((total-free)/1024/1024),
|
|
59
|
+
ramTotal: Math.round(total/1024/1024),
|
|
60
|
+
ramPct: Math.round((total-free)/total*100),
|
|
61
|
+
cpus: os.cpus().length,
|
|
62
|
+
hostname: os.hostname(),
|
|
63
|
+
env: process.env.NODE_ENV || 'development',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── Global settings ────────────────────────────────────────────────────────
|
|
68
|
+
router.get('/settings', ...sa, async (req, res) => { try { res.json(await GlobalSettings.get()); } catch(e) { res.status(500).json({error:e.message}); } });
|
|
69
|
+
router.post('/settings', ...sa, async (req, res) => { try { res.json(await GlobalSettings.save(req.body)); } catch(e) { res.status(500).json({error:e.message}); } });
|
|
70
|
+
|
|
71
|
+
// ── Super-admin accounts ───────────────────────────────────────────────────
|
|
72
|
+
router.get('/admins', ...sa, async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const SFILE = path.join(config.DATA_DIR, 'super-admins.json');
|
|
75
|
+
let sas = {};
|
|
76
|
+
try { sas = JSON.parse(await fsP.readFile(SFILE,'utf8')); } catch {}
|
|
77
|
+
res.json(Object.values(sas).map(s => ({ username: s.username, createdAt: s.createdAt })));
|
|
78
|
+
} catch(e) { res.status(500).json({error:e.message}); }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
router.delete('/admins/:username', ...sa, async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
if (req.params.username === req.user.username) return res.status(400).json({ error: 'Cannot remove yourself' });
|
|
84
|
+
const SFILE = path.join(config.DATA_DIR, 'super-admins.json');
|
|
85
|
+
let sas = {};
|
|
86
|
+
try { sas = JSON.parse(await fsP.readFile(SFILE,'utf8')); } catch {}
|
|
87
|
+
delete sas[req.params.username];
|
|
88
|
+
await fsP.writeFile(SFILE, JSON.stringify(sas, null, 2), 'utf8');
|
|
89
|
+
res.json({ success: true });
|
|
90
|
+
} catch(e) { res.status(500).json({error:e.message}); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Platform audit ─────────────────────────────────────────────────────────
|
|
94
|
+
router.get('/audit', ...sa, async (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const SAUDIT = path.join(config.DATA_DIR, 'super-audit.ndjson');
|
|
97
|
+
let lines = [];
|
|
98
|
+
try { lines = (await fsP.readFile(SAUDIT,'utf8')).split('\n').filter(Boolean).reverse().slice(0,200).map(l=>{try{return JSON.parse(l);}catch{return{raw:l};}}); } catch {}
|
|
99
|
+
res.json({ entries: lines, total: lines.length });
|
|
100
|
+
} catch(e) { res.status(500).json({error:e.message}); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
function formatBytes(b) {
|
|
104
|
+
if (b < 1024) return b+'B';
|
|
105
|
+
if (b < 1024*1024) return (b/1024).toFixed(1)+'KB';
|
|
106
|
+
if (b < 1024*1024*1024) return (b/1024/1024).toFixed(1)+'MB';
|
|
107
|
+
return (b/1024/1024/1024).toFixed(2)+'GB';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = router;
|
|
111
|
+
|
|
112
|
+
// ── Super-admin change password ───────────────────────────────────────────
|
|
113
|
+
router.post('/change-password', ...sa, async (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const { currentPassword, newPassword } = req.body;
|
|
116
|
+
if (!newPassword || newPassword.length < 8) return res.status(400).json({ error: 'New password must be at least 8 characters' });
|
|
117
|
+
const SFILE = path.join(config.DATA_DIR, 'super-admins.json');
|
|
118
|
+
const bcrypt = require('bcryptjs');
|
|
119
|
+
let sas = {};
|
|
120
|
+
try { sas = JSON.parse(await fsP.readFile(SFILE,'utf8')); } catch {}
|
|
121
|
+
const sa = sas[req.user.username];
|
|
122
|
+
if (!sa) return res.status(404).json({ error: 'Account not found' });
|
|
123
|
+
const valid = await bcrypt.compare(currentPassword, sa.password);
|
|
124
|
+
if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });
|
|
125
|
+
sas[req.user.username].password = await bcrypt.hash(newPassword, 10);
|
|
126
|
+
await fsP.writeFile(SFILE, JSON.stringify(sas, null, 2), 'utf8');
|
|
127
|
+
res.json({ success: true });
|
|
128
|
+
} catch(e) { res.status(500).json({ error: e.message }); }
|
|
129
|
+
});
|
package/routes/ui.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router } = require('express');
|
|
3
|
+
const { authenticateUI } = require('../middleware/auth');
|
|
4
|
+
const { requirePage } = require('../middleware/pageAccess');
|
|
5
|
+
const { requireSuperAdmin } = require('../middleware/org');
|
|
6
|
+
const OrgController = require('../controllers/OrgController');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
const orgCtrl = new OrgController();
|
|
10
|
+
let ctrl; // set by setWriter() after batchWriter is ready
|
|
11
|
+
|
|
12
|
+
// ── Inject LogService + AnalyticsService ─────────────────────────────────
|
|
13
|
+
const LogService = require('../services/LogService');
|
|
14
|
+
const AnalyticsService = require('../services/AnalyticsService');
|
|
15
|
+
|
|
16
|
+
module.exports.setWriter = function (bw) {
|
|
17
|
+
const UiController = require('../controllers/UiController');
|
|
18
|
+
ctrl = new UiController(new LogService(bw), new AnalyticsService());
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ── Public ────────────────────────────────────────────────────────────────
|
|
22
|
+
router.get('/login', (req, res) => ctrl ? ctrl.loginPage(req, res) : res.redirect('/login'));
|
|
23
|
+
router.get('/invite/:token', (req, res) => orgCtrl.invitePage(req, res));
|
|
24
|
+
|
|
25
|
+
// ── Main ──────────────────────────────────────────────────────────────────
|
|
26
|
+
router.get('/', authenticateUI, requirePage('dashboard'), (req, res) => ctrl.dashboard(req, res));
|
|
27
|
+
router.get('/dashboard', authenticateUI, requirePage('dashboard'), (req, res) => ctrl.dashboard(req, res));
|
|
28
|
+
router.get('/logs', authenticateUI, requirePage('logs'), (req, res) => ctrl.logsPage(req, res));
|
|
29
|
+
router.get('/live', authenticateUI, requirePage('live'), (req, res) => ctrl.livePage(req, res));
|
|
30
|
+
router.get('/insights', authenticateUI, requirePage('insights'), (req, res) => ctrl.insightsPage(req, res));
|
|
31
|
+
|
|
32
|
+
// ── Monitor ───────────────────────────────────────────────────────────────
|
|
33
|
+
router.get('/health', authenticateUI, requirePage('health'), (req, res) => ctrl.healthPage(req, res));
|
|
34
|
+
router.get('/alerts', authenticateUI, requirePage('alerts'), (req, res) => ctrl.alertsPage(req, res));
|
|
35
|
+
router.get('/archive', authenticateUI, requirePage('archive'), (req, res) => ctrl.archivePage(req, res));
|
|
36
|
+
|
|
37
|
+
// ── Admin ─────────────────────────────────────────────────────────────────
|
|
38
|
+
router.get('/users', authenticateUI, requirePage('users'), (req, res) => ctrl.usersPage(req, res));
|
|
39
|
+
router.get('/api-keys', authenticateUI, requirePage('api-keys'), (req, res) => ctrl.apiKeysPage(req, res));
|
|
40
|
+
router.get('/roles', authenticateUI, requirePage('roles'), (req, res) => ctrl.rolesPage(req, res));
|
|
41
|
+
router.get('/settings', authenticateUI, requirePage('settings'), (req, res) => ctrl.settingsPage(req, res));
|
|
42
|
+
router.get('/audit', authenticateUI, requirePage('audit'), (req, res) => ctrl.auditPage(req, res));
|
|
43
|
+
|
|
44
|
+
// ── Extra ─────────────────────────────────────────────────────────────────
|
|
45
|
+
router.get('/bookmarks', authenticateUI, (req, res) => ctrl.bookmarksPage(req, res));
|
|
46
|
+
router.get('/saved-searches', authenticateUI, (req, res) => ctrl.savedSearchesPage(req, res));
|
|
47
|
+
router.get('/notifications', authenticateUI, (req, res) => ctrl.notificationsPage(req, res));
|
|
48
|
+
router.get('/custom-dashboard',authenticateUI, (req, res) => ctrl.customDashboardPage(req, res));
|
|
49
|
+
router.get('/heatmap', authenticateUI, requirePage('insights'), (req, res) => ctrl.heatmapPage(req, res));
|
|
50
|
+
router.get('/diff', authenticateUI, requirePage('insights'), (req, res) => ctrl.diffPage(req, res));
|
|
51
|
+
router.get('/service-map', authenticateUI, requirePage('insights'), (req, res) => ctrl.serviceMapPage(req, res));
|
|
52
|
+
|
|
53
|
+
// ── Super-admin ───────────────────────────────────────────────────────────
|
|
54
|
+
router.get('/super-admin/orgs', authenticateUI, requireSuperAdmin, (req, res) => orgCtrl.orgsPage(req, res));
|
|
55
|
+
|
|
56
|
+
// ── Legacy redirects ──────────────────────────────────────────────────────
|
|
57
|
+
router.get('/analytics', authenticateUI, (req, res) => res.redirect(`/insights?tab=log${req.query.service ? `&service=${req.query.service}` : ''}${req.query.date ? `&date=${req.query.date}` : ''}`));
|
|
58
|
+
router.get('/api-analytics', authenticateUI, (req, res) => res.redirect('/insights?tab=api'));
|
|
59
|
+
|
|
60
|
+
module.exports.router = router;
|
|
61
|
+
// ── Super-admin UI pages ─────────────────────────────────────────────────
|
|
62
|
+
router.get('/super-admin/analytics', authenticateUI, requireSuperAdmin, async (req, res) => {
|
|
63
|
+
const gs = require('../services/GlobalSettingsService');
|
|
64
|
+
const { buildThemeSnippet } = require('../lib/theme');
|
|
65
|
+
let settings = {};
|
|
66
|
+
try { settings = await new (require('../services/SettingsService'))(null).get(); } catch {}
|
|
67
|
+
res.render('super-admin-analytics', {
|
|
68
|
+
title:'Platform Analytics', user:req.user, allowedPages:[], allowedCards:[],
|
|
69
|
+
settings, appName:settings.appName||'LogBoard', appLogoUrl:settings.appLogoUrl||'/public/logo.png',
|
|
70
|
+
themeSnippet: buildThemeSnippet(settings),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
router.get('/super-admin/system', authenticateUI, requireSuperAdmin, async (req, res) => {
|
|
75
|
+
const { buildThemeSnippet } = require('../lib/theme');
|
|
76
|
+
let settings = {};
|
|
77
|
+
try { settings = await new (require('../services/SettingsService'))(null).get(); } catch {}
|
|
78
|
+
res.render('super-admin-system', {
|
|
79
|
+
title:'System Health', user:req.user, allowedPages:[], allowedCards:[],
|
|
80
|
+
settings, appName:settings.appName||'LogBoard', appLogoUrl:settings.appLogoUrl||'/public/logo.png',
|
|
81
|
+
themeSnippet: buildThemeSnippet(settings),
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
router.get('/super-admin/settings', authenticateUI, requireSuperAdmin, async (req, res) => {
|
|
86
|
+
const GlobalSettings = require('../services/GlobalSettingsService');
|
|
87
|
+
const { buildThemeSnippet } = require('../lib/theme');
|
|
88
|
+
let settings = {}, globalSettings = {};
|
|
89
|
+
try { settings = await new (require('../services/SettingsService'))(null).get(); } catch {}
|
|
90
|
+
try { globalSettings = await GlobalSettings.get(); } catch {}
|
|
91
|
+
res.render('super-admin-settings', {
|
|
92
|
+
title:'Global Settings', user:req.user, allowedPages:[], allowedCards:[],
|
|
93
|
+
settings, globalSettings, appName:settings.appName||'LogBoard',
|
|
94
|
+
appLogoUrl:settings.appLogoUrl||'/public/logo.png',
|
|
95
|
+
themeSnippet:buildThemeSnippet(settings),
|
|
96
|
+
baseUrl: globalSettings.appBaseUrl||'http://localhost:9900',
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
router.get('/super-admin/admins', authenticateUI, requireSuperAdmin, async (req, res) => {
|
|
101
|
+
const { buildThemeSnippet } = require('../lib/theme');
|
|
102
|
+
let settings = {};
|
|
103
|
+
try { settings = await new (require('../services/SettingsService'))(null).get(); } catch {}
|
|
104
|
+
res.render('super-admin-admins', {
|
|
105
|
+
title:'Super Admins', user:req.user, allowedPages:[], allowedCards:[],
|
|
106
|
+
settings, appName:settings.appName||'LogBoard', appLogoUrl:settings.appLogoUrl||'/public/logo.png',
|
|
107
|
+
themeSnippet:buildThemeSnippet(settings),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
router.get('/super-admin/profile', authenticateUI, requireSuperAdmin, async (req, res) => {
|
|
112
|
+
const { buildThemeSnippet } = require('../lib/theme');
|
|
113
|
+
let settings = {};
|
|
114
|
+
try { settings = await new (require('../services/SettingsService'))(null).get(); } catch {}
|
|
115
|
+
res.render('super-admin-profile', {
|
|
116
|
+
title:'My Profile', user:req.user, allowedPages:[], allowedCards:[],
|
|
117
|
+
settings, appName:settings.appName||'LogBoard', appLogoUrl:settings.appLogoUrl||'/public/logo.png',
|
|
118
|
+
themeSnippet:buildThemeSnippet(settings),
|
|
119
|
+
});
|
|
120
|
+
});
|
package/routes/users.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Router }=require('express');
|
|
3
|
+
const UserController=require('../controllers/UserController');
|
|
4
|
+
const { authenticate }=require('../middleware/auth');
|
|
5
|
+
const requireRole=require('../middleware/roles');
|
|
6
|
+
const router=Router(), ctrl=new UserController(), 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.put('/:username/role', ...admin, (req, res) => ctrl.updateRole(req, res));
|
|
10
|
+
router.put('/:username/reset-password', ...admin, (req, res) => ctrl.resetPassword(req, res));
|
|
11
|
+
router.delete('/:username', ...admin, (req, res) => ctrl.remove(req, res));
|
|
12
|
+
router.delete('/:username/2fa', ...admin, (req, res) => ctrl.revoke2fa(req, res));
|
|
13
|
+
module.exports=router;
|