thevoidforge 21.0.8 → 21.0.10
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/dist/wizard/api/auth.js +20 -14
- package/dist/wizard/server.js +4 -5
- package/dist/wizard/ui/lobby.js +2 -2
- package/dist/wizard/ui/login.js +23 -8
- package/package.json +1 -1
package/dist/wizard/api/auth.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { addRoute } from '../router.js';
|
|
6
6
|
import { parseJsonBody } from '../lib/body-parser.js';
|
|
7
|
-
import { hasUsers, createUser, login, logout, validateSession, parseSessionCookie, buildSessionCookie, clearSessionCookie, isRemoteMode, checkRateLimit, getClientIp, getUserRole, isValidUsername, } from '../lib/tower-auth.js';
|
|
7
|
+
import { hasUsers, createUser, login, logout, validateSession, parseSessionCookie, buildSessionCookie, clearSessionCookie, isRemoteMode, isLanMode, checkRateLimit, getClientIp, getUserRole, isValidUsername, } from '../lib/tower-auth.js';
|
|
8
8
|
import { audit } from '../lib/audit-log.js';
|
|
9
9
|
import { sendJson } from '../lib/http-helpers.js';
|
|
10
10
|
// POST /api/auth/setup — Create initial admin user (only when no users exist)
|
|
11
11
|
addRoute('POST', '/api/auth/setup', async (req, res) => {
|
|
12
|
-
if (!isRemoteMode()) {
|
|
13
|
-
sendJson(res, 400, { success: false, error: 'Auth setup is only available in remote mode' });
|
|
12
|
+
if (!isRemoteMode() && !isLanMode()) {
|
|
13
|
+
sendJson(res, 400, { success: false, error: 'Auth setup is only available in remote or LAN mode' });
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
// Rate-limit the setup endpoint (prevents race-to-setup attacks)
|
|
@@ -58,8 +58,8 @@ addRoute('POST', '/api/auth/setup', async (req, res) => {
|
|
|
58
58
|
});
|
|
59
59
|
// POST /api/auth/login — Authenticate with username + password + TOTP
|
|
60
60
|
addRoute('POST', '/api/auth/login', async (req, res) => {
|
|
61
|
-
if (!isRemoteMode()) {
|
|
62
|
-
sendJson(res, 400, { success: false, error: 'Auth is only required in remote mode' });
|
|
61
|
+
if (!isRemoteMode() && !isLanMode()) {
|
|
62
|
+
sendJson(res, 400, { success: false, error: 'Auth is only required in remote or LAN mode' });
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
const body = await parseJsonBody(req);
|
|
@@ -68,19 +68,25 @@ addRoute('POST', '/api/auth/login', async (req, res) => {
|
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
const { username, password, totpCode } = body;
|
|
71
|
-
if (typeof username !== 'string' || typeof password !== 'string'
|
|
72
|
-
sendJson(res, 400, { success: false, error: 'username
|
|
71
|
+
if (typeof username !== 'string' || typeof password !== 'string') {
|
|
72
|
+
sendJson(res, 400, { success: false, error: 'username and password are required strings' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// LAN mode: TOTP is optional (password-only auth)
|
|
76
|
+
const requireTotp = isRemoteMode();
|
|
77
|
+
const totp = typeof totpCode === 'string' ? totpCode : '';
|
|
78
|
+
if (requireTotp && (totp.length !== 6 || !/^\d{6}$/.test(totp))) {
|
|
79
|
+
sendJson(res, 400, { success: false, error: 'totpCode must be exactly 6 digits' });
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
75
82
|
// Cap field lengths to prevent DoS via oversized PBKDF2 input
|
|
76
|
-
|
|
77
|
-
if (username.length > 64 || password.length > 256 || totpCode.length !== 6 || !/^\d{6}$/.test(totpCode)) {
|
|
83
|
+
if (username.length > 64 || password.length > 256) {
|
|
78
84
|
sendJson(res, 400, { success: false, error: 'Field length exceeded' });
|
|
79
85
|
return;
|
|
80
86
|
}
|
|
81
87
|
const ip = getClientIp(req);
|
|
82
|
-
await audit('login_attempt', ip, username.slice(0, 64), { method: 'password+totp' });
|
|
83
|
-
const result = await login(username, password,
|
|
88
|
+
await audit('login_attempt', ip, username.slice(0, 64), { method: requireTotp ? 'password+totp' : 'password' });
|
|
89
|
+
const result = await login(username, password, totp, ip);
|
|
84
90
|
if ('error' in result) {
|
|
85
91
|
await audit('login_failure', ip, username.slice(0, 64), { reason: result.error });
|
|
86
92
|
const status = result.retryAfterMs ? 429 : 401;
|
|
@@ -114,8 +120,8 @@ addRoute('POST', '/api/auth/logout', async (req, res) => {
|
|
|
114
120
|
});
|
|
115
121
|
// GET /api/auth/session — Check if current session is valid
|
|
116
122
|
addRoute('GET', '/api/auth/session', async (req, res) => {
|
|
117
|
-
if (!isRemoteMode()) {
|
|
118
|
-
sendJson(res, 200, { success: true, data: { authenticated: true, username: 'local', role: 'admin', remoteMode: false } });
|
|
123
|
+
if (!isRemoteMode() && !isLanMode()) {
|
|
124
|
+
sendJson(res, 200, { success: true, data: { authenticated: true, username: 'local', role: 'admin', remoteMode: false, lanMode: false } });
|
|
119
125
|
return;
|
|
120
126
|
}
|
|
121
127
|
const token = parseSessionCookie(req.headers.cookie);
|
|
@@ -129,5 +135,5 @@ addRoute('GET', '/api/auth/session', async (req, res) => {
|
|
|
129
135
|
sendJson(res, 200, { success: true, data: { authenticated: false, needsSetup: false } });
|
|
130
136
|
return;
|
|
131
137
|
}
|
|
132
|
-
sendJson(res, 200, { success: true, data: { authenticated: true, username: session.username, role: session.role, remoteMode:
|
|
138
|
+
sendJson(res, 200, { success: true, data: { authenticated: true, username: session.username, role: session.role, remoteMode: isRemoteMode(), lanMode: isLanMode() } });
|
|
133
139
|
});
|
package/dist/wizard/server.js
CHANGED
|
@@ -159,11 +159,10 @@ async function handleRequest(req, res) {
|
|
|
159
159
|
sendJson(res, 403, { success: false, error: 'Missing X-VoidForge-Request header' });
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
if (isRemoteMode()) {
|
|
162
|
+
// Auth middleware — in remote and LAN modes, require valid session for non-exempt paths.
|
|
163
|
+
// LAN mode has full access (same as remote) but simpler auth (password only, no TOTP).
|
|
164
|
+
// Local mode (127.0.0.1) has no auth — it's your own machine.
|
|
165
|
+
if (isRemoteMode() || isLanMode()) {
|
|
167
166
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
168
167
|
if (!isAuthExempt(url.pathname)) {
|
|
169
168
|
const token = parseSessionCookie(req.headers.cookie);
|
package/dist/wizard/ui/lobby.js
CHANGED
|
@@ -710,14 +710,14 @@
|
|
|
710
710
|
const res = await fetch('/api/auth/session');
|
|
711
711
|
const body = await res.json();
|
|
712
712
|
const data = body.data || {};
|
|
713
|
-
if (data.remoteMode && data.authenticated) {
|
|
713
|
+
if ((data.remoteMode || data.lanMode) && data.authenticated) {
|
|
714
714
|
currentUser = { username: data.username || '', role: data.role || 'viewer' };
|
|
715
715
|
const roleLabel = { admin: 'Admin', deployer: 'Deployer', viewer: 'Viewer' }[data.role] || '';
|
|
716
716
|
authUser.textContent = data.username + (roleLabel ? ' (' + roleLabel + ')' : '');
|
|
717
717
|
authUser.style.display = '';
|
|
718
718
|
btnLogout.style.display = '';
|
|
719
719
|
}
|
|
720
|
-
if (data.remoteMode && !data.authenticated) {
|
|
720
|
+
if ((data.remoteMode || data.lanMode) && !data.authenticated) {
|
|
721
721
|
window.location.href = '/login.html';
|
|
722
722
|
}
|
|
723
723
|
} catch { /* local mode — no auth needed */ }
|
package/dist/wizard/ui/login.js
CHANGED
|
@@ -92,12 +92,19 @@
|
|
|
92
92
|
|
|
93
93
|
try {
|
|
94
94
|
const data = await setupAccount(username, password);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
if (requireTotp && data.totpSecret) {
|
|
96
|
+
// Show TOTP setup (remote mode)
|
|
97
|
+
setupStatus.textContent = '';
|
|
98
|
+
setupStatus.className = 'status-row';
|
|
99
|
+
totpSecret.textContent = data.totpSecret;
|
|
100
|
+
totpSetup.style.display = 'block';
|
|
101
|
+
setupSubmit.style.display = 'none';
|
|
102
|
+
} else {
|
|
103
|
+
// LAN mode — skip TOTP, go straight to login
|
|
104
|
+
setupStatus.textContent = 'Account created. Please log in.';
|
|
105
|
+
setupStatus.className = 'status-row success';
|
|
106
|
+
setTimeout(function () { showLogin(); }, 1000);
|
|
107
|
+
}
|
|
101
108
|
} catch (err) {
|
|
102
109
|
setupStatus.textContent = err.message;
|
|
103
110
|
setupStatus.className = 'status-row error';
|
|
@@ -111,7 +118,15 @@
|
|
|
111
118
|
|
|
112
119
|
// ── Login flow ─────────────────────────────────────
|
|
113
120
|
|
|
114
|
-
// TOTP
|
|
121
|
+
// Detect if TOTP is required (remote mode) or optional (LAN mode)
|
|
122
|
+
var requireTotp = true;
|
|
123
|
+
fetch('/api/auth/session').then(function (r) { return r.json(); }).then(function (d) {
|
|
124
|
+
var data = d.data || {};
|
|
125
|
+
if (data.lanMode && !data.remoteMode) {
|
|
126
|
+
requireTotp = false;
|
|
127
|
+
if (totpField) totpField.style.display = 'none';
|
|
128
|
+
}
|
|
129
|
+
}).catch(function () {});
|
|
115
130
|
|
|
116
131
|
loginSubmit.addEventListener('click', async () => {
|
|
117
132
|
const username = loginUsername.value.trim();
|
|
@@ -123,7 +138,7 @@
|
|
|
123
138
|
loginStatus.className = 'status-row error';
|
|
124
139
|
return;
|
|
125
140
|
}
|
|
126
|
-
if (!totp || totp.length !== 6) {
|
|
141
|
+
if (requireTotp && (!totp || totp.length !== 6)) {
|
|
127
142
|
loginStatus.textContent = 'Enter your 6-digit authenticator code';
|
|
128
143
|
loginStatus.className = 'status-row error';
|
|
129
144
|
loginTotp.focus();
|