@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
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const svc = require('../services/AuditService');
4
+ const { authenticate } = require('../middleware/auth');
5
+ const requireRole = require('../middleware/roles');
6
+
7
+ const router = Router();
8
+ const admin = [authenticate, requireRole('admin')];
9
+ const auth = [authenticate];
10
+
11
+ // List audit entries
12
+ router.get('/', ...admin, async (req, res) => {
13
+ try {
14
+ const { limit=200, offset=0, q='', action='' } = req.query;
15
+ res.json(await svc.list({ limit: Number(limit), offset: Number(offset), q, action }));
16
+ } catch (e) { res.status(500).json({ error: e.message }); }
17
+ });
18
+
19
+ // Analytics
20
+ router.get('/analytics', ...admin, async (req, res) => {
21
+ try {
22
+ const days = Number(req.query.days||7);
23
+ res.json(await svc.getAnalytics(days));
24
+ } catch (e) { res.status(500).json({ error: e.message }); }
25
+ });
26
+
27
+ // Page view beacon (called from client JS on page unload)
28
+ router.post('/page-view', ...auth, async (req, res) => {
29
+ try {
30
+ const { page, durationSec } = req.body;
31
+ await svc.logPageView(req.user.username, page, durationSec, req.ip);
32
+ res.json({ ok: true });
33
+ } catch (e) { res.status(500).json({ error: e.message }); }
34
+ });
35
+
36
+ // Log view event
37
+ router.post('/log-view', ...auth, async (req, res) => {
38
+ try {
39
+ const { service, date, q } = req.body;
40
+ await svc.logLogAccess(req.user.username, service, date, q, req.ip);
41
+ res.json({ ok: true });
42
+ } catch (e) { res.status(500).json({ error: e.message }); }
43
+ });
44
+
45
+ // Clear
46
+ router.delete('/', ...admin, async (req, res) => {
47
+ try { await svc.clear(); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); }
48
+ });
49
+
50
+ module.exports = router;
package/routes/auth.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const AuthService = require('../services/AuthService');
4
+ const AuthController = require('../controllers/AuthController');
5
+ const { authenticate } = require('../middleware/auth');
6
+ const rateLimit = require('../middleware/rateLimit');
7
+
8
+ const router = Router();
9
+ const svc = new AuthService();
10
+ const ctrl = new AuthController(svc);
11
+ const loginLimit = rateLimit({ windowMs: 15 * 60_000, max: 10, message: 'Too many login attempts' });
12
+
13
+ router.post('/login', loginLimit, (req, res) => ctrl.login(req, res));
14
+ router.post('/logout', (req, res) => ctrl.logout(req, res));
15
+ router.get('/verify', (req, res) => ctrl.verify(req, res));
16
+ router.get('/2fa/setup', authenticate, (req, res) => ctrl.setup2fa(req, res));
17
+ router.post('/2fa/enable', authenticate, (req, res) => ctrl.enable2fa(req, res));
18
+ router.post('/2fa/disable', authenticate, (req, res) => ctrl.disable2fa(req, res));
19
+ router.get('/2fa/status', authenticate, (req, res) => ctrl.totpStatus(req, res));
20
+ router.post('/change-password', authenticate, (req, res) => ctrl.changePassword(req, res));
21
+
22
+ module.exports = router;
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const svc = require('../services/BookmarkService');
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.put('/:id/note', authenticate, async (req, res) => { try { res.json(await svc.updateNote(req.user.username, req.params.id, req.body.note||'')); } catch (e) { res.status(500).json({ error: e.message }); } });
11
+ 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 }); } });
12
+
13
+ module.exports = router;
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const HealthController = require('../controllers/HealthController');
4
+
5
+ const router = Router();
6
+ const ctrl = new HealthController();
7
+
8
+ router.get('/', (req, res) => ctrl.health(req, res));
9
+ router.get('/metrics', (req, res) => ctrl.metrics(req, res));
10
+
11
+ module.exports = router;
package/routes/logs.js ADDED
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const LogController = require('../controllers/LogController');
4
+ const validateApiKey = require('../middleware/apiKey');
5
+ const { authenticate } = require('../middleware/auth');
6
+ const requireRole = require('../middleware/roles');
7
+ const rateLimit = require('../middleware/rateLimit');
8
+
9
+ const router = Router();
10
+ const ingestLimit = rateLimit({ windowMs: 60_000, max: 1000, message: 'Ingest rate limit exceeded' });
11
+
12
+ // batchWriter is injected by server.js via module.exports.setWriter
13
+ let ctrl;
14
+ module.exports.setWriter = function (batchWriter) {
15
+ const LogService = require('../services/LogService');
16
+ ctrl = new LogController(new LogService(batchWriter));
17
+ };
18
+
19
+ // ── Ingest (API key auth) ────────────────────────────────────────────────
20
+ router.post('/', validateApiKey(), ingestLimit, (req, res) => ctrl.ingest(req, res));
21
+
22
+ router.post('/bulk', validateApiKey(), ingestLimit, (req, res) => ctrl.ingest(req, res));
23
+
24
+ // ── Read (JWT auth, viewer+) ─────────────────────────────────────────────
25
+ router.get('/services', authenticate, (req, res) => ctrl.services(req, res));
26
+ router.get('/tail/:appName/:date', authenticate, (req, res) => ctrl.tail(req, res));
27
+ router.get('/search', authenticate, (req, res) => ctrl.search(req, res));
28
+ router.get('/replay', authenticate, (req, res) => ctrl.replay(req, res));
29
+ router.get('/download/:appName/:date', authenticate, (req, res) => ctrl.download(req, res));
30
+ router.get('/context/:appName/:date', authenticate, (req, res) => ctrl.context(req, res));
31
+
32
+ router.get('/global-search', authenticate, async (req, res) => {
33
+ try {
34
+ const { q, limit = 50, dates } = req.query;
35
+ if (!q || q.length < 2) { return res.json([]); }
36
+ const ls = ctrl ? ctrl.svc : new (require('../services/LogService'))(null);
37
+ const services = await ls.getServices();
38
+ const today = new Date().toISOString().slice(0, 10);
39
+ const yesterday = new Date(Date.now()-864e5).toISOString().slice(0, 10);
40
+ const searchDates = dates ? dates.split(',').filter(Boolean) : [today, yesterday];
41
+ const results = [];
42
+ const ql = q.toLowerCase();
43
+
44
+ for (const { appName } of services.slice(0, 15)) {
45
+ for (const date of searchDates) {
46
+ try {
47
+ const r = await ls.tail(appName, date, 300);
48
+ const matches = (r.lines||[]).filter((l) => l.toLowerCase().includes(ql)).slice(0, 4);
49
+ matches.forEach((line) => {
50
+ let msg=line, ts='';
51
+ try { const p=JSON.parse(line); msg=p.message||line; ts=p.ts?p.ts.replace('T', ' ').slice(0, 19):''; } catch {}
52
+ results.push({ appName, date, msg: msg.slice(0, 120), ts });
53
+ });
54
+ } catch {}
55
+ if (results.length >= Number(limit)) { break; }
56
+ }
57
+ if (results.length >= Number(limit)) { break; }
58
+ }
59
+ res.json(results.slice(0, Number(limit)));
60
+ } catch (e) { res.status(500).json({ error: e.message }); }
61
+ });
62
+ router.get('/trace', authenticate, (req, res) => {
63
+ if (!ctrl) { return res.status(503).json({ error: 'Log service not ready yet' }); }
64
+ ctrl.traceSearch(req, res);
65
+ });
66
+ router.get('/search-range', authenticate, (req, res) => {
67
+ if (!ctrl) { return res.status(503).json({ error: 'Log service not ready yet' }); }
68
+ ctrl.searchRange(req, res);
69
+ });
70
+ router.get('/clusters/:appName/:date', authenticate, (req, res) => {
71
+ if (!ctrl) { return res.status(503).json({ error: 'Log service not ready yet' }); }
72
+ ctrl.clusters(req, res);
73
+ });
74
+ router.get('/clusters-range/:appName', authenticate, async (req, res) => {
75
+ if (!ctrl) { return res.status(503).json({ error: 'Log service not ready yet' }); }
76
+ try {
77
+ const { fromDate, toDate } = req.query;
78
+ if (!fromDate || !toDate) { return res.status(400).json({ error: 'fromDate and toDate required' }); }
79
+ res.json(await ctrl.svc.clusterErrorsRange(req.params.appName, fromDate, toDate));
80
+ } catch (e) { res.status(500).json({ error: e.message }); }
81
+ });
82
+
83
+ router.get('/anomalies/:appName', authenticate, (req, res) => {
84
+ if (!ctrl) { return res.status(503).json({ error: 'Log service not ready yet' }); }
85
+ ctrl.anomalies(req, res);
86
+ });
87
+
88
+ module.exports.router = router;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ const { Router } = require("express");
3
+ const { authenticate } = require("../middleware/auth");
4
+ const validateApiKey = require("../middleware/apiKey");
5
+ const MetricsService = require("../services/MetricsService");
6
+
7
+ const router = Router();
8
+
9
+ // Helper to get org-aware MetricsService
10
+ function getSvc(req) {
11
+ return new MetricsService(req.org || null);
12
+ }
13
+
14
+ // POST /api/metrics — SDK pushes metrics (API key auth, scope: metrics:write or logs:write)
15
+ router.post("/", validateApiKey("logs:write"), async (req, res) => {
16
+ // Attach org from API key's orgSlug if available
17
+ if (!req.org && req.apiKey) {
18
+ const slug = req.apiKey.orgSlug || "default";
19
+ try {
20
+ const OrgService = require("../services/OrgService");
21
+ req.org = await OrgService.getOrg(slug);
22
+ } catch {}
23
+ }
24
+ try {
25
+ const appName = req.body.appName || req.apiKey?.name;
26
+ if (!appName) {
27
+ return res.status(400).json({ error: "appName required" });
28
+ }
29
+ await getSvc(req).ingest(appName, req.body);
30
+ res.json({ success: true });
31
+ } catch (e) {
32
+ res.status(500).json({ error: e.message });
33
+ }
34
+ });
35
+
36
+ // GET /api/metrics/services — list services that have metrics
37
+ router.get("/services", authenticate, async (req, res) => {
38
+ try {
39
+ res.json({ services: await getSvc(req).listServices() });
40
+ } catch (e) {
41
+ res.status(500).json({ error: e.message });
42
+ }
43
+ });
44
+
45
+ // GET /api/metrics/latest — latest snapshot per service (for status cards)
46
+ router.get("/latest", authenticate, async (req, res) => {
47
+ try {
48
+ const allowedApps = req.query.apps ? req.query.apps.split(",") : [];
49
+ res.json({ services: await getSvc(req).getLatestAll(allowedApps) });
50
+ } catch (e) {
51
+ res.status(500).json({ error: e.message });
52
+ }
53
+ });
54
+
55
+ // GET /api/metrics/:appName?range=1440 — time series for one service
56
+ router.get("/:appName", authenticate, async (req, res) => {
57
+ try {
58
+ const range = parseInt(req.query.range) || 1440;
59
+ const rows = await getSvc(req).getTimeSeries(req.params.appName, range);
60
+ res.json({ appName: req.params.appName, count: rows.length, rows });
61
+ } catch (e) {
62
+ res.status(500).json({ error: e.message });
63
+ }
64
+ });
65
+
66
+ module.exports = router;
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const svc = require('../services/NotificationService');
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(100)); } catch (e) { res.status(500).json({ error: e.message }); } });
9
+ router.get('/unread', authenticate, async (req, res) => { try { res.json({ count: await svc.countUnread() }); } catch (e) { res.status(500).json({ error: e.message }); } });
10
+ router.put('/read', authenticate, async (req, res) => { try { await svc.markRead(null); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } });
11
+ router.put('/read/:id', authenticate, async (req, res) => { try { await svc.markRead(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } });
12
+ router.delete('/:id', authenticate, async (req, res) => { try { await svc.delete(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } });
13
+
14
+ module.exports = router;
package/routes/orgs.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const OrgService = require('../services/OrgService');
4
+ const OrgController = require('../controllers/OrgController');
5
+ const { authenticate, authenticateUI } = require('../middleware/auth');
6
+ const { requireSuperAdmin } = require('../middleware/org');
7
+ const requireRole = require('../middleware/roles');
8
+
9
+ const router = Router();
10
+ const ctrl = new OrgController();
11
+
12
+ // ── Public ────────────────────────────────────────────────────────────────
13
+ router.get ('/invites/:token', (req,res) => ctrl.getInvite(req,res));
14
+ router.post('/invites/accept', (req,res) => ctrl.acceptInvite(req,res));
15
+ router.get ('/invite/:token', (req,res) => ctrl.invitePage(req,res)); // UI page
16
+
17
+ // ── Org admin: invite users ────────────────────────────────────────────────
18
+ router.post('/invites', authenticate, requireRole('admin'), (req,res) => ctrl.createInvite(req,res));
19
+
20
+ // ── Super-admin: list + create orgs ──────────────────────────────────────
21
+ router.get ('/', authenticate, requireSuperAdmin, (req,res) => ctrl.listOrgs(req,res));
22
+ router.post('/', authenticate, requireSuperAdmin, (req,res) => ctrl.createOrg(req,res));
23
+
24
+ // ── Super-admin: bootstrap + migration (no auth required for first-time setup) ──
25
+ router.post('/super-admin/create', (req,res) => ctrl.createSuperAdmin(req,res));
26
+ router.post('/migrate', authenticate, requireSuperAdmin, (req,res) => ctrl.runMigration(req,res));
27
+
28
+ // ── IMPORTANT: specific /:slug/sub routes MUST come before /:slug wildcard ──
29
+
30
+ // ── Super-admin: org detail ───────────────────────────────────────────────
31
+ router.get('/:slug/detail', authenticate, requireSuperAdmin, async (req, res) => {
32
+ try {
33
+ const org = await OrgService.getOrg(req.params.slug);
34
+ if (!org) return res.status(404).json({ error: 'Org not found' });
35
+ const fsP = require('fs').promises;
36
+ const path = require('path');
37
+
38
+ let users = [];
39
+ try {
40
+ const raw = JSON.parse(await fsP.readFile(org.usersFile, 'utf8'));
41
+ users = Object.values(raw).map(u => ({ username: u.username, role: u.role, email: u.email||null, createdAt: u.createdAt }));
42
+ } catch {}
43
+
44
+ const SettingsService = require('../services/SettingsService');
45
+ let settings = {};
46
+ try { settings = await new SettingsService(org).get(); } catch {}
47
+
48
+ let services = [];
49
+ try {
50
+ const dirs = await fsP.readdir(org.logsDir);
51
+ services = dirs.filter(d => !['_archive','app','unknown'].includes(d));
52
+ } catch {}
53
+
54
+ res.json({ slug: org.slug, name: org.name, plan: org.plan, logsDir: org.logsDir, users, settings, services, createdAt: org.createdAt });
55
+ } catch (e) { res.status(500).json({ error: e.message }); }
56
+ });
57
+
58
+ // ── Super-admin: create user in org ──────────────────────────────────────
59
+ router.post('/:slug/create-user', authenticate, requireSuperAdmin, async (req, res) => {
60
+ try {
61
+ const org = await OrgService.getOrg(req.params.slug);
62
+ if (!org) return res.status(404).json({ error: 'Org not found' });
63
+ const { username, password, role = 'admin' } = req.body;
64
+ if (!username || !password || password.length < 8) return res.status(400).json({ error: 'Username and password (min 8 chars) required' });
65
+ const fsP = require('fs').promises;
66
+ const bcrypt = require('bcryptjs');
67
+ let users = {};
68
+ try { users = JSON.parse(await fsP.readFile(org.usersFile, 'utf8')); } catch {}
69
+ if (users[username]) return res.status(409).json({ error: 'Username already exists in this org' });
70
+ users[username] = { username, role, password: await bcrypt.hash(password, 10), createdAt: new Date().toISOString() };
71
+ await fsP.writeFile(org.usersFile, JSON.stringify(users, null, 2), 'utf8');
72
+ res.json({ success: true, username, role });
73
+ } catch (e) { res.status(500).json({ error: e.message }); }
74
+ });
75
+
76
+ // ── Super-admin: remove user from org ────────────────────────────────────
77
+ router.delete('/:slug/users/:username', authenticate, requireSuperAdmin, async (req, res) => {
78
+ try {
79
+ const org = await OrgService.getOrg(req.params.slug);
80
+ if (!org) return res.status(404).json({ error: 'Org not found' });
81
+ const fsP = require('fs').promises;
82
+ let users = {};
83
+ try { users = JSON.parse(await fsP.readFile(org.usersFile, 'utf8')); } catch {}
84
+ if (!users[req.params.username]) return res.status(404).json({ error: 'User not found' });
85
+ delete users[req.params.username];
86
+ await fsP.writeFile(org.usersFile, JSON.stringify(users, null, 2), 'utf8');
87
+ res.json({ success: true });
88
+ } catch (e) { res.status(500).json({ error: e.message }); }
89
+ });
90
+
91
+ // ── /:slug wildcard (must be last) ────────────────────────────────────────
92
+ router.get ('/:slug', authenticate, requireSuperAdmin, (req,res) => ctrl.getOrg(req,res));
93
+ router.delete('/:slug', authenticate, requireSuperAdmin, (req,res) => ctrl.deleteOrg(req,res));
94
+
95
+ // ── Super-admin UI ─────────────────────────────────────────────────────────
96
+ router.get('/ui/orgs', authenticateUI, requireSuperAdmin, (req,res) => ctrl.orgsPage(req,res));
97
+
98
+ module.exports = router;
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+ const { Router } = require('express');
3
+ const bcrypt = require('bcryptjs');
4
+ const jwt = require('jsonwebtoken');
5
+ const OrgService = require('../services/OrgService');
6
+ const GlobalSettings = require('../services/GlobalSettingsService');
7
+ const config = require('../config');
8
+
9
+ const router = Router();
10
+
11
+ // ── Email registration ────────────────────────────────────────────────────
12
+ router.post('/register', async (req, res) => {
13
+ try {
14
+ const gs = await GlobalSettings.get();
15
+ if (gs.registrationMode !== 'open') return res.status(403).json({ error: 'Registration is currently closed. Contact your administrator.' });
16
+
17
+ const { orgName, username, email, password } = req.body;
18
+ if (!orgName || !username || !email || !password) return res.status(400).json({ error: 'All fields required' });
19
+ if (password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' });
20
+ if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) return res.status(400).json({ error: 'Username must be 3–32 alphanumeric chars' });
21
+
22
+ const org = await OrgService.create({
23
+ name: orgName,
24
+ ownerUsername: username,
25
+ ownerPassword: password,
26
+ plan: gs.defaultPlan || 'free',
27
+ });
28
+
29
+ // Set email on user record
30
+ try {
31
+ const fsP = require('fs').promises;
32
+ const users = JSON.parse(await fsP.readFile(org.usersFile,'utf8'));
33
+ users[username].email = email;
34
+ await fsP.writeFile(org.usersFile, JSON.stringify(users, null, 2), 'utf8');
35
+ } catch {}
36
+
37
+ // Issue JWT
38
+ const token = jwt.sign({ username, role: 'admin', orgSlug: org.slug }, config.JWT_SECRET, { expiresIn: config.JWT_EXPIRES_IN || '24h' });
39
+ res.cookie(config.SESSION_NAME, token, { httpOnly: true, sameSite: 'strict', maxAge: 86400000 });
40
+ res.json({ success: true, token, orgSlug: org.slug, redirect: '/dashboard' });
41
+ } catch(e) { res.status(e.status || 500).json({ error: e.message }); }
42
+ });
43
+
44
+ // ── Registration page ─────────────────────────────────────────────────────
45
+ router.get('/register', async (req, res) => {
46
+ try {
47
+ const gs = await GlobalSettings.get();
48
+ const settings = {};
49
+ try {
50
+ const SettingsService = require('../services/SettingsService');
51
+ Object.assign(settings, await new SettingsService(null).get());
52
+ } catch {}
53
+ const { buildThemeSnippet } = require('../lib/theme');
54
+ res.render('register', {
55
+ title: 'Create Organisation',
56
+ settings,
57
+ themeSnippet: buildThemeSnippet(settings),
58
+ allowedPages: [],
59
+ allowedCards: [],
60
+ user: null,
61
+ error: req.query.error || null,
62
+ githubEnabled: !!(gs.githubClientId && gs.githubClientSecret),
63
+ googleEnabled: !!(gs.googleClientId && gs.googleClientSecret),
64
+ registrationOpen: gs.registrationMode === 'open',
65
+ sessionName: config.SESSION_NAME,
66
+ baseUrl: gs.appBaseUrl || `http://localhost:${config.PORT}`,
67
+ });
68
+ } catch(e) { res.status(500).send(e.message); }
69
+ });
70
+
71
+ // ── GitHub OAuth ──────────────────────────────────────────────────────────
72
+ router.get('/auth/github', async (req, res) => {
73
+ try {
74
+ const gs = await GlobalSettings.get();
75
+ if (!gs.githubClientId) return res.redirect('/register?error=GitHub+OAuth+not+configured');
76
+ const base = gs.appBaseUrl || `http://localhost:${config.PORT}`;
77
+ const state = require('crypto').randomBytes(16).toString('hex');
78
+ res.cookie('oauth_state', state, { httpOnly: true, maxAge: 600000 });
79
+ const url = `https://github.com/login/oauth/authorize?client_id=${gs.githubClientId}&redirect_uri=${encodeURIComponent(base+'/auth/github/callback')}&scope=user:email&state=${state}`;
80
+ res.redirect(url);
81
+ } catch(e) { res.redirect('/register?error='+encodeURIComponent(e.message)); }
82
+ });
83
+
84
+ router.get('/auth/github/callback', async (req, res) => {
85
+ try {
86
+ const gs = await GlobalSettings.get();
87
+ const { code, state } = req.query;
88
+ if (!code) return res.redirect('/register?error=GitHub+auth+cancelled');
89
+
90
+ // Exchange code for token
91
+ const axios = require('axios');
92
+ const tokenRes = await axios.post('https://github.com/login/oauth/access_token', {
93
+ client_id: gs.githubClientId, client_secret: gs.githubClientSecret, code,
94
+ }, { headers: { Accept: 'application/json' } });
95
+
96
+ const accessToken = tokenRes.data.access_token;
97
+ if (!accessToken) return res.redirect('/register?error=GitHub+token+exchange+failed');
98
+
99
+ // Get GitHub user
100
+ const userRes = await axios.get('https://api.github.com/user', { headers: { Authorization: `token ${accessToken}` } });
101
+ const ghUser = userRes.data;
102
+ const username = ghUser.login;
103
+ const email = ghUser.email || username + '@github.oauth';
104
+
105
+ await _oauthLogin(req, res, { provider: 'github', id: String(ghUser.id), username, email, displayName: ghUser.name || username }, gs);
106
+ } catch(e) { res.redirect('/register?error='+encodeURIComponent(e.message)); }
107
+ });
108
+
109
+ // ── Google OAuth ──────────────────────────────────────────────────────────
110
+ router.get('/auth/google', async (req, res) => {
111
+ try {
112
+ const gs = await GlobalSettings.get();
113
+ if (!gs.googleClientId) return res.redirect('/register?error=Google+OAuth+not+configured');
114
+ const base = gs.appBaseUrl || `http://localhost:${config.PORT}`;
115
+ const state = require('crypto').randomBytes(16).toString('hex');
116
+ res.cookie('oauth_state', state, { httpOnly: true, maxAge: 600000 });
117
+ const params = new URLSearchParams({
118
+ client_id: gs.googleClientId, redirect_uri: base+'/auth/google/callback',
119
+ response_type: 'code', scope: 'openid email profile', state,
120
+ });
121
+ res.redirect('https://accounts.google.com/o/oauth2/v2/auth?' + params.toString());
122
+ } catch(e) { res.redirect('/register?error='+encodeURIComponent(e.message)); }
123
+ });
124
+
125
+ router.get('/auth/google/callback', async (req, res) => {
126
+ try {
127
+ const gs = await GlobalSettings.get();
128
+ const { code } = req.query;
129
+ if (!code) return res.redirect('/register?error=Google+auth+cancelled');
130
+ const base = gs.appBaseUrl || `http://localhost:${config.PORT}`;
131
+
132
+ const axios = require('axios');
133
+ const tokenRes = await axios.post('https://oauth2.googleapis.com/token', {
134
+ code, client_id: gs.googleClientId, client_secret: gs.googleClientSecret,
135
+ redirect_uri: base+'/auth/google/callback', grant_type: 'authorization_code',
136
+ });
137
+ const idToken = tokenRes.data.id_token;
138
+ const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());
139
+ const username = (payload.email.split('@')[0]).replace(/[^a-zA-Z0-9._-]/g, '_');
140
+ const email = payload.email;
141
+
142
+ await _oauthLogin(req, res, { provider: 'google', id: payload.sub, username, email, displayName: payload.name }, gs);
143
+ } catch(e) { res.redirect('/register?error='+encodeURIComponent(e.message)); }
144
+ });
145
+
146
+ // ── Shared OAuth login/register logic ─────────────────────────────────────
147
+ async function _oauthLogin(req, res, profile, gs) {
148
+ const fsP = require('fs').promises;
149
+ const OrgSvc = require('../services/OrgService');
150
+
151
+ // Check if this OAuth account is linked to an existing org
152
+ const orgs = await OrgSvc.getOrgs();
153
+ for (const slug of Object.keys(orgs)) {
154
+ const p = OrgSvc.orgPaths(slug);
155
+ try {
156
+ const users = JSON.parse(await fsP.readFile(p.usersFile,'utf8'));
157
+ for (const u of Object.values(users)) {
158
+ if (u.oauthProvider === profile.provider && u.oauthId === profile.id) {
159
+ // Existing user — issue JWT and log in
160
+ const token = jwt.sign({ username: u.username, role: u.role||'admin', orgSlug: slug }, config.JWT_SECRET, { expiresIn: '24h' });
161
+ res.cookie(config.SESSION_NAME, token, { httpOnly: true, sameSite: 'strict', maxAge: 86400000 });
162
+ return res.redirect('/dashboard');
163
+ }
164
+ }
165
+ } catch {}
166
+ }
167
+
168
+ // New user — create org
169
+ if (gs.registrationMode !== 'open') return res.redirect('/register?error=Registration+is+closed');
170
+
171
+ let username = profile.username;
172
+ // Ensure username is unique across orgs
173
+ const taken = await _isUsernameTaken(username, orgs);
174
+ if (taken) username = username + '_' + Date.now().toString(36).slice(-4);
175
+
176
+ const org = await OrgSvc.create({ name: profile.displayName + "'s Org", ownerUsername: username, ownerPassword: require('crypto').randomBytes(32).toString('hex'), plan: gs.defaultPlan || 'free' });
177
+
178
+ // Update user record with OAuth info
179
+ try {
180
+ const users = JSON.parse(await fsP.readFile(org.usersFile,'utf8'));
181
+ users[username].email = profile.email;
182
+ users[username].oauthProvider = profile.provider;
183
+ users[username].oauthId = profile.id;
184
+ users[username].displayName = profile.displayName;
185
+ await fsP.writeFile(org.usersFile, JSON.stringify(users, null, 2), 'utf8');
186
+ } catch {}
187
+
188
+ const token = jwt.sign({ username, role: 'admin', orgSlug: org.slug }, config.JWT_SECRET, { expiresIn: '24h' });
189
+ res.cookie(config.SESSION_NAME, token, { httpOnly: true, sameSite: 'strict', maxAge: 86400000 });
190
+ res.redirect('/dashboard');
191
+ }
192
+
193
+ async function _isUsernameTaken(username, orgs) {
194
+ const fsP = require('fs').promises;
195
+ for (const slug of Object.keys(orgs)) {
196
+ const p = require('../services/OrgService').orgPaths(slug);
197
+ try { const u = JSON.parse(await fsP.readFile(p.usersFile,'utf8')); if (u[username]) return true; } catch {}
198
+ }
199
+ return false;
200
+ }
201
+
202
+ module.exports = router;