@pixelbyte-software/pixcode 1.54.0 → 1.54.2
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/assets/index-DvudRU5X.css +32 -0
- package/dist/assets/{index-CEw93JQ7.js → index-lqiCTrCl.js} +205 -205
- package/dist/index.html +2 -2
- package/dist-server/server/claude-sdk.js +6 -0
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/index.js +224 -46
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/account-lockout.js +101 -0
- package/dist-server/server/middleware/account-lockout.js.map +1 -0
- package/dist-server/server/middleware/auth.js +71 -9
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/middleware/rate-limiter.js +62 -0
- package/dist-server/server/middleware/rate-limiter.js.map +1 -0
- package/dist-server/server/routes/agent.js +3 -4
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/auth.js +75 -13
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/codex.js +1 -1
- package/dist-server/server/routes/codex.js.map +1 -1
- package/dist-server/server/routes/diagnostics.js +6 -3
- package/dist-server/server/routes/diagnostics.js.map +1 -1
- package/dist-server/server/routes/gemini.js +1 -1
- package/dist-server/server/routes/gemini.js.map +1 -1
- package/dist-server/server/routes/git.js +9 -9
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/live-view.js +2 -2
- package/dist-server/server/routes/live-view.js.map +1 -1
- package/dist-server/server/routes/plugins.js +12 -12
- package/dist-server/server/routes/plugins.js.map +1 -1
- package/dist-server/server/routes/projects.js +2 -2
- package/dist-server/server/routes/projects.js.map +1 -1
- package/dist-server/server/routes/qwen.js +1 -1
- package/dist-server/server/routes/qwen.js.map +1 -1
- package/dist-server/server/routes/remote.js +1 -1
- package/dist-server/server/routes/remote.js.map +1 -1
- package/dist-server/server/routes/webhooks.js +1 -1
- package/dist-server/server/routes/webhooks.js.map +1 -1
- package/dist-server/server/services/startup-update.js +31 -6
- package/dist-server/server/services/startup-update.js.map +1 -1
- package/dist-server/server/services/telegram/bot.js +6 -4
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/setup-wizard.js +12 -6
- package/dist-server/server/setup-wizard.js.map +1 -1
- package/dist-server/server/utils/plugin-loader.js +3 -0
- package/dist-server/server/utils/plugin-loader.js.map +1 -1
- package/dist-server/server/utils/port-access.js +25 -8
- package/dist-server/server/utils/port-access.js.map +1 -1
- package/dist-server/server/utils/security-log.js +89 -0
- package/dist-server/server/utils/security-log.js.map +1 -0
- package/package.json +1 -1
- package/server/claude-sdk.js +7 -0
- package/server/index.js +232 -46
- package/server/middleware/account-lockout.js +112 -0
- package/server/middleware/auth.js +72 -9
- package/server/middleware/rate-limiter.js +82 -0
- package/server/routes/agent.js +3 -4
- package/server/routes/auth.js +86 -13
- package/server/routes/codex.js +1 -1
- package/server/routes/diagnostics.js +6 -3
- package/server/routes/gemini.js +1 -1
- package/server/routes/git.js +9 -9
- package/server/routes/live-view.js +2 -2
- package/server/routes/plugins.js +12 -12
- package/server/routes/projects.js +2 -2
- package/server/routes/qwen.js +1 -1
- package/server/routes/remote.js +1 -1
- package/server/routes/webhooks.js +1 -1
- package/server/services/startup-update.js +31 -6
- package/server/services/telegram/bot.js +6 -4
- package/server/setup-wizard.js +12 -6
- package/server/utils/plugin-loader.js +4 -0
- package/server/utils/port-access.js +26 -8
- package/server/utils/security-log.js +84 -0
- package/dist/assets/index-B6Ssy_IG.css +0 -32
|
@@ -2,9 +2,12 @@ import jwt from 'jsonwebtoken';
|
|
|
2
2
|
|
|
3
3
|
import { userDb, appConfigDb, apiKeysDb } from '../database/db.js';
|
|
4
4
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
5
|
+
import { securityLog, getClientIp } from '../utils/security-log.js';
|
|
5
6
|
|
|
6
7
|
// Use env var if set, otherwise auto-generate a unique secret per installation
|
|
7
8
|
const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
|
|
9
|
+
const JWT_ALGORITHM = 'HS256';
|
|
10
|
+
const JWT_EXPIRY = process.env.JWT_EXPIRY || '24h';
|
|
8
11
|
const isPixcodeApiKey = (token) => typeof token === 'string' && (token.startsWith('px_') || token.startsWith('ck_'));
|
|
9
12
|
const ADMIN_ROLES = new Set(['owner', 'admin']);
|
|
10
13
|
const PLATFORM_AUTH_BYPASS_ENABLED = IS_PLATFORM && process.env.PIXCODE_ALLOW_PLATFORM_AUTH_BYPASS === '1';
|
|
@@ -18,6 +21,11 @@ const validateApiKey = (req, res, next) => {
|
|
|
18
21
|
|
|
19
22
|
const apiKey = req.headers['x-api-key'];
|
|
20
23
|
if (apiKey !== process.env.API_KEY) {
|
|
24
|
+
securityLog('api_key_validation_failed', {
|
|
25
|
+
ip: getClientIp(req),
|
|
26
|
+
endpoint: req.path,
|
|
27
|
+
method: req.method,
|
|
28
|
+
});
|
|
21
29
|
return res.status(401).json({ error: 'Invalid API key' });
|
|
22
30
|
}
|
|
23
31
|
next();
|
|
@@ -47,12 +55,23 @@ const authenticateToken = async (req, res, next) => {
|
|
|
47
55
|
// - ?apiKey=<apikey> (EventSource workaround)
|
|
48
56
|
// Auth-token mode is decided by the prefix: new keys generated by Pixcode
|
|
49
57
|
// start with `px_`; older `ck_` keys remain valid for existing installs.
|
|
58
|
+
// SECURITY NOTE: Query-param credentials may appear in server logs, proxy
|
|
59
|
+
// logs, and browser history. Prefer Authorization headers for new clients.
|
|
50
60
|
const authHeader = req.headers['authorization'];
|
|
51
61
|
const bearerToken = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : null;
|
|
52
62
|
const apiKeyHeader = req.headers['x-api-key'];
|
|
53
63
|
const queryToken = typeof req.query.token === 'string' ? req.query.token : null;
|
|
54
64
|
const queryApiKey = typeof req.query.apiKey === 'string' ? req.query.apiKey : null;
|
|
55
65
|
|
|
66
|
+
if (queryToken || queryApiKey) {
|
|
67
|
+
securityLog('query_param_credential_used', {
|
|
68
|
+
ip: getClientIp(req),
|
|
69
|
+
endpoint: req.path,
|
|
70
|
+
method: req.method,
|
|
71
|
+
userAgent: req.headers['user-agent'],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
// Try API-key paths first when the credential is unambiguously an API key.
|
|
57
76
|
const explicitApiKey = apiKeyHeader || queryApiKey
|
|
58
77
|
|| (isPixcodeApiKey(bearerToken) ? bearerToken : null)
|
|
@@ -62,8 +81,21 @@ const authenticateToken = async (req, res, next) => {
|
|
|
62
81
|
try {
|
|
63
82
|
const user = apiKeysDb.validateApiKey(explicitApiKey);
|
|
64
83
|
if (!user) {
|
|
84
|
+
securityLog('api_key_auth_failed', {
|
|
85
|
+
ip: getClientIp(req),
|
|
86
|
+
endpoint: req.path,
|
|
87
|
+
method: req.method,
|
|
88
|
+
userId: user?.id,
|
|
89
|
+
});
|
|
65
90
|
return res.status(401).json({ error: 'Invalid or inactive API key' });
|
|
66
91
|
}
|
|
92
|
+
securityLog('api_key_auth_success', {
|
|
93
|
+
ip: getClientIp(req),
|
|
94
|
+
endpoint: req.path,
|
|
95
|
+
method: req.method,
|
|
96
|
+
userId: user.id,
|
|
97
|
+
username: user.username,
|
|
98
|
+
});
|
|
67
99
|
req.user = user;
|
|
68
100
|
return next();
|
|
69
101
|
} catch (error) {
|
|
@@ -75,15 +107,26 @@ const authenticateToken = async (req, res, next) => {
|
|
|
75
107
|
// Otherwise fall back to JWT.
|
|
76
108
|
const jwtToken = bearerToken || queryToken;
|
|
77
109
|
if (!jwtToken) {
|
|
110
|
+
securityLog('auth_no_token', {
|
|
111
|
+
ip: getClientIp(req),
|
|
112
|
+
endpoint: req.path,
|
|
113
|
+
method: req.method,
|
|
114
|
+
});
|
|
78
115
|
return res.status(401).json({ error: 'Access denied. No token provided.' });
|
|
79
116
|
}
|
|
80
117
|
|
|
81
118
|
try {
|
|
82
|
-
const decoded = jwt.verify(jwtToken, JWT_SECRET);
|
|
119
|
+
const decoded = jwt.verify(jwtToken, JWT_SECRET, { algorithms: [JWT_ALGORITHM] });
|
|
83
120
|
|
|
84
121
|
// Verify user still exists and is active
|
|
85
122
|
const user = userDb.getUserById(decoded.userId);
|
|
86
123
|
if (!user) {
|
|
124
|
+
securityLog('jwt_user_not_found', {
|
|
125
|
+
ip: getClientIp(req),
|
|
126
|
+
endpoint: req.path,
|
|
127
|
+
method: req.method,
|
|
128
|
+
userId: decoded.userId,
|
|
129
|
+
});
|
|
87
130
|
return res.status(401).json({ error: 'Invalid token. User not found.' });
|
|
88
131
|
}
|
|
89
132
|
|
|
@@ -100,7 +143,12 @@ const authenticateToken = async (req, res, next) => {
|
|
|
100
143
|
req.user = user;
|
|
101
144
|
next();
|
|
102
145
|
} catch (error) {
|
|
103
|
-
|
|
146
|
+
securityLog('jwt_verification_failed', {
|
|
147
|
+
ip: getClientIp(req),
|
|
148
|
+
endpoint: req.path,
|
|
149
|
+
method: req.method,
|
|
150
|
+
reason: error.name || 'unknown',
|
|
151
|
+
});
|
|
104
152
|
return res.status(403).json({ error: 'Invalid token' });
|
|
105
153
|
}
|
|
106
154
|
};
|
|
@@ -111,6 +159,13 @@ const requireAdmin = (req, res, next) => {
|
|
|
111
159
|
}
|
|
112
160
|
|
|
113
161
|
if (!ADMIN_ROLES.has(req.user.role)) {
|
|
162
|
+
securityLog('admin_access_denied', {
|
|
163
|
+
ip: getClientIp(req),
|
|
164
|
+
endpoint: req.path,
|
|
165
|
+
method: req.method,
|
|
166
|
+
userId: req.user.id,
|
|
167
|
+
username: req.user.username,
|
|
168
|
+
});
|
|
114
169
|
return res.status(403).json({ error: 'Admin access required.' });
|
|
115
170
|
}
|
|
116
171
|
|
|
@@ -120,6 +175,12 @@ const requireAdmin = (req, res, next) => {
|
|
|
120
175
|
!req.user.api_key_scopes?.includes('system') &&
|
|
121
176
|
!req.user.api_key_scopes?.includes('*')
|
|
122
177
|
) {
|
|
178
|
+
securityLog('admin_scope_denied', {
|
|
179
|
+
ip: getClientIp(req),
|
|
180
|
+
endpoint: req.path,
|
|
181
|
+
method: req.method,
|
|
182
|
+
userId: req.user.id,
|
|
183
|
+
});
|
|
123
184
|
return res.status(403).json({ error: 'API key lacks admin scope.' });
|
|
124
185
|
}
|
|
125
186
|
|
|
@@ -136,6 +197,13 @@ const requireApiScope = (scope) => (req, res, next) => {
|
|
|
136
197
|
return next();
|
|
137
198
|
}
|
|
138
199
|
|
|
200
|
+
securityLog('api_scope_denied', {
|
|
201
|
+
ip: getClientIp(req),
|
|
202
|
+
endpoint: req.path,
|
|
203
|
+
method: req.method,
|
|
204
|
+
userId: req.user.id,
|
|
205
|
+
reason: `Missing scope: ${scope}`,
|
|
206
|
+
});
|
|
139
207
|
return res.status(403).json({ error: `API key lacks required scope: ${scope}` });
|
|
140
208
|
};
|
|
141
209
|
|
|
@@ -148,7 +216,7 @@ const generateToken = (user) => {
|
|
|
148
216
|
role: user.role || null,
|
|
149
217
|
},
|
|
150
218
|
JWT_SECRET,
|
|
151
|
-
{ expiresIn:
|
|
219
|
+
{ expiresIn: JWT_EXPIRY, algorithm: JWT_ALGORITHM }
|
|
152
220
|
);
|
|
153
221
|
};
|
|
154
222
|
|
|
@@ -170,10 +238,6 @@ const authenticateWebSocket = (token) => {
|
|
|
170
238
|
|
|
171
239
|
// Normal OSS validation — accept either an API key (`px_…` or legacy
|
|
172
240
|
// `ck_…`) or a JWT.
|
|
173
|
-
// Mirrors the REST `authenticateToken` middleware so any tool that has
|
|
174
|
-
// a Pixcode API key (CI scripts, the api-tester subagent, the user's own
|
|
175
|
-
// automation, ...) can also open a WebSocket without first exchanging
|
|
176
|
-
// the key for a JWT.
|
|
177
241
|
if (!token) {
|
|
178
242
|
return null;
|
|
179
243
|
}
|
|
@@ -190,8 +254,7 @@ const authenticateWebSocket = (token) => {
|
|
|
190
254
|
}
|
|
191
255
|
|
|
192
256
|
try {
|
|
193
|
-
const decoded = jwt.verify(token, JWT_SECRET);
|
|
194
|
-
// Verify user actually exists in database (matches REST authenticateToken behavior)
|
|
257
|
+
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: [JWT_ALGORITHM] });
|
|
195
258
|
const user = userDb.getUserById(decoded.userId);
|
|
196
259
|
if (!user) {
|
|
197
260
|
return null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { securityLog, getClientIp } from '../utils/security-log.js';
|
|
2
|
+
|
|
3
|
+
function createRateLimiter(options = {}) {
|
|
4
|
+
const {
|
|
5
|
+
windowMs = 15 * 60 * 1000,
|
|
6
|
+
max = 100,
|
|
7
|
+
keyGenerator = (req) => getClientIp(req),
|
|
8
|
+
message = 'Too many requests from this IP, please try again later.',
|
|
9
|
+
statusCode = 429,
|
|
10
|
+
skip = (_req) => false,
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
const hits = new Map();
|
|
14
|
+
|
|
15
|
+
function cleanupExpired() {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
for (const [key, entry] of hits.entries()) {
|
|
18
|
+
if (entry.resetTime <= now) {
|
|
19
|
+
hits.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cleanupInterval = setInterval(cleanupExpired, windowMs);
|
|
25
|
+
cleanupInterval.unref();
|
|
26
|
+
|
|
27
|
+
return (req, res, next) => {
|
|
28
|
+
if (skip(req)) {
|
|
29
|
+
return next();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const key = keyGenerator(req);
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
|
|
35
|
+
let entry = hits.get(key);
|
|
36
|
+
if (!entry || entry.resetTime <= now) {
|
|
37
|
+
entry = {
|
|
38
|
+
count: 0,
|
|
39
|
+
resetTime: now + windowMs,
|
|
40
|
+
};
|
|
41
|
+
hits.set(key, entry);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
entry.count += 1;
|
|
45
|
+
|
|
46
|
+
res.setHeader('X-RateLimit-Limit', String(max));
|
|
47
|
+
res.setHeader('X-RateLimit-Remaining', String(Math.max(0, max - entry.count)));
|
|
48
|
+
res.setHeader('X-RateLimit-Reset', String(Math.ceil(entry.resetTime / 1000)));
|
|
49
|
+
|
|
50
|
+
if (entry.count > max) {
|
|
51
|
+
securityLog('rate_limit_exceeded', {
|
|
52
|
+
ip: getClientIp(req),
|
|
53
|
+
endpoint: req.path,
|
|
54
|
+
method: req.method,
|
|
55
|
+
userAgent: req.headers['user-agent'],
|
|
56
|
+
});
|
|
57
|
+
return res.status(statusCode).json({
|
|
58
|
+
success: false,
|
|
59
|
+
error: {
|
|
60
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
61
|
+
message,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
next();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const authRateLimiter = createRateLimiter({
|
|
71
|
+
windowMs: 15 * 60 * 1000,
|
|
72
|
+
max: 10,
|
|
73
|
+
message: 'Too many authentication attempts. Please try again in 15 minutes.',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const apiRateLimiter = createRateLimiter({
|
|
77
|
+
windowMs: 60 * 1000,
|
|
78
|
+
max: 120,
|
|
79
|
+
message: 'Too many API requests. Please slow down.',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export { createRateLimiter };
|
package/server/routes/agent.js
CHANGED
|
@@ -1412,8 +1412,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1412
1412
|
if (!res.writableEnded) {
|
|
1413
1413
|
writer.send({
|
|
1414
1414
|
type: 'error',
|
|
1415
|
-
error:
|
|
1416
|
-
message:
|
|
1415
|
+
error: 'Failed to process agent request.',
|
|
1416
|
+
message: 'Failed to process agent request.'
|
|
1417
1417
|
});
|
|
1418
1418
|
writer.end();
|
|
1419
1419
|
}
|
|
@@ -1435,14 +1435,13 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1435
1435
|
if (errorText) collectedError = errorText;
|
|
1436
1436
|
} catch { /* ignore — fall back to error.message */ }
|
|
1437
1437
|
}
|
|
1438
|
-
const failureDetails = describeProviderFailure(collectedError ||
|
|
1438
|
+
const failureDetails = describeProviderFailure(collectedError || 'Provider returned no assistant text.', provider);
|
|
1439
1439
|
res.status(502).json({
|
|
1440
1440
|
success: false,
|
|
1441
1441
|
sessionId: writer && typeof writer.getSessionId === 'function' ? writer.getSessionId() : null,
|
|
1442
1442
|
error: failureDetails.message,
|
|
1443
1443
|
rawError: failureDetails.rawMessage,
|
|
1444
1444
|
errorDetails: failureDetails,
|
|
1445
|
-
wrapperError: collectedError ? error.message : undefined,
|
|
1446
1445
|
messages: collectedMessages,
|
|
1447
1446
|
});
|
|
1448
1447
|
}
|
package/server/routes/auth.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
// bcryptjs is a pure-JS drop-in (same hash/compare API, same output format)
|
|
3
|
-
// — switching from native `bcrypt` here eliminated one C++ compile from
|
|
4
|
-
// the install path. Existing $2a$/$2b$ hashes in the DB remain valid;
|
|
5
|
-
// bcryptjs recognizes both prefixes so logins work across the swap.
|
|
6
2
|
import bcrypt from 'bcryptjs';
|
|
7
3
|
|
|
8
4
|
import { userDb, db } from '../database/db.js';
|
|
9
5
|
import { generateToken, authenticateToken, requireAdmin } from '../middleware/auth.js';
|
|
6
|
+
import { authRateLimiter } from '../middleware/rate-limiter.js';
|
|
7
|
+
import {
|
|
8
|
+
checkAccountLockout,
|
|
9
|
+
recordFailedLogin,
|
|
10
|
+
recordSuccessfulLogin,
|
|
11
|
+
validatePasswordPolicy,
|
|
12
|
+
validateUsername,
|
|
13
|
+
} from '../middleware/account-lockout.js';
|
|
14
|
+
import { securityLog, getClientIp } from '../utils/security-log.js';
|
|
10
15
|
import {
|
|
11
16
|
consumeQrLoginToken,
|
|
12
17
|
createQrLoginToken,
|
|
@@ -49,7 +54,10 @@ router.get('/connection-mode', (req, res) => {
|
|
|
49
54
|
res.json({ success: true, connection: getPublicRemoteConnectionConfig() });
|
|
50
55
|
});
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
// Changing the connection mode is a state-changing operation that must
|
|
58
|
+
// require authentication. Without auth, any unauthenticated caller could
|
|
59
|
+
// redirect the app to a malicious remote server.
|
|
60
|
+
router.put('/connection-mode', authenticateToken, requireAdmin, (req, res) => {
|
|
53
61
|
try {
|
|
54
62
|
res.json({ success: true, connection: saveRemoteConnectionConfig(req.body || {}) });
|
|
55
63
|
} catch (error) {
|
|
@@ -69,7 +77,7 @@ router.put('/qr-login/settings', authenticateToken, requireAdmin, (req, res) =>
|
|
|
69
77
|
}
|
|
70
78
|
});
|
|
71
79
|
|
|
72
|
-
router.post('/qr-login/token', authenticateToken, requireAdmin, (req, res) => {
|
|
80
|
+
router.post('/qr-login/token', authenticateToken, requireAdmin, authRateLimiter, (req, res) => {
|
|
73
81
|
try {
|
|
74
82
|
const baseUrl = typeof req.body?.baseUrl === 'string' && req.body.baseUrl.trim()
|
|
75
83
|
? req.body.baseUrl.trim()
|
|
@@ -81,11 +89,16 @@ router.post('/qr-login/token', authenticateToken, requireAdmin, (req, res) => {
|
|
|
81
89
|
}
|
|
82
90
|
});
|
|
83
91
|
|
|
84
|
-
router.post('/qr-login', (req, res) => {
|
|
92
|
+
router.post('/qr-login', authRateLimiter, (req, res) => {
|
|
85
93
|
try {
|
|
86
94
|
const user = consumeQrLoginToken(req.body?.token);
|
|
87
95
|
const token = generateToken(user);
|
|
88
96
|
userDb.updateLastLogin(user.id);
|
|
97
|
+
securityLog('qr_login_success', {
|
|
98
|
+
ip: getClientIp(req),
|
|
99
|
+
userId: user.id,
|
|
100
|
+
username: user.username,
|
|
101
|
+
});
|
|
89
102
|
res.json({
|
|
90
103
|
success: true,
|
|
91
104
|
user: publicUser(user),
|
|
@@ -93,22 +106,33 @@ router.post('/qr-login', (req, res) => {
|
|
|
93
106
|
});
|
|
94
107
|
} catch (error) {
|
|
95
108
|
const status = error?.code === 'QR_LOGIN_DISABLED' ? 409 : 401;
|
|
109
|
+
securityLog('qr_login_failed', {
|
|
110
|
+
ip: getClientIp(req),
|
|
111
|
+
reason: error?.code || 'unknown',
|
|
112
|
+
});
|
|
96
113
|
res.status(status).json({ success: false, error: error.message });
|
|
97
114
|
}
|
|
98
115
|
});
|
|
99
116
|
|
|
100
117
|
// User registration (setup) - only allowed if no users exist
|
|
101
|
-
router.post('/register', async (req, res) => {
|
|
118
|
+
router.post('/register', authRateLimiter, async (req, res) => {
|
|
102
119
|
try {
|
|
103
120
|
const { username, password } = req.body;
|
|
121
|
+
const clientIp = getClientIp(req);
|
|
104
122
|
|
|
105
123
|
// Validate input
|
|
106
124
|
if (!username || !password) {
|
|
107
125
|
return res.status(400).json({ error: 'Username and password are required' });
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
const usernameValidation = validateUsername(username);
|
|
129
|
+
if (!usernameValidation.valid) {
|
|
130
|
+
return res.status(400).json({ error: usernameValidation.error });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const passwordValidation = validatePasswordPolicy(password);
|
|
134
|
+
if (!passwordValidation.valid) {
|
|
135
|
+
return res.status(400).json({ error: passwordValidation.error });
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
// Use a transaction to prevent race conditions
|
|
@@ -136,6 +160,12 @@ router.post('/register', async (req, res) => {
|
|
|
136
160
|
// Update last login (non-fatal, outside transaction)
|
|
137
161
|
userDb.updateLastLogin(user.id);
|
|
138
162
|
|
|
163
|
+
securityLog('user_registered', {
|
|
164
|
+
ip: clientIp,
|
|
165
|
+
userId: user.id,
|
|
166
|
+
username: user.username,
|
|
167
|
+
});
|
|
168
|
+
|
|
139
169
|
res.json({
|
|
140
170
|
success: true,
|
|
141
171
|
user: publicUser(user),
|
|
@@ -157,32 +187,72 @@ router.post('/register', async (req, res) => {
|
|
|
157
187
|
});
|
|
158
188
|
|
|
159
189
|
// User login
|
|
160
|
-
router.post('/login', async (req, res) => {
|
|
190
|
+
router.post('/login', authRateLimiter, async (req, res) => {
|
|
161
191
|
try {
|
|
162
192
|
const { username, password } = req.body;
|
|
193
|
+
const clientIp = getClientIp(req);
|
|
163
194
|
|
|
164
195
|
// Validate input
|
|
165
196
|
if (!username || !password) {
|
|
166
197
|
return res.status(400).json({ error: 'Username and password are required' });
|
|
167
198
|
}
|
|
199
|
+
|
|
200
|
+
// Check account lockout before attempting login
|
|
201
|
+
const lockoutStatus = checkAccountLockout(String(username), clientIp);
|
|
202
|
+
if (lockoutStatus.locked) {
|
|
203
|
+
securityLog('login_blocked_locked', {
|
|
204
|
+
ip: clientIp,
|
|
205
|
+
username,
|
|
206
|
+
reason: 'Account locked due to failed attempts',
|
|
207
|
+
});
|
|
208
|
+
return res.status(423).json({ error: lockoutStatus.message });
|
|
209
|
+
}
|
|
168
210
|
|
|
169
211
|
// Get user from database
|
|
170
212
|
const user = userDb.getUserByUsername(username);
|
|
171
213
|
if (!user) {
|
|
214
|
+
const failResult = recordFailedLogin(String(username), clientIp);
|
|
215
|
+
securityLog('login_failed', {
|
|
216
|
+
ip: clientIp,
|
|
217
|
+
username,
|
|
218
|
+
reason: 'User not found',
|
|
219
|
+
});
|
|
220
|
+
if (failResult.locked) {
|
|
221
|
+
return res.status(423).json({ error: failResult.message });
|
|
222
|
+
}
|
|
172
223
|
return res.status(401).json({ error: 'Invalid username or password' });
|
|
173
224
|
}
|
|
174
225
|
|
|
175
226
|
// Verify password
|
|
176
227
|
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
|
177
228
|
if (!isValidPassword) {
|
|
229
|
+
const failResult = recordFailedLogin(String(username), clientIp);
|
|
230
|
+
securityLog('login_failed', {
|
|
231
|
+
ip: clientIp,
|
|
232
|
+
username,
|
|
233
|
+
userId: user.id,
|
|
234
|
+
reason: 'Invalid password',
|
|
235
|
+
});
|
|
236
|
+
if (failResult.locked) {
|
|
237
|
+
return res.status(423).json({ error: failResult.message });
|
|
238
|
+
}
|
|
178
239
|
return res.status(401).json({ error: 'Invalid username or password' });
|
|
179
240
|
}
|
|
241
|
+
|
|
242
|
+
// Clear failed attempts on successful login
|
|
243
|
+
recordSuccessfulLogin(String(username), clientIp);
|
|
180
244
|
|
|
181
245
|
// Generate token
|
|
182
246
|
const token = generateToken(user);
|
|
183
247
|
|
|
184
248
|
// Update last login
|
|
185
249
|
userDb.updateLastLogin(user.id);
|
|
250
|
+
|
|
251
|
+
securityLog('login_success', {
|
|
252
|
+
ip: clientIp,
|
|
253
|
+
userId: user.id,
|
|
254
|
+
username: user.username,
|
|
255
|
+
});
|
|
186
256
|
|
|
187
257
|
res.json({
|
|
188
258
|
success: true,
|
|
@@ -205,8 +275,11 @@ router.get('/user', authenticateToken, (req, res) => {
|
|
|
205
275
|
|
|
206
276
|
// Logout (client-side token removal, but this endpoint can be used for logging)
|
|
207
277
|
router.post('/logout', authenticateToken, (req, res) => {
|
|
208
|
-
|
|
209
|
-
|
|
278
|
+
securityLog('user_logout', {
|
|
279
|
+
ip: getClientIp(req),
|
|
280
|
+
userId: req.user?.id,
|
|
281
|
+
username: req.user?.username,
|
|
282
|
+
});
|
|
210
283
|
res.json({ success: true, message: 'Logged out successfully' });
|
|
211
284
|
});
|
|
212
285
|
|
package/server/routes/codex.js
CHANGED
|
@@ -13,7 +13,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
|
|
|
13
13
|
res.json({ success: true });
|
|
14
14
|
} catch (error) {
|
|
15
15
|
console.error(`Error deleting Codex session ${req.params.sessionId}:`, error);
|
|
16
|
-
res.status(500).json({ success: false, error: error
|
|
16
|
+
res.status(500).json({ success: false, error: "Internal server error" });
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
|
|
3
3
|
import { collectDiagnostics } from '../services/diagnostics.js';
|
|
4
|
+
import { requireAdmin } from '../middleware/auth.js';
|
|
4
5
|
|
|
5
6
|
const router = express.Router();
|
|
6
7
|
|
|
@@ -16,11 +17,13 @@ function buildDiagnostics(req) {
|
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// Diagnostics expose internal system state (active sessions, errors,
|
|
21
|
+
// provider health) — restrict to admins to prevent information leakage.
|
|
22
|
+
router.get('/', requireAdmin, (req, res) => {
|
|
20
23
|
res.json(buildDiagnostics(req));
|
|
21
24
|
});
|
|
22
25
|
|
|
23
|
-
router.post('/refresh', (req, res) => {
|
|
26
|
+
router.post('/refresh', requireAdmin, (req, res) => {
|
|
24
27
|
req.app.locals.diagnosticsCache = {
|
|
25
28
|
...(req.app.locals.diagnosticsCache || {}),
|
|
26
29
|
diagnosticsUpdatedAt: new Date().toISOString(),
|
|
@@ -29,7 +32,7 @@ router.post('/refresh', (req, res) => {
|
|
|
29
32
|
res.json(buildDiagnostics(req));
|
|
30
33
|
});
|
|
31
34
|
|
|
32
|
-
router.get('/bundle', (req, res) => {
|
|
35
|
+
router.get('/bundle', requireAdmin, (req, res) => {
|
|
33
36
|
const diagnostics = buildDiagnostics(req);
|
|
34
37
|
res.json({
|
|
35
38
|
generatedAt: diagnostics.timestamp,
|
package/server/routes/gemini.js
CHANGED
|
@@ -18,7 +18,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
|
|
|
18
18
|
res.json({ success: true });
|
|
19
19
|
} catch (error) {
|
|
20
20
|
console.error(`Error deleting Gemini session ${req.params.sessionId}:`, error);
|
|
21
|
-
res.status(500).json({ success: false, error: error
|
|
21
|
+
res.status(500).json({ success: false, error: "Internal server error" });
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
|
package/server/routes/git.js
CHANGED
|
@@ -887,7 +887,7 @@ router.post('/initial-commit', async (req, res) => {
|
|
|
887
887
|
});
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
-
res.status(500).json({ error: error
|
|
890
|
+
res.status(500).json({ error: "Internal server error" });
|
|
891
891
|
}
|
|
892
892
|
});
|
|
893
893
|
|
|
@@ -918,7 +918,7 @@ router.post('/commit', async (req, res) => {
|
|
|
918
918
|
res.json({ success: true, output: stdout });
|
|
919
919
|
} catch (error) {
|
|
920
920
|
console.error('Git commit error:', error);
|
|
921
|
-
res.status(500).json({ error: error
|
|
921
|
+
res.status(500).json({ error: "Internal server error" });
|
|
922
922
|
}
|
|
923
923
|
});
|
|
924
924
|
|
|
@@ -965,7 +965,7 @@ router.post('/revert-local-commit', async (req, res) => {
|
|
|
965
965
|
});
|
|
966
966
|
} catch (error) {
|
|
967
967
|
console.error('Git revert local commit error:', error);
|
|
968
|
-
res.status(500).json({ error: error
|
|
968
|
+
res.status(500).json({ error: "Internal server error" });
|
|
969
969
|
}
|
|
970
970
|
});
|
|
971
971
|
|
|
@@ -1031,7 +1031,7 @@ router.post('/checkout', async (req, res) => {
|
|
|
1031
1031
|
res.json({ success: true, output: stdout });
|
|
1032
1032
|
} catch (error) {
|
|
1033
1033
|
console.error('Git checkout error:', error);
|
|
1034
|
-
res.status(500).json({ error: error
|
|
1034
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1035
1035
|
}
|
|
1036
1036
|
});
|
|
1037
1037
|
|
|
@@ -1053,7 +1053,7 @@ router.post('/create-branch', async (req, res) => {
|
|
|
1053
1053
|
res.json({ success: true, output: stdout });
|
|
1054
1054
|
} catch (error) {
|
|
1055
1055
|
console.error('Git create branch error:', error);
|
|
1056
|
-
res.status(500).json({ error: error
|
|
1056
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1057
1057
|
}
|
|
1058
1058
|
});
|
|
1059
1059
|
|
|
@@ -1079,7 +1079,7 @@ router.post('/delete-branch', async (req, res) => {
|
|
|
1079
1079
|
res.json({ success: true, output: stdout });
|
|
1080
1080
|
} catch (error) {
|
|
1081
1081
|
console.error('Git delete branch error:', error);
|
|
1082
|
-
res.status(500).json({ error: error
|
|
1082
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1083
1083
|
}
|
|
1084
1084
|
});
|
|
1085
1085
|
|
|
@@ -1234,7 +1234,7 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
1234
1234
|
res.json({ message });
|
|
1235
1235
|
} catch (error) {
|
|
1236
1236
|
console.error('Generate commit message error:', error);
|
|
1237
|
-
res.status(500).json({ error: error
|
|
1237
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1238
1238
|
}
|
|
1239
1239
|
});
|
|
1240
1240
|
|
|
@@ -1764,7 +1764,7 @@ router.post('/discard', async (req, res) => {
|
|
|
1764
1764
|
res.json({ success: true, message: `Changes discarded for ${repositoryRelativeFilePath}` });
|
|
1765
1765
|
} catch (error) {
|
|
1766
1766
|
console.error('Git discard error:', error);
|
|
1767
|
-
res.status(500).json({ error: error
|
|
1767
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1768
1768
|
}
|
|
1769
1769
|
});
|
|
1770
1770
|
|
|
@@ -1815,7 +1815,7 @@ router.post('/delete-untracked', async (req, res) => {
|
|
|
1815
1815
|
}
|
|
1816
1816
|
} catch (error) {
|
|
1817
1817
|
console.error('Git delete untracked error:', error);
|
|
1818
|
-
res.status(500).json({ error: error
|
|
1818
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1819
1819
|
}
|
|
1820
1820
|
});
|
|
1821
1821
|
|
|
@@ -298,7 +298,7 @@ router.post('/:projectName/restart', requireLiveViewProjectAccess('useShell'), a
|
|
|
298
298
|
environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
|
|
299
299
|
});
|
|
300
300
|
} catch (error) {
|
|
301
|
-
res.status(500).json({ error:
|
|
301
|
+
res.status(500).json({ error: 'Failed to restart Live View' });
|
|
302
302
|
}
|
|
303
303
|
});
|
|
304
304
|
|
|
@@ -315,7 +315,7 @@ router.post('/:projectName/stop', requireLiveViewProjectAccess('useShell'), asyn
|
|
|
315
315
|
environment: attachEnvironmentRuntimeState(null, urls, tunnel),
|
|
316
316
|
});
|
|
317
317
|
} catch (error) {
|
|
318
|
-
res.status(500).json({ error:
|
|
318
|
+
res.status(500).json({ error: 'Failed to stop Live View' });
|
|
319
319
|
}
|
|
320
320
|
});
|
|
321
321
|
|