@friggframework/core 2.0.0-next.56 → 2.0.0-next.57
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/credential/repositories/credential-repository-documentdb.js +3 -3
- package/credential/repositories/credential-repository-mongo.js +1 -1
- package/credential/repositories/credential-repository-postgres.js +8 -3
- package/database/index.js +39 -12
- package/database/utils/prisma-runner.js +71 -0
- package/handlers/backend-utils.js +16 -10
- package/handlers/routers/db-migration.js +72 -1
- package/integrations/integration-base.js +32 -3
- package/integrations/integration-router.js +5 -9
- package/modules/use-cases/get-entity-options-by-id.js +17 -5
- package/modules/use-cases/get-module.js +21 -2
- package/modules/use-cases/process-authorization-callback.js +12 -1
- package/modules/use-cases/refresh-entity-options.js +18 -5
- package/modules/use-cases/test-module-auth.js +19 -2
- package/package.json +5 -5
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +0 -44
- package/queues/queuer-util.js +0 -8
- package/user/repositories/user-repository-documentdb.js +20 -0
- package/user/repositories/user-repository-interface.js +14 -0
- package/user/repositories/user-repository-mongo.js +18 -0
- package/user/repositories/user-repository-postgres.js +22 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +47 -21
- package/user/user.js +32 -0
|
@@ -106,7 +106,7 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
106
106
|
const updateDocument = {
|
|
107
107
|
userId: existing.userId,
|
|
108
108
|
externalId: existing.externalId,
|
|
109
|
-
authIsValid: authIsValid,
|
|
109
|
+
authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
110
110
|
data: mergedData,
|
|
111
111
|
updatedAt: now,
|
|
112
112
|
};
|
|
@@ -143,7 +143,7 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
const plainDocument = {
|
|
146
|
-
userId: identifiers.userId,
|
|
146
|
+
userId: toObjectId(identifiers.userId),
|
|
147
147
|
externalId: identifiers.externalId,
|
|
148
148
|
authIsValid: details.authIsValid,
|
|
149
149
|
data: { ...oauthData },
|
|
@@ -245,7 +245,7 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
245
245
|
if (idObj) filter._id = idObj;
|
|
246
246
|
}
|
|
247
247
|
if (identifiers.userId) {
|
|
248
|
-
filter.userId = identifiers.userId;
|
|
248
|
+
filter.userId = toObjectId(identifiers.userId);
|
|
249
249
|
}
|
|
250
250
|
if (identifiers.externalId !== undefined) {
|
|
251
251
|
filter.externalId = identifiers.externalId;
|
|
@@ -121,7 +121,7 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
121
121
|
data: {
|
|
122
122
|
userId: existing.userId,
|
|
123
123
|
externalId: existing.externalId,
|
|
124
|
-
authIsValid: authIsValid,
|
|
124
|
+
authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
125
125
|
data: mergedData,
|
|
126
126
|
},
|
|
127
127
|
});
|
|
@@ -111,7 +111,8 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
111
111
|
if (!identifiers)
|
|
112
112
|
throw new Error('identifiers required to upsert credential');
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// Support both userId (preferred) and user (legacy) for backward compatibility
|
|
115
|
+
if (!identifiers.userId && !identifiers.user) {
|
|
115
116
|
throw new Error('userId required in identifiers');
|
|
116
117
|
}
|
|
117
118
|
if (!identifiers.externalId) {
|
|
@@ -138,7 +139,7 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
138
139
|
data: {
|
|
139
140
|
userId: this._convertId(existing.userId),
|
|
140
141
|
externalId: existing.externalId,
|
|
141
|
-
authIsValid: authIsValid,
|
|
142
|
+
authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
142
143
|
data: mergedData,
|
|
143
144
|
},
|
|
144
145
|
});
|
|
@@ -154,7 +155,8 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
154
155
|
|
|
155
156
|
const created = await this.prisma.credential.create({
|
|
156
157
|
data: {
|
|
157
|
-
userId
|
|
158
|
+
// Use userId from where clause (supports both userId and user fields)
|
|
159
|
+
userId: where.userId,
|
|
158
160
|
externalId,
|
|
159
161
|
authIsValid: authIsValid,
|
|
160
162
|
data: oauthData,
|
|
@@ -257,8 +259,11 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
257
259
|
const where = {};
|
|
258
260
|
|
|
259
261
|
if (identifiers.id) where.id = this._convertId(identifiers.id);
|
|
262
|
+
// Support both userId (preferred) and user (legacy) for backward compatibility
|
|
260
263
|
if (identifiers.userId)
|
|
261
264
|
where.userId = this._convertId(identifiers.userId);
|
|
265
|
+
else if (identifiers.user)
|
|
266
|
+
where.userId = this._convertId(identifiers.user);
|
|
262
267
|
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
263
268
|
|
|
264
269
|
return where;
|
package/database/index.js
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* etc.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// Lazy-load mongoose to avoid importing mongodb when using PostgreSQL only
|
|
16
|
+
let _mongoose = null;
|
|
17
|
+
let _IndividualUser = null;
|
|
18
|
+
let _OrganizationUser = null;
|
|
19
|
+
let _UserModel = null;
|
|
20
|
+
let _WebsocketConnection = null;
|
|
20
21
|
|
|
21
|
-
// Prisma exports
|
|
22
|
+
// Prisma exports (always available)
|
|
22
23
|
const { prisma } = require('./prisma');
|
|
23
24
|
const { TokenRepository } = require('../token/repositories/token-repository');
|
|
24
25
|
const {
|
|
@@ -26,12 +27,38 @@ const {
|
|
|
26
27
|
} = require('../websocket/repositories/websocket-connection-repository');
|
|
27
28
|
|
|
28
29
|
module.exports = {
|
|
29
|
-
mongoose
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// Lazy-loaded mongoose exports (only load when accessed)
|
|
31
|
+
get mongoose() {
|
|
32
|
+
if (!_mongoose) {
|
|
33
|
+
_mongoose = require('./mongoose').mongoose;
|
|
34
|
+
}
|
|
35
|
+
return _mongoose;
|
|
36
|
+
},
|
|
37
|
+
get IndividualUser() {
|
|
38
|
+
if (!_IndividualUser) {
|
|
39
|
+
_IndividualUser = require('./models/IndividualUser').IndividualUser;
|
|
40
|
+
}
|
|
41
|
+
return _IndividualUser;
|
|
42
|
+
},
|
|
43
|
+
get OrganizationUser() {
|
|
44
|
+
if (!_OrganizationUser) {
|
|
45
|
+
_OrganizationUser = require('./models/OrganizationUser').OrganizationUser;
|
|
46
|
+
}
|
|
47
|
+
return _OrganizationUser;
|
|
48
|
+
},
|
|
49
|
+
get UserModel() {
|
|
50
|
+
if (!_UserModel) {
|
|
51
|
+
_UserModel = require('./models/UserModel').UserModel;
|
|
52
|
+
}
|
|
53
|
+
return _UserModel;
|
|
54
|
+
},
|
|
55
|
+
get WebsocketConnection() {
|
|
56
|
+
if (!_WebsocketConnection) {
|
|
57
|
+
_WebsocketConnection = require('./models/WebsocketConnection').WebsocketConnection;
|
|
58
|
+
}
|
|
59
|
+
return _WebsocketConnection;
|
|
60
|
+
},
|
|
61
|
+
// Prisma (always available)
|
|
35
62
|
prisma,
|
|
36
63
|
TokenRepository,
|
|
37
64
|
WebsocketConnectionRepository,
|
|
@@ -373,6 +373,76 @@ async function runPrismaDbPush(verbose = false, nonInteractive = false) {
|
|
|
373
373
|
});
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Runs Prisma migrate resolve to mark a migration as applied or rolled back
|
|
378
|
+
* @param {string} migrationName - Name of the migration to resolve (e.g., '20251112195422_update_user_unique_constraints')
|
|
379
|
+
* @param {'applied'|'rolled-back'} action - Whether to mark as applied or rolled back
|
|
380
|
+
* @param {boolean} verbose - Enable verbose output
|
|
381
|
+
* @returns {Promise<Object>} { success: boolean, output?: string, error?: string }
|
|
382
|
+
*/
|
|
383
|
+
async function runPrismaMigrateResolve(migrationName, action = 'applied', verbose = false) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
try {
|
|
386
|
+
const schemaPath = getPrismaSchemaPath('postgresql');
|
|
387
|
+
|
|
388
|
+
// Get Prisma binary path (checks multiple locations)
|
|
389
|
+
const prismaBin = getPrismaBinaryPath();
|
|
390
|
+
|
|
391
|
+
// Determine args based on whether we're using direct binary or npx
|
|
392
|
+
const isDirectBinary = prismaBin !== 'npx prisma';
|
|
393
|
+
const args = isDirectBinary
|
|
394
|
+
? ['migrate', 'resolve', `--${action}`, migrationName, '--schema', schemaPath]
|
|
395
|
+
: ['prisma', 'migrate', 'resolve', `--${action}`, migrationName, '--schema', schemaPath];
|
|
396
|
+
|
|
397
|
+
if (verbose) {
|
|
398
|
+
const displayCmd = isDirectBinary
|
|
399
|
+
? `${prismaBin} ${args.join(' ')}`
|
|
400
|
+
: `npx ${args.join(' ')}`;
|
|
401
|
+
console.log(chalk.gray(`Running: ${displayCmd}`));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Execute the command (prismaBin might be 'node /path/to/index.js' or 'npx prisma')
|
|
405
|
+
const [executable, ...executableArgs] = prismaBin.split(' ');
|
|
406
|
+
const fullArgs = [...executableArgs, ...args];
|
|
407
|
+
|
|
408
|
+
const proc = spawn(executable, fullArgs, {
|
|
409
|
+
stdio: 'inherit',
|
|
410
|
+
env: {
|
|
411
|
+
...process.env,
|
|
412
|
+
PRISMA_HIDE_UPDATE_MESSAGE: '1'
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
proc.on('error', (error) => {
|
|
417
|
+
resolve({
|
|
418
|
+
success: false,
|
|
419
|
+
error: error.message
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
proc.on('close', (code) => {
|
|
424
|
+
if (code === 0) {
|
|
425
|
+
resolve({
|
|
426
|
+
success: true,
|
|
427
|
+
output: `Migration ${migrationName} marked as ${action}`
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
resolve({
|
|
431
|
+
success: false,
|
|
432
|
+
error: `Resolve process exited with code ${code}`
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
} catch (error) {
|
|
438
|
+
resolve({
|
|
439
|
+
success: false,
|
|
440
|
+
error: error.message
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
376
446
|
/**
|
|
377
447
|
* Determines migration command based on STAGE environment variable
|
|
378
448
|
* @param {string} stage - Stage from CLI option or environment
|
|
@@ -401,6 +471,7 @@ module.exports = {
|
|
|
401
471
|
runPrismaGenerate,
|
|
402
472
|
checkDatabaseState,
|
|
403
473
|
runPrismaMigrate,
|
|
474
|
+
runPrismaMigrateResolve,
|
|
404
475
|
runPrismaDbPush,
|
|
405
476
|
getMigrationCommand
|
|
406
477
|
};
|
|
@@ -92,13 +92,16 @@ const loadIntegrationForWebhook = async (integrationId) => {
|
|
|
92
92
|
integrationId
|
|
93
93
|
);
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
const instance = await getIntegrationInstance.execute(
|
|
96
96
|
integrationId,
|
|
97
97
|
integrationRecord.userId
|
|
98
98
|
);
|
|
99
|
+
|
|
100
|
+
return instance;
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const loadIntegrationForProcess = async (processId, integrationClass) => {
|
|
104
|
+
|
|
102
105
|
const { processRepository, integrationRepository, moduleRepository } =
|
|
103
106
|
initializeRepositories();
|
|
104
107
|
|
|
@@ -122,10 +125,12 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
|
|
|
122
125
|
throw new Error(`Process not found: ${processId}`);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
const instance = await getIntegrationInstance.execute(
|
|
126
129
|
process.integrationId,
|
|
127
130
|
process.userId
|
|
128
131
|
);
|
|
132
|
+
|
|
133
|
+
return instance;
|
|
129
134
|
};
|
|
130
135
|
|
|
131
136
|
const createQueueWorker = (integrationClass) => {
|
|
@@ -133,18 +138,19 @@ const createQueueWorker = (integrationClass) => {
|
|
|
133
138
|
async _run(params, context) {
|
|
134
139
|
try {
|
|
135
140
|
let integrationInstance;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
params.data.integrationId
|
|
142
|
-
);
|
|
143
|
-
} else if (params.data?.processId) {
|
|
141
|
+
|
|
142
|
+
// Prioritize processId first (for sync handler compatibility),
|
|
143
|
+
// then integrationId (for ANY event type that needs hydration),
|
|
144
|
+
// fallback to unhydrated instance
|
|
145
|
+
if (params.data?.processId) {
|
|
144
146
|
integrationInstance = await loadIntegrationForProcess(
|
|
145
147
|
params.data.processId,
|
|
146
148
|
integrationClass
|
|
147
149
|
);
|
|
150
|
+
} else if (params.data?.integrationId) {
|
|
151
|
+
integrationInstance = await loadIntegrationForWebhook(
|
|
152
|
+
params.data.integrationId
|
|
153
|
+
);
|
|
148
154
|
} else {
|
|
149
155
|
// Instantiates a DRY integration class without database records.
|
|
150
156
|
// There will be cases where we need to use helpers that the api modules can export.
|
|
@@ -235,6 +235,77 @@ router.get(
|
|
|
235
235
|
})
|
|
236
236
|
);
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* POST /db-migrate/resolve
|
|
240
|
+
*
|
|
241
|
+
* Resolve a failed migration by marking it as applied or rolled back
|
|
242
|
+
*
|
|
243
|
+
* Request body:
|
|
244
|
+
* {
|
|
245
|
+
* migrationName: string (e.g., '20251112195422_update_user_unique_constraints'),
|
|
246
|
+
* action: 'applied' | 'rolled-back',
|
|
247
|
+
* stage: string (optional, defaults to STAGE env var or 'production')
|
|
248
|
+
* }
|
|
249
|
+
*
|
|
250
|
+
* Response (200 OK):
|
|
251
|
+
* {
|
|
252
|
+
* success: true,
|
|
253
|
+
* message: string,
|
|
254
|
+
* migrationName: string,
|
|
255
|
+
* action: string
|
|
256
|
+
* }
|
|
257
|
+
*/
|
|
258
|
+
router.post(
|
|
259
|
+
'/db-migrate/resolve',
|
|
260
|
+
catchAsyncError(async (req, res) => {
|
|
261
|
+
const { migrationName, action = 'applied' } = req.body;
|
|
262
|
+
|
|
263
|
+
console.log(`Migration resolve request: migration=${migrationName}, action=${action}`);
|
|
264
|
+
|
|
265
|
+
// Validation
|
|
266
|
+
if (!migrationName) {
|
|
267
|
+
return res.status(400).json({
|
|
268
|
+
success: false,
|
|
269
|
+
error: 'migrationName is required'
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!['applied', 'rolled-back'].includes(action)) {
|
|
274
|
+
return res.status(400).json({
|
|
275
|
+
success: false,
|
|
276
|
+
error: 'action must be either "applied" or "rolled-back"'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Import prismaRunner here to avoid circular dependencies
|
|
282
|
+
const prismaRunner = require('../../database/utils/prisma-runner');
|
|
283
|
+
|
|
284
|
+
const result = await prismaRunner.runPrismaMigrateResolve(migrationName, action, true);
|
|
285
|
+
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
return res.status(500).json({
|
|
288
|
+
success: false,
|
|
289
|
+
error: `Failed to resolve migration: ${result.error}`
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
res.status(200).json({
|
|
294
|
+
success: true,
|
|
295
|
+
message: `Migration ${migrationName} marked as ${action}`,
|
|
296
|
+
migrationName,
|
|
297
|
+
action
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Migration resolve failed:', error);
|
|
301
|
+
return res.status(500).json({
|
|
302
|
+
success: false,
|
|
303
|
+
error: error.message
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
|
|
238
309
|
// Minimal Lambda handler (avoids app-handler-helpers which loads core/index.js → user/**)
|
|
239
310
|
const serverlessHttp = require('serverless-http');
|
|
240
311
|
const express = require('express');
|
|
@@ -244,7 +315,7 @@ const app = express();
|
|
|
244
315
|
app.use(cors());
|
|
245
316
|
app.use(express.json());
|
|
246
317
|
app.use(router);
|
|
247
|
-
app.use((err,
|
|
318
|
+
app.use((err, _req, res, _next) => {
|
|
248
319
|
console.error('Migration Router Error:', err);
|
|
249
320
|
res.status(500).json({ message: 'Internal Server Error' });
|
|
250
321
|
});
|
|
@@ -203,22 +203,53 @@ class IntegrationBase {
|
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
205
|
* Returns the modules as object with keys as module names.
|
|
206
|
+
* Uses the keys from Definition.modules to attach modules correctly.
|
|
207
|
+
*
|
|
208
|
+
* Example:
|
|
209
|
+
* Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
|
|
210
|
+
* Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
|
|
211
|
+
*
|
|
206
212
|
* @private
|
|
207
213
|
* @param {Array} integrationModules - Array of module instances
|
|
208
214
|
* @returns {Object} The modules object
|
|
209
215
|
*/
|
|
210
216
|
_appendModules(integrationModules) {
|
|
211
217
|
const modules = {};
|
|
218
|
+
|
|
219
|
+
// Build reverse mapping: definition.getName() → referenceKey
|
|
220
|
+
// e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
|
|
221
|
+
const moduleNameToKey = {};
|
|
222
|
+
if (this.constructor.Definition?.modules) {
|
|
223
|
+
for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
|
|
224
|
+
const definition = moduleConfig.definition;
|
|
225
|
+
if (definition) {
|
|
226
|
+
// Use getName() if available, fallback to moduleName
|
|
227
|
+
const definitionName = typeof definition.getName === 'function'
|
|
228
|
+
? definition.getName()
|
|
229
|
+
: definition.moduleName;
|
|
230
|
+
if (definitionName) {
|
|
231
|
+
moduleNameToKey[definitionName] = key;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
212
237
|
for (const module of integrationModules) {
|
|
213
|
-
const
|
|
238
|
+
const moduleName =
|
|
214
239
|
typeof module.getName === 'function'
|
|
215
240
|
? module.getName()
|
|
216
241
|
: module.name;
|
|
242
|
+
|
|
243
|
+
// Use the reference key from Definition.modules if available,
|
|
244
|
+
// otherwise fall back to moduleName
|
|
245
|
+
const key = moduleNameToKey[moduleName] || moduleName;
|
|
246
|
+
|
|
217
247
|
if (key) {
|
|
218
248
|
modules[key] = module;
|
|
219
249
|
this[key] = module;
|
|
220
250
|
}
|
|
221
251
|
}
|
|
252
|
+
|
|
222
253
|
return modules;
|
|
223
254
|
}
|
|
224
255
|
|
|
@@ -333,7 +364,6 @@ class IntegrationBase {
|
|
|
333
364
|
return {};
|
|
334
365
|
}
|
|
335
366
|
async loadUserActions({ actionType } = {}) {
|
|
336
|
-
console.log('loadUserActions called with actionType:', actionType);
|
|
337
367
|
const userActions = {};
|
|
338
368
|
for (const [key, event] of Object.entries(this.events)) {
|
|
339
369
|
if (event.type === constantsToBeMigrated.types.USER_ACTION) {
|
|
@@ -389,7 +419,6 @@ class IntegrationBase {
|
|
|
389
419
|
|
|
390
420
|
async onWebhook({ data }) {
|
|
391
421
|
// Default: no-op, integrations override this
|
|
392
|
-
console.log('Webhook received:', data);
|
|
393
422
|
}
|
|
394
423
|
|
|
395
424
|
async queueWebhook(data) {
|
|
@@ -601,11 +601,10 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
601
601
|
router.route('/api/entities/:entityId/test-auth').get(
|
|
602
602
|
catchAsyncError(async (req, res) => {
|
|
603
603
|
const user = await authenticateUser.execute(req);
|
|
604
|
-
const userId = user.getId();
|
|
605
604
|
const params = checkRequiredParams(req.params, ['entityId']);
|
|
606
605
|
const testAuthResponse = await testModuleAuth.execute(
|
|
607
606
|
params.entityId,
|
|
608
|
-
|
|
607
|
+
user // Pass User object for proper validation
|
|
609
608
|
);
|
|
610
609
|
|
|
611
610
|
if (!testAuthResponse) {
|
|
@@ -614,7 +613,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
614
613
|
errors: [
|
|
615
614
|
{
|
|
616
615
|
title: 'Authentication Error',
|
|
617
|
-
message: `There was an error with your
|
|
616
|
+
message: `There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.`,
|
|
618
617
|
timestamp: Date.now(),
|
|
619
618
|
},
|
|
620
619
|
],
|
|
@@ -628,9 +627,8 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
628
627
|
router.route('/api/entities/:entityId').get(
|
|
629
628
|
catchAsyncError(async (req, res) => {
|
|
630
629
|
const user = await authenticateUser.execute(req);
|
|
631
|
-
const userId = user.getId();
|
|
632
630
|
const params = checkRequiredParams(req.params, ['entityId']);
|
|
633
|
-
const module = await getModule.execute(params.entityId,
|
|
631
|
+
const module = await getModule.execute(params.entityId, user); // Pass User object
|
|
634
632
|
|
|
635
633
|
res.json(module);
|
|
636
634
|
})
|
|
@@ -639,12 +637,11 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
639
637
|
router.route('/api/entities/:entityId/options').post(
|
|
640
638
|
catchAsyncError(async (req, res) => {
|
|
641
639
|
const user = await authenticateUser.execute(req);
|
|
642
|
-
const userId = user.getId();
|
|
643
640
|
const params = checkRequiredParams(req.params, ['entityId']);
|
|
644
641
|
|
|
645
642
|
const entityOptions = await getEntityOptionsById.execute(
|
|
646
643
|
params.entityId,
|
|
647
|
-
|
|
644
|
+
user // Pass User object
|
|
648
645
|
);
|
|
649
646
|
|
|
650
647
|
res.json(entityOptions);
|
|
@@ -654,11 +651,10 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
654
651
|
router.route('/api/entities/:entityId/options/refresh').post(
|
|
655
652
|
catchAsyncError(async (req, res) => {
|
|
656
653
|
const user = await authenticateUser.execute(req);
|
|
657
|
-
const userId = user.getId();
|
|
658
654
|
const params = checkRequiredParams(req.params, ['entityId']);
|
|
659
655
|
const updatedOptions = await refreshEntityOptions.execute(
|
|
660
656
|
params.entityId,
|
|
661
|
-
|
|
657
|
+
user, // Pass User object
|
|
662
658
|
req.body
|
|
663
659
|
);
|
|
664
660
|
|
|
@@ -12,11 +12,18 @@ class GetEntityOptionsById {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Retrieve
|
|
16
|
-
*
|
|
17
|
-
* @param {string} entityId
|
|
15
|
+
* Retrieve entity options for a given entity
|
|
16
|
+
*
|
|
17
|
+
* @param {string|number} entityId - Entity ID to retrieve options for
|
|
18
|
+
* @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
|
|
19
|
+
* @returns {Promise<Object>} Entity options
|
|
18
20
|
*/
|
|
19
|
-
async execute(entityId,
|
|
21
|
+
async execute(entityId, userIdOrUser) {
|
|
22
|
+
// Support both userId (backward compatible) and User object (new pattern)
|
|
23
|
+
const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
|
|
24
|
+
? userIdOrUser.getId()
|
|
25
|
+
: userIdOrUser;
|
|
26
|
+
|
|
20
27
|
const entity = await this.moduleRepository.findEntityById(
|
|
21
28
|
entityId,
|
|
22
29
|
userId
|
|
@@ -26,7 +33,12 @@ class GetEntityOptionsById {
|
|
|
26
33
|
throw new Error(`Entity ${entityId} not found`);
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
// Validate entity ownership
|
|
37
|
+
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
38
|
+
? userIdOrUser.ownsUserId(entity.userId)
|
|
39
|
+
: entity.userId?.toString() === userId?.toString();
|
|
40
|
+
|
|
41
|
+
if (!isOwned) {
|
|
30
42
|
throw new Error(
|
|
31
43
|
`Entity ${entityId} does not belong to user ${userId}`
|
|
32
44
|
);
|
|
@@ -6,7 +6,19 @@ class GetModule {
|
|
|
6
6
|
this.moduleDefinitions = moduleDefinitions;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Get module instance for an entity
|
|
11
|
+
*
|
|
12
|
+
* @param {string|number} entityId - Entity ID to retrieve
|
|
13
|
+
* @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
|
|
14
|
+
* @returns {Promise<Object>} Module details
|
|
15
|
+
*/
|
|
16
|
+
async execute(entityId, userIdOrUser) {
|
|
17
|
+
// Support both userId (backward compatible) and User object (new pattern)
|
|
18
|
+
const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
|
|
19
|
+
? userIdOrUser.getId()
|
|
20
|
+
: userIdOrUser;
|
|
21
|
+
|
|
10
22
|
const entity = await this.moduleRepository.findEntityById(
|
|
11
23
|
entityId,
|
|
12
24
|
userId
|
|
@@ -16,7 +28,14 @@ class GetModule {
|
|
|
16
28
|
throw new Error(`Entity ${entityId} not found`);
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
// Validate entity ownership
|
|
32
|
+
// If User object provided, use ownsUserId to check linked users
|
|
33
|
+
// Otherwise fall back to simple equality check
|
|
34
|
+
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
35
|
+
? userIdOrUser.ownsUserId(entity.userId)
|
|
36
|
+
: entity.userId?.toString() === userId?.toString();
|
|
37
|
+
|
|
38
|
+
if (!isOwned) {
|
|
20
39
|
throw new Error(
|
|
21
40
|
`Entity ${entityId} does not belong to user ${userId}`
|
|
22
41
|
);
|
|
@@ -100,9 +100,20 @@ class ProcessAuthorizationCallback {
|
|
|
100
100
|
async findOrCreateEntity(entityDetails, moduleName, credentialId) {
|
|
101
101
|
const { identifiers, details } = entityDetails;
|
|
102
102
|
|
|
103
|
+
// Support both 'user' and 'userId' field names from module definitions
|
|
104
|
+
// Some modules use 'user' (legacy), others use 'userId' (newer pattern)
|
|
105
|
+
const userId = identifiers.user || identifiers.userId;
|
|
106
|
+
|
|
107
|
+
if (!userId) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Module definition for ${moduleName} must return 'user' or 'userId' in identifiers from getEntityDetails(). ` +
|
|
110
|
+
`Without userId, entity lookup would match across all users (security issue).`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
const existingEntity = await this.moduleRepository.findEntity({
|
|
104
115
|
externalId: identifiers.externalId,
|
|
105
|
-
user:
|
|
116
|
+
user: userId,
|
|
106
117
|
moduleName: moduleName,
|
|
107
118
|
});
|
|
108
119
|
|
|
@@ -12,11 +12,19 @@ class RefreshEntityOptions {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @param {string} entityId
|
|
15
|
+
* Refresh entity options for a given entity
|
|
16
|
+
*
|
|
17
|
+
* @param {string|number} entityId - Entity ID to refresh
|
|
18
|
+
* @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
|
|
19
|
+
* @param {Object} options - Refresh options
|
|
20
|
+
* @returns {Promise<Object>} Updated entity options
|
|
18
21
|
*/
|
|
19
|
-
async execute(entityId,
|
|
22
|
+
async execute(entityId, userIdOrUser, options) {
|
|
23
|
+
// Support both userId (backward compatible) and User object (new pattern)
|
|
24
|
+
const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
|
|
25
|
+
? userIdOrUser.getId()
|
|
26
|
+
: userIdOrUser;
|
|
27
|
+
|
|
20
28
|
const entity = await this.moduleRepository.findEntityById(
|
|
21
29
|
entityId,
|
|
22
30
|
userId
|
|
@@ -26,7 +34,12 @@ class RefreshEntityOptions {
|
|
|
26
34
|
throw new Error(`Entity ${entityId} not found`);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
// Validate entity ownership
|
|
38
|
+
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
39
|
+
? userIdOrUser.ownsUserId(entity.userId)
|
|
40
|
+
: entity.userId?.toString() === userId?.toString();
|
|
41
|
+
|
|
42
|
+
if (!isOwned) {
|
|
30
43
|
throw new Error(
|
|
31
44
|
`Entity ${entityId} does not belong to user ${userId}`
|
|
32
45
|
);
|
|
@@ -11,7 +11,19 @@ class TestModuleAuth {
|
|
|
11
11
|
this.moduleDefinitions = moduleDefinitions;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Test authentication for a module entity
|
|
16
|
+
*
|
|
17
|
+
* @param {string|number} entityId - Entity ID to test
|
|
18
|
+
* @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
|
|
19
|
+
* @returns {Promise<boolean>} Authentication test result
|
|
20
|
+
*/
|
|
21
|
+
async execute(entityId, userIdOrUser) {
|
|
22
|
+
// Support both userId (backward compatible) and User object (new pattern)
|
|
23
|
+
const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
|
|
24
|
+
? userIdOrUser.getId()
|
|
25
|
+
: userIdOrUser;
|
|
26
|
+
|
|
15
27
|
const entity = await this.moduleRepository.findEntityById(
|
|
16
28
|
entityId,
|
|
17
29
|
userId
|
|
@@ -21,7 +33,12 @@ class TestModuleAuth {
|
|
|
21
33
|
throw new Error(`Entity ${entityId} not found`);
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
// Validate entity ownership
|
|
37
|
+
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
38
|
+
? userIdOrUser.ownsUserId(entity.userId)
|
|
39
|
+
: entity.userId?.toString() === userId?.toString();
|
|
40
|
+
|
|
41
|
+
if (!isOwned) {
|
|
25
42
|
throw new Error(
|
|
26
43
|
`Entity ${entityId} does not belong to user ${userId}`
|
|
27
44
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.57",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
43
|
-
"@friggframework/test": "2.0.0-next.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0-next.57",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0-next.57",
|
|
43
|
+
"@friggframework/test": "2.0.0-next.57",
|
|
44
44
|
"@prisma/client": "^6.17.0",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "d09ee09decef14a8cdbf657494ac0dc9bf0b4014"
|
|
84
84
|
}
|
package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql
CHANGED
|
@@ -21,49 +21,5 @@ ALTER TABLE "Credential" DROP COLUMN "subType";
|
|
|
21
21
|
-- AlterTable
|
|
22
22
|
ALTER TABLE "Entity" DROP COLUMN "subType";
|
|
23
23
|
|
|
24
|
-
-- CreateTable
|
|
25
|
-
CREATE TABLE "Process" (
|
|
26
|
-
"id" SERIAL NOT NULL,
|
|
27
|
-
"userId" INTEGER NOT NULL,
|
|
28
|
-
"integrationId" INTEGER NOT NULL,
|
|
29
|
-
"name" TEXT NOT NULL,
|
|
30
|
-
"type" TEXT NOT NULL,
|
|
31
|
-
"state" TEXT NOT NULL,
|
|
32
|
-
"context" JSONB NOT NULL DEFAULT '{}',
|
|
33
|
-
"results" JSONB NOT NULL DEFAULT '{}',
|
|
34
|
-
"parentProcessId" INTEGER,
|
|
35
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
37
|
-
|
|
38
|
-
CONSTRAINT "Process_pkey" PRIMARY KEY ("id")
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
-- CreateIndex
|
|
42
|
-
CREATE INDEX "Process_userId_idx" ON "Process"("userId");
|
|
43
|
-
|
|
44
|
-
-- CreateIndex
|
|
45
|
-
CREATE INDEX "Process_integrationId_idx" ON "Process"("integrationId");
|
|
46
|
-
|
|
47
|
-
-- CreateIndex
|
|
48
|
-
CREATE INDEX "Process_type_idx" ON "Process"("type");
|
|
49
|
-
|
|
50
|
-
-- CreateIndex
|
|
51
|
-
CREATE INDEX "Process_state_idx" ON "Process"("state");
|
|
52
|
-
|
|
53
|
-
-- CreateIndex
|
|
54
|
-
CREATE INDEX "Process_name_idx" ON "Process"("name");
|
|
55
|
-
|
|
56
|
-
-- CreateIndex
|
|
57
|
-
CREATE INDEX "Process_parentProcessId_idx" ON "Process"("parentProcessId");
|
|
58
|
-
|
|
59
24
|
-- CreateIndex
|
|
60
25
|
CREATE UNIQUE INDEX "User_username_appUserId_key" ON "User"("username", "appUserId");
|
|
61
|
-
|
|
62
|
-
-- AddForeignKey
|
|
63
|
-
ALTER TABLE "Process" ADD CONSTRAINT "Process_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
64
|
-
|
|
65
|
-
-- AddForeignKey
|
|
66
|
-
ALTER TABLE "Process" ADD CONSTRAINT "Process_integrationId_fkey" FOREIGN KEY ("integrationId") REFERENCES "Integration"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
67
|
-
|
|
68
|
-
-- AddForeignKey
|
|
69
|
-
ALTER TABLE "Process" ADD CONSTRAINT "Process_parentProcessId_fkey" FOREIGN KEY ("parentProcessId") REFERENCES "Process"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
package/queues/queuer-util.js
CHANGED
|
@@ -4,7 +4,6 @@ const { SQSClient, SendMessageCommand, SendMessageBatchCommand } = require('@aws
|
|
|
4
4
|
const awsConfigOptions = () => {
|
|
5
5
|
const config = {};
|
|
6
6
|
if (process.env.IS_OFFLINE) {
|
|
7
|
-
console.log('Running in offline mode');
|
|
8
7
|
config.credentials = {
|
|
9
8
|
accessKeyId: 'test-aws-key',
|
|
10
9
|
secretAccessKey: 'test-aws-secret',
|
|
@@ -21,7 +20,6 @@ const sqs = new SQSClient(awsConfigOptions());
|
|
|
21
20
|
|
|
22
21
|
const QueuerUtil = {
|
|
23
22
|
send: async (message, queueUrl) => {
|
|
24
|
-
console.log(`Enqueuing message to SQS queue ${queueUrl}`);
|
|
25
23
|
const command = new SendMessageCommand({
|
|
26
24
|
MessageBody: JSON.stringify(message),
|
|
27
25
|
QueueUrl: queueUrl,
|
|
@@ -30,9 +28,6 @@ const QueuerUtil = {
|
|
|
30
28
|
},
|
|
31
29
|
|
|
32
30
|
batchSend: async (entries = [], queueUrl) => {
|
|
33
|
-
console.log(
|
|
34
|
-
`Enqueuing ${entries.length} entries on SQS to queue ${queueUrl}`
|
|
35
|
-
);
|
|
36
31
|
const buffer = [];
|
|
37
32
|
const batchSize = 10;
|
|
38
33
|
|
|
@@ -43,7 +38,6 @@ const QueuerUtil = {
|
|
|
43
38
|
});
|
|
44
39
|
// Sends 10, then purges the buffer
|
|
45
40
|
if (buffer.length === batchSize) {
|
|
46
|
-
console.log('Buffer at 10, sending batch');
|
|
47
41
|
const command = new SendMessageBatchCommand({
|
|
48
42
|
Entries: buffer,
|
|
49
43
|
QueueUrl: queueUrl,
|
|
@@ -53,11 +47,9 @@ const QueuerUtil = {
|
|
|
53
47
|
buffer.splice(0, buffer.length);
|
|
54
48
|
}
|
|
55
49
|
}
|
|
56
|
-
console.log('Buffer at end, sending final batch');
|
|
57
50
|
|
|
58
51
|
// If any remaining entries under 10 are left in the buffer, send and return
|
|
59
52
|
if (buffer.length > 0) {
|
|
60
|
-
console.log(buffer);
|
|
61
53
|
const command = new SendMessageBatchCommand({
|
|
62
54
|
Entries: buffer,
|
|
63
55
|
QueueUrl: queueUrl,
|
|
@@ -427,6 +427,26 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
427
427
|
const date = new Date(value);
|
|
428
428
|
return isNaN(date.getTime()) ? undefined : date;
|
|
429
429
|
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Link an individual user to an organization user
|
|
433
|
+
* @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
|
|
434
|
+
* @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
|
|
435
|
+
* @returns {Promise<Object>} Updated individual user object
|
|
436
|
+
*/
|
|
437
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
438
|
+
const doc = await updateOne(
|
|
439
|
+
this.prisma,
|
|
440
|
+
'User',
|
|
441
|
+
{ _id: toObjectId(individualUserId), type: 'INDIVIDUAL' },
|
|
442
|
+
{ $set: { organizationId: toObjectId(organizationUserId) } }
|
|
443
|
+
);
|
|
444
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
445
|
+
'User',
|
|
446
|
+
doc
|
|
447
|
+
);
|
|
448
|
+
return this._mapUser(decrypted);
|
|
449
|
+
}
|
|
430
450
|
}
|
|
431
451
|
|
|
432
452
|
module.exports = { UserRepositoryDocumentDB };
|
|
@@ -193,6 +193,20 @@ class UserRepositoryInterface {
|
|
|
193
193
|
async deleteUser(userId) {
|
|
194
194
|
throw new Error('Method deleteUser must be implemented by subclass');
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Link an individual user to an organization user
|
|
199
|
+
*
|
|
200
|
+
* @param {string|number} individualUserId - Individual user ID
|
|
201
|
+
* @param {string|number} organizationUserId - Organization user ID
|
|
202
|
+
* @returns {Promise<Object>} Updated individual user object
|
|
203
|
+
* @abstract
|
|
204
|
+
*/
|
|
205
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
'Method linkIndividualToOrganization must be implemented by subclass'
|
|
208
|
+
);
|
|
209
|
+
}
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
module.exports = { UserRepositoryInterface };
|
|
@@ -287,6 +287,24 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
287
287
|
throw error;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Link an individual user to an organization user
|
|
293
|
+
* @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
|
|
294
|
+
* @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
|
|
295
|
+
* @returns {Promise<Object>} Updated individual user object
|
|
296
|
+
*/
|
|
297
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
298
|
+
return await this.prisma.user.update({
|
|
299
|
+
where: {
|
|
300
|
+
id: individualUserId,
|
|
301
|
+
type: 'INDIVIDUAL',
|
|
302
|
+
},
|
|
303
|
+
data: {
|
|
304
|
+
organizationId: organizationUserId,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
module.exports = { UserRepositoryMongo };
|
|
@@ -346,6 +346,28 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
346
346
|
throw error;
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Link an individual user to an organization user
|
|
352
|
+
* @param {string} individualUserId - Individual user ID (string from application layer)
|
|
353
|
+
* @param {string} organizationUserId - Organization user ID (string from application layer)
|
|
354
|
+
* @returns {Promise<Object>} Updated individual user with string IDs
|
|
355
|
+
*/
|
|
356
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
357
|
+
const intIndividualId = this._convertId(individualUserId);
|
|
358
|
+
const intOrganizationId = this._convertId(organizationUserId);
|
|
359
|
+
|
|
360
|
+
const user = await this.prisma.user.update({
|
|
361
|
+
where: {
|
|
362
|
+
id: intIndividualId,
|
|
363
|
+
type: 'INDIVIDUAL',
|
|
364
|
+
},
|
|
365
|
+
data: {
|
|
366
|
+
organizationId: intOrganizationId,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
return this._convertUserIds(user);
|
|
370
|
+
}
|
|
349
371
|
}
|
|
350
372
|
|
|
351
373
|
module.exports = { UserRepositoryPostgres };
|
|
@@ -53,7 +53,7 @@ class GetUserFromXFriggHeaders {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// VALIDATION: If both IDs provided and both users exist,
|
|
56
|
+
// VALIDATION/AUTO-LINKING: If both IDs provided and both users exist, handle mismatch
|
|
57
57
|
if (
|
|
58
58
|
appUserId &&
|
|
59
59
|
appOrgId &&
|
|
@@ -66,31 +66,57 @@ class GetUserFromXFriggHeaders {
|
|
|
66
66
|
const expectedOrgId = organizationUserData.id?.toString();
|
|
67
67
|
|
|
68
68
|
if (individualOrgId !== expectedOrgId) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// Default behavior: Auto-link disconnected users
|
|
70
|
+
// Opt-in strict mode: Throw error on mismatch
|
|
71
|
+
if (this.userConfig.strictUserValidation) {
|
|
72
|
+
throw Boom.badRequest(
|
|
73
|
+
'User ID mismatch: x-frigg-appUserId and x-frigg-appOrgId refer to different users. ' +
|
|
74
|
+
'Provide only one identifier or ensure they belong to the same user.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Auto-link the users
|
|
79
|
+
individualUserData = await this.userRepository.linkIndividualToOrganization(
|
|
80
|
+
individualUserData.id,
|
|
81
|
+
organizationUserData.id
|
|
72
82
|
);
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
// Auto-create
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// Auto-create users independently if they don't exist and are required
|
|
87
|
+
if (
|
|
88
|
+
!individualUserData &&
|
|
89
|
+
appUserId &&
|
|
90
|
+
this.userConfig.individualUserRequired !== false
|
|
91
|
+
) {
|
|
92
|
+
individualUserData =
|
|
93
|
+
await this.userRepository.createIndividualUser({
|
|
94
|
+
appUserId,
|
|
95
|
+
username: `app-user-${appUserId}`,
|
|
96
|
+
email: `${appUserId}@app.local`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
!organizationUserData &&
|
|
102
|
+
appOrgId &&
|
|
103
|
+
this.userConfig.organizationUserRequired
|
|
104
|
+
) {
|
|
105
|
+
organizationUserData =
|
|
106
|
+
await this.userRepository.createOrganizationUser({
|
|
107
|
+
appOrgId,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Link individual user to newly created org user if individual exists
|
|
111
|
+
if (individualUserData && organizationUserData) {
|
|
112
|
+
individualUserData = await this.userRepository.linkIndividualToOrganization(
|
|
113
|
+
individualUserData.id,
|
|
114
|
+
organizationUserData.id
|
|
115
|
+
);
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
|
|
119
|
+
const user = new User(
|
|
94
120
|
individualUserData,
|
|
95
121
|
organizationUserData,
|
|
96
122
|
this.userConfig.usePassword,
|
|
@@ -98,9 +124,9 @@ class GetUserFromXFriggHeaders {
|
|
|
98
124
|
this.userConfig.individualUserRequired,
|
|
99
125
|
this.userConfig.organizationUserRequired
|
|
100
126
|
);
|
|
127
|
+
|
|
128
|
+
return user;
|
|
101
129
|
}
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
module.exports = { GetUserFromXFriggHeaders };
|
|
105
|
-
|
|
106
|
-
|
package/user/user.js
CHANGED
|
@@ -88,6 +88,38 @@ class User {
|
|
|
88
88
|
getAppOrgId() {
|
|
89
89
|
return this.organizationUser?.appOrgId || null;
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks if a given userId belongs to this user (either primary or linked).
|
|
94
|
+
* When primary is 'organization', entities owned by the linked individual user
|
|
95
|
+
* should still be accessible to the organization.
|
|
96
|
+
*
|
|
97
|
+
* @param {string|number} userId - The userId to check
|
|
98
|
+
* @returns {boolean} True if the userId belongs to this user or their linked user
|
|
99
|
+
*/
|
|
100
|
+
ownsUserId(userId) {
|
|
101
|
+
const userIdStr = userId?.toString();
|
|
102
|
+
const primaryId = this.getPrimaryUser()?.id?.toString();
|
|
103
|
+
const individualId = this.individualUser?.id?.toString();
|
|
104
|
+
const organizationId = this.organizationUser?.id?.toString();
|
|
105
|
+
|
|
106
|
+
// Check if userId matches primary user
|
|
107
|
+
if (userIdStr === primaryId) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// When primary is 'organization', also check linked individual user
|
|
112
|
+
if (this.config.primary === 'organization' && userIdStr === individualId) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// When primary is 'individual', also check linked organization user if required
|
|
117
|
+
if (this.config.primary === 'individual' && this.config.organizationUserRequired && userIdStr === organizationId) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
91
123
|
}
|
|
92
124
|
|
|
93
125
|
module.exports = { User };
|