@pixelbyte-software/pixcode 1.51.3 → 1.51.5
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-17CwxHSZ.js → index-CpbSjzK7.js} +179 -182
- package/dist/assets/index-oDq76nSq.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/cli.js +1 -1
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/database/db.js +14 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +592 -55
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +35 -7
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +33 -17
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/routes/auth.js +12 -5
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +20 -9
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/git.js +12 -0
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/live-view.js +30 -7
- package/dist-server/server/routes/live-view.js.map +1 -1
- package/dist-server/server/routes/messages.js +30 -0
- package/dist-server/server/routes/messages.js.map +1 -1
- package/dist-server/server/routes/network.js +3 -2
- package/dist-server/server/routes/network.js.map +1 -1
- package/dist-server/server/routes/platformization.js +7 -6
- package/dist-server/server/routes/platformization.js.map +1 -1
- package/dist-server/server/routes/projects.js +4 -3
- package/dist-server/server/routes/projects.js.map +1 -1
- package/dist-server/server/routes/remote.js +2 -1
- package/dist-server/server/routes/remote.js.map +1 -1
- package/dist-server/server/services/platformization.js +131 -2
- package/dist-server/server/services/platformization.js.map +1 -1
- package/package.json +1 -1
- package/scripts/update-git-install.mjs +15 -10
- package/server/cli.js +4 -4
- package/server/database/db.js +39 -26
- package/server/index.js +618 -55
- package/server/middleware/auth.js +59 -20
- package/server/modules/providers/provider.routes.ts +91 -53
- package/server/routes/auth.js +25 -17
- package/server/routes/commands.js +43 -32
- package/server/routes/git.js +24 -9
- package/server/routes/live-view.js +47 -24
- package/server/routes/messages.js +47 -12
- package/server/routes/network.js +9 -8
- package/server/routes/platformization.js +22 -21
- package/server/routes/projects.js +7 -6
- package/server/routes/remote.js +6 -5
- package/server/services/platformization.js +169 -31
- package/dist/assets/index-B9N-gfOQ.css +0 -32
|
@@ -4,8 +4,10 @@ import { userDb, appConfigDb, apiKeysDb } from '../database/db.js';
|
|
|
4
4
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
5
5
|
|
|
6
6
|
// Use env var if set, otherwise auto-generate a unique secret per installation
|
|
7
|
-
const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
|
|
8
|
-
const isPixcodeApiKey = (token) => typeof token === 'string' && (token.startsWith('px_') || token.startsWith('ck_'));
|
|
7
|
+
const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
|
|
8
|
+
const isPixcodeApiKey = (token) => typeof token === 'string' && (token.startsWith('px_') || token.startsWith('ck_'));
|
|
9
|
+
const ADMIN_ROLES = new Set(['owner', 'admin']);
|
|
10
|
+
const PLATFORM_AUTH_BYPASS_ENABLED = IS_PLATFORM && process.env.PIXCODE_ALLOW_PLATFORM_AUTH_BYPASS === '1';
|
|
9
11
|
|
|
10
12
|
// Optional API key middleware
|
|
11
13
|
const validateApiKey = (req, res, next) => {
|
|
@@ -22,9 +24,9 @@ const validateApiKey = (req, res, next) => {
|
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
// JWT authentication middleware
|
|
25
|
-
const authenticateToken = async (req, res, next) => {
|
|
27
|
+
const authenticateToken = async (req, res, next) => {
|
|
26
28
|
// Platform mode: use single database user
|
|
27
|
-
if (
|
|
29
|
+
if (PLATFORM_AUTH_BYPASS_ENABLED) {
|
|
28
30
|
try {
|
|
29
31
|
const user = userDb.getFirstUser();
|
|
30
32
|
if (!user) {
|
|
@@ -101,15 +103,50 @@ const authenticateToken = async (req, res, next) => {
|
|
|
101
103
|
console.error('Token verification error:', error);
|
|
102
104
|
return res.status(403).json({ error: 'Invalid token' });
|
|
103
105
|
}
|
|
104
|
-
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const requireAdmin = (req, res, next) => {
|
|
109
|
+
if (!req.user) {
|
|
110
|
+
return res.status(401).json({ error: 'Access denied. No authenticated user.' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!ADMIN_ROLES.has(req.user.role)) {
|
|
114
|
+
return res.status(403).json({ error: 'Admin access required.' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
req.user.api_key_id &&
|
|
119
|
+
!req.user.api_key_scopes?.includes('admin') &&
|
|
120
|
+
!req.user.api_key_scopes?.includes('system') &&
|
|
121
|
+
!req.user.api_key_scopes?.includes('*')
|
|
122
|
+
) {
|
|
123
|
+
return res.status(403).json({ error: 'API key lacks admin scope.' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
next();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const requireApiScope = (scope) => (req, res, next) => {
|
|
130
|
+
if (!req.user?.api_key_id) {
|
|
131
|
+
return next();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scopes = Array.isArray(req.user.api_key_scopes) ? req.user.api_key_scopes : [];
|
|
135
|
+
if (scopes.includes('*') || scopes.includes(scope)) {
|
|
136
|
+
return next();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return res.status(403).json({ error: `API key lacks required scope: ${scope}` });
|
|
140
|
+
};
|
|
105
141
|
|
|
106
142
|
// Generate JWT token
|
|
107
143
|
const generateToken = (user) => {
|
|
108
144
|
return jwt.sign(
|
|
109
145
|
{
|
|
110
|
-
userId: user.id,
|
|
111
|
-
username: user.username
|
|
112
|
-
|
|
146
|
+
userId: user.id,
|
|
147
|
+
username: user.username,
|
|
148
|
+
role: user.role || null,
|
|
149
|
+
},
|
|
113
150
|
JWT_SECRET,
|
|
114
151
|
{ expiresIn: '7d' }
|
|
115
152
|
);
|
|
@@ -118,12 +155,12 @@ const generateToken = (user) => {
|
|
|
118
155
|
// WebSocket authentication function
|
|
119
156
|
const authenticateWebSocket = (token) => {
|
|
120
157
|
// Platform mode: bypass token validation, return first user
|
|
121
|
-
if (
|
|
158
|
+
if (PLATFORM_AUTH_BYPASS_ENABLED) {
|
|
122
159
|
try {
|
|
123
|
-
const user = userDb.getFirstUser();
|
|
124
|
-
if (user) {
|
|
125
|
-
return { id: user.id, userId: user.id, username: user.username };
|
|
126
|
-
}
|
|
160
|
+
const user = userDb.getFirstUser();
|
|
161
|
+
if (user) {
|
|
162
|
+
return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
|
|
163
|
+
}
|
|
127
164
|
return null;
|
|
128
165
|
} catch (error) {
|
|
129
166
|
console.error('Platform mode WebSocket error:', error);
|
|
@@ -143,9 +180,9 @@ const authenticateWebSocket = (token) => {
|
|
|
143
180
|
|
|
144
181
|
if (isPixcodeApiKey(token)) {
|
|
145
182
|
try {
|
|
146
|
-
const user = apiKeysDb.validateApiKey(token);
|
|
147
|
-
if (!user) return null;
|
|
148
|
-
return { userId: user.id, username: user.username };
|
|
183
|
+
const user = apiKeysDb.validateApiKey(token);
|
|
184
|
+
if (!user) return null;
|
|
185
|
+
return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
|
|
149
186
|
} catch (error) {
|
|
150
187
|
console.error('WebSocket API key validation error:', error);
|
|
151
188
|
return null;
|
|
@@ -159,7 +196,7 @@ const authenticateWebSocket = (token) => {
|
|
|
159
196
|
if (!user) {
|
|
160
197
|
return null;
|
|
161
198
|
}
|
|
162
|
-
return { userId: user.id, username: user.username };
|
|
199
|
+
return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
|
|
163
200
|
} catch (error) {
|
|
164
201
|
console.error('WebSocket token verification error:', error);
|
|
165
202
|
return null;
|
|
@@ -167,9 +204,11 @@ const authenticateWebSocket = (token) => {
|
|
|
167
204
|
};
|
|
168
205
|
|
|
169
206
|
export {
|
|
170
|
-
validateApiKey,
|
|
171
|
-
authenticateToken,
|
|
172
|
-
|
|
207
|
+
validateApiKey,
|
|
208
|
+
authenticateToken,
|
|
209
|
+
requireAdmin,
|
|
210
|
+
requireApiScope,
|
|
211
|
+
generateToken,
|
|
173
212
|
authenticateWebSocket,
|
|
174
213
|
JWT_SECRET
|
|
175
214
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable import-x/order */
|
|
2
|
-
import express, { type Request, type Response } from 'express';
|
|
2
|
+
import express, { type NextFunction, type Request, type Response } from 'express';
|
|
3
3
|
|
|
4
4
|
import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
|
|
5
5
|
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
@@ -101,7 +101,28 @@ const PROVIDER_MANUAL_INSTALL: Partial<Record<LLMProvider, {
|
|
|
101
101
|
},
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
const router = express.Router();
|
|
104
|
+
const router = express.Router();
|
|
105
|
+
const ADMIN_ROLES = new Set(['owner', 'admin']);
|
|
106
|
+
|
|
107
|
+
function requireProviderAdmin(req: Request, res: Response, next: NextFunction) {
|
|
108
|
+
const user = (req as Request & { user?: { role?: string; api_key_id?: number; api_key_scopes?: string[] } }).user;
|
|
109
|
+
if (!user || !ADMIN_ROLES.has(user.role || '')) {
|
|
110
|
+
res.status(403).json({ error: 'Admin access required.' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
user.api_key_id &&
|
|
116
|
+
!user.api_key_scopes?.includes('admin') &&
|
|
117
|
+
!user.api_key_scopes?.includes('system') &&
|
|
118
|
+
!user.api_key_scopes?.includes('*')
|
|
119
|
+
) {
|
|
120
|
+
res.status(403).json({ error: 'API key lacks admin scope.' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
next();
|
|
125
|
+
}
|
|
105
126
|
|
|
106
127
|
const readPathParam = (value: unknown, name: string): string => {
|
|
107
128
|
if (typeof value === 'string') {
|
|
@@ -260,9 +281,10 @@ router.get(
|
|
|
260
281
|
}),
|
|
261
282
|
);
|
|
262
283
|
|
|
263
|
-
router.get(
|
|
264
|
-
'/:provider/mcp/servers',
|
|
265
|
-
|
|
284
|
+
router.get(
|
|
285
|
+
'/:provider/mcp/servers',
|
|
286
|
+
requireProviderAdmin,
|
|
287
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
266
288
|
const provider = parseProvider(req.params.provider);
|
|
267
289
|
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
|
268
290
|
const scope = parseMcpScope(req.query.scope);
|
|
@@ -278,9 +300,10 @@ router.get(
|
|
|
278
300
|
}),
|
|
279
301
|
);
|
|
280
302
|
|
|
281
|
-
router.post(
|
|
282
|
-
'/:provider/mcp/servers',
|
|
283
|
-
|
|
303
|
+
router.post(
|
|
304
|
+
'/:provider/mcp/servers',
|
|
305
|
+
requireProviderAdmin,
|
|
306
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
284
307
|
const provider = parseProvider(req.params.provider);
|
|
285
308
|
const payload = parseMcpUpsertPayload(req.body);
|
|
286
309
|
const server = await providerMcpService.upsertProviderMcpServer(provider, payload);
|
|
@@ -288,9 +311,10 @@ router.post(
|
|
|
288
311
|
}),
|
|
289
312
|
);
|
|
290
313
|
|
|
291
|
-
router.delete(
|
|
292
|
-
'/:provider/mcp/servers/:name',
|
|
293
|
-
|
|
314
|
+
router.delete(
|
|
315
|
+
'/:provider/mcp/servers/:name',
|
|
316
|
+
requireProviderAdmin,
|
|
317
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
294
318
|
const provider = parseProvider(req.params.provider);
|
|
295
319
|
const scope = parseMcpScope(req.query.scope);
|
|
296
320
|
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
|
@@ -308,9 +332,10 @@ router.delete(
|
|
|
308
332
|
* Summary for every provider (hasKey + baseUrl + updatedAt). Used by the
|
|
309
333
|
* Settings UI to pre-fill the "API Key" tab.
|
|
310
334
|
*/
|
|
311
|
-
router.get(
|
|
312
|
-
'/credentials',
|
|
313
|
-
|
|
335
|
+
router.get(
|
|
336
|
+
'/credentials',
|
|
337
|
+
requireProviderAdmin,
|
|
338
|
+
asyncHandler(async (_req: Request, res: Response) => {
|
|
314
339
|
const summaries = await listProviderCredentialSummaries();
|
|
315
340
|
res.json(createApiSuccessResponse(summaries));
|
|
316
341
|
}),
|
|
@@ -322,9 +347,10 @@ router.get(
|
|
|
322
347
|
* ~/.pixcode/provider-credentials.json and applies them to process.env
|
|
323
348
|
* so the next CLI spawn/SDK call picks them up. Empty apiKey clears.
|
|
324
349
|
*/
|
|
325
|
-
router.post(
|
|
326
|
-
'/:provider/auth/api-key',
|
|
327
|
-
|
|
350
|
+
router.post(
|
|
351
|
+
'/:provider/auth/api-key',
|
|
352
|
+
requireProviderAdmin,
|
|
353
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
328
354
|
const provider = parseProvider(req.params.provider);
|
|
329
355
|
if (!(provider in PROVIDER_ENV_VARS)) {
|
|
330
356
|
throw new AppError(`Provider "${provider}" does not accept API-key auth.`, {
|
|
@@ -357,9 +383,10 @@ router.post(
|
|
|
357
383
|
* GET to the VPS-side 127.0.0.1:PORT so the CLI's local handler completes
|
|
358
384
|
* the token exchange.
|
|
359
385
|
*/
|
|
360
|
-
router.post(
|
|
361
|
-
'/:provider/oauth-paste',
|
|
362
|
-
|
|
386
|
+
router.post(
|
|
387
|
+
'/:provider/oauth-paste',
|
|
388
|
+
requireProviderAdmin,
|
|
389
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
363
390
|
parseProvider(req.params.provider); // validate id but we don't use it further
|
|
364
391
|
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
365
392
|
const raw = typeof body.callbackUrl === 'string' ? body.callbackUrl.trim() : '';
|
|
@@ -440,9 +467,10 @@ router.get(
|
|
|
440
467
|
}),
|
|
441
468
|
);
|
|
442
469
|
|
|
443
|
-
router.delete(
|
|
444
|
-
'/:provider/models/cache',
|
|
445
|
-
|
|
470
|
+
router.delete(
|
|
471
|
+
'/:provider/models/cache',
|
|
472
|
+
requireProviderAdmin,
|
|
473
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
446
474
|
const provider = parseProvider(req.params.provider);
|
|
447
475
|
await clearProviderModelRegistryCache(provider);
|
|
448
476
|
res.json(createApiSuccessResponse({ cleared: true, provider }));
|
|
@@ -459,9 +487,10 @@ router.delete(
|
|
|
459
487
|
* proxies, service-worker reloads, or Vite HMR and short-circuit
|
|
460
488
|
* an in-flight install. The child now outlives the request.
|
|
461
489
|
*/
|
|
462
|
-
router.post(
|
|
463
|
-
'/:provider/install',
|
|
464
|
-
|
|
490
|
+
router.post(
|
|
491
|
+
'/:provider/install',
|
|
492
|
+
requireProviderAdmin,
|
|
493
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
465
494
|
const parsed = parseProvider(req.params.provider);
|
|
466
495
|
const packageName = PROVIDER_INSTALL_PACKAGES[parsed];
|
|
467
496
|
const installCmd = PROVIDER_INSTALL_COMMANDS[parsed];
|
|
@@ -502,9 +531,10 @@ router.post(
|
|
|
502
531
|
* ?token=... as a fallback auth channel (same pattern the search
|
|
503
532
|
* endpoint uses).
|
|
504
533
|
*/
|
|
505
|
-
router.get(
|
|
506
|
-
'/:provider/install/:jobId/stream',
|
|
507
|
-
|
|
534
|
+
router.get(
|
|
535
|
+
'/:provider/install/:jobId/stream',
|
|
536
|
+
requireProviderAdmin,
|
|
537
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
508
538
|
const parsed = parseProvider(req.params.provider);
|
|
509
539
|
const jobId = readPathParam(req.params.jobId, 'jobId');
|
|
510
540
|
const job = getInstallJob(jobId);
|
|
@@ -582,9 +612,10 @@ router.get(
|
|
|
582
612
|
}),
|
|
583
613
|
);
|
|
584
614
|
|
|
585
|
-
router.delete(
|
|
586
|
-
'/:provider/install/:jobId',
|
|
587
|
-
|
|
615
|
+
router.delete(
|
|
616
|
+
'/:provider/install/:jobId',
|
|
617
|
+
requireProviderAdmin,
|
|
618
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
588
619
|
const parsed = parseProvider(req.params.provider);
|
|
589
620
|
const jobId = readPathParam(req.params.jobId, 'jobId');
|
|
590
621
|
const job = getInstallJob(jobId);
|
|
@@ -599,9 +630,10 @@ router.delete(
|
|
|
599
630
|
}),
|
|
600
631
|
);
|
|
601
632
|
|
|
602
|
-
router.post(
|
|
603
|
-
'/mcp/servers/global',
|
|
604
|
-
|
|
633
|
+
router.post(
|
|
634
|
+
'/mcp/servers/global',
|
|
635
|
+
requireProviderAdmin,
|
|
636
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
605
637
|
const payload = parseMcpUpsertPayload(req.body);
|
|
606
638
|
if (payload.scope === 'local') {
|
|
607
639
|
throw new AppError('Global MCP add supports only "user" or "project" scopes.', {
|
|
@@ -729,9 +761,10 @@ async function buildProviderPluginState(provider: string) {
|
|
|
729
761
|
};
|
|
730
762
|
}
|
|
731
763
|
|
|
732
|
-
router.get(
|
|
733
|
-
'/plugin-state',
|
|
734
|
-
|
|
764
|
+
router.get(
|
|
765
|
+
'/plugin-state',
|
|
766
|
+
requireProviderAdmin,
|
|
767
|
+
asyncHandler(async (_req: Request, res: Response) => {
|
|
735
768
|
const providers = await Promise.all(
|
|
736
769
|
Object.keys(PROVIDER_CONFIG_FILES).map((provider) => buildProviderPluginState(provider)),
|
|
737
770
|
);
|
|
@@ -739,17 +772,19 @@ router.get(
|
|
|
739
772
|
}),
|
|
740
773
|
);
|
|
741
774
|
|
|
742
|
-
router.get(
|
|
743
|
-
'/plugin-state/:provider',
|
|
744
|
-
|
|
775
|
+
router.get(
|
|
776
|
+
'/plugin-state/:provider',
|
|
777
|
+
requireProviderAdmin,
|
|
778
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
745
779
|
const provider = parseProvider(req.params.provider);
|
|
746
780
|
res.json(createApiSuccessResponse(await buildProviderPluginState(provider)));
|
|
747
781
|
}),
|
|
748
782
|
);
|
|
749
783
|
|
|
750
|
-
router.get(
|
|
751
|
-
'/:provider/config-files',
|
|
752
|
-
|
|
784
|
+
router.get(
|
|
785
|
+
'/:provider/config-files',
|
|
786
|
+
requireProviderAdmin,
|
|
787
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
753
788
|
const provider = String(req.params.provider);
|
|
754
789
|
const list = PROVIDER_CONFIG_FILES[provider];
|
|
755
790
|
if (!list) {
|
|
@@ -795,9 +830,10 @@ router.get(
|
|
|
795
830
|
}),
|
|
796
831
|
);
|
|
797
832
|
|
|
798
|
-
router.get(
|
|
799
|
-
'/:provider/config-files/:fileId',
|
|
800
|
-
|
|
833
|
+
router.get(
|
|
834
|
+
'/:provider/config-files/:fileId',
|
|
835
|
+
requireProviderAdmin,
|
|
836
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
801
837
|
const provider = String(req.params.provider);
|
|
802
838
|
const fileId = String(req.params.fileId);
|
|
803
839
|
const { descriptor, absolutePath } = resolveConfigFile(provider, fileId);
|
|
@@ -852,9 +888,10 @@ router.get(
|
|
|
852
888
|
}),
|
|
853
889
|
);
|
|
854
890
|
|
|
855
|
-
router.put(
|
|
856
|
-
'/:provider/config-files/:fileId',
|
|
857
|
-
|
|
891
|
+
router.put(
|
|
892
|
+
'/:provider/config-files/:fileId',
|
|
893
|
+
requireProviderAdmin,
|
|
894
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
858
895
|
const provider = String(req.params.provider);
|
|
859
896
|
const fileId = String(req.params.fileId);
|
|
860
897
|
const { descriptor, absolutePath } = resolveConfigFile(provider, fileId);
|
|
@@ -903,9 +940,10 @@ router.put(
|
|
|
903
940
|
}),
|
|
904
941
|
);
|
|
905
942
|
|
|
906
|
-
router.post(
|
|
907
|
-
'/:provider/config-files/:fileId/validate',
|
|
908
|
-
|
|
943
|
+
router.post(
|
|
944
|
+
'/:provider/config-files/:fileId/validate',
|
|
945
|
+
requireProviderAdmin,
|
|
946
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
909
947
|
const provider = String(req.params.provider);
|
|
910
948
|
const fileId = String(req.params.fileId);
|
|
911
949
|
const { descriptor, absolutePath } = resolveConfigFile(provider, fileId);
|
package/server/routes/auth.js
CHANGED
|
@@ -12,7 +12,15 @@ import {
|
|
|
12
12
|
saveRemoteConnectionConfig,
|
|
13
13
|
} from '../services/remote-connection.js';
|
|
14
14
|
|
|
15
|
-
const router = express.Router();
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
|
|
17
|
+
function publicUser(user) {
|
|
18
|
+
return {
|
|
19
|
+
id: user.id,
|
|
20
|
+
username: user.username,
|
|
21
|
+
role: user.role || 'member',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
16
24
|
|
|
17
25
|
// Check auth status and setup requirements
|
|
18
26
|
router.get('/status', async (req, res) => {
|
|
@@ -60,19 +68,19 @@ router.post('/register', async (req, res) => {
|
|
|
60
68
|
// Use a transaction to prevent race conditions
|
|
61
69
|
db.prepare('BEGIN').run();
|
|
62
70
|
try {
|
|
63
|
-
// Check if users already exist
|
|
64
|
-
const hasUsers = userDb.hasUsers();
|
|
65
|
-
if (hasUsers) {
|
|
66
|
-
db.prepare('ROLLBACK').run();
|
|
67
|
-
return res.status(403).json({ error: '
|
|
68
|
-
}
|
|
71
|
+
// Check if users already exist. Additional accounts are created by admins.
|
|
72
|
+
const hasUsers = userDb.hasUsers();
|
|
73
|
+
if (hasUsers) {
|
|
74
|
+
db.prepare('ROLLBACK').run();
|
|
75
|
+
return res.status(403).json({ error: 'Initial admin already exists. Ask an admin to create another account.' });
|
|
76
|
+
}
|
|
69
77
|
|
|
70
78
|
// Hash password
|
|
71
79
|
const saltRounds = 12;
|
|
72
80
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
|
73
81
|
|
|
74
82
|
// Create user
|
|
75
|
-
const user = userDb.createUser(username, passwordHash);
|
|
83
|
+
const user = userDb.createUser(username, passwordHash, { role: 'admin' });
|
|
76
84
|
|
|
77
85
|
// Generate token
|
|
78
86
|
const token = generateToken(user);
|
|
@@ -83,10 +91,10 @@ router.post('/register', async (req, res) => {
|
|
|
83
91
|
userDb.updateLastLogin(user.id);
|
|
84
92
|
|
|
85
93
|
res.json({
|
|
86
|
-
success: true,
|
|
87
|
-
user:
|
|
88
|
-
token
|
|
89
|
-
});
|
|
94
|
+
success: true,
|
|
95
|
+
user: publicUser(user),
|
|
96
|
+
token
|
|
97
|
+
});
|
|
90
98
|
} catch (error) {
|
|
91
99
|
db.prepare('ROLLBACK').run();
|
|
92
100
|
throw error;
|
|
@@ -130,11 +138,11 @@ router.post('/login', async (req, res) => {
|
|
|
130
138
|
// Update last login
|
|
131
139
|
userDb.updateLastLogin(user.id);
|
|
132
140
|
|
|
133
|
-
res.json({
|
|
134
|
-
success: true,
|
|
135
|
-
user:
|
|
136
|
-
token
|
|
137
|
-
});
|
|
141
|
+
res.json({
|
|
142
|
+
success: true,
|
|
143
|
+
user: publicUser(user),
|
|
144
|
+
token
|
|
145
|
+
});
|
|
138
146
|
|
|
139
147
|
} catch (error) {
|
|
140
148
|
console.error('Login error:', error);
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
getProviderModelRegistryEntry,
|
|
11
11
|
getStaticProviderModels,
|
|
12
12
|
} from '../services/model-registry.js';
|
|
13
|
-
import { parseFrontmatter } from '../utils/frontmatter.js';
|
|
14
|
-
import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
|
|
13
|
+
import { parseFrontmatter } from '../utils/frontmatter.js';
|
|
14
|
+
import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
|
|
15
|
+
import { isAdminUser, userHasProjectPathAccess } from '../services/platformization.js';
|
|
15
16
|
|
|
16
17
|
const __dirname = getModuleDir(import.meta.url);
|
|
17
18
|
// This route reads the top-level package.json for the status command, so it needs the real
|
|
@@ -421,31 +422,37 @@ Custom commands can be created in:
|
|
|
421
422
|
* POST /api/commands/list
|
|
422
423
|
* List all available commands from project and user directories
|
|
423
424
|
*/
|
|
424
|
-
router.post('/list', async (req, res) => {
|
|
425
|
-
try {
|
|
426
|
-
const { projectPath } = req.body;
|
|
427
|
-
const allCommands = [...builtInCommands];
|
|
428
|
-
|
|
429
|
-
// Scan project-level commands (.claude/commands/)
|
|
430
|
-
if (projectPath) {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
425
|
+
router.post('/list', async (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const { projectPath } = req.body;
|
|
428
|
+
const allCommands = [...builtInCommands];
|
|
429
|
+
|
|
430
|
+
// Scan project-level commands (.claude/commands/)
|
|
431
|
+
if (projectPath) {
|
|
432
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
433
|
+
if (!userHasProjectPathAccess(req.user, { fullPath: resolvedProjectPath, path: resolvedProjectPath, projectPath: resolvedProjectPath }, resolvedProjectPath, 'viewFiles')) {
|
|
434
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
435
|
+
}
|
|
436
|
+
const projectCommandsDir = path.join(resolvedProjectPath, '.claude', 'commands');
|
|
437
|
+
const projectCommands = await scanCommandsDirectory(
|
|
438
|
+
projectCommandsDir,
|
|
439
|
+
projectCommandsDir,
|
|
435
440
|
'project'
|
|
436
441
|
);
|
|
437
442
|
allCommands.push(...projectCommands);
|
|
438
443
|
}
|
|
439
444
|
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
// User-level commands belong to the server OS account; expose them only to admins.
|
|
446
|
+
if (isAdminUser(req.user)) {
|
|
447
|
+
const homeDir = os.homedir();
|
|
448
|
+
const userCommandsDir = path.join(homeDir, '.claude', 'commands');
|
|
449
|
+
const userCommands = await scanCommandsDirectory(
|
|
450
|
+
userCommandsDir,
|
|
451
|
+
userCommandsDir,
|
|
452
|
+
'user'
|
|
453
|
+
);
|
|
454
|
+
allCommands.push(...userCommands);
|
|
455
|
+
}
|
|
449
456
|
|
|
450
457
|
// Separate built-in and custom commands
|
|
451
458
|
const customCommands = allCommands.filter(cmd => cmd.namespace !== 'builtin');
|
|
@@ -509,20 +516,24 @@ router.post('/execute', async (req, res) => {
|
|
|
509
516
|
});
|
|
510
517
|
}
|
|
511
518
|
|
|
512
|
-
// Load command content
|
|
513
|
-
// Security: validate commandPath is within allowed directories
|
|
514
|
-
{
|
|
515
|
-
const resolvedPath = path.resolve(commandPath);
|
|
516
|
-
const userBase = path.resolve(path.join(os.homedir(), '.claude', 'commands'));
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
:
|
|
519
|
+
// Load command content
|
|
520
|
+
// Security: validate commandPath is within allowed directories
|
|
521
|
+
{
|
|
522
|
+
const resolvedPath = path.resolve(commandPath);
|
|
523
|
+
const userBase = path.resolve(path.join(os.homedir(), '.claude', 'commands'));
|
|
524
|
+
const contextProjectPath = typeof context?.projectPath === 'string' ? path.resolve(context.projectPath) : null;
|
|
525
|
+
if (contextProjectPath && !userHasProjectPathAccess(req.user, { fullPath: contextProjectPath, path: contextProjectPath, projectPath: contextProjectPath }, resolvedPath, 'viewFiles')) {
|
|
526
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
527
|
+
}
|
|
528
|
+
const projectBase = contextProjectPath
|
|
529
|
+
? path.resolve(path.join(contextProjectPath, '.claude', 'commands'))
|
|
530
|
+
: null;
|
|
520
531
|
const isUnder = (base) => {
|
|
521
532
|
const rel = path.relative(base, resolvedPath);
|
|
522
533
|
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
523
534
|
};
|
|
524
|
-
if (!(isUnder(userBase) || (projectBase && isUnder(projectBase)))) {
|
|
525
|
-
return res.status(403).json({
|
|
535
|
+
if (!((isAdminUser(req.user) && isUnder(userBase)) || (projectBase && isUnder(projectBase)))) {
|
|
536
|
+
return res.status(403).json({
|
|
526
537
|
error: 'Access denied',
|
|
527
538
|
message: 'Command must be in .claude/commands directory'
|
|
528
539
|
});
|
package/server/routes/git.js
CHANGED
|
@@ -4,16 +4,17 @@ import { promises as fs } from 'fs';
|
|
|
4
4
|
|
|
5
5
|
import express from 'express';
|
|
6
6
|
|
|
7
|
-
import { extractProjectDirectory } from '../projects.js';
|
|
8
|
-
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
9
|
-
import { spawnCursor } from '../cursor-cli.js';
|
|
7
|
+
import { extractProjectDirectory } from '../projects.js';
|
|
8
|
+
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
9
|
+
import { spawnCursor } from '../cursor-cli.js';
|
|
10
|
+
import { userHasProjectAccess } from '../services/platformization.js';
|
|
10
11
|
|
|
11
|
-
const router = express.Router();
|
|
12
|
+
const router = express.Router();
|
|
12
13
|
const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
|
|
13
14
|
const FILESYSTEM_SCAN_MAX_FILES = 5_000;
|
|
14
15
|
const FILESYSTEM_SCAN_MAX_DEPTH = 10;
|
|
15
|
-
const filesystemChangeSnapshots = new Map();
|
|
16
|
-
const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
|
|
16
|
+
const filesystemChangeSnapshots = new Map();
|
|
17
|
+
const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
|
|
17
18
|
'.git',
|
|
18
19
|
'.hg',
|
|
19
20
|
'.svn',
|
|
@@ -28,9 +29,23 @@ const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
|
|
|
28
29
|
'.turbo',
|
|
29
30
|
'.cache',
|
|
30
31
|
'.pixcode-dev',
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
router.use((req, res, next) => {
|
|
35
|
+
const project = req.query.project || req.body?.project;
|
|
36
|
+
if (!project) {
|
|
37
|
+
return next();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const capability = req.method === 'GET' ? 'viewFiles' : 'editFiles';
|
|
41
|
+
if (!userHasProjectAccess(req.user, { name: String(project), projectName: String(project) }, capability)) {
|
|
42
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
next();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function isNotGitRepositoryMessage(message = '') {
|
|
34
49
|
return message.includes('Not a git repository')
|
|
35
50
|
|| message.includes('not a git repository')
|
|
36
51
|
|| message.includes('Project directory is not a git repository');
|