@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
|
@@ -5,13 +5,14 @@ import express from 'express';
|
|
|
5
5
|
|
|
6
6
|
import { extractProjectDirectory } from '../projects.js';
|
|
7
7
|
import { getTunnelState } from '../services/external-access.js';
|
|
8
|
-
import {
|
|
9
|
-
getLiveViewSessionByShareId,
|
|
10
|
-
getLiveViewState,
|
|
11
|
-
restartLiveView,
|
|
12
|
-
startLiveView,
|
|
13
|
-
stopLiveView,
|
|
14
|
-
} from '../services/live-view.js';
|
|
8
|
+
import {
|
|
9
|
+
getLiveViewSessionByShareId,
|
|
10
|
+
getLiveViewState,
|
|
11
|
+
restartLiveView,
|
|
12
|
+
startLiveView,
|
|
13
|
+
stopLiveView,
|
|
14
|
+
} from '../services/live-view.js';
|
|
15
|
+
import { userHasProjectPathAccess } from '../services/platformization.js';
|
|
15
16
|
|
|
16
17
|
const router = express.Router();
|
|
17
18
|
|
|
@@ -210,20 +211,36 @@ function sendLiveViewDiagnostic(req, res, session, statusCode, options = {}) {
|
|
|
210
211
|
res.json(payload);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
async function resolveProjectPath(projectName) {
|
|
214
|
+
async function resolveProjectPath(projectName) {
|
|
214
215
|
const projectPath = await extractProjectDirectory(projectName);
|
|
215
216
|
if (!projectPath || typeof projectPath !== 'string') {
|
|
216
217
|
const error = new Error('Project path could not be resolved.');
|
|
217
218
|
error.statusCode = 404;
|
|
218
219
|
throw error;
|
|
219
220
|
}
|
|
220
|
-
return projectPath;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
return projectPath;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function requireLiveViewProjectAccess(capability) {
|
|
225
|
+
return async (req, res, next) => {
|
|
226
|
+
try {
|
|
227
|
+
const { projectName } = req.params;
|
|
228
|
+
const projectPath = await resolveProjectPath(projectName);
|
|
229
|
+
if (!userHasProjectPathAccess(req.user, { name: projectName, projectName, fullPath: projectPath, path: projectPath }, projectPath, capability)) {
|
|
230
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
231
|
+
}
|
|
232
|
+
req.projectPath = projectPath;
|
|
233
|
+
return next();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return res.status(error.statusCode || 500).json({ error: error.message || 'Failed to resolve project access' });
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
router.get('/:projectName/status', requireLiveViewProjectAccess('viewFiles'), async (req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const { projectName } = req.params;
|
|
243
|
+
const projectPath = req.projectPath;
|
|
227
244
|
const state = await getLiveViewState(projectName, projectPath);
|
|
228
245
|
const urls = buildUrls(req, state.session);
|
|
229
246
|
const tunnel = getTunnelState();
|
|
@@ -238,10 +255,13 @@ router.get('/:projectName/status', async (req, res) => {
|
|
|
238
255
|
}
|
|
239
256
|
});
|
|
240
257
|
|
|
241
|
-
router.post('/:projectName/start', async (req, res) => {
|
|
242
|
-
try {
|
|
243
|
-
const { projectName } = req.params;
|
|
244
|
-
const projectPath =
|
|
258
|
+
router.post('/:projectName/start', requireLiveViewProjectAccess('useShell'), async (req, res) => {
|
|
259
|
+
try {
|
|
260
|
+
const { projectName } = req.params;
|
|
261
|
+
const projectPath = req.projectPath;
|
|
262
|
+
if (req.body?.customCommand && !['admin', 'owner'].includes(req.user?.role)) {
|
|
263
|
+
return res.status(403).json({ error: 'Custom Live View commands require admin access.' });
|
|
264
|
+
}
|
|
245
265
|
const session = await startLiveView(projectName, projectPath, req.body || {});
|
|
246
266
|
const state = await getLiveViewState(projectName, projectPath);
|
|
247
267
|
const urls = buildUrls(req, session);
|
|
@@ -259,10 +279,13 @@ router.post('/:projectName/start', async (req, res) => {
|
|
|
259
279
|
}
|
|
260
280
|
});
|
|
261
281
|
|
|
262
|
-
router.post('/:projectName/restart', async (req, res) => {
|
|
263
|
-
try {
|
|
264
|
-
const { projectName } = req.params;
|
|
265
|
-
const projectPath =
|
|
282
|
+
router.post('/:projectName/restart', requireLiveViewProjectAccess('useShell'), async (req, res) => {
|
|
283
|
+
try {
|
|
284
|
+
const { projectName } = req.params;
|
|
285
|
+
const projectPath = req.projectPath;
|
|
286
|
+
if (req.body?.customCommand && !['admin', 'owner'].includes(req.user?.role)) {
|
|
287
|
+
return res.status(403).json({ error: 'Custom Live View commands require admin access.' });
|
|
288
|
+
}
|
|
266
289
|
const session = await restartLiveView(projectName, projectPath, req.body || {});
|
|
267
290
|
const state = await getLiveViewState(projectName, projectPath);
|
|
268
291
|
const urls = buildUrls(req, session);
|
|
@@ -279,7 +302,7 @@ router.post('/:projectName/restart', async (req, res) => {
|
|
|
279
302
|
}
|
|
280
303
|
});
|
|
281
304
|
|
|
282
|
-
router.post('/:projectName/stop', async (req, res) => {
|
|
305
|
+
router.post('/:projectName/stop', requireLiveViewProjectAccess('useShell'), async (req, res) => {
|
|
283
306
|
try {
|
|
284
307
|
const session = await stopLiveView(req.params.projectName);
|
|
285
308
|
const urls = buildUrls(req, session);
|
|
@@ -9,11 +9,30 @@
|
|
|
9
9
|
* @module routes/messages
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import express from 'express';
|
|
15
|
+
|
|
16
|
+
import { sessionsService } from '../modules/providers/services/sessions.service.js';
|
|
17
|
+
import { userHasProjectAccess } from '../services/platformization.js';
|
|
18
|
+
import { appConfigDb } from '../database/db.js';
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const router = express.Router();
|
|
21
|
+
const SESSION_OWNERSHIP_KEY = 'session_ownership';
|
|
22
|
+
|
|
23
|
+
function readSessionOwnership() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(appConfigDb.get(SESSION_OWNERSHIP_KEY) || '{}');
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function canUserReadSession(user, provider, sessionId) {
|
|
32
|
+
if (['admin', 'owner'].includes(user?.role)) return true;
|
|
33
|
+
const owner = readSessionOwnership()[`${provider || 'claude'}:${sessionId}`];
|
|
34
|
+
return Boolean(owner?.userId && Number(owner.userId) === Number(user?.id ?? user?.userId));
|
|
35
|
+
}
|
|
17
36
|
|
|
18
37
|
/**
|
|
19
38
|
* GET /api/sessions/:sessionId/messages
|
|
@@ -31,8 +50,8 @@ router.get('/:sessionId/messages', async (req, res) => {
|
|
|
31
50
|
try {
|
|
32
51
|
const { sessionId } = req.params;
|
|
33
52
|
const provider = String(req.query.provider || 'claude').trim().toLowerCase();
|
|
34
|
-
const projectName = req.query.projectName || '';
|
|
35
|
-
const projectPath = req.query.projectPath || '';
|
|
53
|
+
const projectName = req.query.projectName || '';
|
|
54
|
+
const projectPath = req.query.projectPath || '';
|
|
36
55
|
const limitParam = req.query.limit;
|
|
37
56
|
const limit = limitParam !== undefined && limitParam !== null && limitParam !== ''
|
|
38
57
|
? parseInt(limitParam, 10)
|
|
@@ -40,12 +59,28 @@ router.get('/:sessionId/messages', async (req, res) => {
|
|
|
40
59
|
const offset = parseInt(req.query.offset || '0', 10);
|
|
41
60
|
|
|
42
61
|
const availableProviders = sessionsService.listProviderIds();
|
|
43
|
-
if (!availableProviders.includes(provider)) {
|
|
44
|
-
const available = availableProviders.join(', ');
|
|
45
|
-
return res.status(400).json({ error: `Unknown provider: ${provider}. Available: ${available}` });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
if (!availableProviders.includes(provider)) {
|
|
63
|
+
const available = availableProviders.join(', ');
|
|
64
|
+
return res.status(400).json({ error: `Unknown provider: ${provider}. Available: ${available}` });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!projectName && !projectPath) {
|
|
68
|
+
return res.status(400).json({ error: 'projectName or projectPath is required' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const projectRef = projectName
|
|
72
|
+
? { name: String(projectName), projectName: String(projectName) }
|
|
73
|
+
: { fullPath: path.resolve(String(projectPath)), path: path.resolve(String(projectPath)), projectPath: path.resolve(String(projectPath)) };
|
|
74
|
+
|
|
75
|
+
if (!userHasProjectAccess(req.user, projectRef, 'viewFiles')) {
|
|
76
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!canUserReadSession(req.user, provider, sessionId)) {
|
|
80
|
+
return res.status(403).json({ error: 'Session access denied.' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await sessionsService.fetchHistory(provider, sessionId, {
|
|
49
84
|
projectName,
|
|
50
85
|
projectPath,
|
|
51
86
|
limit,
|
package/server/routes/network.js
CHANGED
|
@@ -2,12 +2,13 @@ import os from 'os';
|
|
|
2
2
|
|
|
3
3
|
import express from 'express';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
getTunnelState,
|
|
7
|
-
getUpnpState,
|
|
8
|
-
startTunnel,
|
|
9
|
-
stopTunnel,
|
|
10
|
-
} from '../services/external-access.js';
|
|
5
|
+
import {
|
|
6
|
+
getTunnelState,
|
|
7
|
+
getUpnpState,
|
|
8
|
+
startTunnel,
|
|
9
|
+
stopTunnel,
|
|
10
|
+
} from '../services/external-access.js';
|
|
11
|
+
import { requireAdmin } from '../middleware/auth.js';
|
|
11
12
|
|
|
12
13
|
const router = express.Router();
|
|
13
14
|
|
|
@@ -93,7 +94,7 @@ router.delete('/upnp', (_req, res) => {
|
|
|
93
94
|
});
|
|
94
95
|
});
|
|
95
96
|
|
|
96
|
-
router.post('/tunnel', async (req, res) => {
|
|
97
|
+
router.post('/tunnel', requireAdmin, async (req, res) => {
|
|
97
98
|
const port = resolveServerPort();
|
|
98
99
|
try {
|
|
99
100
|
const state = await startTunnel({ port, persistPreference: true });
|
|
@@ -112,7 +113,7 @@ router.post('/tunnel', async (req, res) => {
|
|
|
112
113
|
}
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
router.delete('/tunnel', async (req, res) => {
|
|
116
|
+
router.delete('/tunnel', requireAdmin, async (req, res) => {
|
|
116
117
|
try {
|
|
117
118
|
const state = await stopTunnel({ persistPreference: true });
|
|
118
119
|
res.json({ success: true, tunnel: state });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import { requireAdmin } from '../middleware/auth.js';
|
|
4
|
+
import {
|
|
4
5
|
checkRemoteAccessHealth,
|
|
5
6
|
createAdminUser,
|
|
6
7
|
createEvaluationRun,
|
|
@@ -66,20 +67,20 @@ router.patch('/team/members/:id', (req, res) => {
|
|
|
66
67
|
res.json({ success: true, member });
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
router.get('/admin/users', (_req, res) => {
|
|
70
|
-
res.json({ success: true, users: getPlatformizationState().adminUsers });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
router.post('/admin/users', async (req, res) => {
|
|
74
|
-
try {
|
|
75
|
-
res.status(201).json({ success: true, user: await createAdminUser(req.body || {}, userId(req)) });
|
|
76
|
-
} catch (error) {
|
|
70
|
+
router.get('/admin/users', requireAdmin, (_req, res) => {
|
|
71
|
+
res.json({ success: true, users: getPlatformizationState().adminUsers });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
router.post('/admin/users', requireAdmin, async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
res.status(201).json({ success: true, user: await createAdminUser(req.body || {}, userId(req)) });
|
|
77
|
+
} catch (error) {
|
|
77
78
|
handleError(res, error);
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
router.patch('/admin/users/:id', (req, res) => {
|
|
82
|
-
const user = updateAdminUser(req.params.id, req.body || {}, userId(req));
|
|
82
|
+
router.patch('/admin/users/:id', requireAdmin, (req, res) => {
|
|
83
|
+
const user = updateAdminUser(req.params.id, req.body || {}, userId(req));
|
|
83
84
|
if (!user) {
|
|
84
85
|
res.status(404).json({ success: false, error: 'Admin user not found.' });
|
|
85
86
|
return;
|
|
@@ -87,20 +88,20 @@ router.patch('/admin/users/:id', (req, res) => {
|
|
|
87
88
|
res.json({ success: true, user });
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
router.get('/project-collaborators', (_req, res) => {
|
|
91
|
-
res.json({ success: true, collaborators: getPlatformizationState().projectCollaborators });
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
router.post('/project-collaborators', (req, res) => {
|
|
95
|
-
try {
|
|
91
|
+
router.get('/project-collaborators', requireAdmin, (_req, res) => {
|
|
92
|
+
res.json({ success: true, collaborators: getPlatformizationState().projectCollaborators });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
router.post('/project-collaborators', requireAdmin, (req, res) => {
|
|
96
|
+
try {
|
|
96
97
|
res.status(201).json({ success: true, collaborator: createProjectCollaborator(req.body || {}, userId(req)) });
|
|
97
98
|
} catch (error) {
|
|
98
99
|
handleError(res, error);
|
|
99
100
|
}
|
|
100
101
|
});
|
|
101
102
|
|
|
102
|
-
router.patch('/project-collaborators/:id', (req, res) => {
|
|
103
|
-
const collaborator = updateProjectCollaborator(req.params.id, req.body || {}, userId(req));
|
|
103
|
+
router.patch('/project-collaborators/:id', requireAdmin, (req, res) => {
|
|
104
|
+
const collaborator = updateProjectCollaborator(req.params.id, req.body || {}, userId(req));
|
|
104
105
|
if (!collaborator) {
|
|
105
106
|
res.status(404).json({ success: false, error: 'Project collaborator not found.' });
|
|
106
107
|
return;
|
|
@@ -3,9 +3,10 @@ import path from 'path';
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
|
|
6
|
-
import express from 'express';
|
|
7
|
-
|
|
8
|
-
import { addProjectManually, extractProjectDirectory } from '../projects.js';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
|
|
8
|
+
import { addProjectManually, extractProjectDirectory } from '../projects.js';
|
|
9
|
+
import { requireAdmin } from '../middleware/auth.js';
|
|
9
10
|
|
|
10
11
|
const router = express.Router();
|
|
11
12
|
|
|
@@ -302,7 +303,7 @@ async function projectHasAnySessions(workspacePath) {
|
|
|
302
303
|
* composer and surface a "directory deleted" warning instead of letting
|
|
303
304
|
* the user fire prompts into a void.
|
|
304
305
|
*/
|
|
305
|
-
router.get('/:projectName/dir-status', async (req, res) => {
|
|
306
|
+
router.get('/:projectName/dir-status', requireAdmin, async (req, res) => {
|
|
306
307
|
const { projectName } = req.params;
|
|
307
308
|
try {
|
|
308
309
|
const actualPath = await extractProjectDirectory(projectName);
|
|
@@ -340,7 +341,7 @@ router.get('/:projectName/dir-status', async (req, res) => {
|
|
|
340
341
|
* matches ChatGPT's "New chat" which reuses the empty canvas until the
|
|
341
342
|
* user actually commits a message.
|
|
342
343
|
*/
|
|
343
|
-
router.post('/quick-start', async (req, res) => {
|
|
344
|
+
router.post('/quick-start', requireAdmin, async (req, res) => {
|
|
344
345
|
try {
|
|
345
346
|
await fs.mkdir(WORKSPACES_BASE, { recursive: true });
|
|
346
347
|
|
|
@@ -424,7 +425,7 @@ router.post('/quick-start', async (req, res) => {
|
|
|
424
425
|
* - githubTokenId?: number (optional, ID of stored token)
|
|
425
426
|
* - newGithubToken?: string (optional, one-time token)
|
|
426
427
|
*/
|
|
427
|
-
router.post('/create-workspace', async (req, res) => {
|
|
428
|
+
router.post('/create-workspace', requireAdmin, async (req, res) => {
|
|
428
429
|
try {
|
|
429
430
|
const { workspaceType, path: workspacePath, githubUrl, githubTokenId, newGithubToken, subfolderName } = req.body;
|
|
430
431
|
|
package/server/routes/remote.js
CHANGED
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
getPublicRemoteConnectionConfig,
|
|
6
6
|
saveRemoteConnectionConfig,
|
|
7
7
|
} from '../services/remote-connection.js';
|
|
8
|
-
import {
|
|
9
|
-
buildControlRoomSnapshot,
|
|
10
|
-
buildMobileConsoleLayout,
|
|
11
|
-
} from '../services/control-room.js';
|
|
8
|
+
import {
|
|
9
|
+
buildControlRoomSnapshot,
|
|
10
|
+
buildMobileConsoleLayout,
|
|
11
|
+
} from '../services/control-room.js';
|
|
12
|
+
import { requireAdmin } from '../middleware/auth.js';
|
|
12
13
|
|
|
13
14
|
const router = express.Router();
|
|
14
15
|
|
|
@@ -16,7 +17,7 @@ router.get('/config', (req, res) => {
|
|
|
16
17
|
res.json({ success: true, connection: getPublicRemoteConnectionConfig() });
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
router.put('/config', (req, res) => {
|
|
20
|
+
router.put('/config', requireAdmin, (req, res) => {
|
|
20
21
|
try {
|
|
21
22
|
const connection = saveRemoteConnectionConfig(req.body || {});
|
|
22
23
|
res.json({ success: true, connection });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
|
|
6
7
|
import bcrypt from 'bcryptjs';
|
|
@@ -116,10 +117,14 @@ function writeStore(store) {
|
|
|
116
117
|
appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function compact(text, max = 120) {
|
|
120
|
-
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
121
|
-
return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
|
|
122
|
-
}
|
|
120
|
+
function compact(text, max = 120) {
|
|
121
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
122
|
+
return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function compactProjectIdentifier(value) {
|
|
126
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
127
|
+
}
|
|
123
128
|
|
|
124
129
|
function slugify(value) {
|
|
125
130
|
const slug = compact(value, 72)
|
|
@@ -140,9 +145,132 @@ function addAudit(store, action, actorId, details = {}) {
|
|
|
140
145
|
store.auditLog = store.auditLog.slice(0, 250);
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
function normalizeRole(role) {
|
|
144
|
-
return TEAM_ROLES[role] ? role : 'viewer';
|
|
145
|
-
}
|
|
148
|
+
function normalizeRole(role) {
|
|
149
|
+
return TEAM_ROLES[role] ? role : 'viewer';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function isAdminUser(user = {}) {
|
|
153
|
+
return user?.role === 'admin' || user?.role === 'owner';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveUser(input = {}) {
|
|
157
|
+
const users = userDb.listUsers();
|
|
158
|
+
const userId = Number(input.userId);
|
|
159
|
+
if (Number.isFinite(userId)) {
|
|
160
|
+
return users.find((user) => user.id === userId && user.is_active) || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const userRef = compact(input.userRef || input.email || input.username || '').toLowerCase();
|
|
164
|
+
if (!userRef) return null;
|
|
165
|
+
return users.find((user) => user.is_active && String(user.username).toLowerCase() === userRef) || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function projectMatches(collaborator, project = {}) {
|
|
169
|
+
const projectName = compactProjectIdentifier(project.name || project.projectName || project);
|
|
170
|
+
const projectPath = compactProjectIdentifier(project.fullPath || project.path || project.projectPath || '');
|
|
171
|
+
|
|
172
|
+
return Boolean(
|
|
173
|
+
(projectName && collaborator.projectName === projectName) ||
|
|
174
|
+
(projectPath && collaborator.projectPath === projectPath)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isPathInside(basePath, targetPath) {
|
|
179
|
+
const relative = path.relative(path.resolve(basePath), path.resolve(targetPath));
|
|
180
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeAllowedRoots(input) {
|
|
184
|
+
const roots = Array.isArray(input) ? input : [];
|
|
185
|
+
const normalized = roots
|
|
186
|
+
.filter((entry) => typeof entry === 'string')
|
|
187
|
+
.map((entry) => entry.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/g, ''))
|
|
188
|
+
.map((entry) => entry || '.')
|
|
189
|
+
.filter((entry) => !entry.includes('..'));
|
|
190
|
+
return Array.from(new Set(normalized.length > 0 ? normalized : ['.']));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function collaboratorAllowedRoots(collaborator) {
|
|
194
|
+
return normalizeAllowedRoots(collaborator.allowedRoots || collaborator.allowedFolders || ['.']);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function userHasProjectAccess(user, project, capability = 'viewFiles') {
|
|
198
|
+
if (isAdminUser(user)) return true;
|
|
199
|
+
if (!user?.id && !user?.userId) return false;
|
|
200
|
+
|
|
201
|
+
const userId = Number(user.id ?? user.userId);
|
|
202
|
+
const username = String(user.username || '').toLowerCase();
|
|
203
|
+
const store = readStore();
|
|
204
|
+
|
|
205
|
+
return store.projectCollaborators.some((collaborator) => {
|
|
206
|
+
if (collaborator.status === 'disabled') return false;
|
|
207
|
+
if (!projectMatches(collaborator, project)) return false;
|
|
208
|
+
|
|
209
|
+
const sameUser = Number(collaborator.userId) === userId ||
|
|
210
|
+
String(collaborator.userRef || '').toLowerCase() === username;
|
|
211
|
+
if (!sameUser) return false;
|
|
212
|
+
|
|
213
|
+
if (capability === 'viewFiles') {
|
|
214
|
+
return collaborator.capabilities?.viewFiles !== false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return collaborator.capabilities?.[capability] === true;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function getProjectAccessForUser(user, project, capability = 'viewFiles') {
|
|
222
|
+
if (isAdminUser(user)) {
|
|
223
|
+
return { unrestricted: true, allowedRoots: ['.'] };
|
|
224
|
+
}
|
|
225
|
+
if (!user?.id && !user?.userId) return { unrestricted: false, allowedRoots: [] };
|
|
226
|
+
|
|
227
|
+
const userId = Number(user.id ?? user.userId);
|
|
228
|
+
const username = String(user.username || '').toLowerCase();
|
|
229
|
+
const store = readStore();
|
|
230
|
+
const allowedRoots = [];
|
|
231
|
+
|
|
232
|
+
for (const collaborator of store.projectCollaborators) {
|
|
233
|
+
if (collaborator.status === 'disabled') continue;
|
|
234
|
+
if (!projectMatches(collaborator, project)) continue;
|
|
235
|
+
const sameUser = Number(collaborator.userId) === userId ||
|
|
236
|
+
String(collaborator.userRef || '').toLowerCase() === username;
|
|
237
|
+
if (!sameUser) continue;
|
|
238
|
+
const capabilityAllowed = capability === 'viewFiles'
|
|
239
|
+
? collaborator.capabilities?.viewFiles !== false
|
|
240
|
+
: collaborator.capabilities?.[capability] === true;
|
|
241
|
+
if (!capabilityAllowed) continue;
|
|
242
|
+
allowedRoots.push(...collaboratorAllowedRoots(collaborator));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { unrestricted: false, allowedRoots: Array.from(new Set(allowedRoots)) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function userHasProjectPathAccess(user, project, targetPath, capability = 'viewFiles') {
|
|
249
|
+
if (isAdminUser(user)) return true;
|
|
250
|
+
const projectPath = project?.fullPath || project?.path || project?.projectPath;
|
|
251
|
+
if (!projectPath || !targetPath) return false;
|
|
252
|
+
const access = getProjectAccessForUser(user, project, capability);
|
|
253
|
+
return access.allowedRoots.some((root) => {
|
|
254
|
+
const allowedPath = root === '.' ? projectPath : path.resolve(projectPath, root);
|
|
255
|
+
return isPathInside(allowedPath, targetPath);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function filterFileTreeForUser(files = [], user, project, capability = 'viewFiles') {
|
|
260
|
+
if (isAdminUser(user)) return files;
|
|
261
|
+
const projectPath = project?.fullPath || project?.path || project?.projectPath;
|
|
262
|
+
if (!projectPath) return [];
|
|
263
|
+
return files.filter((entry) => {
|
|
264
|
+
const entryPath = entry?.path || entry?.fullPath || entry?.relativePath || '';
|
|
265
|
+
const absoluteEntryPath = path.isAbsolute(entryPath) ? entryPath : path.resolve(projectPath, entryPath);
|
|
266
|
+
return userHasProjectPathAccess(user, { ...project, fullPath: projectPath }, absoluteEntryPath, capability);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function filterProjectsForUser(projects = [], user) {
|
|
271
|
+
if (isAdminUser(user)) return projects;
|
|
272
|
+
return projects.filter((project) => userHasProjectAccess(user, project, 'viewFiles'));
|
|
273
|
+
}
|
|
146
274
|
|
|
147
275
|
function normalizeScope(scope) {
|
|
148
276
|
return SECRET_SCOPES.includes(scope) ? scope : 'project';
|
|
@@ -338,13 +466,18 @@ export function updateAdminUser(userId, patch = {}, actorId = null) {
|
|
|
338
466
|
};
|
|
339
467
|
}
|
|
340
468
|
|
|
341
|
-
export function createProjectCollaborator(input = {}, actorId = null) {
|
|
342
|
-
const projectName =
|
|
343
|
-
const projectPath = input.projectPath || null;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
469
|
+
export function createProjectCollaborator(input = {}, actorId = null) {
|
|
470
|
+
const projectName = compactProjectIdentifier(input.projectName || input.project || '');
|
|
471
|
+
const projectPath = input.projectPath || null;
|
|
472
|
+
const targetUser = resolveUser(input);
|
|
473
|
+
const userRef = compact(input.userRef || input.email || input.username || targetUser?.username || '');
|
|
474
|
+
if (!projectName || !userRef) {
|
|
475
|
+
throw new Error('Project collaborator requires a project name and user reference.');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!targetUser) {
|
|
479
|
+
throw new Error('Create the user account before assigning project access.');
|
|
480
|
+
}
|
|
348
481
|
|
|
349
482
|
const role = ['partner', 'worker', 'reviewer', 'viewer'].includes(input.role) ? input.role : 'worker';
|
|
350
483
|
const capabilities = {
|
|
@@ -357,16 +490,18 @@ export function createProjectCollaborator(input = {}, actorId = null) {
|
|
|
357
490
|
manageProjectSettings: role === 'partner',
|
|
358
491
|
};
|
|
359
492
|
const collaborator = {
|
|
360
|
-
id: crypto.randomUUID(),
|
|
361
|
-
projectName,
|
|
362
|
-
projectPath,
|
|
363
|
-
|
|
493
|
+
id: crypto.randomUUID(),
|
|
494
|
+
projectName,
|
|
495
|
+
projectPath,
|
|
496
|
+
userId: targetUser.id,
|
|
497
|
+
userRef,
|
|
364
498
|
role,
|
|
365
|
-
capabilities: {
|
|
366
|
-
...capabilities,
|
|
367
|
-
...(input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : {}),
|
|
368
|
-
},
|
|
369
|
-
|
|
499
|
+
capabilities: {
|
|
500
|
+
...capabilities,
|
|
501
|
+
...(input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : {}),
|
|
502
|
+
},
|
|
503
|
+
allowedRoots: normalizeAllowedRoots(input.allowedRoots || input.allowedFolders || ['.']),
|
|
504
|
+
status: input.status || 'active',
|
|
370
505
|
createdAt: nowIso(),
|
|
371
506
|
updatedAt: nowIso(),
|
|
372
507
|
};
|
|
@@ -386,11 +521,14 @@ export function updateProjectCollaborator(collaboratorId, patch = {}, actorId =
|
|
|
386
521
|
...collaborator,
|
|
387
522
|
...patch,
|
|
388
523
|
id: collaborator.id,
|
|
389
|
-
capabilities: {
|
|
390
|
-
...collaborator.capabilities,
|
|
391
|
-
...(patch.capabilities && typeof patch.capabilities === 'object' ? patch.capabilities : {}),
|
|
392
|
-
},
|
|
393
|
-
|
|
524
|
+
capabilities: {
|
|
525
|
+
...collaborator.capabilities,
|
|
526
|
+
...(patch.capabilities && typeof patch.capabilities === 'object' ? patch.capabilities : {}),
|
|
527
|
+
},
|
|
528
|
+
allowedRoots: patch.allowedRoots || patch.allowedFolders
|
|
529
|
+
? normalizeAllowedRoots(patch.allowedRoots || patch.allowedFolders)
|
|
530
|
+
: collaboratorAllowedRoots(collaborator),
|
|
531
|
+
updatedAt: nowIso(),
|
|
394
532
|
};
|
|
395
533
|
return updated;
|
|
396
534
|
});
|