ti2 1.0.38 → 1.0.41

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.
@@ -1,6 +1,6 @@
1
1
  name: Tests Workflow
2
2
 
3
- on: [workflow_dispatch, push, pull_request]
3
+ on: [workflow_dispatch, push]
4
4
 
5
5
  jobs:
6
6
  test:
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:
@@ -132,6 +137,20 @@ components:
132
137
  type: string
133
138
  updatedAt:
134
139
  type: string
140
+ UserList:
141
+ type: object
142
+ properties:
143
+ users:
144
+ type: array
145
+ items:
146
+ type: object
147
+ properties:
148
+ userId:
149
+ type: string
150
+ createdAt:
151
+ type: string
152
+ updatedAt:
153
+ type: string
135
154
  AppTokenList:
136
155
  type: object
137
156
  properties:
@@ -320,6 +339,13 @@ components:
320
339
  id:
321
340
  type: string
322
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
323
349
  paths:
324
350
  /ping:
325
351
  get:
@@ -419,6 +445,21 @@ paths:
419
445
  application/json:
420
446
  schema:
421
447
  $ref: '#/components/schemas/IntegrationList'
448
+ /users:
449
+ get:
450
+ security:
451
+ - bearerAuth: ['admin']
452
+ tags:
453
+ - admin
454
+ summary: list all created users
455
+ operationId: listUsers
456
+ responses:
457
+ 200:
458
+ description: success
459
+ content:
460
+ application/json:
461
+ schema:
462
+ $ref: '#/components/schemas/UserList'
422
463
  /app/resetAppKey/{appKey}:
423
464
  get:
424
465
  security:
@@ -799,6 +840,98 @@ paths:
799
840
  content:
800
841
  appication/json:
801
842
  schema: '#/components/schemas/appMethods'
843
+ /settings/{appKey}/{userId}:
844
+ post:
845
+ security:
846
+ - bearerAuth: ['admin', 'user']
847
+ tags:
848
+ - user
849
+ summary: Creates user settings for an app, shared across tokens per user
850
+ operationId: createAppSettings
851
+ parameters:
852
+ - name: appKey
853
+ in: path
854
+ description: name of the app to use for the settings
855
+ required: true
856
+ example: 'expedia'
857
+ schema:
858
+ type: string
859
+ - name: userId
860
+ in: path
861
+ description: user id for the setting to be created
862
+ required: true
863
+ example: '536830b6ed19afa44a000002'
864
+ schema:
865
+ type: string
866
+ requestBody:
867
+ description: Creation Payload
868
+ required: true
869
+ content:
870
+ application/json:
871
+ schema:
872
+ $ref: '#/components/schemas/settingsBody'
873
+ responses:
874
+ 200:
875
+ description: success
876
+ content:
877
+ application/json:
878
+ schema: '#/components/schemas/Success'
879
+ get:
880
+ security:
881
+ - bearerAuth: ['admin', 'user']
882
+ tags:
883
+ - user
884
+ summary: Retrieves a user settings for an app, shared across tokens per user
885
+ operationId: getAppSettings
886
+ parameters:
887
+ - name: appKey
888
+ in: path
889
+ description: name of the app to use for the settings
890
+ required: true
891
+ example: 'expedia'
892
+ schema:
893
+ type: string
894
+ - name: userId
895
+ in: path
896
+ description: user id for the setting to be created
897
+ required: true
898
+ example: '536830b6ed19afa44a000002'
899
+ schema:
900
+ type: string
901
+ responses:
902
+ 200:
903
+ description: success
904
+ content:
905
+ application/json:
906
+ schema: '#/components/schemas/settingsBody'
907
+ delete:
908
+ security:
909
+ - bearerAuth: ['admin', 'user']
910
+ tags:
911
+ - user
912
+ summary: Deletes a user settings for an app, shared across tokens per user
913
+ operationId: deleteAppSettings
914
+ parameters:
915
+ - name: appKey
916
+ in: path
917
+ description: name of the app to use for the settings
918
+ required: true
919
+ example: 'expedia'
920
+ schema:
921
+ type: string
922
+ - name: userId
923
+ in: path
924
+ description: user id for the setting to be created
925
+ required: true
926
+ example: '536830b6ed19afa44a000002'
927
+ schema:
928
+ type: string
929
+ responses:
930
+ 200:
931
+ description: success
932
+ content:
933
+ application/json:
934
+ $ref: '#/components/schemas/Status'
802
935
  /bookings/{appKey}/{userId}/search:
803
936
  post:
804
937
  security:
@@ -59,4 +59,11 @@ describe('admin', () => {
59
59
  });
60
60
  expect(value).toBeTruthy();
61
61
  });
62
+ it('the created user should be on the user list', async () => {
63
+ const { users } = await doApiGet({
64
+ url: '/users',
65
+ token: adminKey,
66
+ });
67
+ expect(users.find(currentUser => currentUser.userId === userId)).toBeTruthy();
68
+ });
62
69
  });
@@ -76,7 +76,6 @@ describe('app', () => {
76
76
  token: appKey,
77
77
  payload: {
78
78
  tokenHint: apiKey.split('-')[0],
79
- token,
80
79
  },
81
80
  });
82
81
  expect(valid).toBe(true);
@@ -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,44 @@ describe('user', () => {
133
135
  ]),
134
136
  );
135
137
  });
138
+ it('should be able to create an app setting', async () => {
139
+ const returnValue = await doApiPost({
140
+ url: `/settings/${appName}/${userId}`,
141
+ token: userKey,
142
+ payload: {
143
+ settings: {
144
+ custom: true,
145
+ },
146
+ },
147
+ });
148
+ expect(returnValue.success).toBe(true);
149
+ });
150
+ it('should be able to get an app settings', async () => {
151
+ const returnValue = await doApiGet({
152
+ url: `/settings/${appName}/${userId}`,
153
+ token: userKey,
154
+ });
155
+ expect(returnValue.settings.custom).toBe(true);
156
+ });
157
+ it('testing the user token should include the user app seting', async () => {
158
+ const { valid } = await doApiPost({
159
+ url: `/${appName}/${userId}/validate`,
160
+ token: userKey,
161
+ payload: {
162
+ tokenHint: apiKey.split('-')[0],
163
+ },
164
+ });
165
+ expect(plugins[0].validateToken).toHaveBeenCalled();
166
+ expect(valid).toBe(true);
167
+ expect(plugins[0].validateToken.mock.calls[0][0].token).toEqual({ custom: true, ...token });
168
+ });
169
+ it('should be able to delete an app settings', async () => {
170
+ const returnValue = await doApiDelete({
171
+ url: `/settings/${appName}/${userId}`,
172
+ token: userKey,
173
+ });
174
+ expect(returnValue.success).toBe(true);
175
+ });
136
176
  it('should be able to get all the methods for an app', async () => {
137
177
  const { methods } = await doApiGet({
138
178
  url: `/app/${appName}/methods`,
@@ -60,6 +60,15 @@ const listApps = async (req, res, next) => {
60
60
  }
61
61
  };
62
62
 
63
+ const listUsers = async (req, res, next) => {
64
+ try {
65
+ const users = await sqldb.User.findAll();
66
+ return res.json({ users });
67
+ } catch (err) {
68
+ return next(err);
69
+ }
70
+ };
71
+
63
72
  const createUserToken = async (req, res, next) => {
64
73
  const {
65
74
  body: {
@@ -140,6 +149,7 @@ module.exports = {
140
149
  createUserToken,
141
150
  deleteApp,
142
151
  listApps,
152
+ listUsers,
143
153
  migrate,
144
154
  resetIntegrationToken,
145
155
  };
@@ -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.appKey;
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,
@@ -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 alrady has the same app with the same hint
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.appKey;
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,89 @@ 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
+
313
389
  module.exports = plugins => ({
314
390
  createAppToken,
391
+ createAppSettings,
392
+ getAppSettings,
315
393
  deleteAppToken,
394
+ deleteAppSettings,
316
395
  getAppScheduledJobs,
317
396
  getJobStatus,
318
397
  jwtEncode,
@@ -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.appKey;
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.appKey;
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.appKey;
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.appKey;
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.appKey;
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.appKey;
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.appKey;
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,
@@ -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
+ };
@@ -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
@@ -11,6 +11,9 @@ User.associate = models => {
11
11
  User.hasMany(models.UserAppKey, {
12
12
  foreignKey: 'userId',
13
13
  });
14
+ User.hasMany(models.UserIntegrationSettings, {
15
+ foreignKey: 'userId',
16
+ });
14
17
  // associations can be defined here
15
18
  };
16
19
 
@@ -1,6 +1,6 @@
1
1
  const jwt = require('jsonwebtoken');
2
2
  const Sequelize = require('sequelize');
3
- const { omit } = require('ramda');
3
+ const { omit, pathOr } = require('ramda');
4
4
  const db = require('./db');
5
5
 
6
6
  const { env: { dbCryptoKey } } = process;
@@ -45,6 +45,16 @@ const UserAppKey = db.define('UserAppKey', {
45
45
  return omit(['iat'], retValue);
46
46
  },
47
47
  },
48
+ token: {
49
+ type: Sequelize.VIRTUAL,
50
+ async get() {
51
+ const sqldb = require('./index');
52
+ const userIntegrationSettings = await sqldb.UserIntegrationSettings.findOne({
53
+ where: { userId: this.getDataValue('userId'), integrationId: this.getDataValue('integrationId') },
54
+ });
55
+ return { ...pathOr({}, ['settings'], userIntegrationSettings), ...this.appKey };
56
+ },
57
+ },
48
58
  }, {});
49
59
  UserAppKey.associate = models => {
50
60
  UserAppKey.belongsTo(models.User, {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.38",
3
+ "version": "1.0.41",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {