ti2 1.0.29 → 1.0.32

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/api.yml CHANGED
@@ -1342,3 +1342,104 @@ paths:
1342
1342
  application/json:
1343
1343
  schema:
1344
1344
  $ref: '#/components/schemas/AnyValue'
1345
+ /allotment/{appKey}/{userId}:
1346
+ get:
1347
+ security:
1348
+ - bearerAuth: ['admin', 'user']
1349
+ tags:
1350
+ - allotment
1351
+ summary: Query the allotment
1352
+ operationId: queryAllotment
1353
+ parameters:
1354
+ - name: appKey
1355
+ in: path
1356
+ description: app to create the booking for, defaults to first token
1357
+ required: true
1358
+ example: 'tourplan'
1359
+ schema:
1360
+ type: string
1361
+ - name: userId
1362
+ in: path
1363
+ description: the user/company that the appKey belongs to
1364
+ required: true
1365
+ schema:
1366
+ type: string
1367
+ - in: query
1368
+ name: startDate
1369
+ schema:
1370
+ type: string
1371
+ description: the start date of the requested allotment (dd/mm/yyyy)
1372
+ default: "01/04/2023"
1373
+ - in: query
1374
+ name: endDate
1375
+ schema:
1376
+ type: string
1377
+ description: the end date of the requested allotment (dd/mm/yyyy)
1378
+ default: "03/04/2023"
1379
+ - in: query
1380
+ name: keyPath
1381
+ schema:
1382
+ type: string
1383
+ description: a string with the product path separater by pipes ie. supplierId|locationId|productID
1384
+ default: "MAGLUX|7CQLDACMAGLUXDELSUI"
1385
+ responses:
1386
+ '200':
1387
+ description: Allotment Details
1388
+ content:
1389
+ application/json:
1390
+ schema:
1391
+ $ref: '#/components/schemas/AnyValue'
1392
+ /allotment/{appKey}/{hint}/{userId}:
1393
+ get:
1394
+ security:
1395
+ - bearerAuth: ['admin', 'user']
1396
+ tags:
1397
+ - allotment
1398
+ summary: Query the allotment with an specific hit
1399
+ operationId: queryAllotment
1400
+ parameters:
1401
+ - name: appKey
1402
+ in: path
1403
+ description: app to create the booking for, defaults to first token
1404
+ required: true
1405
+ example: 'tourplan'
1406
+ schema:
1407
+ type: string
1408
+ - name: hint
1409
+ in: path
1410
+ description: a specific token hint to be used
1411
+ required: true
1412
+ example: 'tourplan-australia'
1413
+ schema:
1414
+ type: string
1415
+ - name: userId
1416
+ in: path
1417
+ description: the user/company that the appKey belongs to
1418
+ required: true
1419
+ schema:
1420
+ type: string
1421
+ - in: query
1422
+ name: startDate
1423
+ schema:
1424
+ type: string
1425
+ description: the start date of the requested allotment (dd/mm/yyyy)
1426
+ default: "01/04/2023"
1427
+ - in: query
1428
+ name: endDate
1429
+ schema:
1430
+ type: string
1431
+ description: the end date of the requested allotment (dd/mm/yyyy)
1432
+ default: "03/04/2023"
1433
+ - in: query
1434
+ name: keyPath
1435
+ schema:
1436
+ type: string
1437
+ description: a string with the product path separater by pipes ie. supplierId|locationId|productID
1438
+ default: "MAGLUX|7CQLDACMAGLUXDELSUI"
1439
+ responses:
1440
+ '200':
1441
+ description: Allotment Details
1442
+ content:
1443
+ application/json:
1444
+ schema:
1445
+ $ref: '#/components/schemas/AnyValue'
@@ -1,6 +1,5 @@
1
1
  const jsonwebtoken = require('jsonwebtoken');
2
2
  const Promise = require('bluebird');
3
- const assert = require('assert');
4
3
 
5
4
  const sqldb = require('../models');
6
5
 
@@ -12,7 +11,8 @@ const {
12
11
  },
13
12
  } = process;
14
13
 
15
- const invalid = next => next({ status: 401, message: 'Unauthorized' });
14
+ const invalid = (res, msg) => res.status(401).json({ error: msg || 'Unauthorized' });
15
+
16
16
  const getToken = ({ req }) => {
17
17
  if (
18
18
  !req.header('Authorization')
@@ -20,61 +20,101 @@ const getToken = ({ req }) => {
20
20
  ) return null;
21
21
  return req.header('Authorization').split(' ')[1];
22
22
  };
23
+
23
24
  // the authentication middleware
24
- const app = async (req, res, next) => {
25
- if (!req.pathParams || !req.pathParams.app) return invalid(next);
25
+ const appCheck = async req => {
26
+ if (!req.pathParams || !req.pathParams.app) {
27
+ return 'App parameters not found';
28
+ }
26
29
  const token = getToken({ req });
27
- if (!token) return invalid(next);
28
- // try {
30
+ if (!token) return 'Api key not found';
29
31
  const appRecord = await sqldb.Integration.findOne({
30
32
  where: {
31
33
  name: req.pathParams.app,
32
34
  },
33
35
  });
34
- if (!appRecord) return invalid(next);
36
+ if (!appRecord) return 'App not found';
35
37
  if (appRecord.apiKey !== token) {
36
- return invalid(next);
38
+ return 'Invalid api key';
37
39
  }
38
40
  req.appRecord = appRecord.dataValues;
41
+ return undefined;
42
+ };
43
+
44
+ const app = async (req, res, next) => {
45
+ const error = await appCheck(req);
46
+ if (error) {
47
+ return invalid(res, error);
48
+ }
39
49
  return next();
40
50
  };
41
51
 
42
52
  // The requester is an admin; createApps and other admin tasks
43
- const admin = async (req, res, next) => {
53
+ const adminCheck = async req => {
44
54
  const token = getToken({ req });
45
- if (token !== adminKey) return invalid(next);
55
+ if (token !== adminKey) return 'Invalid admin key';
56
+ return undefined;
57
+ };
58
+
59
+ const admin = async (req, res, next) => {
60
+ const error = await adminCheck(req);
61
+ if (error) {
62
+ return invalid(res, error);
63
+ }
46
64
  return next();
47
65
  };
48
66
 
49
67
  // the request is from a user, checke the userId agains the url param
50
- const user = async (req, res, next) => {
68
+ const userCheck = async req => {
51
69
  const token = getToken({ req });
52
- if (!token) return invalid(next);
70
+ if (!token) return 'Token not found';
71
+ let userId;
53
72
  try {
54
- const { userId } = jsonwebtoken.verify(token, jwtSecret);
55
- assert(userId);
56
- if (req.params.userId && userId !== req.params.userId) return invalid(next);
73
+ ({ userId } = jsonwebtoken.verify(token, jwtSecret));
57
74
  } catch (err) {
58
- return invalid(next);
75
+ return 'Invalid token';
76
+ }
77
+ if (!userId) return 'UserId not found';
78
+ if (req.params.userId && userId !== req.params.userId) {
79
+ return 'Non matching user id';
80
+ }
81
+ return undefined;
82
+ };
83
+
84
+ const user = async (req, res, next) => {
85
+ const error = await userCheck(req);
86
+ if (error) {
87
+ return invalid(res, error);
59
88
  }
60
89
  return next();
61
90
  };
62
91
 
63
92
  // The requester comes from frontend; frontend driven tasks
64
- const frontend = async (req, res, next) => {
93
+ const frontendCheck = async req => {
65
94
  const token = getToken({ req });
66
- if (token !== frontendKey) return invalid(next);
95
+ if (token !== frontendKey) {
96
+ return 'Invalid frontend token';
97
+ }
98
+ return undefined;
99
+ };
100
+
101
+ const frontend = async (req, res, next) => {
102
+ const error = await frontendCheck(req);
103
+ if (error) {
104
+ return invalid(res, error);
105
+ }
67
106
  return next();
68
107
  };
69
108
 
70
- const adminOrUser = levels => async (req, res, next) => {
71
- const result = await Promise.all(levels.map(
72
- level => level(req, res, err => err),
109
+ const multipleAuthEval = levels => async (req, res, next) => {
110
+ // res.send = true;
111
+ const results = await Promise.all(levels.map(
112
+ levelCheck => levelCheck(req),
73
113
  ));
74
- if (result.some(val => (!val))) {
114
+ if (results.some(val => !val)) {
75
115
  return next();
76
116
  }
77
- return next(result[0]);
117
+ return invalid(res, results.join(' or '));
78
118
  };
79
119
 
80
120
  module.exports = {
@@ -82,6 +122,6 @@ module.exports = {
82
122
  frontend,
83
123
  admin,
84
124
  user,
85
- 'admin,user': adminOrUser([admin, user]),
86
- 'admin,app,user': adminOrUser([admin, app, user]),
125
+ 'admin,user': multipleAuthEval([adminCheck, userCheck]),
126
+ 'admin,app,user': multipleAuthEval([adminCheck, appCheck, userCheck]),
87
127
  };
@@ -0,0 +1,106 @@
1
+ /* globals beforeAll describe it expect */
2
+
3
+ const chance = require('chance').Chance();
4
+ const testUtils = require('../../test/utils');
5
+
6
+ const { env: { adminKey } } = process;
7
+
8
+ describe('allotment', () => {
9
+ const newApp = {
10
+ name: 'tourplantest',
11
+ packageName: 'ti2-tourplanTest',
12
+ adminEmail: 'engineering+tourplantest@tourconnect.com',
13
+ };
14
+ const appKey = newApp.name;
15
+ const userId = '551394be5ac58e5c76000019';
16
+ const token = {
17
+ endpoint: 'https://someendpoint.com',
18
+ username: chance.guid(),
19
+ password: chance.string(),
20
+ };
21
+ const newIntegration = {
22
+ tokenHint: 'testingToken',
23
+ token,
24
+ };
25
+ let doApiDelete;
26
+ let doApiGet;
27
+ let doApiPost;
28
+ let plugins;
29
+ beforeAll(async () => {
30
+ ({
31
+ doApiDelete,
32
+ doApiGet,
33
+ doApiPost,
34
+ plugins,
35
+ } = await testUtils({
36
+ plugins: [appKey],
37
+ }));
38
+ });
39
+
40
+ let userToken;
41
+ it('drop any existing user integration', async () => {
42
+ try {
43
+ await doApiDelete({
44
+ url: `/${appKey}/${userId}`,
45
+ token: adminKey,
46
+ payload: { tokenHint: newIntegration.tokenHint },
47
+ });
48
+ } catch (err) {
49
+ // console.debug(err);
50
+ }
51
+ });
52
+ it('create the user', async () => {
53
+ ({ value: userToken } = await doApiPost({
54
+ url: '/user',
55
+ token: adminKey,
56
+ payload: { userId },
57
+ }));
58
+ expect(userToken).toBeTruthy();
59
+ });
60
+ it('create the App+User setup', async () => {
61
+ const { userAppKeys } = await doApiGet({
62
+ url: `/user/${userId}/apps`,
63
+ token: userToken,
64
+ });
65
+ if (!userAppKeys.map(e => e.integrationId).includes(appKey)) {
66
+ // relation does not exits
67
+ const { value } = await doApiPost({
68
+ url: `/${appKey}/${userId}`,
69
+ token: userToken,
70
+ payload: newIntegration,
71
+ });
72
+ expect(value).toBeTruthy();
73
+ }
74
+ });
75
+ const payload = {
76
+ dateFormat: 'DD-MM-YYYY',
77
+ startDate: '01-12-2022',
78
+ endDate: '15-12-2022',
79
+ keyPath: `${chance.guid()}|${chance.guid()}`,
80
+ };
81
+ const urlParams = new URLSearchParams(payload).toString();
82
+ it('should be able to get some allotments', async () => {
83
+ const { allotments } = await doApiGet({
84
+ url: `/allotment/${appKey}/${userId}?${urlParams}`,
85
+ token: userToken,
86
+ payload,
87
+ });
88
+ expect(plugins[0].queryAllotment).toHaveBeenCalled();
89
+ expect(Array.isArray(allotments)).toBeTruthy();
90
+ const call = plugins[0].queryAllotment.mock.calls[0][0];
91
+ expect(call.payload).toEqual(payload);
92
+ expect(call.token).toEqual(token);
93
+ });
94
+ it('should be able to get some allotments with a hint', async () => {
95
+ const { allotments } = await doApiGet({
96
+ url: `/allotment/${appKey}/${newIntegration.tokenHint}/${userId}?${urlParams}`,
97
+ token: userToken,
98
+ payload,
99
+ });
100
+ expect(plugins[0].queryAllotment).toHaveBeenCalled();
101
+ expect(Array.isArray(allotments)).toBeTruthy();
102
+ const call = plugins[0].queryAllotment.mock.calls[0][0];
103
+ expect(call.payload).toEqual(payload);
104
+ expect(call.token).toEqual(token);
105
+ });
106
+ });
@@ -89,7 +89,7 @@ describe('user: booking search', () => {
89
89
  });
90
90
  it('should be able to search a for a booking', async () => {
91
91
  const payload = {
92
- bookingId: '', supplierId: '', name: '',
92
+ bookingId: '', supplierBookingId: '', name: '',
93
93
  };
94
94
  const { bookings } = await doApiPost({
95
95
  url: `/bookings/${appKey}/${userId}/search`,
@@ -0,0 +1,40 @@
1
+ const assert = require('assert');
2
+
3
+ const { UserAppKey } = require('../models/index');
4
+
5
+ const queryAllotment = plugins => async (req, res, next) => {
6
+ const {
7
+ params: {
8
+ appKey,
9
+ hint,
10
+ userId,
11
+ },
12
+ query,
13
+ } = req;
14
+ try {
15
+ const app = plugins.find(({ name }) => name === appKey);
16
+ assert(app, `could not find the app ${appKey}`);
17
+ const userAppKeys = await UserAppKey.findOne({
18
+ where: {
19
+ userId,
20
+ integrationId: appKey,
21
+ ...(hint ? { hint } : {}),
22
+ },
23
+ });
24
+ assert(userAppKeys, 'could not find the app key');
25
+ const token = userAppKeys.appKey;
26
+ assert(app.queryAllotment, 'could not find the allotment method');
27
+ const search = app.queryAllotment;
28
+ const results = await search({
29
+ token,
30
+ payload: query,
31
+ });
32
+ return res.json(results);
33
+ } catch (err) {
34
+ return next(err);
35
+ }
36
+ };
37
+
38
+ module.exports = plugins => ({
39
+ queryAllotment: queryAllotment(plugins),
40
+ });
package/index.js CHANGED
@@ -17,6 +17,7 @@ const adminController = require('./controllers/admin');
17
17
  const appController = require('./controllers/app');
18
18
  const userController = require('./controllers/user');
19
19
  const bookingsController = require('./controllers/bookings');
20
+ const allotmentController = require('./controllers/allotment');
20
21
  const { Integration } = require('./models');
21
22
  const cache = require('./cache');
22
23
 
@@ -26,7 +27,6 @@ const rebuild = fn => obj =>
26
27
 
27
28
  const isNumber = value => !Number.isNaN(Number(value));
28
29
 
29
-
30
30
  module.exports = async ({
31
31
  apiDocs = true,
32
32
  elasticLogIndex = 'apilog_ti2',
@@ -67,7 +67,8 @@ module.exports = async ({
67
67
  },
68
68
  raw: true,
69
69
  });
70
- const missingIntegrations = R.difference(pluginNames, matchedIntegrations.map(R.prop('name')));
70
+ const matchedIntegrationsNames = matchedIntegrations.map(R.prop('name'));
71
+ const missingIntegrations = R.difference(pluginNames, matchedIntegrationsNames);
71
72
  if (missingIntegrations.length > 0) {
72
73
  // need to crete the missing integrations
73
74
  await Integration.bulkCreate(missingIntegrations.map(name => ({
@@ -84,6 +85,7 @@ module.exports = async ({
84
85
  ...appController,
85
86
  ...userController(plugins),
86
87
  ...bookingsController(plugins),
88
+ ...allotmentController(plugins),
87
89
  cache: R.omit(['cache'], cache),
88
90
  }; // mehthods that should map to the yaml api spec
89
91
  app.plugins = plugins;
@@ -124,6 +126,8 @@ module.exports = async ({
124
126
  );
125
127
  const connect = connector(api, allSchema, {
126
128
  security: auth,
129
+ notFound: (req, res) => res.sendStatus(404),
130
+ notImplemented: (req, res) => res.sendStatus(501),
127
131
  });
128
132
  if (elasticLogsClient) { // API request logs are saved on elastic
129
133
  // setup the index
@@ -204,9 +208,11 @@ module.exports = async ({
204
208
  }
205
209
  connect(app);
206
210
  app.use(middleware.mock());
207
- app.use((req, res) => res.sendStatus(404));
208
211
  // global error Handling
209
- app.use((err, req, res) => {
212
+ app.use((err, req, res, next) => {
213
+ if (res.headersSent) {
214
+ return next(err);
215
+ }
210
216
  if (process.env.CONSOLE_ERRORS || process.env.JEST_WORKER_ID) {
211
217
  console.error(err);
212
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.29",
3
+ "version": "1.0.32",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/plugin.js CHANGED
@@ -53,6 +53,7 @@ class Plugin {
53
53
  });
54
54
  this.searchQuote = jestPlugin.fn(() => ({ quote: [{ id: chance.guid() }] }));
55
55
  this.createBooking = jestPlugin.fn(() => {});
56
+ this.queryAllotment = jestPlugin.fn(() => ({ allotments: [] }));
56
57
 
57
58
  /**
58
59
  * Background and schedule Jos
@@ -292,7 +293,7 @@ class Plugin {
292
293
  * @property {Cancelled|Active|Pending} status The currrent booking status.
293
294
  * @property {Holder} holder Booking holder information.
294
295
  * @property {string} telephone Contact telephone.
295
- * @property {string} supplierId Supplier of the Booking
296
+ * @property {string} supplierBookingId Booking Id for the supplier booking system
296
297
  * @property {string} hotelId A HotelId if the booking is for an accommodation.
297
298
  * @property {string} hotelName A Hotel Name if the booking is for an accommodation.
298
299
  * @property {BookingRoom[]} rooms Booking Room details.
@@ -437,6 +438,25 @@ class Plugin {
437
438
  * @returns {Booking} retVal.booking - A Booking object.
438
439
  */
439
440
  createBooking() {}
441
+
442
+ /**
443
+ * Query Allotment
444
+ * @async
445
+ * @param {Object} args - Allotment query arguments.
446
+ * @param {Object} args.token - A token definition, it's content varies between integrations.
447
+ * @param {Object} args.payload - Search spect object.
448
+ * dateFormat = 'DD/MM/YYYY',
449
+ startDate,
450
+ endDate,
451
+ keyPath,
452
+ * @param {string} args.payload.dateFormat - Date format to use for params and return value (i.e. DD/MM/YYYY
453
+ * @param {string} args.payload.startDate - Start date for the query of allotment objects
454
+ * @param {string} args.payload.endDate - End date for the query of allotment objects
455
+ * @param {string} args.payload.keyPath - End date for the query of allotment objects
456
+ * @returns {object} retVal - the return object.
457
+ * @returns {Booking} retVal.booking - A Booking object.
458
+ */
459
+ queryAllotment() {}
440
460
  }
441
461
 
442
462
  module.exports = Plugin;