@friggframework/core 2.0.0-next.44 → 2.0.0-next.45
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/handlers/WEBHOOKS.md +653 -0
- package/handlers/backend-utils.js +118 -3
- package/handlers/integration-event-dispatcher.test.js +68 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/integration-webhook-routers.test.js +126 -0
- package/handlers/webhook-flow.integration.test.js +356 -0
- package/handlers/workers/integration-defined-workers.test.js +184 -0
- package/index.js +16 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/integration-base.js +74 -3
- package/integrations/repositories/process-repository-postgres.js +43 -20
- package/integrations/tests/doubles/dummy-integration-class.js +1 -8
- package/package.json +5 -5
- package/queues/queuer-util.js +10 -0
- package/user/repositories/user-repository-mongo.js +53 -12
- package/user/repositories/user-repository-postgres.js +53 -14
- package/user/tests/use-cases/login-user.test.js +85 -5
- package/user/tests/user-password-encryption-isolation.test.js +237 -0
- package/user/tests/user-password-hashing.test.js +235 -0
- package/user/use-cases/login-user.js +1 -1
- package/user/user.js +2 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Webhook Quick Start Guide
|
|
2
|
+
|
|
3
|
+
Get webhooks working in your Frigg integration in 3 simple steps.
|
|
4
|
+
|
|
5
|
+
## Step 1: Enable Webhooks
|
|
6
|
+
|
|
7
|
+
Add `webhooks: true` to your Integration Definition:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
class MyIntegration extends IntegrationBase {
|
|
11
|
+
static Definition = {
|
|
12
|
+
name: 'my-integration',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
modules: {
|
|
15
|
+
myapi: { definition: MyApiDefinition },
|
|
16
|
+
},
|
|
17
|
+
webhooks: true, // ← Add this line
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Step 2: Handle Webhook Processing
|
|
23
|
+
|
|
24
|
+
Override the `onWebhook` handler to process webhooks:
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
class MyIntegration extends IntegrationBase {
|
|
28
|
+
// ... Definition ...
|
|
29
|
+
|
|
30
|
+
async onWebhook({ data }) {
|
|
31
|
+
const { body } = data;
|
|
32
|
+
|
|
33
|
+
// You have full access to:
|
|
34
|
+
// - this.myapi (your API modules)
|
|
35
|
+
// - this.config (integration config)
|
|
36
|
+
// - Database operations
|
|
37
|
+
|
|
38
|
+
if (body.event === 'item.created') {
|
|
39
|
+
await this.myapi.api.createItem(body.data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { processed: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Step 3: Deploy
|
|
48
|
+
|
|
49
|
+
Deploy your Frigg app - webhook routes are automatically created:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
POST /api/my-integration-integration/webhooks/:integrationId
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## That's It!
|
|
56
|
+
|
|
57
|
+
The default behavior handles:
|
|
58
|
+
- ✅ Receiving webhooks (instant 200 OK response)
|
|
59
|
+
- ✅ Queuing to SQS
|
|
60
|
+
- ✅ Loading your integration with DB and API modules
|
|
61
|
+
- ✅ Calling your `onWebhook` handler
|
|
62
|
+
|
|
63
|
+
## Optional: Custom Signature Verification
|
|
64
|
+
|
|
65
|
+
Override `onWebhookReceived` for custom signature checks:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
async onWebhookReceived({ req, res }) {
|
|
69
|
+
// Verify signature
|
|
70
|
+
const signature = req.headers['x-webhook-signature'];
|
|
71
|
+
if (!this.verifySignature(req.body, signature)) {
|
|
72
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Queue for processing (default behavior)
|
|
76
|
+
await this.queueWebhook({
|
|
77
|
+
integrationId: req.params.integrationId,
|
|
78
|
+
body: req.body,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
res.status(200).json({ received: true });
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Two Webhook Routes
|
|
86
|
+
|
|
87
|
+
### With Integration ID (Recommended)
|
|
88
|
+
```
|
|
89
|
+
POST /api/{name}-integration/webhooks/:integrationId
|
|
90
|
+
```
|
|
91
|
+
- Full integration loaded in worker
|
|
92
|
+
- Access to DB, config, and API modules
|
|
93
|
+
- Use `this.myapi`, `this.config`, etc.
|
|
94
|
+
|
|
95
|
+
### Without Integration ID
|
|
96
|
+
```
|
|
97
|
+
POST /api/{name}-integration/webhooks
|
|
98
|
+
```
|
|
99
|
+
- Unhydrated integration
|
|
100
|
+
- Useful for system-wide events
|
|
101
|
+
- Limited context
|
|
102
|
+
|
|
103
|
+
## Need Help?
|
|
104
|
+
|
|
105
|
+
See full documentation: `packages/core/handlers/WEBHOOKS.md`
|
|
106
|
+
|
|
107
|
+
## Common Patterns
|
|
108
|
+
|
|
109
|
+
### Slack
|
|
110
|
+
```javascript
|
|
111
|
+
async onWebhookReceived({ req, res }) {
|
|
112
|
+
if (req.body.type === 'url_verification') {
|
|
113
|
+
return res.json({ challenge: req.body.challenge });
|
|
114
|
+
}
|
|
115
|
+
// ... verify signature, queue ...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Stripe
|
|
120
|
+
```javascript
|
|
121
|
+
async onWebhookReceived({ req, res }) {
|
|
122
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
123
|
+
const event = stripe.webhooks.constructEvent(
|
|
124
|
+
JSON.stringify(req.body),
|
|
125
|
+
req.headers['stripe-signature'],
|
|
126
|
+
process.env.STRIPE_WEBHOOK_SECRET
|
|
127
|
+
);
|
|
128
|
+
await this.queueWebhook({ body: event });
|
|
129
|
+
res.status(200).json({ received: true });
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### GitHub
|
|
134
|
+
```javascript
|
|
135
|
+
async onWebhookReceived({ req, res }) {
|
|
136
|
+
const crypto = require('crypto');
|
|
137
|
+
const signature = req.headers['x-hub-signature-256'];
|
|
138
|
+
const hash = crypto
|
|
139
|
+
.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
|
|
140
|
+
.update(JSON.stringify(req.body))
|
|
141
|
+
.digest('hex');
|
|
142
|
+
|
|
143
|
+
if (`sha256=${hash}` !== signature) {
|
|
144
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await this.queueWebhook({ integrationId: req.params.integrationId, body: req.body });
|
|
148
|
+
res.status(200).json({ received: true });
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
@@ -22,6 +22,8 @@ const constantsToBeMigrated = {
|
|
|
22
22
|
GET_USER_ACTIONS: 'GET_USER_ACTIONS',
|
|
23
23
|
GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
|
|
24
24
|
REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
|
|
25
|
+
WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
|
|
26
|
+
ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
|
|
25
27
|
// etc...
|
|
26
28
|
},
|
|
27
29
|
types: {
|
|
@@ -130,6 +132,14 @@ class IntegrationBase {
|
|
|
130
132
|
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
131
133
|
handler: this.refreshActionOptions,
|
|
132
134
|
},
|
|
135
|
+
[constantsToBeMigrated.defaultEvents.WEBHOOK_RECEIVED]: {
|
|
136
|
+
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
137
|
+
handler: this.onWebhookReceived,
|
|
138
|
+
},
|
|
139
|
+
[constantsToBeMigrated.defaultEvents.ON_WEBHOOK]: {
|
|
140
|
+
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
141
|
+
handler: this.onWebhook,
|
|
142
|
+
},
|
|
133
143
|
};
|
|
134
144
|
}
|
|
135
145
|
|
|
@@ -294,7 +304,9 @@ class IntegrationBase {
|
|
|
294
304
|
await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
|
|
295
305
|
}
|
|
296
306
|
|
|
297
|
-
async onUpdate(params) {
|
|
307
|
+
async onUpdate(params) {
|
|
308
|
+
return this.validateConfig();
|
|
309
|
+
}
|
|
298
310
|
|
|
299
311
|
async onDelete(params) {}
|
|
300
312
|
|
|
@@ -357,6 +369,50 @@ class IntegrationBase {
|
|
|
357
369
|
return options;
|
|
358
370
|
}
|
|
359
371
|
|
|
372
|
+
/**
|
|
373
|
+
* WEBHOOK EVENT HANDLERS
|
|
374
|
+
*/
|
|
375
|
+
async onWebhookReceived({ req, res }) {
|
|
376
|
+
// Default: queue webhook for processing
|
|
377
|
+
const body = req.body;
|
|
378
|
+
const integrationId = req.params.integrationId || null;
|
|
379
|
+
|
|
380
|
+
await this.queueWebhook({
|
|
381
|
+
integrationId,
|
|
382
|
+
body,
|
|
383
|
+
headers: req.headers,
|
|
384
|
+
query: req.query,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
res.status(200).json({ received: true });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async onWebhook({ data }) {
|
|
391
|
+
// Default: no-op, integrations override this
|
|
392
|
+
console.log('Webhook received:', data);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async queueWebhook(data) {
|
|
396
|
+
const { QueuerUtil } = require('../queues');
|
|
397
|
+
|
|
398
|
+
const queueName = `${this.constructor.Definition.name
|
|
399
|
+
.toUpperCase()
|
|
400
|
+
.replace(/-/g, '_')}_QUEUE_URL`;
|
|
401
|
+
const queueUrl = process.env[queueName];
|
|
402
|
+
|
|
403
|
+
if (!queueUrl) {
|
|
404
|
+
throw new Error(`Queue URL not found for ${queueName}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return QueuerUtil.send(
|
|
408
|
+
{
|
|
409
|
+
event: 'ON_WEBHOOK',
|
|
410
|
+
data,
|
|
411
|
+
},
|
|
412
|
+
queueUrl
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
360
416
|
// === Domain Methods (moved from Integration.js) ===
|
|
361
417
|
|
|
362
418
|
getConfig() {
|
|
@@ -403,8 +459,14 @@ class IntegrationBase {
|
|
|
403
459
|
return this.userId.toString() === userId.toString();
|
|
404
460
|
}
|
|
405
461
|
|
|
462
|
+
registerEventHandlers() {
|
|
463
|
+
this.on = {
|
|
464
|
+
...this.defaultEvents,
|
|
465
|
+
...this.events,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
406
469
|
async initialize() {
|
|
407
|
-
// Load dynamic user actions
|
|
408
470
|
try {
|
|
409
471
|
const additionalUserActions = await this.loadDynamicUserActions();
|
|
410
472
|
this.events = { ...this.events, ...additionalUserActions };
|
|
@@ -412,7 +474,16 @@ class IntegrationBase {
|
|
|
412
474
|
this.addError(e);
|
|
413
475
|
}
|
|
414
476
|
|
|
415
|
-
|
|
477
|
+
this.registerEventHandlers();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async send(event, object) {
|
|
481
|
+
if (!this.on[event]) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Event ${event} is not defined in the Integration event object`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return this.on[event].handler.call(this, object);
|
|
416
487
|
}
|
|
417
488
|
|
|
418
489
|
getOptionDetails() {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
ProcessRepositoryInterface,
|
|
4
|
+
} = require('./process-repository-interface');
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* PostgreSQL Process Repository Adapter
|
|
@@ -22,6 +24,22 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
22
24
|
this.prisma = prisma;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Convert string ID to integer for PostgreSQL queries
|
|
29
|
+
* @private
|
|
30
|
+
* @param {string|number|null|undefined} id - ID to convert
|
|
31
|
+
* @returns {number|null|undefined} Integer ID or null/undefined
|
|
32
|
+
* @throws {Error} If ID cannot be converted to integer
|
|
33
|
+
*/
|
|
34
|
+
_convertId(id) {
|
|
35
|
+
if (id === null || id === undefined) return id;
|
|
36
|
+
const parsed = parseInt(id, 10);
|
|
37
|
+
if (isNaN(parsed)) {
|
|
38
|
+
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
/**
|
|
26
44
|
* Create a new process record
|
|
27
45
|
* @param {Object} processData - Process data to create
|
|
@@ -30,15 +48,14 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
30
48
|
async create(processData) {
|
|
31
49
|
const process = await this.prisma.process.create({
|
|
32
50
|
data: {
|
|
33
|
-
userId: processData.userId,
|
|
34
|
-
integrationId: processData.integrationId,
|
|
51
|
+
userId: this._convertId(processData.userId),
|
|
52
|
+
integrationId: this._convertId(processData.integrationId),
|
|
35
53
|
name: processData.name,
|
|
36
54
|
type: processData.type,
|
|
37
55
|
state: processData.state || 'INITIALIZING',
|
|
38
56
|
context: processData.context || {},
|
|
39
57
|
results: processData.results || {},
|
|
40
|
-
|
|
41
|
-
parentProcessId: processData.parentProcessId || null,
|
|
58
|
+
parentProcessId: this._convertId(processData.parentProcessId),
|
|
42
59
|
},
|
|
43
60
|
});
|
|
44
61
|
|
|
@@ -52,7 +69,7 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
52
69
|
*/
|
|
53
70
|
async findById(processId) {
|
|
54
71
|
const process = await this.prisma.process.findUnique({
|
|
55
|
-
where: { id: processId },
|
|
72
|
+
where: { id: this._convertId(processId) },
|
|
56
73
|
});
|
|
57
74
|
|
|
58
75
|
return process ? this._toPlainObject(process) : null;
|
|
@@ -77,15 +94,14 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
77
94
|
if (updates.results !== undefined) {
|
|
78
95
|
updateData.results = updates.results;
|
|
79
96
|
}
|
|
80
|
-
if (updates.childProcesses !== undefined) {
|
|
81
|
-
updateData.childProcesses = updates.childProcesses;
|
|
82
|
-
}
|
|
83
97
|
if (updates.parentProcessId !== undefined) {
|
|
84
|
-
updateData.parentProcessId =
|
|
98
|
+
updateData.parentProcessId = this._convertId(
|
|
99
|
+
updates.parentProcessId
|
|
100
|
+
);
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
const process = await this.prisma.process.update({
|
|
88
|
-
where: { id: processId },
|
|
104
|
+
where: { id: this._convertId(processId) },
|
|
89
105
|
data: updateData,
|
|
90
106
|
});
|
|
91
107
|
|
|
@@ -101,7 +117,7 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
101
117
|
async findByIntegrationAndType(integrationId, type) {
|
|
102
118
|
const processes = await this.prisma.process.findMany({
|
|
103
119
|
where: {
|
|
104
|
-
integrationId,
|
|
120
|
+
integrationId: this._convertId(integrationId),
|
|
105
121
|
type,
|
|
106
122
|
},
|
|
107
123
|
orderBy: {
|
|
@@ -118,10 +134,13 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
118
134
|
* @param {string[]} [excludeStates=['COMPLETED', 'ERROR']] - States to exclude
|
|
119
135
|
* @returns {Promise<Array>} Array of active process records
|
|
120
136
|
*/
|
|
121
|
-
async findActiveProcesses(
|
|
137
|
+
async findActiveProcesses(
|
|
138
|
+
integrationId,
|
|
139
|
+
excludeStates = ['COMPLETED', 'ERROR']
|
|
140
|
+
) {
|
|
122
141
|
const processes = await this.prisma.process.findMany({
|
|
123
142
|
where: {
|
|
124
|
-
integrationId,
|
|
143
|
+
integrationId: this._convertId(integrationId),
|
|
125
144
|
state: {
|
|
126
145
|
notIn: excludeStates,
|
|
127
146
|
},
|
|
@@ -157,7 +176,7 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
157
176
|
*/
|
|
158
177
|
async deleteById(processId) {
|
|
159
178
|
await this.prisma.process.delete({
|
|
160
|
-
where: { id: processId },
|
|
179
|
+
where: { id: this._convertId(processId) },
|
|
161
180
|
});
|
|
162
181
|
}
|
|
163
182
|
|
|
@@ -179,11 +198,16 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
179
198
|
context: process.context,
|
|
180
199
|
results: process.results,
|
|
181
200
|
childProcesses: Array.isArray(process.childProcesses)
|
|
182
|
-
?
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
? process.childProcesses.length > 0 &&
|
|
202
|
+
typeof process.childProcesses[0] === 'object' &&
|
|
203
|
+
process.childProcesses[0] !== null
|
|
204
|
+
? process.childProcesses.map((child) => String(child.id))
|
|
205
|
+
: process.childProcesses
|
|
185
206
|
: [],
|
|
186
|
-
parentProcessId:
|
|
207
|
+
parentProcessId:
|
|
208
|
+
process.parentProcessId !== null
|
|
209
|
+
? String(process.parentProcessId)
|
|
210
|
+
: null,
|
|
187
211
|
createdAt: process.createdAt,
|
|
188
212
|
updatedAt: process.updatedAt,
|
|
189
213
|
};
|
|
@@ -191,4 +215,3 @@ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
|
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
module.exports = { ProcessRepositoryPostgres };
|
|
194
|
-
|
|
@@ -47,23 +47,16 @@ class DummyIntegration extends IntegrationBase {
|
|
|
47
47
|
this.updateIntegrationMessages = {
|
|
48
48
|
execute: jest.fn().mockResolvedValue({})
|
|
49
49
|
};
|
|
50
|
-
|
|
51
|
-
this.registerEventHandlers();
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
async loadDynamicUserActions() {
|
|
55
53
|
return {};
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
async registerEventHandlers() {
|
|
59
|
-
super.registerEventHandlers();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
56
|
async send(event, data) {
|
|
64
57
|
this.sendSpy(event, data);
|
|
65
58
|
this.eventCallHistory.push({ event, data, timestamp: Date.now() });
|
|
66
|
-
return
|
|
59
|
+
return { event, data };
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
async initialize() {
|
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.45",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"@prisma/client": "^6.16.3",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"uuid": "^9.0.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
27
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
28
|
-
"@friggframework/test": "2.0.0-next.
|
|
26
|
+
"@friggframework/eslint-config": "2.0.0-next.45",
|
|
27
|
+
"@friggframework/prettier-config": "2.0.0-next.45",
|
|
28
|
+
"@friggframework/test": "2.0.0-next.45",
|
|
29
29
|
"@types/lodash": "4.17.15",
|
|
30
30
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
31
31
|
"chai": "^4.3.6",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "996a15bcdfaa4252b9891a33f1b1c84548d66bbc"
|
|
68
68
|
}
|
package/queues/queuer-util.js
CHANGED
|
@@ -14,6 +14,16 @@ AWS.config.update(awsConfigOptions());
|
|
|
14
14
|
const sqs = new AWS.SQS();
|
|
15
15
|
|
|
16
16
|
const QueuerUtil = {
|
|
17
|
+
send: async (message, queueUrl) => {
|
|
18
|
+
console.log(`Enqueuing message to SQS queue ${queueUrl}`);
|
|
19
|
+
return sqs
|
|
20
|
+
.sendMessage({
|
|
21
|
+
MessageBody: JSON.stringify(message),
|
|
22
|
+
QueueUrl: queueUrl,
|
|
23
|
+
})
|
|
24
|
+
.promise();
|
|
25
|
+
},
|
|
26
|
+
|
|
17
27
|
batchSend: async (entries = [], queueUrl) => {
|
|
18
28
|
console.log(
|
|
19
29
|
`Enqueuing ${entries.length} entries on SQS to queue ${queueUrl}`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
2
|
const { prisma } = require('../../database/prisma');
|
|
3
3
|
const {
|
|
4
4
|
createTokenRepository,
|
|
@@ -95,19 +95,38 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
95
95
|
* Replaces: IndividualUser.create(params)
|
|
96
96
|
*
|
|
97
97
|
* @param {Object} params - User creation parameters
|
|
98
|
+
* @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
98
99
|
* @returns {Promise<Object>} Created user object with string IDs
|
|
99
100
|
*/
|
|
100
101
|
async createIndividualUser(params) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
const data = {
|
|
103
|
+
type: 'INDIVIDUAL',
|
|
104
|
+
email: params.email,
|
|
105
|
+
username: params.username,
|
|
106
|
+
appUserId: params.appUserId,
|
|
107
|
+
organizationId: params.organization || params.organizationId,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
params.hashword !== undefined &&
|
|
112
|
+
params.hashword !== null &&
|
|
113
|
+
params.hashword !== ''
|
|
114
|
+
) {
|
|
115
|
+
if (typeof params.hashword !== 'string') {
|
|
116
|
+
throw new Error('Password must be a string');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
120
|
+
if (params.hashword.startsWith('$2')) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
data.hashword = await bcrypt.hash(params.hashword, 10);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return await this.prisma.user.create({ data });
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
/**
|
|
@@ -204,12 +223,34 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
204
223
|
* Update individual user
|
|
205
224
|
* @param {string} userId - User ID
|
|
206
225
|
* @param {Object} updates - Fields to update
|
|
226
|
+
* @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
207
227
|
* @returns {Promise<Object>} Updated user object with string IDs
|
|
208
228
|
*/
|
|
209
229
|
async updateIndividualUser(userId, updates) {
|
|
230
|
+
const data = { ...updates };
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
data.hashword !== undefined &&
|
|
234
|
+
data.hashword !== null &&
|
|
235
|
+
data.hashword !== ''
|
|
236
|
+
) {
|
|
237
|
+
if (typeof data.hashword !== 'string') {
|
|
238
|
+
throw new Error('Password must be a string');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
242
|
+
if (data.hashword.startsWith('$2')) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
data.hashword = await bcrypt.hash(data.hashword, 10);
|
|
249
|
+
}
|
|
250
|
+
|
|
210
251
|
return await this.prisma.user.update({
|
|
211
252
|
where: { id: userId },
|
|
212
|
-
data
|
|
253
|
+
data,
|
|
213
254
|
});
|
|
214
255
|
}
|
|
215
256
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
2
|
const { prisma } = require('../../database/prisma');
|
|
3
3
|
const {
|
|
4
4
|
createTokenRepository,
|
|
@@ -130,21 +130,40 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
130
130
|
* Replaces: IndividualUser.create(params)
|
|
131
131
|
*
|
|
132
132
|
* @param {Object} params - User creation parameters (with string IDs from application layer)
|
|
133
|
+
* @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
133
134
|
* @returns {Promise<Object>} Created user object with string IDs
|
|
134
135
|
*/
|
|
135
136
|
async createIndividualUser(params) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
const data = {
|
|
138
|
+
type: 'INDIVIDUAL',
|
|
139
|
+
email: params.email,
|
|
140
|
+
username: params.username,
|
|
141
|
+
appUserId: params.appUserId,
|
|
142
|
+
organizationId: this._convertId(
|
|
143
|
+
params.organization || params.organizationId
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
params.hashword !== undefined &&
|
|
149
|
+
params.hashword !== null &&
|
|
150
|
+
params.hashword !== ''
|
|
151
|
+
) {
|
|
152
|
+
if (typeof params.hashword !== 'string') {
|
|
153
|
+
throw new Error('Password must be a string');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
157
|
+
if (params.hashword.startsWith('$2')) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
data.hashword = await bcrypt.hash(params.hashword, 10);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const user = await this.prisma.user.create({ data });
|
|
148
167
|
return this._convertUserIds(user);
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -249,13 +268,14 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
249
268
|
* Update individual user
|
|
250
269
|
* @param {string} userId - User ID (string from application layer)
|
|
251
270
|
* @param {Object} updates - Fields to update (with string IDs from application layer)
|
|
271
|
+
* @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
252
272
|
* @returns {Promise<Object>} Updated user object with string IDs
|
|
253
273
|
*/
|
|
254
274
|
async updateIndividualUser(userId, updates) {
|
|
255
275
|
const intId = this._convertId(userId);
|
|
256
276
|
|
|
257
|
-
// Convert organizationId if present in updates
|
|
258
277
|
const data = { ...updates };
|
|
278
|
+
|
|
259
279
|
if (data.organizationId !== undefined) {
|
|
260
280
|
data.organizationId = this._convertId(data.organizationId);
|
|
261
281
|
}
|
|
@@ -264,6 +284,25 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
264
284
|
delete data.organization;
|
|
265
285
|
}
|
|
266
286
|
|
|
287
|
+
if (
|
|
288
|
+
data.hashword !== undefined &&
|
|
289
|
+
data.hashword !== null &&
|
|
290
|
+
data.hashword !== ''
|
|
291
|
+
) {
|
|
292
|
+
if (typeof data.hashword !== 'string') {
|
|
293
|
+
throw new Error('Password must be a string');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
297
|
+
if (data.hashword.startsWith('$2')) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
data.hashword = await bcrypt.hash(data.hashword, 10);
|
|
304
|
+
}
|
|
305
|
+
|
|
267
306
|
const user = await this.prisma.user.update({
|
|
268
307
|
where: { id: intId },
|
|
269
308
|
data,
|