ti2 1.0.0

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.
@@ -0,0 +1,136 @@
1
+ /* globals describe it expect beforeAll, afterAll, jest */
2
+
3
+ const faker = require('faker');
4
+ const request = require('supertest');
5
+ const { getRequireMocks } = require('../../../test/utils');
6
+ const app = require('../../../app');
7
+ const sqldb = require('../../../models');
8
+
9
+ const { env: { adminKey } } = process;
10
+
11
+ describe('user', () => {
12
+ const appName = faker.helpers.slugify(
13
+ faker.company.companyName(),
14
+ ).toLowerCase();
15
+ const newApp = {
16
+ name: appName,
17
+ packageName: `ti2-${appName}`,
18
+ adminEmail: faker.internet.email(),
19
+ };
20
+ let appKey;
21
+ const userId = faker.random.uuid();
22
+ let apiKey = faker.random.uuid();
23
+ // this token can be as rare as needed
24
+ const token = {
25
+ endpoint: faker.internet.url(),
26
+ apiKey,
27
+ };
28
+ beforeAll(async () => {
29
+ // create an App
30
+ const resp = await request(app)
31
+ .post('/app')
32
+ .set('Authorization', `Bearer ${adminKey}`)
33
+ .send(newApp);
34
+ expect(resp.statusCode).toBe(200);
35
+ expect(resp.body.value).toBeTruthy();
36
+ appKey = resp.body.value;
37
+ // create an App+User Mapping
38
+ await request(app)
39
+ .post(`/${appName}/${userId}`)
40
+ .set('Authorization', `Bearer ${appKey}`)
41
+ .send({
42
+ tokenHint: apiKey.split('-')[0],
43
+ token,
44
+ });
45
+ });
46
+ let userKey;
47
+ afterAll(async () => {
48
+ await sqldb.sequelize.connectionManager.close();
49
+ });
50
+ afterEach(() => jest.clearAllMocks());
51
+ it('a user should be able to get a user token via an admin key', async () => {
52
+ const resp = await request(app)
53
+ .post('/user')
54
+ .set('Authorization', `Bearer ${adminKey}`)
55
+ .send({
56
+ value: userId,
57
+ });
58
+ expect(resp.body.value).toBeTruthy();
59
+ userKey = resp.body.value;
60
+ });
61
+ it('a user should be able to get a list of it\'s mapped apps', async () => {
62
+ const resp = await request(app)
63
+ .get(`/user/${userId}/apps`)
64
+ .set('Authorization', `Bearer ${userKey}`);
65
+ expect(resp.statusCode).toBe(200);
66
+ expect(resp.body.userAppKeys).toEqual(
67
+ expect.arrayContaining([
68
+ expect.objectContaining({
69
+ hint: apiKey.split('-')[0],
70
+ userId,
71
+ integrationId: appName,
72
+ }),
73
+ ]),
74
+ );
75
+ });
76
+ it('should be able to delete a user/app key', async () => {
77
+ const resp = await request(app)
78
+ .delete(`/${appName}/${userId}`)
79
+ .set('Authorization', `Bearer ${userKey}`)
80
+ .send({ tokenHint: apiKey.split('-')[0] });
81
+ expect(resp.statusCode).toBe(200);
82
+ // make sure the app token is NOT there
83
+ const respList = await request(app)
84
+ .get(`/user/${userId}/apps`)
85
+ .set('Authorization', `Bearer ${userKey}`);
86
+ expect(respList.statusCode).toBe(200);
87
+ expect(respList.body.userAppKeys).toEqual(
88
+ expect.not.arrayContaining([
89
+ expect.objectContaining({
90
+ hint: apiKey.split('-')[0],
91
+ userId,
92
+ integrationId: appName,
93
+ }),
94
+ ]),
95
+ );
96
+ });
97
+ it('should be able to create a user/app integration', async () => {
98
+ // set up new token
99
+ apiKey = faker.random.uuid();
100
+ token.apiKey = apiKey;
101
+ const resp = await request(app)
102
+ .post(`/${appName}/${userId}`)
103
+ .set('Authorization', `Bearer ${userKey}`)
104
+ .send({
105
+ tokenHint: apiKey.split('-')[0],
106
+ token,
107
+ });
108
+ expect(resp.statusCode).toBe(200);
109
+ // make sure the app token is there
110
+ const respList = await request(app)
111
+ .get(`/user/${userId}/apps`)
112
+ .set('Authorization', `Bearer ${userKey}`);
113
+ expect(respList.statusCode).toBe(200);
114
+ expect(respList.body.userAppKeys).toEqual(
115
+ expect.arrayContaining([
116
+ expect.objectContaining({
117
+ hint: apiKey.split('-')[0],
118
+ userId,
119
+ integrationId: appName,
120
+ }),
121
+ ]),
122
+ );
123
+ });
124
+ it('should be able to get all the methods for an app', async () => {
125
+ getRequireMocks({ jest, app: newApp });
126
+ const methods = await request(app)
127
+ .get(`/app/${appName}/methods`)
128
+ .set('Authorization', `Bearer ${userKey}`);
129
+ expect(methods.statusCode).toBe(200);
130
+ expect(methods.body.methods).toEqual(
131
+ expect.arrayContaining([
132
+ 'validateToken', 'getProduct',
133
+ ]),
134
+ );
135
+ });
136
+ });
@@ -0,0 +1,77 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const assert = require('assert');
3
+ const { omit } = require('ramda');
4
+ const sqldb = require('../models');
5
+
6
+ const { env: { jwtSecret } } = process;
7
+
8
+ const createApp = async (req, res, next) => {
9
+ const {
10
+ name,
11
+ packageName,
12
+ adminEmail,
13
+ } = req.body;
14
+ try {
15
+ const newApp = await sqldb.Integration.create({
16
+ name,
17
+ packageName,
18
+ adminEmail,
19
+ });
20
+ const { apiKey } = newApp;
21
+ return res.json({ value: apiKey });
22
+ } catch (err) {
23
+ return next(err);
24
+ }
25
+ };
26
+
27
+ const listApps = async (req, res, next) => {
28
+ try {
29
+ const integrations = await sqldb.Integration.findAll().map(int => omit(['apiKey'], int.dataValues));
30
+ return res.json({ integrations });
31
+ } catch (err) {
32
+ return next(err);
33
+ }
34
+ };
35
+
36
+ const createUserToken = async (req, res, next) => {
37
+ const {
38
+ body: {
39
+ userId,
40
+ },
41
+ } = req;
42
+ if (!userId) {
43
+ assert(userId, 'No user provided');
44
+ // return next({ status: 404 });
45
+ }
46
+ try {
47
+ // check that the user exists or create it
48
+ if (!(await sqldb.User.findOne(({ where: { userId }, raw: true })))) {
49
+ await sqldb.User.create({ userId });
50
+ }
51
+ const value = jwt.sign({ userId }, jwtSecret);
52
+ return res.json({ value });
53
+ } catch (err) {
54
+ return next(err);
55
+ }
56
+ };
57
+
58
+ const resetIntegrationToken = async (req, res, next) => {
59
+ const { pathParams: { appKey: name } } = req;
60
+ try {
61
+ const integration = await sqldb.Integration.findOne({ where: { name } });
62
+ assert(Boolean(integration) !== false);
63
+ const value = sqldb.Integration.generateKey();
64
+ integration.apiKey = value;
65
+ integration.save();
66
+ return res.json({ value });
67
+ } catch (err) {
68
+ return next(err);
69
+ }
70
+ };
71
+
72
+ module.exports = {
73
+ createApp,
74
+ createUserToken,
75
+ listApps,
76
+ resetIntegrationToken,
77
+ };
@@ -0,0 +1,91 @@
1
+ const jwt = require('jwt-promise');
2
+ const { omit } = require('ramda');
3
+ const sqldb = require('../models');
4
+
5
+ const { env: { jwtSecret } } = process;
6
+
7
+ const jwtEncode = async (req, res, next) => {
8
+ try {
9
+ const value = await jwt.sign(req.body, `${req.appRecord.name}.${jwtSecret}`);
10
+ return res.json({ value });
11
+ } catch (err) {
12
+ return next(err);
13
+ }
14
+ };
15
+
16
+ const createAppToken = async (req, res, next) => {
17
+ const {
18
+ body: {
19
+ tokenHint: hint,
20
+ token: appKey,
21
+ },
22
+ params: {
23
+ app: integrationId,
24
+ userId,
25
+ },
26
+ } = req;
27
+ try {
28
+ const payload = {
29
+ integrationId,
30
+ userId,
31
+ hint,
32
+ appKey,
33
+ };
34
+ // check if the user exists
35
+ const userRecord = await sqldb.User.findOne({ where: { userId } });
36
+ if (!userRecord) { // create the user record
37
+ await sqldb.User.create({ userId });
38
+ }
39
+ // check if the user alrady has the same app with the same hint
40
+ const userAppKeyDup = await sqldb.UserAppKey.findOne({
41
+ where: { userId, integrationId, hint },
42
+ });
43
+ if (userAppKeyDup) await userAppKeyDup.destroy();
44
+ const newAppKey = await sqldb.UserAppKey.create(payload);
45
+ return res.json({ value: newAppKey.get('id').toString() });
46
+ } catch (err) {
47
+ return next(err);
48
+ }
49
+ };
50
+
51
+ const deleteAppToken = async (req, res, next) => {
52
+ const {
53
+ body: {
54
+ tokenHint: hint,
55
+ },
56
+ params: {
57
+ app: integrationId,
58
+ userId,
59
+ },
60
+ } = req;
61
+ // check if the user exists
62
+ const userRecord = await sqldb.User.findOne({ where: { userId } });
63
+ if (!userRecord) return next({ status: 404, message: 'User does not exists' });
64
+ const retVal = await sqldb.UserAppKey.destroy({
65
+ where: {
66
+ integrationId,
67
+ userId,
68
+ hint,
69
+ },
70
+ });
71
+ if (retVal === 0) return next({ status: 404, message: 'Key not found' });
72
+ return res.json({ message: `${retVal} erased` });
73
+ };
74
+
75
+ const listAppTokens = async (req, res, next) => {
76
+ const { params: { app: integrationId } } = req;
77
+ try {
78
+ const userAppKeys = (await sqldb.UserAppKey.findAll({ where: { integrationId } }))
79
+ .map(userAppKey => userAppKey.dataValues);
80
+ return res.json({ userAppKeys: userAppKeys.map(userAppKey => omit(['appKey', 'id'], userAppKey)) });
81
+ } catch (err) {
82
+ return next(err);
83
+ }
84
+ };
85
+
86
+ module.exports = {
87
+ jwtEncode,
88
+ createAppToken,
89
+ deleteAppToken,
90
+ listAppTokens,
91
+ };
@@ -0,0 +1,63 @@
1
+ const assert = require('assert');
2
+
3
+ const { UserAppKey } = require('../models/index');
4
+
5
+ const bookingsSearch = plugins => async (req, res, next) => {
6
+ const {
7
+ params: { appKey, userId, hint },
8
+ body,
9
+ } = req;
10
+ try {
11
+ const app = plugins.find(({ name }) => name === appKey);
12
+ const userAppKeys = await UserAppKey.findOne({
13
+ where: {
14
+ userId,
15
+ integrationId: appKey,
16
+ ...(hint ? { hint } : {}),
17
+ },
18
+ });
19
+ assert(userAppKeys, 'could not find the app key');
20
+ const token = userAppKeys.appKey;
21
+ const search = app.searchHotelBooking || app.searchBooking;
22
+ const results = await search({
23
+ token,
24
+ payload: body,
25
+ });
26
+ return res.json(results);
27
+ } catch (err) {
28
+ console.log({ err });
29
+ return next(err);
30
+ }
31
+ };
32
+
33
+ const bookingsCancel = plugins => async (req, res, next) => {
34
+ const {
35
+ params: { appKey, userId, hint },
36
+ body,
37
+ } = req;
38
+ try {
39
+ const app = plugins.find(({ name }) => name === appKey);
40
+ // const app = load(appKey);
41
+ const userAppKeys = (await UserAppKey.findOne({
42
+ where: {
43
+ userId,
44
+ integrationId: appKey,
45
+ ...(hint ? { hint } : {}),
46
+ },
47
+ }));
48
+ assert(userAppKeys, 'could not find the app key');
49
+ const token = userAppKeys.appKey;
50
+ const results = await app.cancelBooking({
51
+ token,
52
+ payload: body,
53
+ });
54
+ return res.json(results);
55
+ } catch (err) {
56
+ return next(err);
57
+ }
58
+ };
59
+
60
+ module.exports = plugins => ({
61
+ bookingsSearch: bookingsSearch(plugins),
62
+ bookingsCancel: bookingsCancel(plugins),
63
+ });
@@ -0,0 +1,27 @@
1
+ /*
2
+ Functions in a127 controllers used for operations should take two parameters:
3
+
4
+ Param 1: a handle to the request object
5
+ Param 2: a handle to the response object
6
+ */
7
+ const {
8
+ name,
9
+ version,
10
+ description,
11
+ } = require('../package.json');
12
+
13
+ const ping = (_req, res, next) => {
14
+ try {
15
+ return res.json({
16
+ name,
17
+ description,
18
+ version,
19
+ uptime: process.uptime(),
20
+ });
21
+ } catch (err) {
22
+ return next(err);
23
+ } };
24
+
25
+ module.exports = {
26
+ ping,
27
+ };
@@ -0,0 +1,37 @@
1
+ // controllers/user.js
2
+ const { omit, flatten } = require('ramda');
3
+ const assert = require('assert');
4
+ const Promise = require('bluebird');
5
+ const moment = require('moment');
6
+
7
+ const { UserAppKey, Sequelize: { Op } } = require('../models');
8
+ // const { load } = require('../../plugins/index.js');
9
+
10
+ const userAppList = async (req, res, next) => {
11
+ const { params: { userId } } = req;
12
+ try {
13
+ const userAppKeys = (await UserAppKey.findAll({ where: { userId } }))
14
+ .map(userAppKey => userAppKey.dataValues);
15
+ return res.json({ userAppKeys: userAppKeys.map(userAppKey => omit(['appKey', 'id'], userAppKey)) });
16
+ } catch (err) {
17
+ return next(err);
18
+ }
19
+ };
20
+
21
+ const getAppMethods = plugins => async (req, res, next) => {
22
+ const { params: { appKey } } = req;
23
+ try {
24
+ const app = plugins.filter(({ name }) => name === appKey)[0];
25
+ // load(appKey);
26
+ return res.json({
27
+ methods: Object.getOwnPropertyNames(Object.getPrototypeOf(app)),
28
+ });
29
+ } catch (err) {
30
+ return next(err);
31
+ }
32
+ };
33
+
34
+ module.exports = plugins => ({
35
+ userAppList,
36
+ getAppMethods: getAppMethods(plugins),
37
+ });
package/index.js ADDED
@@ -0,0 +1,173 @@
1
+ const express = require('express');
2
+ const createMiddleware = require('swagger-express-middleware');
3
+ const { connector } = require('swagger-routes-express');
4
+ const swaggerUi = require('swagger-ui-express');
5
+ const yaml = require('js-yaml');
6
+ const fs = require('fs');
7
+ const { pickBy } = require('ramda');
8
+
9
+ const schema = yaml.load(fs.readFileSync(`${__dirname}/api.yml`));
10
+
11
+ const auth = require('./auth/authHandler');
12
+ const pingController = require('./controllers/ping');
13
+ const adminController = require('./controllers/admin');
14
+ const appController = require('./controllers/app');
15
+ const userController = require('./controllers/user');
16
+ const bookingsController = require('./controllers/bookings');
17
+
18
+ module.exports = ({
19
+ apiDocs = true,
20
+ elasticLogIndex = 'apilog_ti2',
21
+ elasticLogsClient,
22
+ plugins: pluginsParam = {},
23
+ port: portParam,
24
+ startServer = true,
25
+ }) => {
26
+ const port = portParam || process.env.PORT || 10010;
27
+ const app = express();
28
+ const plugins = Object.entries(pluginsParam).map(([pluginName, Plugin]) => {
29
+ // pass all env variables
30
+ const pluginEnv = pickBy(
31
+ (_val, key) => key.substring(0, `ti2_${pluginName}`.length)
32
+ .replace('-', '_') === `ti2_${pluginName}`,
33
+ process.env,
34
+ );
35
+ const params = {};
36
+ Object.entries(pluginEnv).forEach(([attr, value]) => {
37
+ const nuName = attr.replace(/_/g, '-').replace(`ti2-${pluginName}-`, '');
38
+ params[nuName] = value;
39
+ });
40
+ return new Plugin({ name: pluginName, ...params });
41
+ });
42
+ const api = {
43
+ ...pingController,
44
+ ...adminController,
45
+ ...appController,
46
+ ...userController(plugins),
47
+ ...bookingsController(plugins),
48
+ }; // mehthods that should map to the yaml api spec
49
+ createMiddleware(schema, app, (_err, middleware) => {
50
+ if (apiDocs) {
51
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(schema));
52
+ }
53
+ app.use(
54
+ middleware.metadata(),
55
+ middleware.CORS(),
56
+ middleware.parseRequest(),
57
+ // middleware.validateRequest(),
58
+ );
59
+ const connect = connector(api, schema, {
60
+ security: auth,
61
+ });
62
+ if (elasticLogsClient) { // API request logs are saved on elastic
63
+ // setup the index
64
+ elasticLogsClient.indices.existsTemplate({
65
+ name: 'apilog',
66
+ }).then(async ({ body }) => {
67
+ if (!body) {
68
+ // the template has to be created
69
+ console.log('=- creating index template -=');
70
+ await elasticLogsClient.indices.putTemplate({
71
+ name: 'apilog',
72
+ body: {
73
+ index_patterns: [`${elasticLogIndex}*`],
74
+ settings: {
75
+ number_of_shards: 1,
76
+ },
77
+ mappings: {
78
+ dynamic: true,
79
+ properties: {
80
+ body: { type: 'object' },
81
+ client: { type: 'keyword' },
82
+ date: { type: 'long' },
83
+ method: { type: 'keyword' },
84
+ params: { type: 'object' },
85
+ query: { type: 'object' },
86
+ url: { type: 'text' },
87
+ responseStatusCode: { type: 'long' },
88
+ responseTimeInMs: { type: 'long' },
89
+ },
90
+ },
91
+ },
92
+ });
93
+ }
94
+ const exists = await elasticLogsClient.indices.exists({
95
+ index: elasticLogIndex,
96
+ });
97
+ if (!exists || !exists.body) {
98
+ console.log('=- creating index =-');
99
+ await elasticLogsClient.indices.create({
100
+ index: elasticLogIndex,
101
+ });
102
+ }
103
+ }).catch(console.log);
104
+ app.use((req, res, next) => {
105
+ const startHrTime = process.hrtime();
106
+ const body = {
107
+ date: Math.floor(Date.now() / 1e3),
108
+ url: req.url,
109
+ // body: req.body,
110
+ params: req.pathParams,
111
+ query: req.query,
112
+ appRecord: req.appRecord,
113
+ method: req.method,
114
+ client: req.headers['x-forwarded-for'] || req.connection.remoteAddress,
115
+ };
116
+ elasticLogsClient.index({
117
+ index: elasticLogIndex,
118
+ body,
119
+ }).then(({ body: { _id } }) => {
120
+ res.on('finish', () => {
121
+ const elapsedHrTime = process.hrtime(startHrTime);
122
+ const responseTimeInMs = parseInt(elapsedHrTime[0] * 1e3 + elapsedHrTime[1] / 1e6, 10);
123
+ elasticLogsClient.index({
124
+ index: elasticLogIndex,
125
+ id: _id,
126
+ body: {
127
+ ...body,
128
+ ...{
129
+ responseTimeInMs,
130
+ responseStatusCode: res.statusCode,
131
+ },
132
+ },
133
+ }).catch((err, response) => console.log(err, response));
134
+ });
135
+ }).catch((err, response) => console.log(err, response));
136
+ next();
137
+ });
138
+ }
139
+ connect(app);
140
+ app.use(middleware.mock());
141
+ // global error Handling
142
+ app.use((err, req, res, next) => {
143
+ // console.log(req.headers.['X-Request-Id'], err);
144
+ res.status(err.status || 500);
145
+ return res.json({
146
+ message: err.message || 'Internal Error',
147
+ });
148
+ });
149
+ });
150
+
151
+ if (startServer) {
152
+ app.listen(port, () => console.log(`ti2 waiting on port ${port}`));
153
+ }
154
+ // first create a generic "terminator"
155
+ const terminator = sig => {
156
+ if (typeof sig === 'string') {
157
+ console.log('%s: Received %s - terminating ti2 ...',
158
+ Date(Date.now()), sig);
159
+ process.exit(1);
160
+ }
161
+ console.log('%s: Node server stopped.', Date(Date.now()));
162
+ };
163
+
164
+ // then implement it for every process signal related to exit/quit
165
+ [
166
+ 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
167
+ 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM',
168
+ ].forEach(element => {
169
+ process.on(element, () => { terminator(element); });
170
+ });
171
+
172
+ return app;
173
+ }
@@ -0,0 +1,31 @@
1
+ module.exports = {
2
+ up: (queryInterface, Sequelize) => queryInterface
3
+ .createTable('Integrations', {
4
+ name: {
5
+ type: Sequelize.STRING,
6
+ allowNull: false,
7
+ primaryKey: true,
8
+ },
9
+ packageName: {
10
+ type: Sequelize.STRING,
11
+ },
12
+ adminEmail: {
13
+ type: Sequelize.STRING,
14
+ },
15
+ apiKey: {
16
+ type: Sequelize.STRING,
17
+ },
18
+ lastSeen: {
19
+ type: Sequelize.INTEGER,
20
+ },
21
+ createdAt: {
22
+ allowNull: false,
23
+ type: Sequelize.DATE,
24
+ },
25
+ updatedAt: {
26
+ allowNull: false,
27
+ type: Sequelize.DATE,
28
+ },
29
+ }),
30
+ down: queryInterface => queryInterface.dropTable('Integrations'),
31
+ };
@@ -0,0 +1,19 @@
1
+ module.exports = {
2
+ up: (queryInterface, Sequelize) => queryInterface
3
+ .createTable('Users', {
4
+ userId: {
5
+ allowNull: false,
6
+ primaryKey: true,
7
+ type: Sequelize.STRING,
8
+ },
9
+ createdAt: {
10
+ allowNull: false,
11
+ type: Sequelize.DATE,
12
+ },
13
+ updatedAt: {
14
+ allowNull: false,
15
+ type: Sequelize.DATE,
16
+ },
17
+ }),
18
+ down: queryInterface => queryInterface.dropTable('Users'),
19
+ };
@@ -0,0 +1,43 @@
1
+ module.exports = {
2
+ up: (queryInterface, Sequelize) => queryInterface.createTable('UserAppKeys', {
3
+ id: {
4
+ allowNull: false,
5
+ autoIncrement: true,
6
+ primaryKey: true,
7
+ type: Sequelize.INTEGER,
8
+ },
9
+ integrationId: {
10
+ type: Sequelize.STRING,
11
+ allowNull: false,
12
+ references: {
13
+ model: 'Integrations',
14
+ key: 'name',
15
+ },
16
+ },
17
+ userId: {
18
+ type: Sequelize.STRING,
19
+ allowNull: false,
20
+ references: {
21
+ model: 'Users',
22
+ key: 'userId',
23
+ },
24
+ },
25
+ appKey: {
26
+ type: Sequelize.STRING,
27
+ allowNull: false,
28
+ },
29
+ hint: {
30
+ type: Sequelize.STRING,
31
+ allowNull: false,
32
+ },
33
+ createdAt: {
34
+ allowNull: false,
35
+ type: Sequelize.DATE,
36
+ },
37
+ updatedAt: {
38
+ allowNull: false,
39
+ type: Sequelize.DATE,
40
+ },
41
+ }),
42
+ down: queryInterface => queryInterface.dropTable('UserAppKeys'),
43
+ };