@pixelbyte-software/pixcode 1.51.3 → 1.51.4
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-HfGHXhD6.js} +77 -77
- package/dist/index.html +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 +69 -28
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +16 -5
- package/dist-server/server/middleware/auth.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/git.js +12 -0
- package/dist-server/server/routes/git.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/services/platformization.js +58 -2
- package/dist-server/server/services/platformization.js.map +1 -1
- package/package.json +1 -1
- package/server/database/db.js +39 -26
- package/server/index.js +73 -28
- package/server/middleware/auth.js +33 -18
- package/server/routes/auth.js +25 -17
- package/server/routes/git.js +24 -9
- package/server/routes/platformization.js +22 -21
- package/server/services/platformization.js +83 -18
|
@@ -4,8 +4,9 @@ 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']);
|
|
9
10
|
|
|
10
11
|
// Optional API key middleware
|
|
11
12
|
const validateApiKey = (req, res, next) => {
|
|
@@ -22,7 +23,7 @@ const validateApiKey = (req, res, next) => {
|
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
// JWT authentication middleware
|
|
25
|
-
const authenticateToken = async (req, res, next) => {
|
|
26
|
+
const authenticateToken = async (req, res, next) => {
|
|
26
27
|
// Platform mode: use single database user
|
|
27
28
|
if (IS_PLATFORM) {
|
|
28
29
|
try {
|
|
@@ -101,15 +102,28 @@ const authenticateToken = async (req, res, next) => {
|
|
|
101
102
|
console.error('Token verification error:', error);
|
|
102
103
|
return res.status(403).json({ error: 'Invalid token' });
|
|
103
104
|
}
|
|
104
|
-
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const requireAdmin = (req, res, next) => {
|
|
108
|
+
if (!req.user) {
|
|
109
|
+
return res.status(401).json({ error: 'Access denied. No authenticated user.' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!ADMIN_ROLES.has(req.user.role)) {
|
|
113
|
+
return res.status(403).json({ error: 'Admin access required.' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
next();
|
|
117
|
+
};
|
|
105
118
|
|
|
106
119
|
// Generate JWT token
|
|
107
120
|
const generateToken = (user) => {
|
|
108
121
|
return jwt.sign(
|
|
109
122
|
{
|
|
110
|
-
userId: user.id,
|
|
111
|
-
username: user.username
|
|
112
|
-
|
|
123
|
+
userId: user.id,
|
|
124
|
+
username: user.username,
|
|
125
|
+
role: user.role || null,
|
|
126
|
+
},
|
|
113
127
|
JWT_SECRET,
|
|
114
128
|
{ expiresIn: '7d' }
|
|
115
129
|
);
|
|
@@ -120,10 +134,10 @@ const authenticateWebSocket = (token) => {
|
|
|
120
134
|
// Platform mode: bypass token validation, return first user
|
|
121
135
|
if (IS_PLATFORM) {
|
|
122
136
|
try {
|
|
123
|
-
const user = userDb.getFirstUser();
|
|
124
|
-
if (user) {
|
|
125
|
-
return { id: user.id, userId: user.id, username: user.username };
|
|
126
|
-
}
|
|
137
|
+
const user = userDb.getFirstUser();
|
|
138
|
+
if (user) {
|
|
139
|
+
return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
|
|
140
|
+
}
|
|
127
141
|
return null;
|
|
128
142
|
} catch (error) {
|
|
129
143
|
console.error('Platform mode WebSocket error:', error);
|
|
@@ -143,9 +157,9 @@ const authenticateWebSocket = (token) => {
|
|
|
143
157
|
|
|
144
158
|
if (isPixcodeApiKey(token)) {
|
|
145
159
|
try {
|
|
146
|
-
const user = apiKeysDb.validateApiKey(token);
|
|
147
|
-
if (!user) return null;
|
|
148
|
-
return { userId: user.id, username: user.username };
|
|
160
|
+
const user = apiKeysDb.validateApiKey(token);
|
|
161
|
+
if (!user) return null;
|
|
162
|
+
return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
|
|
149
163
|
} catch (error) {
|
|
150
164
|
console.error('WebSocket API key validation error:', error);
|
|
151
165
|
return null;
|
|
@@ -159,7 +173,7 @@ const authenticateWebSocket = (token) => {
|
|
|
159
173
|
if (!user) {
|
|
160
174
|
return null;
|
|
161
175
|
}
|
|
162
|
-
return { userId: user.id, username: user.username };
|
|
176
|
+
return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
|
|
163
177
|
} catch (error) {
|
|
164
178
|
console.error('WebSocket token verification error:', error);
|
|
165
179
|
return null;
|
|
@@ -167,9 +181,10 @@ const authenticateWebSocket = (token) => {
|
|
|
167
181
|
};
|
|
168
182
|
|
|
169
183
|
export {
|
|
170
|
-
validateApiKey,
|
|
171
|
-
authenticateToken,
|
|
172
|
-
|
|
184
|
+
validateApiKey,
|
|
185
|
+
authenticateToken,
|
|
186
|
+
requireAdmin,
|
|
187
|
+
generateToken,
|
|
173
188
|
authenticateWebSocket,
|
|
174
189
|
JWT_SECRET
|
|
175
190
|
};
|
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);
|
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');
|
|
@@ -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;
|
|
@@ -116,10 +116,14 @@ function writeStore(store) {
|
|
|
116
116
|
appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
|
|
117
117
|
}
|
|
118
118
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
123
|
+
|
|
124
|
+
function compactProjectIdentifier(value) {
|
|
125
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
126
|
+
}
|
|
123
127
|
|
|
124
128
|
function slugify(value) {
|
|
125
129
|
const slug = compact(value, 72)
|
|
@@ -140,9 +144,64 @@ function addAudit(store, action, actorId, details = {}) {
|
|
|
140
144
|
store.auditLog = store.auditLog.slice(0, 250);
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
function normalizeRole(role) {
|
|
144
|
-
return TEAM_ROLES[role] ? role : 'viewer';
|
|
145
|
-
}
|
|
147
|
+
function normalizeRole(role) {
|
|
148
|
+
return TEAM_ROLES[role] ? role : 'viewer';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isAdminUser(user = {}) {
|
|
152
|
+
return user?.role === 'admin' || user?.role === 'owner';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveUser(input = {}) {
|
|
156
|
+
const users = userDb.listUsers();
|
|
157
|
+
const userId = Number(input.userId);
|
|
158
|
+
if (Number.isFinite(userId)) {
|
|
159
|
+
return users.find((user) => user.id === userId && user.is_active) || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const userRef = compact(input.userRef || input.email || input.username || '').toLowerCase();
|
|
163
|
+
if (!userRef) return null;
|
|
164
|
+
return users.find((user) => user.is_active && String(user.username).toLowerCase() === userRef) || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function projectMatches(collaborator, project = {}) {
|
|
168
|
+
const projectName = compactProjectIdentifier(project.name || project.projectName || project);
|
|
169
|
+
const projectPath = compactProjectIdentifier(project.fullPath || project.path || project.projectPath || '');
|
|
170
|
+
|
|
171
|
+
return Boolean(
|
|
172
|
+
(projectName && collaborator.projectName === projectName) ||
|
|
173
|
+
(projectPath && collaborator.projectPath === projectPath)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function userHasProjectAccess(user, project, capability = 'viewFiles') {
|
|
178
|
+
if (isAdminUser(user)) return true;
|
|
179
|
+
if (!user?.id && !user?.userId) return false;
|
|
180
|
+
|
|
181
|
+
const userId = Number(user.id ?? user.userId);
|
|
182
|
+
const username = String(user.username || '').toLowerCase();
|
|
183
|
+
const store = readStore();
|
|
184
|
+
|
|
185
|
+
return store.projectCollaborators.some((collaborator) => {
|
|
186
|
+
if (collaborator.status === 'disabled') return false;
|
|
187
|
+
if (!projectMatches(collaborator, project)) return false;
|
|
188
|
+
|
|
189
|
+
const sameUser = Number(collaborator.userId) === userId ||
|
|
190
|
+
String(collaborator.userRef || '').toLowerCase() === username;
|
|
191
|
+
if (!sameUser) return false;
|
|
192
|
+
|
|
193
|
+
if (capability === 'viewFiles') {
|
|
194
|
+
return collaborator.capabilities?.viewFiles !== false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return collaborator.capabilities?.[capability] === true;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function filterProjectsForUser(projects = [], user) {
|
|
202
|
+
if (isAdminUser(user)) return projects;
|
|
203
|
+
return projects.filter((project) => userHasProjectAccess(user, project, 'viewFiles'));
|
|
204
|
+
}
|
|
146
205
|
|
|
147
206
|
function normalizeScope(scope) {
|
|
148
207
|
return SECRET_SCOPES.includes(scope) ? scope : 'project';
|
|
@@ -338,13 +397,18 @@ export function updateAdminUser(userId, patch = {}, actorId = null) {
|
|
|
338
397
|
};
|
|
339
398
|
}
|
|
340
399
|
|
|
341
|
-
export function createProjectCollaborator(input = {}, actorId = null) {
|
|
342
|
-
const projectName =
|
|
343
|
-
const projectPath = input.projectPath || null;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
400
|
+
export function createProjectCollaborator(input = {}, actorId = null) {
|
|
401
|
+
const projectName = compactProjectIdentifier(input.projectName || input.project || '');
|
|
402
|
+
const projectPath = input.projectPath || null;
|
|
403
|
+
const targetUser = resolveUser(input);
|
|
404
|
+
const userRef = compact(input.userRef || input.email || input.username || targetUser?.username || '');
|
|
405
|
+
if (!projectName || !userRef) {
|
|
406
|
+
throw new Error('Project collaborator requires a project name and user reference.');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!targetUser) {
|
|
410
|
+
throw new Error('Create the user account before assigning project access.');
|
|
411
|
+
}
|
|
348
412
|
|
|
349
413
|
const role = ['partner', 'worker', 'reviewer', 'viewer'].includes(input.role) ? input.role : 'worker';
|
|
350
414
|
const capabilities = {
|
|
@@ -357,10 +421,11 @@ export function createProjectCollaborator(input = {}, actorId = null) {
|
|
|
357
421
|
manageProjectSettings: role === 'partner',
|
|
358
422
|
};
|
|
359
423
|
const collaborator = {
|
|
360
|
-
id: crypto.randomUUID(),
|
|
361
|
-
projectName,
|
|
362
|
-
projectPath,
|
|
363
|
-
|
|
424
|
+
id: crypto.randomUUID(),
|
|
425
|
+
projectName,
|
|
426
|
+
projectPath,
|
|
427
|
+
userId: targetUser.id,
|
|
428
|
+
userRef,
|
|
364
429
|
role,
|
|
365
430
|
capabilities: {
|
|
366
431
|
...capabilities,
|