@friggframework/core 2.0.0-next.56 → 2.0.0-next.58
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/application/commands/README.md +90 -60
- package/application/commands/user-commands.js +36 -5
- 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/encryption/documentdb-encryption-service.md +1537 -1232
- 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 -11
- package/user/repositories/user-repository-factory.js +2 -1
- package/user/repositories/user-repository-interface.js +14 -11
- package/user/repositories/user-repository-mongo.js +18 -11
- package/user/repositories/user-repository-postgres.js +22 -13
- package/user/use-cases/get-user-from-x-frigg-headers.js +47 -21
- package/user/user.js +32 -0
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,
|
|
@@ -238,17 +238,6 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
238
238
|
return this._mapUser(decrypted);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
async findUserById(userId) {
|
|
242
|
-
const doc = await findOne(this.prisma, 'User', {
|
|
243
|
-
_id: toObjectId(userId),
|
|
244
|
-
});
|
|
245
|
-
const decrypted = await this.encryptionService.decryptFields(
|
|
246
|
-
'User',
|
|
247
|
-
doc
|
|
248
|
-
);
|
|
249
|
-
return this._mapUser(decrypted);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
241
|
async findIndividualUserByEmail(email) {
|
|
253
242
|
const doc = await findOne(this.prisma, 'User', {
|
|
254
243
|
type: 'INDIVIDUAL',
|
|
@@ -427,6 +416,26 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
427
416
|
const date = new Date(value);
|
|
428
417
|
return isNaN(date.getTime()) ? undefined : date;
|
|
429
418
|
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Link an individual user to an organization user
|
|
422
|
+
* @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
|
|
423
|
+
* @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
|
|
424
|
+
* @returns {Promise<Object>} Updated individual user object
|
|
425
|
+
*/
|
|
426
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
427
|
+
const doc = await updateOne(
|
|
428
|
+
this.prisma,
|
|
429
|
+
'User',
|
|
430
|
+
{ _id: toObjectId(individualUserId), type: 'INDIVIDUAL' },
|
|
431
|
+
{ $set: { organizationId: toObjectId(organizationUserId) } }
|
|
432
|
+
);
|
|
433
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
434
|
+
'User',
|
|
435
|
+
doc
|
|
436
|
+
);
|
|
437
|
+
return this._mapUser(decrypted);
|
|
438
|
+
}
|
|
430
439
|
}
|
|
431
440
|
|
|
432
441
|
module.exports = { UserRepositoryDocumentDB };
|
|
@@ -17,7 +17,8 @@ const databaseConfig = require('../../database/config');
|
|
|
17
17
|
* Usage:
|
|
18
18
|
* ```javascript
|
|
19
19
|
* const repository = createUserRepository();
|
|
20
|
-
* const user = await repository.
|
|
20
|
+
* const user = await repository.findIndividualUserById(id); // ID is string
|
|
21
|
+
* const orgUser = await repository.findOrganizationUserById(id); // ID is string
|
|
21
22
|
* ```
|
|
22
23
|
*
|
|
23
24
|
* @returns {UserRepositoryInterface} Configured repository adapter
|
|
@@ -131,17 +131,6 @@ class UserRepositoryInterface {
|
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
* Find user by ID (any type)
|
|
136
|
-
*
|
|
137
|
-
* @param {string|number} userId - User ID
|
|
138
|
-
* @returns {Promise<Object|null>} User object or null
|
|
139
|
-
* @abstract
|
|
140
|
-
*/
|
|
141
|
-
async findUserById(userId) {
|
|
142
|
-
throw new Error('Method findUserById must be implemented by subclass');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
134
|
/**
|
|
146
135
|
* Find individual user by email
|
|
147
136
|
*
|
|
@@ -193,6 +182,20 @@ class UserRepositoryInterface {
|
|
|
193
182
|
async deleteUser(userId) {
|
|
194
183
|
throw new Error('Method deleteUser must be implemented by subclass');
|
|
195
184
|
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Link an individual user to an organization user
|
|
188
|
+
*
|
|
189
|
+
* @param {string|number} individualUserId - Individual user ID
|
|
190
|
+
* @param {string|number} organizationUserId - Organization user ID
|
|
191
|
+
* @returns {Promise<Object>} Updated individual user object
|
|
192
|
+
* @abstract
|
|
193
|
+
*/
|
|
194
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
'Method linkIndividualToOrganization must be implemented by subclass'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
module.exports = { UserRepositoryInterface };
|
|
@@ -195,17 +195,6 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/**
|
|
199
|
-
* Find user by ID (any type)
|
|
200
|
-
* @param {string} userId - User ID
|
|
201
|
-
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
202
|
-
*/
|
|
203
|
-
async findUserById(userId) {
|
|
204
|
-
return await this.prisma.user.findUnique({
|
|
205
|
-
where: { id: userId },
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
198
|
/**
|
|
210
199
|
* Find individual user by email
|
|
211
200
|
* @param {string} email - Email to search for
|
|
@@ -287,6 +276,24 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
287
276
|
throw error;
|
|
288
277
|
}
|
|
289
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Link an individual user to an organization user
|
|
282
|
+
* @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
|
|
283
|
+
* @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
|
|
284
|
+
* @returns {Promise<Object>} Updated individual user object
|
|
285
|
+
*/
|
|
286
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
287
|
+
return await this.prisma.user.update({
|
|
288
|
+
where: {
|
|
289
|
+
id: individualUserId,
|
|
290
|
+
type: 'INDIVIDUAL',
|
|
291
|
+
},
|
|
292
|
+
data: {
|
|
293
|
+
organizationId: organizationUserId,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
290
297
|
}
|
|
291
298
|
|
|
292
299
|
module.exports = { UserRepositoryMongo };
|
|
@@ -237,19 +237,6 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
237
237
|
return this._convertUserIds(user);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
/**
|
|
241
|
-
* Find user by ID (any type)
|
|
242
|
-
* @param {string} userId - User ID (string from application layer)
|
|
243
|
-
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
244
|
-
*/
|
|
245
|
-
async findUserById(userId) {
|
|
246
|
-
const intId = this._convertId(userId);
|
|
247
|
-
const user = await this.prisma.user.findUnique({
|
|
248
|
-
where: { id: intId },
|
|
249
|
-
});
|
|
250
|
-
return this._convertUserIds(user);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
240
|
/**
|
|
254
241
|
* Find individual user by email
|
|
255
242
|
* @param {string} email - Email to search for
|
|
@@ -346,6 +333,28 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
346
333
|
throw error;
|
|
347
334
|
}
|
|
348
335
|
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Link an individual user to an organization user
|
|
339
|
+
* @param {string} individualUserId - Individual user ID (string from application layer)
|
|
340
|
+
* @param {string} organizationUserId - Organization user ID (string from application layer)
|
|
341
|
+
* @returns {Promise<Object>} Updated individual user with string IDs
|
|
342
|
+
*/
|
|
343
|
+
async linkIndividualToOrganization(individualUserId, organizationUserId) {
|
|
344
|
+
const intIndividualId = this._convertId(individualUserId);
|
|
345
|
+
const intOrganizationId = this._convertId(organizationUserId);
|
|
346
|
+
|
|
347
|
+
const user = await this.prisma.user.update({
|
|
348
|
+
where: {
|
|
349
|
+
id: intIndividualId,
|
|
350
|
+
type: 'INDIVIDUAL',
|
|
351
|
+
},
|
|
352
|
+
data: {
|
|
353
|
+
organizationId: intOrganizationId,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
return this._convertUserIds(user);
|
|
357
|
+
}
|
|
349
358
|
}
|
|
350
359
|
|
|
351
360
|
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 };
|