ti2 1.0.39 → 1.0.42
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/.github/workflows/tests.yml +1 -1
- package/api.yml +173 -1
- package/cli.js +20 -0
- package/controllers/__tests__/app.js +0 -1
- package/controllers/__tests__/user.js +67 -0
- package/controllers/allotment.js +1 -1
- package/controllers/app.js +108 -4
- package/controllers/bookings.js +7 -7
- package/lib/security.js +31 -0
- package/migrations/20221108160527-userIntegrationSettings.js +28 -0
- package/migrations/20221114141152-encryptUserToken.js +45 -0
- package/models/integration.js +4 -24
- package/models/user.js +3 -0
- package/models/userappkey.js +21 -6
- package/models/userintegrationsetting.js +52 -0
- package/package.json +1 -1
package/api.yml
CHANGED
|
@@ -29,6 +29,11 @@ components:
|
|
|
29
29
|
properties:
|
|
30
30
|
value:
|
|
31
31
|
type: string
|
|
32
|
+
Status:
|
|
33
|
+
type: object
|
|
34
|
+
properties:
|
|
35
|
+
success:
|
|
36
|
+
type: boolean
|
|
32
37
|
JobSpec:
|
|
33
38
|
type: object
|
|
34
39
|
properties:
|
|
@@ -334,6 +339,13 @@ components:
|
|
|
334
339
|
id:
|
|
335
340
|
type: string
|
|
336
341
|
description: a quote Id to make a reservation for
|
|
342
|
+
settingsBody:
|
|
343
|
+
type: object
|
|
344
|
+
required: true
|
|
345
|
+
properties:
|
|
346
|
+
settings:
|
|
347
|
+
type: object
|
|
348
|
+
description: an object with the app+user seettings
|
|
337
349
|
paths:
|
|
338
350
|
/ping:
|
|
339
351
|
get:
|
|
@@ -623,7 +635,75 @@ paths:
|
|
|
623
635
|
content:
|
|
624
636
|
application/json:
|
|
625
637
|
schema: '#/components/schemas/Message'
|
|
626
|
-
|
|
638
|
+
/{app}/{userId}/{hint}/token:
|
|
639
|
+
get:
|
|
640
|
+
tags:
|
|
641
|
+
- app
|
|
642
|
+
- user
|
|
643
|
+
- admin
|
|
644
|
+
summary: Get a user app+Token configuration, the authorization can be either from the app or the user
|
|
645
|
+
security:
|
|
646
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
647
|
+
operationId: getAppToken
|
|
648
|
+
parameters:
|
|
649
|
+
- name: app
|
|
650
|
+
in: path
|
|
651
|
+
description: app name to read the userId for (must match app token)
|
|
652
|
+
example: 'expedia'
|
|
653
|
+
required: true
|
|
654
|
+
schema:
|
|
655
|
+
type: string
|
|
656
|
+
- name: hint
|
|
657
|
+
in: path
|
|
658
|
+
description: a specific token hint to be used
|
|
659
|
+
required: true
|
|
660
|
+
example: 'travelgate-australia'
|
|
661
|
+
schema:
|
|
662
|
+
type: string
|
|
663
|
+
- name: userId
|
|
664
|
+
in: path
|
|
665
|
+
description: user to read the app token for
|
|
666
|
+
example: '536830b6ed19afa44a000002'
|
|
667
|
+
required: true
|
|
668
|
+
schema:
|
|
669
|
+
type: string
|
|
670
|
+
responses:
|
|
671
|
+
200:
|
|
672
|
+
description: success
|
|
673
|
+
content:
|
|
674
|
+
application/json:
|
|
675
|
+
schema: '#/components/schemas/UserApp'
|
|
676
|
+
/{app}/{userId}/token:
|
|
677
|
+
get:
|
|
678
|
+
tags:
|
|
679
|
+
- app
|
|
680
|
+
- user
|
|
681
|
+
- admin
|
|
682
|
+
summary: Get a user app+Token configuration, the authorization can be either from the app or the user, it will return the first token found (since no hint is required)
|
|
683
|
+
security:
|
|
684
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
685
|
+
operationId: getAppToken
|
|
686
|
+
parameters:
|
|
687
|
+
- name: app
|
|
688
|
+
in: path
|
|
689
|
+
description: app name to read the userId for (must match app token)
|
|
690
|
+
example: 'expedia'
|
|
691
|
+
required: true
|
|
692
|
+
schema:
|
|
693
|
+
type: string
|
|
694
|
+
- name: userId
|
|
695
|
+
in: path
|
|
696
|
+
description: user to read the app token for
|
|
697
|
+
example: '536830b6ed19afa44a000002'
|
|
698
|
+
required: true
|
|
699
|
+
schema:
|
|
700
|
+
type: string
|
|
701
|
+
responses:
|
|
702
|
+
200:
|
|
703
|
+
description: success
|
|
704
|
+
content:
|
|
705
|
+
application/json:
|
|
706
|
+
schema: '#/components/schemas/UserApp'
|
|
627
707
|
/{app}/{userId}/validate:
|
|
628
708
|
post:
|
|
629
709
|
tags:
|
|
@@ -828,6 +908,98 @@ paths:
|
|
|
828
908
|
content:
|
|
829
909
|
appication/json:
|
|
830
910
|
schema: '#/components/schemas/appMethods'
|
|
911
|
+
/settings/{appKey}/{userId}:
|
|
912
|
+
post:
|
|
913
|
+
security:
|
|
914
|
+
- bearerAuth: ['admin', 'user']
|
|
915
|
+
tags:
|
|
916
|
+
- user
|
|
917
|
+
summary: Creates user settings for an app, shared across tokens per user
|
|
918
|
+
operationId: createAppSettings
|
|
919
|
+
parameters:
|
|
920
|
+
- name: appKey
|
|
921
|
+
in: path
|
|
922
|
+
description: name of the app to use for the settings
|
|
923
|
+
required: true
|
|
924
|
+
example: 'expedia'
|
|
925
|
+
schema:
|
|
926
|
+
type: string
|
|
927
|
+
- name: userId
|
|
928
|
+
in: path
|
|
929
|
+
description: user id for the setting to be created
|
|
930
|
+
required: true
|
|
931
|
+
example: '536830b6ed19afa44a000002'
|
|
932
|
+
schema:
|
|
933
|
+
type: string
|
|
934
|
+
requestBody:
|
|
935
|
+
description: Creation Payload
|
|
936
|
+
required: true
|
|
937
|
+
content:
|
|
938
|
+
application/json:
|
|
939
|
+
schema:
|
|
940
|
+
$ref: '#/components/schemas/settingsBody'
|
|
941
|
+
responses:
|
|
942
|
+
200:
|
|
943
|
+
description: success
|
|
944
|
+
content:
|
|
945
|
+
application/json:
|
|
946
|
+
schema: '#/components/schemas/Success'
|
|
947
|
+
get:
|
|
948
|
+
security:
|
|
949
|
+
- bearerAuth: ['admin', 'user']
|
|
950
|
+
tags:
|
|
951
|
+
- user
|
|
952
|
+
summary: Retrieves a user settings for an app, shared across tokens per user
|
|
953
|
+
operationId: getAppSettings
|
|
954
|
+
parameters:
|
|
955
|
+
- name: appKey
|
|
956
|
+
in: path
|
|
957
|
+
description: name of the app to use for the settings
|
|
958
|
+
required: true
|
|
959
|
+
example: 'expedia'
|
|
960
|
+
schema:
|
|
961
|
+
type: string
|
|
962
|
+
- name: userId
|
|
963
|
+
in: path
|
|
964
|
+
description: user id for the setting to be created
|
|
965
|
+
required: true
|
|
966
|
+
example: '536830b6ed19afa44a000002'
|
|
967
|
+
schema:
|
|
968
|
+
type: string
|
|
969
|
+
responses:
|
|
970
|
+
200:
|
|
971
|
+
description: success
|
|
972
|
+
content:
|
|
973
|
+
application/json:
|
|
974
|
+
schema: '#/components/schemas/settingsBody'
|
|
975
|
+
delete:
|
|
976
|
+
security:
|
|
977
|
+
- bearerAuth: ['admin', 'user']
|
|
978
|
+
tags:
|
|
979
|
+
- user
|
|
980
|
+
summary: Deletes a user settings for an app, shared across tokens per user
|
|
981
|
+
operationId: deleteAppSettings
|
|
982
|
+
parameters:
|
|
983
|
+
- name: appKey
|
|
984
|
+
in: path
|
|
985
|
+
description: name of the app to use for the settings
|
|
986
|
+
required: true
|
|
987
|
+
example: 'expedia'
|
|
988
|
+
schema:
|
|
989
|
+
type: string
|
|
990
|
+
- name: userId
|
|
991
|
+
in: path
|
|
992
|
+
description: user id for the setting to be created
|
|
993
|
+
required: true
|
|
994
|
+
example: '536830b6ed19afa44a000002'
|
|
995
|
+
schema:
|
|
996
|
+
type: string
|
|
997
|
+
responses:
|
|
998
|
+
200:
|
|
999
|
+
description: success
|
|
1000
|
+
content:
|
|
1001
|
+
application/json:
|
|
1002
|
+
$ref: '#/components/schemas/Status'
|
|
831
1003
|
/bookings/{appKey}/{userId}/search:
|
|
832
1004
|
post:
|
|
833
1005
|
security:
|
package/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ const { Command, Argument } = require('commander');
|
|
|
4
4
|
|
|
5
5
|
const { migrateApp } = require('./controllers/app')();
|
|
6
6
|
const { migrate } = require('./controllers/admin');
|
|
7
|
+
const { decrypt, encrypt } = require('./lib/security');
|
|
7
8
|
|
|
8
9
|
const program = new Command();
|
|
9
10
|
|
|
@@ -35,4 +36,23 @@ program.command('db')
|
|
|
35
36
|
process.exit(0);
|
|
36
37
|
});
|
|
37
38
|
|
|
39
|
+
program.command('decrypt')
|
|
40
|
+
.description('decrypt a string')
|
|
41
|
+
.addArgument(
|
|
42
|
+
new Argument('<key>', 'key to decrypt'),
|
|
43
|
+
)
|
|
44
|
+
.action(async key => {
|
|
45
|
+
console.log(await decrypt(key));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
program.command('encrypt')
|
|
49
|
+
.description('encrypt a string')
|
|
50
|
+
.addArgument(
|
|
51
|
+
new Argument('<key>', 'key to encrypt'),
|
|
52
|
+
)
|
|
53
|
+
.action(async key => {
|
|
54
|
+
console.log(await encrypt(key));
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
38
58
|
program.parse();
|
|
@@ -26,11 +26,13 @@ describe('user', () => {
|
|
|
26
26
|
endpoint: chance.url(),
|
|
27
27
|
apiKey,
|
|
28
28
|
};
|
|
29
|
+
let plugins;
|
|
29
30
|
beforeAll(async () => {
|
|
30
31
|
({
|
|
31
32
|
doApiDelete,
|
|
32
33
|
doApiGet,
|
|
33
34
|
doApiPost,
|
|
35
|
+
plugins,
|
|
34
36
|
} = await testUtils({
|
|
35
37
|
plugins: [appName],
|
|
36
38
|
}));
|
|
@@ -133,6 +135,71 @@ describe('user', () => {
|
|
|
133
135
|
]),
|
|
134
136
|
);
|
|
135
137
|
});
|
|
138
|
+
const apiKey2 = chance.guid();
|
|
139
|
+
const token2 = { ...token };
|
|
140
|
+
it('create a second token', async () => {
|
|
141
|
+
// set up new token
|
|
142
|
+
await doApiPost({
|
|
143
|
+
url: `/${appName}/${userId}`,
|
|
144
|
+
token: userKey,
|
|
145
|
+
payload: {
|
|
146
|
+
tokenHint: apiKey2.split('-')[1],
|
|
147
|
+
token: token2,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('should be able to read a user/app token', async () => {
|
|
152
|
+
const returnValue = await doApiGet({
|
|
153
|
+
url: `/${appName}/${userId}/token`,
|
|
154
|
+
token: userKey,
|
|
155
|
+
});
|
|
156
|
+
expect(returnValue.token).toEqual(token);
|
|
157
|
+
});
|
|
158
|
+
it('should be able to read the second user/app token by hint', async () => {
|
|
159
|
+
const returnValue = await doApiGet({
|
|
160
|
+
url: `/${appName}/${userId}/${apiKey2.split('-')[1]}/token`,
|
|
161
|
+
token: userKey,
|
|
162
|
+
});
|
|
163
|
+
expect(returnValue.token).toEqual(token2);
|
|
164
|
+
});
|
|
165
|
+
it('should be able to create an app setting', async () => {
|
|
166
|
+
const returnValue = await doApiPost({
|
|
167
|
+
url: `/settings/${appName}/${userId}`,
|
|
168
|
+
token: userKey,
|
|
169
|
+
payload: {
|
|
170
|
+
settings: {
|
|
171
|
+
custom: true,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
expect(returnValue.success).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
it('should be able to get an app settings', async () => {
|
|
178
|
+
const returnValue = await doApiGet({
|
|
179
|
+
url: `/settings/${appName}/${userId}`,
|
|
180
|
+
token: userKey,
|
|
181
|
+
});
|
|
182
|
+
expect(returnValue.settings.custom).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
it('testing the user token should include the user app setting', async () => {
|
|
185
|
+
const { valid } = await doApiPost({
|
|
186
|
+
url: `/${appName}/${userId}/validate`,
|
|
187
|
+
token: userKey,
|
|
188
|
+
payload: {
|
|
189
|
+
tokenHint: apiKey.split('-')[0],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
expect(plugins[0].validateToken).toHaveBeenCalled();
|
|
193
|
+
expect(valid).toBe(true);
|
|
194
|
+
expect(plugins[0].validateToken.mock.calls[0][0].token).toEqual({ custom: true, ...token });
|
|
195
|
+
});
|
|
196
|
+
it('should be able to delete an app settings', async () => {
|
|
197
|
+
const returnValue = await doApiDelete({
|
|
198
|
+
url: `/settings/${appName}/${userId}`,
|
|
199
|
+
token: userKey,
|
|
200
|
+
});
|
|
201
|
+
expect(returnValue.success).toBe(true);
|
|
202
|
+
});
|
|
136
203
|
it('should be able to get all the methods for an app', async () => {
|
|
137
204
|
const { methods } = await doApiGet({
|
|
138
205
|
url: `/app/${appName}/methods`,
|
package/controllers/allotment.js
CHANGED
|
@@ -22,7 +22,7 @@ const queryAllotment = plugins => async (req, res, next) => {
|
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
24
|
assert(userAppKeys, 'could not find the app key');
|
|
25
|
-
const token = userAppKeys.
|
|
25
|
+
const token = await userAppKeys.token;
|
|
26
26
|
assert(app.queryAllotment, 'could not find the allotment method');
|
|
27
27
|
const results = await app.queryAllotment({
|
|
28
28
|
token,
|
package/controllers/app.js
CHANGED
|
@@ -73,7 +73,7 @@ const createAppToken = async (req, res, next) => {
|
|
|
73
73
|
if (!userRecord) { // create the user record
|
|
74
74
|
await sqldb.User.create({ userId });
|
|
75
75
|
}
|
|
76
|
-
// check if the user
|
|
76
|
+
// check if the user already has the same app with the same hint
|
|
77
77
|
const userAppKeyDup = await sqldb.UserAppKey.findOne({
|
|
78
78
|
where: { userId, integrationId, hint },
|
|
79
79
|
});
|
|
@@ -186,7 +186,7 @@ const validateAppToken = plugins => async (req, res, next) => {
|
|
|
186
186
|
},
|
|
187
187
|
});
|
|
188
188
|
assert(userAppKeys, 'could not find the app key');
|
|
189
|
-
const token = userAppKeys.
|
|
189
|
+
const token = await userAppKeys.token;
|
|
190
190
|
assert(app.validateToken, `could not find the validateToken method for ${appKey}`);
|
|
191
191
|
const valid = await app.validateToken({
|
|
192
192
|
token,
|
|
@@ -197,13 +197,12 @@ const validateAppToken = plugins => async (req, res, next) => {
|
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
|
|
201
200
|
const listAppTokens = async (req, res, next) => {
|
|
202
201
|
const { params: { app: integrationId } } = req;
|
|
203
202
|
try {
|
|
204
203
|
const userAppKeys = (await sqldb.UserAppKey.findAll({ where: { integrationId } }))
|
|
205
204
|
.map(userAppKey => userAppKey.dataValues);
|
|
206
|
-
return res.json({ userAppKeys: userAppKeys.map(userAppKey => omit(['appKey', 'id'], userAppKey)) });
|
|
205
|
+
return res.json({ userAppKeys: userAppKeys.map(userAppKey => omit(['appKey', 'id', 'token'], userAppKey)) });
|
|
207
206
|
} catch (err) {
|
|
208
207
|
return next(err);
|
|
209
208
|
}
|
|
@@ -310,9 +309,114 @@ const getJobStatus = async (req, res, next) => {
|
|
|
310
309
|
}
|
|
311
310
|
};
|
|
312
311
|
|
|
312
|
+
const createAppSettings = async (req, res, next) => {
|
|
313
|
+
const {
|
|
314
|
+
body: {
|
|
315
|
+
settings,
|
|
316
|
+
},
|
|
317
|
+
params: {
|
|
318
|
+
appKey: integrationId,
|
|
319
|
+
userId,
|
|
320
|
+
},
|
|
321
|
+
} = req;
|
|
322
|
+
try {
|
|
323
|
+
const payload = {
|
|
324
|
+
integrationId,
|
|
325
|
+
userId,
|
|
326
|
+
settings,
|
|
327
|
+
};
|
|
328
|
+
// check if the user exists
|
|
329
|
+
const userRecord = await sqldb.User.findOne({ where: { userId } });
|
|
330
|
+
if (!userRecord) { // create the user record
|
|
331
|
+
await sqldb.User.create({ userId });
|
|
332
|
+
}
|
|
333
|
+
// check if the user already has the same app settings
|
|
334
|
+
const userSettings = await sqldb.UserIntegrationSettings.findOne({
|
|
335
|
+
where: { userId, integrationId },
|
|
336
|
+
});
|
|
337
|
+
if (userSettings) {
|
|
338
|
+
userSettings.settings = settings;
|
|
339
|
+
await userSettings.save();
|
|
340
|
+
} else {
|
|
341
|
+
await sqldb.UserIntegrationSettings.create(payload);
|
|
342
|
+
}
|
|
343
|
+
return res.json({ success: true });
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.log(err.stack);
|
|
346
|
+
return next(err);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const deleteAppSettings = async (req, res, next) => {
|
|
351
|
+
const {
|
|
352
|
+
params: {
|
|
353
|
+
appKey: integrationId,
|
|
354
|
+
userId,
|
|
355
|
+
},
|
|
356
|
+
} = req;
|
|
357
|
+
// check if the user exists
|
|
358
|
+
const userRecord = await sqldb.User.findOne({ where: { userId } });
|
|
359
|
+
if (!userRecord) return next({ status: 404, message: 'User does not exists' });
|
|
360
|
+
const retVal = await sqldb.UserIntegrationSettings.destroy({
|
|
361
|
+
where: {
|
|
362
|
+
integrationId,
|
|
363
|
+
userId,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
if (retVal === 0) return next({ status: 404, message: 'Settings not found' });
|
|
367
|
+
return res.json({ success: true });
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const getAppSettings = async (req, res, next) => {
|
|
371
|
+
const {
|
|
372
|
+
params: {
|
|
373
|
+
appKey: integrationId,
|
|
374
|
+
userId,
|
|
375
|
+
},
|
|
376
|
+
} = req;
|
|
377
|
+
// check if the user exists
|
|
378
|
+
const userRecord = await sqldb.User.findOne({ where: { userId } });
|
|
379
|
+
if (!userRecord) return next({ status: 404, message: 'User does not exists' });
|
|
380
|
+
const userIntegrationSettings = await sqldb.UserIntegrationSettings.findOne({
|
|
381
|
+
where: { userId, integrationId },
|
|
382
|
+
});
|
|
383
|
+
if (!userIntegrationSettings) {
|
|
384
|
+
return res.json({ settings: {} });
|
|
385
|
+
}
|
|
386
|
+
return res.json({ settings: userIntegrationSettings.settings });
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const getAppToken = async (req, res, next) => {
|
|
390
|
+
const {
|
|
391
|
+
params: {
|
|
392
|
+
app: integrationId,
|
|
393
|
+
userId,
|
|
394
|
+
hint,
|
|
395
|
+
},
|
|
396
|
+
} = req;
|
|
397
|
+
// check if the user exists
|
|
398
|
+
const userRecord = await sqldb.User.findOne({ where: { userId } });
|
|
399
|
+
if (!userRecord) return next({ status: 404, message: 'User does not exists' });
|
|
400
|
+
const userAppKey = await sqldb.UserAppKey.findOne({
|
|
401
|
+
where: {
|
|
402
|
+
userId,
|
|
403
|
+
integrationId,
|
|
404
|
+
...(hint ? { hint } : {}),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
if (!userAppKey) {
|
|
408
|
+
return next({ status: 404, message: 'User integratio is not found' });
|
|
409
|
+
}
|
|
410
|
+
return res.json({ token: await userAppKey.token });
|
|
411
|
+
};
|
|
412
|
+
|
|
313
413
|
module.exports = plugins => ({
|
|
314
414
|
createAppToken,
|
|
415
|
+
getAppToken,
|
|
416
|
+
createAppSettings,
|
|
417
|
+
getAppSettings,
|
|
315
418
|
deleteAppToken,
|
|
419
|
+
deleteAppSettings,
|
|
316
420
|
getAppScheduledJobs,
|
|
317
421
|
getJobStatus,
|
|
318
422
|
jwtEncode,
|
package/controllers/bookings.js
CHANGED
|
@@ -32,7 +32,7 @@ const bookingsSearch = plugins => async (req, res, next) => {
|
|
|
32
32
|
},
|
|
33
33
|
});
|
|
34
34
|
assert(userAppKeys, 'could not find the app key');
|
|
35
|
-
const token = userAppKeys.
|
|
35
|
+
const token = await userAppKeys.token;
|
|
36
36
|
const search = app.searchHotelBooking || app.searchBooking;
|
|
37
37
|
const results = await search({
|
|
38
38
|
token,
|
|
@@ -61,7 +61,7 @@ const bookingsCancel = plugins => async (req, res, next) => {
|
|
|
61
61
|
},
|
|
62
62
|
}));
|
|
63
63
|
assert(userAppKeys, 'could not find the app key');
|
|
64
|
-
const token = userAppKeys.
|
|
64
|
+
const token = await userAppKeys.token;
|
|
65
65
|
const results = await app.cancelBooking({
|
|
66
66
|
token,
|
|
67
67
|
payload: body,
|
|
@@ -89,7 +89,7 @@ const $bookingsProductSearch = plugins => async ({
|
|
|
89
89
|
},
|
|
90
90
|
}));
|
|
91
91
|
assert(userAppKeys, 'could not find the app key');
|
|
92
|
-
const token = userAppKeys.
|
|
92
|
+
const token = await userAppKeys.token;
|
|
93
93
|
const results = await app.searchProducts({
|
|
94
94
|
token,
|
|
95
95
|
payload,
|
|
@@ -126,7 +126,7 @@ const bookingsAvailabilitySearch = plugins => async (req, res, next) => {
|
|
|
126
126
|
},
|
|
127
127
|
}));
|
|
128
128
|
assert(userAppKeys, 'could not find the app key');
|
|
129
|
-
const token = userAppKeys.
|
|
129
|
+
const token = await userAppKeys.token;
|
|
130
130
|
const results = await app.searchAvailability({
|
|
131
131
|
token,
|
|
132
132
|
payload,
|
|
@@ -154,7 +154,7 @@ const $bookingsAvailabilityCalendar = plugins => async ({
|
|
|
154
154
|
},
|
|
155
155
|
}));
|
|
156
156
|
assert(userAppKeys, 'could not find the app key');
|
|
157
|
-
const token = userAppKeys.
|
|
157
|
+
const token = await userAppKeys.token;
|
|
158
158
|
return app.availabilityCalendar({
|
|
159
159
|
token,
|
|
160
160
|
payload,
|
|
@@ -190,7 +190,7 @@ const searchQuote = plugins => async (req, res, next) => {
|
|
|
190
190
|
},
|
|
191
191
|
}));
|
|
192
192
|
assert(userAppKeys, 'could not find the app key');
|
|
193
|
-
const token = userAppKeys.
|
|
193
|
+
const token = await userAppKeys.token;
|
|
194
194
|
assert(payload.id, 'the availability id is required');
|
|
195
195
|
const results = await app.searchQuote({
|
|
196
196
|
token,
|
|
@@ -219,7 +219,7 @@ const createBooking = plugins => async (req, res, next) => {
|
|
|
219
219
|
},
|
|
220
220
|
}));
|
|
221
221
|
assert(userAppKeys, 'could not find the app key');
|
|
222
|
-
const token = userAppKeys.
|
|
222
|
+
const token = await userAppKeys.token;
|
|
223
223
|
assert(payload.id, 'the quote id is required');
|
|
224
224
|
const results = await app.createBooking({
|
|
225
225
|
token,
|
package/lib/security.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const { env: { dbCryptoKey } } = process;
|
|
4
|
+
|
|
5
|
+
const generateKey = () => {
|
|
6
|
+
const newKey = `${crypto.randomBytes(5).toString('base64').replace(/\W/g, '')}.${crypto.randomBytes(32).toString('base64').replace(/\W/g, '')}`;
|
|
7
|
+
return newKey;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const encrypt = text => {
|
|
11
|
+
const iv = crypto.randomBytes(16);
|
|
12
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(dbCryptoKey, 'base64'), iv);
|
|
13
|
+
let encrypted = cipher.update(text);
|
|
14
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
15
|
+
return `${iv.toString('hex')}%${encrypted.toString('hex')}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const decrypt = text => {
|
|
19
|
+
const iv = Buffer.from(text.split('%')[0], 'hex');
|
|
20
|
+
const encryptedText = Buffer.from(text.split('%')[1], 'hex');
|
|
21
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(dbCryptoKey, 'base64'), iv);
|
|
22
|
+
let decrypted = decipher.update(encryptedText);
|
|
23
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
24
|
+
return decrypted.toString();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
generateKey,
|
|
29
|
+
encrypt,
|
|
30
|
+
decrypt,
|
|
31
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
up: (queryInterface, Sequelize) => queryInterface
|
|
3
|
+
.createTable('UserIntegrationSettings', {
|
|
4
|
+
integrationId: {
|
|
5
|
+
allowNull: false,
|
|
6
|
+
primaryKey: true,
|
|
7
|
+
type: Sequelize.STRING,
|
|
8
|
+
},
|
|
9
|
+
userId: {
|
|
10
|
+
allowNull: false,
|
|
11
|
+
primaryKey: true,
|
|
12
|
+
type: Sequelize.STRING,
|
|
13
|
+
},
|
|
14
|
+
settings: {
|
|
15
|
+
allowNull: false,
|
|
16
|
+
type: Sequelize.TEXT,
|
|
17
|
+
},
|
|
18
|
+
createdAt: {
|
|
19
|
+
allowNull: false,
|
|
20
|
+
type: Sequelize.DATE,
|
|
21
|
+
},
|
|
22
|
+
updatedAt: {
|
|
23
|
+
allowNull: false,
|
|
24
|
+
type: Sequelize.DATE,
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
down: queryInterface => queryInterface.dropTable('UserIntegrationSettings'),
|
|
28
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const R = require('ramda');
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const sqldb = require('../models/index');
|
|
5
|
+
|
|
6
|
+
const { encrypt, decrypt } = require('../lib/security');
|
|
7
|
+
|
|
8
|
+
const { env: { dbCryptoKey } } = process;
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
up: async () => {
|
|
12
|
+
// encrypt all data
|
|
13
|
+
const existingTokens = await sqldb.UserAppKey.findAll();
|
|
14
|
+
const valuesToUpdate = [];
|
|
15
|
+
for (const currentToken of existingTokens) {
|
|
16
|
+
const decodedToken = JSON.stringify(R.omit(
|
|
17
|
+
['iat'],
|
|
18
|
+
jwt.verify(currentToken.dataValues.appKey, dbCryptoKey),
|
|
19
|
+
));
|
|
20
|
+
const encryptedToken = encrypt(decodedToken);
|
|
21
|
+
// make sure we can decrypt it
|
|
22
|
+
const decodedValue = decrypt(encryptedToken);
|
|
23
|
+
assert(R.equals(decodedToken, decodedValue));
|
|
24
|
+
currentToken.appKey = encryptedToken;
|
|
25
|
+
valuesToUpdate.push({
|
|
26
|
+
id: currentToken.id,
|
|
27
|
+
appToken: encryptedToken,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
for (const { id, appToken } of valuesToUpdate) {
|
|
31
|
+
const currentToken = await sqldb.UserAppKey.findOne({ where: { id } });
|
|
32
|
+
currentToken.appKey = appToken;
|
|
33
|
+
await currentToken.save();
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
down: async () => {
|
|
37
|
+
const existingTokens = await sqldb.UserAppKey.findAll();
|
|
38
|
+
for (const currentToken of existingTokens) {
|
|
39
|
+
const decodedToken = decrypt(currentToken.dataValues.appKey);
|
|
40
|
+
const jwtToken = jwt.sign(decodedToken, dbCryptoKey);
|
|
41
|
+
currentToken.appKey = jwtToken;
|
|
42
|
+
await currentToken.save();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
package/models/integration.js
CHANGED
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
1
|
const Sequelize = require('sequelize');
|
|
3
2
|
const db = require('./db');
|
|
4
|
-
|
|
5
|
-
const { env: { dbCryptoKey } } = process;
|
|
6
|
-
const generateKey = () => {
|
|
7
|
-
const newKey = `${crypto.randomBytes(5).toString('base64').replace(/\W/g, '')}.${crypto.randomBytes(32).toString('base64').replace(/\W/g, '')}`;
|
|
8
|
-
return newKey;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const encrypt = text => {
|
|
12
|
-
const iv = crypto.randomBytes(16);
|
|
13
|
-
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(dbCryptoKey, 'base64'), iv);
|
|
14
|
-
let encrypted = cipher.update(text);
|
|
15
|
-
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
16
|
-
return `${iv.toString('hex')}%${encrypted.toString('hex')}`;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const decrypt = text => {
|
|
20
|
-
const iv = Buffer.from(text.split('%')[0], 'hex');
|
|
21
|
-
const encryptedText = Buffer.from(text.split('%')[1], 'hex');
|
|
22
|
-
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(dbCryptoKey, 'base64'), iv);
|
|
23
|
-
let decrypted = decipher.update(encryptedText);
|
|
24
|
-
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
25
|
-
return decrypted.toString();
|
|
26
|
-
};
|
|
3
|
+
const { generateKey, encrypt, decrypt } = require('../lib/security');
|
|
27
4
|
|
|
28
5
|
const Integration = db.define('Integration', {
|
|
29
6
|
name: {
|
|
@@ -65,6 +42,9 @@ Integration.associate = models => {
|
|
|
65
42
|
Integration.hasMany(models.UserAppKey, {
|
|
66
43
|
foreignKey: 'integrationId',
|
|
67
44
|
});
|
|
45
|
+
Integration.hasMany(models.UserIntegrationSettings, {
|
|
46
|
+
foreignKey: 'integrationId',
|
|
47
|
+
});
|
|
68
48
|
};
|
|
69
49
|
Integration.generateKey = generateKey;
|
|
70
50
|
|
package/models/user.js
CHANGED
package/models/userappkey.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const jwt = require('jsonwebtoken');
|
|
2
2
|
const Sequelize = require('sequelize');
|
|
3
|
-
const { omit } = require('ramda');
|
|
3
|
+
const { omit, pathOr } = require('ramda');
|
|
4
|
+
const { encrypt, decrypt } = require('../lib/security');
|
|
4
5
|
const db = require('./db');
|
|
5
6
|
|
|
6
7
|
const { env: { dbCryptoKey } } = process;
|
|
@@ -37,12 +38,26 @@ const UserAppKey = db.define('UserAppKey', {
|
|
|
37
38
|
type: Sequelize.TEXT,
|
|
38
39
|
allowNull: false,
|
|
39
40
|
set: function setter(value, field) {
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
if (typeof value === 'object') {
|
|
42
|
+
const newValue = encrypt(JSON.stringify(value));
|
|
43
|
+
this.setDataValue(field, newValue);
|
|
44
|
+
} else {
|
|
45
|
+
this.setDataValue(field, value);
|
|
46
|
+
}
|
|
42
47
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
},
|
|
49
|
+
token: {
|
|
50
|
+
type: Sequelize.VIRTUAL,
|
|
51
|
+
async get() {
|
|
52
|
+
const sqldb = require('./index');
|
|
53
|
+
const userIntegrationSettings = await sqldb.UserIntegrationSettings.findOne({
|
|
54
|
+
where: { userId: this.getDataValue('userId'), integrationId: this.getDataValue('integrationId') },
|
|
55
|
+
});
|
|
56
|
+
const appKey = JSON.parse(decrypt(this.getDataValue('appKey')));
|
|
57
|
+
return {
|
|
58
|
+
...pathOr({}, ['settings'], userIntegrationSettings),
|
|
59
|
+
...appKey,
|
|
60
|
+
};
|
|
46
61
|
},
|
|
47
62
|
},
|
|
48
63
|
}, {});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const Sequelize = require('sequelize');
|
|
2
|
+
const db = require('./db');
|
|
3
|
+
const { encrypt, decrypt } = require('../lib/security');
|
|
4
|
+
|
|
5
|
+
const UserIntegrationSettings = db.define('UserIntegrationSettings', {
|
|
6
|
+
userId: {
|
|
7
|
+
primaryKey: true,
|
|
8
|
+
type: Sequelize.STRING,
|
|
9
|
+
allowNull: false,
|
|
10
|
+
validate: {
|
|
11
|
+
is: /^((?!true|false|TRUE|FALSE).){1,255}$/,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
integrationId: {
|
|
15
|
+
primaryKey: true,
|
|
16
|
+
type: Sequelize.STRING,
|
|
17
|
+
allowNull: false,
|
|
18
|
+
validate: {
|
|
19
|
+
is: /^((?!true|false|TRUE|FALSE).){1,255}$/,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
settings: {
|
|
23
|
+
type: Sequelize.TEXT,
|
|
24
|
+
allowNull: false,
|
|
25
|
+
},
|
|
26
|
+
}, {
|
|
27
|
+
getterMethods: {
|
|
28
|
+
settings() {
|
|
29
|
+
if (!this.getDataValue('settings')) { // there is no setting
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(decrypt(this.getDataValue('settings')));
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
setterMethods: {
|
|
36
|
+
settings(value) {
|
|
37
|
+
this.setDataValue('settings', encrypt(JSON.stringify(value)));
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
UserIntegrationSettings.associate = models => {
|
|
43
|
+
UserIntegrationSettings.belongsTo(models.User, {
|
|
44
|
+
foreignKey: 'userId',
|
|
45
|
+
as: 'user',
|
|
46
|
+
});
|
|
47
|
+
UserIntegrationSettings.belongsTo(models.Integration, {
|
|
48
|
+
foreignKey: 'integrationId',
|
|
49
|
+
as: 'integration',
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
module.exports = UserIntegrationSettings;
|