@rsktash/beads-ui 0.1.25 → 0.1.27
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/package.json +1 -1
- package/server/app.js +4 -2
- package/server/auth.js +15 -44
package/package.json
CHANGED
package/server/app.js
CHANGED
|
@@ -5,7 +5,7 @@ import express from 'express';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
-
import { authMiddleware, isAuthEnabled, loadUsers, login } from './auth.js';
|
|
8
|
+
import { authMiddleware, isAuthEnabled, loadUsers, login, verifyToken } from './auth.js';
|
|
9
9
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const pkg = require('../package.json');
|
|
@@ -63,7 +63,9 @@ export function createApp(config) {
|
|
|
63
63
|
res.json({ ok: true, authEnabled: false });
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
-
const
|
|
66
|
+
const authHeader = req.headers.authorization;
|
|
67
|
+
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
68
|
+
const user = token ? verifyToken(token) : null;
|
|
67
69
|
if (!user) {
|
|
68
70
|
res.status(401).json({ ok: false, error: 'Unauthorized' });
|
|
69
71
|
return;
|
package/server/auth.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Optional authentication module.
|
|
3
3
|
* When a users config file exists and contains users, auth is enforced.
|
|
4
4
|
* When no users are configured, auth is bypassed entirely.
|
|
5
|
+
* Simple session tokens stored in memory — no JWT complexity.
|
|
5
6
|
*/
|
|
6
|
-
import {
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import { debug } from './logging.js';
|
|
9
10
|
|
|
@@ -12,13 +13,12 @@ const log = debug('auth');
|
|
|
12
13
|
/** @type {{ username: string, password: string, role?: string }[]} */
|
|
13
14
|
let users = [];
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
const
|
|
17
|
-
const TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
16
|
+
/** @type {Map<string, { username: string, role: string }>} */
|
|
17
|
+
const sessions = new Map();
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Load users from config file.
|
|
21
|
-
*
|
|
21
|
+
* @param {string} [filePath] - BEADS_UI_AUTH_FILE env var
|
|
22
22
|
*/
|
|
23
23
|
export function loadUsers() {
|
|
24
24
|
const filePath = process.env.BEADS_UI_AUTH_FILE || '';
|
|
@@ -31,7 +31,7 @@ export function loadUsers() {
|
|
|
31
31
|
const parsed = JSON.parse(raw);
|
|
32
32
|
users = Array.isArray(parsed.users) ? parsed.users : [];
|
|
33
33
|
log('loaded %d users from %s', users.length, filePath);
|
|
34
|
-
} catch
|
|
34
|
+
} catch {
|
|
35
35
|
log('no users file at %s — auth disabled', filePath);
|
|
36
36
|
users = [];
|
|
37
37
|
}
|
|
@@ -42,41 +42,13 @@ export function isAuthEnabled() {
|
|
|
42
42
|
return users.length > 0;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
48
|
-
* @param {{ username: string, role: string }} payload
|
|
49
|
-
* @returns {string}
|
|
50
|
-
*/
|
|
51
|
-
function createToken(payload) {
|
|
52
|
-
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
53
|
-
const body = Buffer.from(JSON.stringify({
|
|
54
|
-
...payload,
|
|
55
|
-
iat: Date.now(),
|
|
56
|
-
exp: Date.now() + TOKEN_EXPIRY_MS
|
|
57
|
-
})).toString('base64url');
|
|
58
|
-
const sig = createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
|
|
59
|
-
return `${header}.${body}.${sig}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Verify and decode a token.
|
|
46
|
+
* Verify a session token.
|
|
64
47
|
* @param {string} token
|
|
65
48
|
* @returns {{ username: string, role: string } | null}
|
|
66
49
|
*/
|
|
67
50
|
export function verifyToken(token) {
|
|
68
|
-
|
|
69
|
-
const parts = token.split('.');
|
|
70
|
-
if (parts.length !== 3) return null;
|
|
71
|
-
const [header, body, sig] = parts;
|
|
72
|
-
const expected = createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
|
|
73
|
-
if (sig !== expected) return null;
|
|
74
|
-
const payload = JSON.parse(Buffer.from(body, 'base64url').toString());
|
|
75
|
-
if (payload.exp && payload.exp < Date.now()) return null;
|
|
76
|
-
return { username: payload.username, role: payload.role };
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
51
|
+
return sessions.get(token) || null;
|
|
80
52
|
}
|
|
81
53
|
|
|
82
54
|
/**
|
|
@@ -90,13 +62,14 @@ export function login(username, password) {
|
|
|
90
62
|
if (!user) return { ok: false, error: 'Invalid credentials' };
|
|
91
63
|
if (password !== user.password) return { ok: false, error: 'Invalid credentials' };
|
|
92
64
|
const role = user.role || '';
|
|
93
|
-
const token =
|
|
65
|
+
const token = randomBytes(32).toString('hex');
|
|
66
|
+
sessions.set(token, { username, role });
|
|
94
67
|
return { ok: true, token, user: { username, role } };
|
|
95
68
|
}
|
|
96
69
|
|
|
97
70
|
/**
|
|
98
71
|
* Express middleware — skips auth if no users configured.
|
|
99
|
-
* Protects
|
|
72
|
+
* Protects API routes only. Static assets and SPA routes pass through.
|
|
100
73
|
* @param {import('express').Request} req
|
|
101
74
|
* @param {import('express').Response} res
|
|
102
75
|
* @param {import('express').NextFunction} next
|
|
@@ -104,12 +77,10 @@ export function login(username, password) {
|
|
|
104
77
|
export function authMiddleware(req, res, next) {
|
|
105
78
|
if (!isAuthEnabled()) return next();
|
|
106
79
|
|
|
107
|
-
// Allow auth endpoints
|
|
80
|
+
// Allow auth endpoints, health check, and config
|
|
108
81
|
if (req.path.startsWith('/api/auth') || req.path === '/healthz' || req.path === '/api/config') return next();
|
|
109
82
|
|
|
110
83
|
// Allow static assets and SPA routes (auth enforced client-side)
|
|
111
|
-
const ext = req.path.split('.').pop();
|
|
112
|
-
if (ext && ['js', 'css', 'html', 'svg', 'png', 'ico', 'woff', 'woff2', 'ttf'].includes(ext)) return next();
|
|
113
84
|
if (!req.path.startsWith('/api/')) return next();
|
|
114
85
|
|
|
115
86
|
// Check Authorization header
|
|
@@ -119,11 +90,11 @@ export function authMiddleware(req, res, next) {
|
|
|
119
90
|
res.status(401).json({ ok: false, error: 'Unauthorized' });
|
|
120
91
|
return;
|
|
121
92
|
}
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
93
|
+
const user = verifyToken(token);
|
|
94
|
+
if (!user) {
|
|
124
95
|
res.status(401).json({ ok: false, error: 'Invalid token' });
|
|
125
96
|
return;
|
|
126
97
|
}
|
|
127
|
-
/** @type {any} */ (req).user =
|
|
98
|
+
/** @type {any} */ (req).user = user;
|
|
128
99
|
next();
|
|
129
100
|
}
|