@crowdin/app-project-module 0.4.0 → 0.6.2

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/README.md CHANGED
@@ -27,6 +27,8 @@ In both options you will need to provide Crowdin App configuration file. Please
27
27
  - [OAuth2 login](#oauth2-support)
28
28
  - [Settings window](#settings-window)
29
29
  - [Info window](#info-window)
30
+ - [Background tasks](#background-tasks)
31
+ - [Error propagation](#error-propagation)
30
32
  - [Contributing](#contributing)
31
33
  - [Seeking Assistance](#seeking-assistance)
32
34
  - [License](#license)
@@ -222,6 +224,54 @@ Main default values:
222
224
  - access token field name should be `refresh_token`
223
225
  - expires in field name should be `expires_in` (value should be in seconds)
224
226
 
227
+ This module rely that OAuth2 protocol is implemented by third party service in this way:
228
+
229
+ - request for access token should be done via POST request to `accessTokenUrl` with JSON body that will contain at least `clientId`, `clientSecret`, `code` and `redirectUri` (also possible to add extra fields via `extraAccessTokenParameters` property)
230
+ - request to refresh token should be done via POST request to `accessTokenUrl` (or `refreshTokenUrl` if definied) with JSON body that will contain at least `clientId`, `clientSecret` and `refreshToken` (also possible to add extra fields via `extraRefreshTokenParameters` property)
231
+ - both requests will return JSON response with body that contains `accessToken` and, if enabled, `refreshToken` (optional) and `expireIn`
232
+
233
+ To override those requests please use `performGetTokenRequest` and `performRefreshTokenRequest` (e.g. when requests should be done with different HTTP methods or data should be tranfered as query string or form data).
234
+
235
+ Mailup example:
236
+
237
+ ```javascript
238
+ const clientId = 'client_id';
239
+ const clientSecret = 'client_secret';
240
+ const tokenUrl = 'https://services.mailup.com/Authorization/OAuth/Token';
241
+
242
+ configuration.oauthLogin = {
243
+ authorizationUrl: 'https://services.mailup.com/Authorization/OAuth/LogOn',
244
+ clientId,
245
+ clientSecret,
246
+ extraAutorizationUrlParameters: {
247
+ response_type: 'code'
248
+ },
249
+ refresh: true,
250
+ performGetTokenRequest: async (code) => {
251
+ const url = `${tokenUrl}?code=${code}&grant_type=authorization_code`;
252
+ const headers = {
253
+ 'Authorization': `Bearer ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
254
+ };
255
+ return (await axios.get(url, { headers })).data;
256
+ },
257
+ performRefreshTokenRequest: async (credentials) => {
258
+ const params = {
259
+ refresh_token: credentials.refreshToken,
260
+ grant_type: 'refresh_token',
261
+ client_id: clientId,
262
+ client_secret: clientSecret
263
+ };
264
+ const data = Object.keys(params)
265
+ .map((key) => `${key}=${encodeURIComponent(params[key])}`)
266
+ .join('&');
267
+ const headers = {
268
+ 'Content-Type': 'application/x-www-form-urlencoded'
269
+ };
270
+ return (await axios.post(tokenUrl, data, { headers })).data;
271
+ }
272
+ }
273
+ ```
274
+
225
275
  Please refer to jsdoc for more details.
226
276
 
227
277
  ## Settings window
@@ -239,7 +289,8 @@ configuration.integration.getConfiguration = (projectId, crowdinClient, integrat
239
289
  {
240
290
  key: 'text',
241
291
  label: 'Text input',
242
- type: 'text'
292
+ type: 'text',
293
+ helpText: 'Help text'
243
294
  },
244
295
  {
245
296
  key: 'option',
@@ -271,6 +322,58 @@ configuration.integration.infoModal = {
271
322
  }
272
323
  ```
273
324
 
325
+ ## Background tasks
326
+
327
+ In order to register background tasks that app will invoke periodically invoke you can use `cronJobs` field.
328
+
329
+ ```javascript
330
+ configuration.integration.cronJobs = [
331
+ {
332
+ //every 10 seconds
333
+ expression: '*/10 * * * * *',
334
+ task: (projectId, client, apiCredentials, appRootFolder, config) => {
335
+ console.log(`Running background task for project : ${projectId}`);
336
+ console.log(`Api credentials : ${JSON.stringify(apiCredentials)}`);
337
+ console.log(`App config : ${JSON.stringify(config)}`);
338
+ console.log(appRootFolder ? JSON.stringify(appRootFolder) : 'No root folder');
339
+ }
340
+ }
341
+ ]
342
+ ```
343
+
344
+ For cron syntax guide please refer to this [documentation](https://github.com/node-cron/node-cron#cron-syntax).
345
+
346
+ ## Error propagation
347
+
348
+ In case if something is wrong with app settings or credentials are invalid you can throw an explanation message that will be then visible on the UI side.
349
+ e.g. check if entered credentials are valid:
350
+
351
+ ```javascript
352
+ configuration.integration.checkConnection = (credentials) => {
353
+ if (!credentials.password || credentials.password.length < 6) {
354
+ throw 'Password is too weak';
355
+ }
356
+ //or call an service API with those credentials and check if request will be successful
357
+ };
358
+ ```
359
+
360
+ Or if you need to manually control users liveness session you can throw an error with `401` code then your app will automatically do a log out action.
361
+ e.g. when your service has some specific session duration timeout or extra conditions which are not covered by this framework
362
+
363
+ ```javascript
364
+ configuration.integrartion.getIntegrationFiles = async (credentials, appSettings) => {
365
+ //do a request/custom logic here
366
+ const sessionStillValid = false;
367
+ if (!sessionStillValid) {
368
+ throw {
369
+ message: 'session expired',
370
+ code: 401
371
+ }
372
+ }
373
+ //business logic
374
+ }
375
+ ```
376
+
274
377
  ## Contributing
275
378
 
276
379
  If you want to contribute please read the [Contributing](/CONTRIBUTING.md) guidelines.
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const crowdin_apps_functions_1 = require("@crowdin/crowdin-apps-functions");
13
+ const models_1 = require("../models");
13
14
  const storage_1 = require("../storage");
14
15
  const util_1 = require("../util");
15
16
  function handle(config) {
@@ -21,6 +22,7 @@ function handle(config) {
21
22
  accessToken: (0, util_1.encryptData)(config.clientSecret, token.accessToken),
22
23
  refreshToken: (0, util_1.encryptData)(config.clientSecret, token.refreshToken),
23
24
  expire: (new Date().getTime() / 1000 + token.expiresIn).toString(),
25
+ type: event.domain ? models_1.AccountType.ENTERPRISE : models_1.AccountType.NORMAL,
24
26
  };
25
27
  yield (0, storage_1.saveCrowdinCredentials)(credentials);
26
28
  res.status(204).end();
@@ -18,18 +18,25 @@ function handle(config) {
18
18
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
19
19
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
20
20
  const code = req.query[((_b = (_a = config.oauthLogin) === null || _a === void 0 ? void 0 : _a.fieldsMapping) === null || _b === void 0 ? void 0 : _b.code) || 'code'];
21
- const request = {};
22
21
  const oauthLogin = config.oauthLogin;
23
- request[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.code) || 'code'] = code;
24
- request[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId;
25
- request[((_e = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _e === void 0 ? void 0 : _e.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret;
26
- request[((_f = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _f === void 0 ? void 0 : _f.redirectUri) || 'redirect_uri'] = `${config.baseUrl}${(0, util_1.getOauthRoute)(config)}`;
27
- if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraAccessTokenParameters) {
28
- Object.entries(oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraAccessTokenParameters).forEach(([key, value]) => (request[key] = value));
22
+ let credentials;
23
+ if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.performGetTokenRequest) {
24
+ credentials = yield oauthLogin.performGetTokenRequest(code);
25
+ }
26
+ else {
27
+ const request = {};
28
+ const oauthLogin = config.oauthLogin;
29
+ request[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.code) || 'code'] = code;
30
+ request[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId;
31
+ request[((_e = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _e === void 0 ? void 0 : _e.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret;
32
+ request[((_f = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _f === void 0 ? void 0 : _f.redirectUri) || 'redirect_uri'] = `${config.baseUrl}${(0, util_1.getOauthRoute)(config)}`;
33
+ if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraAccessTokenParameters) {
34
+ Object.entries(oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraAccessTokenParameters).forEach(([key, value]) => (request[key] = value));
35
+ }
36
+ credentials = (yield axios_1.default.post(((_g = config.oauthLogin) === null || _g === void 0 ? void 0 : _g.accessTokenUrl) || '', request, {
37
+ headers: { Accept: 'application/json' },
38
+ })).data;
29
39
  }
30
- const credentials = (yield axios_1.default.post(((_g = config.oauthLogin) === null || _g === void 0 ? void 0 : _g.accessTokenUrl) || '', request, {
31
- headers: { Accept: 'application/json' },
32
- })).data;
33
40
  const message = {
34
41
  uid: 'oauth_popup',
35
42
  };
package/out/index.js CHANGED
@@ -25,6 +25,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.createApp = exports.addCrowdinEndpoints = void 0;
26
26
  const express_1 = __importStar(require("express"));
27
27
  const express_handlebars_1 = __importDefault(require("express-handlebars"));
28
+ const cron = __importStar(require("node-cron"));
28
29
  const path_1 = require("path");
29
30
  const crowdin_file_progress_1 = __importDefault(require("./handlers/crowdin-file-progress"));
30
31
  const crowdin_files_1 = __importDefault(require("./handlers/crowdin-files"));
@@ -81,6 +82,11 @@ function addCrowdinEndpoints(app, config) {
81
82
  app.get('/api/integration/data', json_response_1.default, (0, crowdin_client_1.default)(config), (0, integration_credentials_1.default)(config), (0, integration_data_1.default)(config));
82
83
  app.post('/api/crowdin/update', json_response_1.default, (0, crowdin_client_1.default)(config), (0, integration_credentials_1.default)(config), (0, crowdin_update_1.default)(config));
83
84
  app.post('/api/integration/update', json_response_1.default, (0, crowdin_client_1.default)(config), (0, integration_credentials_1.default)(config), (0, integration_update_1.default)(config));
85
+ if (config.integration.cronJobs) {
86
+ config.integration.cronJobs.forEach(job => {
87
+ cron.schedule(job.expression, () => (0, util_1.runJob)(config, job).catch(console.error));
88
+ });
89
+ }
84
90
  }
85
91
  exports.addCrowdinEndpoints = addCrowdinEndpoints;
86
92
  function createApp(config) {
@@ -8,11 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
- const crowdin_api_client_1 = __importDefault(require("@crowdin/crowdin-api-client"));
16
12
  const crowdin_apps_functions_1 = require("@crowdin/crowdin-apps-functions");
17
13
  const storage_1 = require("../storage");
18
14
  const util_1 = require("../util");
@@ -43,27 +39,7 @@ function handle(config, optional = false) {
43
39
  }
44
40
  return res.status(403).send({ error: "Can't find organization by id" });
45
41
  }
46
- const isExpired = +credentials.expire < +new Date().getTime() / 1000;
47
- if (!isExpired) {
48
- const crowdinToken = (0, util_1.decryptData)(config.clientSecret, credentials.accessToken);
49
- req.crowdinApiClient = new crowdin_api_client_1.default({
50
- token: crowdinToken,
51
- organization: req.crowdinContext.jwtPayload.domain ? credentials.id : undefined,
52
- });
53
- }
54
- else {
55
- const newCredentials = yield (0, crowdin_apps_functions_1.refreshOAuthToken)(config.clientId, config.clientSecret, (0, util_1.decryptData)(config.clientSecret, credentials.refreshToken));
56
- (0, storage_1.updateCrowdinCredentials)({
57
- id: req.crowdinContext.crowdinId,
58
- refreshToken: (0, util_1.encryptData)(config.clientSecret, newCredentials.refreshToken),
59
- accessToken: (0, util_1.encryptData)(config.clientSecret, newCredentials.accessToken),
60
- expire: (new Date().getTime() / 1000 + newCredentials.expiresIn).toString(),
61
- });
62
- req.crowdinApiClient = new crowdin_api_client_1.default({
63
- token: newCredentials.accessToken,
64
- organization: req.crowdinContext.jwtPayload.domain ? credentials.id : undefined,
65
- });
66
- }
42
+ req.crowdinApiClient = yield (0, util_1.prepareCrowdinClient)(config, credentials);
67
43
  next();
68
44
  }
69
45
  catch (e) {
@@ -8,16 +8,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
- const axios_1 = __importDefault(require("axios"));
16
12
  const storage_1 = require("../storage");
17
13
  const util_1 = require("../util");
18
14
  function handle(config, optional = false) {
19
15
  return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
20
- var _a, _b, _c, _d, _e, _f, _g, _h;
21
16
  const clientId = req.crowdinContext.clientId;
22
17
  const integrationCredentials = yield (0, storage_1.getIntegrationCredentials)(clientId);
23
18
  if (!integrationCredentials) {
@@ -29,35 +24,13 @@ function handle(config, optional = false) {
29
24
  if (integrationCredentials.config) {
30
25
  req.integrationSettings = JSON.parse(integrationCredentials.config);
31
26
  }
32
- const credentials = JSON.parse((0, util_1.decryptData)(config.clientSecret, integrationCredentials.credentials));
33
- if ((_a = config.oauthLogin) === null || _a === void 0 ? void 0 : _a.refresh) {
34
- const oauthLogin = config.oauthLogin;
35
- const { expireIn } = credentials;
36
- //2 min as an extra buffer
37
- const isExpired = expireIn + 120 < Date.now() / 1000;
38
- if (isExpired) {
39
- const url = oauthLogin.refreshTokenUrl || oauthLogin.accessTokenUrl;
40
- const request = {};
41
- request[((_b = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _b === void 0 ? void 0 : _b.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId;
42
- request[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret;
43
- request[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.refreshToken) || 'refresh_token'] = credentials.refreshToken;
44
- if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraRefreshTokenParameters) {
45
- Object.entries(oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraRefreshTokenParameters).forEach(([key, value]) => (request[key] = value));
46
- }
47
- const newCredentials = (yield axios_1.default.post(url || '', request, {
48
- headers: { Accept: 'application/json' },
49
- })).data;
50
- credentials.accessToken = newCredentials[((_e = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _e === void 0 ? void 0 : _e.accessToken) || 'access_token'];
51
- credentials.expireIn =
52
- Number(newCredentials[((_f = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _f === void 0 ? void 0 : _f.expiresIn) || 'expires_in']) + Date.now() / 1000;
53
- if (newCredentials[((_g = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _g === void 0 ? void 0 : _g.refreshToken) || 'refresh_token']) {
54
- credentials.refreshToken =
55
- newCredentials[((_h = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _h === void 0 ? void 0 : _h.refreshToken) || 'refresh_token'];
56
- }
57
- yield (0, storage_1.updateIntegrationCredentials)(req.crowdinContext.clientId, (0, util_1.encryptData)(config.clientSecret, JSON.stringify(credentials)));
58
- }
27
+ try {
28
+ req.integrationCredentials = yield (0, util_1.prepareIntegrationCredentials)(config, integrationCredentials);
29
+ }
30
+ catch (e) {
31
+ console.error(e);
32
+ throw new util_1.CodeError('Credentials to integration either exprired or invalid', 401);
59
33
  }
60
- req.integrationCredentials = credentials;
61
34
  next();
62
35
  }));
63
36
  }
@@ -91,11 +91,16 @@ export interface IntegrationLogic {
91
91
  title: string;
92
92
  content: string;
93
93
  };
94
+ /**
95
+ * background jobs that will be executed for each crowdin project and user
96
+ */
97
+ cronJobs?: CronJob[];
94
98
  }
95
99
  export interface ConfigurationField {
96
100
  key: string;
97
101
  label: string;
98
102
  type: 'text' | 'checkbox' | 'select';
103
+ helpText?: string;
99
104
  /**
100
105
  * only for select
101
106
  */
@@ -199,6 +204,14 @@ export interface OAuthLogin {
199
204
  extraRefreshTokenParameters?: {
200
205
  [key: string]: any;
201
206
  };
207
+ /**
208
+ * Override to implement request for retrieving access token (and refresh token if 'refresh' is enabled)
209
+ */
210
+ performGetTokenRequest?: (code: string) => Promise<any>;
211
+ /**
212
+ * Override to implement request for refreshing token (only if 'refresh' is enabled)
213
+ */
214
+ performRefreshTokenRequest?: (currentCredentials: any) => Promise<any>;
202
215
  }
203
216
  export interface FormField {
204
217
  key: string;
@@ -232,6 +245,11 @@ export interface CrowdinCredentials {
232
245
  accessToken: string;
233
246
  refreshToken: string;
234
247
  expire: string;
248
+ type: AccountType;
249
+ }
250
+ export declare enum AccountType {
251
+ NORMAL = "normal",
252
+ ENTERPRISE = "enterprise"
235
253
  }
236
254
  export interface CrowdinContextInfo {
237
255
  jwtPayload: JwtPayload;
@@ -253,3 +271,7 @@ export interface IntegrationFile {
253
271
  export interface UpdateIntegrationRequest {
254
272
  [fileId: string]: string[];
255
273
  }
274
+ export interface CronJob {
275
+ task: (projectId: number, client: Crowdin, apiCredentials: any, appRootFolder?: SourceFilesModel.Directory, config?: any) => Promise<void>;
276
+ expression: string;
277
+ }
@@ -1,2 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountType = void 0;
4
+ var AccountType;
5
+ (function (AccountType) {
6
+ AccountType["NORMAL"] = "normal";
7
+ AccountType["ENTERPRISE"] = "enterprise";
8
+ })(AccountType = exports.AccountType || (exports.AccountType = {}));
@@ -44,6 +44,10 @@ function showToast(message) {
44
44
  }
45
45
 
46
46
  function catchRejection(e, message) {
47
+ //session expired
48
+ if (e.code && e.code === 401) {
49
+ reloadLocation();
50
+ }
47
51
  showToast(e.message || message);
48
52
  }
49
53
 
@@ -3,9 +3,11 @@ export declare function connect(folder: string): Promise<void>;
3
3
  export declare function saveCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
4
4
  export declare function updateCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
5
5
  export declare function getCrowdinCredentials(id: string): Promise<CrowdinCredentials | undefined>;
6
+ export declare function getAllCrowdinCredentials(): Promise<CrowdinCredentials[]>;
6
7
  export declare function deleteCrowdinCredentials(id: string): Promise<void>;
7
8
  export declare function saveIntegrationCredentials(id: string, credentials: any, crowdinId: string): Promise<void>;
8
9
  export declare function updateIntegrationCredentials(id: string, credentials: any): Promise<void>;
9
10
  export declare function updateIntegrationConfig(id: string, config: any): Promise<void>;
10
11
  export declare function getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
12
+ export declare function getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
11
13
  export declare function deleteIntegrationCredentials(id: string): Promise<void>;
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  return (mod && mod.__esModule) ? mod : { "default": mod };
14
14
  };
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.deleteIntegrationCredentials = exports.getIntegrationCredentials = exports.updateIntegrationConfig = exports.updateIntegrationCredentials = exports.saveIntegrationCredentials = exports.deleteCrowdinCredentials = exports.getCrowdinCredentials = exports.updateCrowdinCredentials = exports.saveCrowdinCredentials = exports.connect = void 0;
16
+ exports.deleteIntegrationCredentials = exports.getAllIntegrationCredentials = exports.getIntegrationCredentials = exports.updateIntegrationConfig = exports.updateIntegrationCredentials = exports.saveIntegrationCredentials = exports.deleteCrowdinCredentials = exports.getAllCrowdinCredentials = exports.getCrowdinCredentials = exports.updateCrowdinCredentials = exports.saveCrowdinCredentials = exports.connect = void 0;
17
17
  const path_1 = require("path");
18
18
  const sqlite3_1 = __importDefault(require("sqlite3"));
19
19
  let _res;
@@ -61,7 +61,8 @@ function connect(folder) {
61
61
  id varchar not null primary key,
62
62
  access_token varchar not null,
63
63
  refresh_token varchar not null,
64
- expire varchar not null
64
+ expire varchar not null,
65
+ type varchar not null
65
66
  );
66
67
  `, []);
67
68
  yield _run(`
@@ -111,13 +112,24 @@ function get(query, params) {
111
112
  });
112
113
  });
113
114
  }
115
+ function each(query, params) {
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ yield dbPromise;
118
+ return new Promise((res, rej) => {
119
+ const result = [];
120
+ db.each(query, params, (err, row) => {
121
+ if (err) {
122
+ rej(err);
123
+ }
124
+ else {
125
+ result.push(row);
126
+ }
127
+ }, () => res(result));
128
+ });
129
+ });
130
+ }
114
131
  function saveCrowdinCredentials(credentials) {
115
- return run('INSERT INTO crowdin_credentials(id, access_token, refresh_token, expire) VALUES (?, ?, ?, ?)', [
116
- credentials.id,
117
- credentials.accessToken,
118
- credentials.refreshToken,
119
- credentials.expire,
120
- ]);
132
+ return run('INSERT INTO crowdin_credentials(id, access_token, refresh_token, expire, type) VALUES (?, ?, ?, ?, ?)', [credentials.id, credentials.accessToken, credentials.refreshToken, credentials.expire, credentials.type]);
121
133
  }
122
134
  exports.saveCrowdinCredentials = saveCrowdinCredentials;
123
135
  function updateCrowdinCredentials(credentials) {
@@ -131,13 +143,19 @@ function updateCrowdinCredentials(credentials) {
131
143
  exports.updateCrowdinCredentials = updateCrowdinCredentials;
132
144
  function getCrowdinCredentials(id) {
133
145
  return __awaiter(this, void 0, void 0, function* () {
134
- const row = yield get('SELECT id, access_token as accessToken, refresh_token as refreshToken, expire FROM crowdin_credentials WHERE id = ?', [id]);
146
+ const row = yield get('SELECT id, access_token as accessToken, refresh_token as refreshToken, expire, type FROM crowdin_credentials WHERE id = ?', [id]);
135
147
  if (row) {
136
148
  return row;
137
149
  }
138
150
  });
139
151
  }
140
152
  exports.getCrowdinCredentials = getCrowdinCredentials;
153
+ function getAllCrowdinCredentials() {
154
+ return __awaiter(this, void 0, void 0, function* () {
155
+ return each('SELECT id, access_token as accessToken, refresh_token as refreshToken, expire, type FROM crowdin_credentials', []);
156
+ });
157
+ }
158
+ exports.getAllCrowdinCredentials = getAllCrowdinCredentials;
141
159
  function deleteCrowdinCredentials(id) {
142
160
  return __awaiter(this, void 0, void 0, function* () {
143
161
  yield run('DELETE FROM crowdin_credentials where id = ?', [id]);
@@ -170,6 +188,12 @@ function getIntegrationCredentials(id) {
170
188
  });
171
189
  }
172
190
  exports.getIntegrationCredentials = getIntegrationCredentials;
191
+ function getAllIntegrationCredentials(crowdinId) {
192
+ return __awaiter(this, void 0, void 0, function* () {
193
+ return each('SELECT id, credentials, config, crowdin_id as crowdinId FROM integration_credentials WHERE crowdin_id = ?', [crowdinId]);
194
+ });
195
+ }
196
+ exports.getAllIntegrationCredentials = getAllIntegrationCredentials;
173
197
  function deleteIntegrationCredentials(id) {
174
198
  return run('DELETE FROM integration_credentials where id = ?', [id]);
175
199
  }
@@ -1,9 +1,16 @@
1
1
  import Crowdin, { SourceFilesModel } from '@crowdin/crowdin-api-client';
2
2
  import { Request, Response } from 'express';
3
- import { Config } from '../models';
3
+ import { Config, CronJob, CrowdinCredentials, IntegrationCredentials } from '../models';
4
+ export declare class CodeError extends Error {
5
+ code: number | undefined;
6
+ constructor(message: string, code?: number);
7
+ }
4
8
  export declare function runAsyncWrapper(callback: Function): (req: Request, res: Response, next: Function) => void;
5
9
  export declare function encryptData(secret: string, data: string): string;
6
10
  export declare function decryptData(secret: string, data: string): string;
7
11
  export declare function getOauthRoute(config: Config): string;
8
12
  export declare function getRootFolder(config: Config, client: Crowdin, projectId: number): Promise<SourceFilesModel.Directory | undefined>;
9
13
  export declare function applyDefaults(config: Config): void;
14
+ export declare function prepareCrowdinClient(config: Config, credentials: CrowdinCredentials): Promise<Crowdin>;
15
+ export declare function prepareIntegrationCredentials(config: Config, integrationCredentials: IntegrationCredentials): Promise<any>;
16
+ export declare function runJob(config: Config, job: CronJob): Promise<void>;
package/out/util/index.js CHANGED
@@ -27,13 +27,47 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
27
27
  step((generator = generator.apply(thisArg, _arguments || [])).next());
28
28
  });
29
29
  };
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
30
33
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.applyDefaults = exports.getRootFolder = exports.getOauthRoute = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = void 0;
34
+ exports.runJob = exports.prepareIntegrationCredentials = exports.prepareCrowdinClient = exports.applyDefaults = exports.getRootFolder = exports.getOauthRoute = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = exports.CodeError = void 0;
35
+ const crowdin_api_client_1 = __importDefault(require("@crowdin/crowdin-api-client"));
32
36
  const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
37
+ const axios_1 = __importDefault(require("axios"));
33
38
  const crypto = __importStar(require("crypto-js"));
39
+ const models_1 = require("../models");
40
+ const storage_1 = require("../storage");
41
+ class CodeError extends Error {
42
+ constructor(message, code) {
43
+ super(message);
44
+ this.code = code;
45
+ }
46
+ }
47
+ exports.CodeError = CodeError;
48
+ function isCrowdinClientRequest(req) {
49
+ return req.crowdinContext;
50
+ }
51
+ function handleError(err, req, res) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ console.error(err);
54
+ const code = err.code ? err.code : 500;
55
+ if (code === 401 && isCrowdinClientRequest(req)) {
56
+ yield (0, storage_1.deleteIntegrationCredentials)(req.crowdinContext.clientId);
57
+ }
58
+ if (code === 401 && req.path === '/') {
59
+ res.redirect('/');
60
+ return;
61
+ }
62
+ res.status(code).send({
63
+ message: err.message ? err.message : JSON.stringify(err),
64
+ code,
65
+ });
66
+ });
67
+ }
34
68
  function runAsyncWrapper(callback) {
35
69
  return (req, res, next) => {
36
- callback(req, res, next).catch((e) => res.status(500).send({ error: JSON.stringify(e) }));
70
+ callback(req, res, next).catch((e) => handleError(e, req, res));
37
71
  };
38
72
  }
39
73
  exports.runAsyncWrapper = runAsyncWrapper;
@@ -69,7 +103,11 @@ function applyDefaults(config) {
69
103
  if (rootFolder) {
70
104
  allDirectories = (yield client.sourceFilesApi
71
105
  .withFetchAll()
72
- .listProjectDirectories(projectId, undefined, rootFolder.id, undefined, undefined, undefined, 'true')).data.map(d => d.data);
106
+ // @ts-expect-error: Method overloading
107
+ .listProjectDirectories(projectId, {
108
+ directoryId: rootFolder.id,
109
+ recursion: 'true',
110
+ })).data.map(d => d.data);
73
111
  }
74
112
  else {
75
113
  allDirectories = (yield client.sourceFilesApi.withFetchAll().listProjectDirectories(projectId)).data.map(d => d.data);
@@ -113,3 +151,89 @@ function applyDefaults(config) {
113
151
  }
114
152
  }
115
153
  exports.applyDefaults = applyDefaults;
154
+ function prepareCrowdinClient(config, credentials) {
155
+ return __awaiter(this, void 0, void 0, function* () {
156
+ const isExpired = +credentials.expire < +new Date().getTime() / 1000;
157
+ if (!isExpired) {
158
+ const crowdinToken = decryptData(config.clientSecret, credentials.accessToken);
159
+ return new crowdin_api_client_1.default({
160
+ token: crowdinToken,
161
+ organization: credentials.type === models_1.AccountType.ENTERPRISE ? credentials.id : undefined,
162
+ });
163
+ }
164
+ else {
165
+ const newCredentials = yield crowdinAppFunctions.refreshOAuthToken(config.clientId, config.clientSecret, decryptData(config.clientSecret, credentials.refreshToken));
166
+ yield (0, storage_1.updateCrowdinCredentials)({
167
+ id: credentials.id,
168
+ refreshToken: encryptData(config.clientSecret, newCredentials.refreshToken),
169
+ accessToken: encryptData(config.clientSecret, newCredentials.accessToken),
170
+ expire: (new Date().getTime() / 1000 + newCredentials.expiresIn).toString(),
171
+ type: credentials.type,
172
+ });
173
+ return new crowdin_api_client_1.default({
174
+ token: newCredentials.accessToken,
175
+ organization: credentials.type === models_1.AccountType.ENTERPRISE ? credentials.id : undefined,
176
+ });
177
+ }
178
+ });
179
+ }
180
+ exports.prepareCrowdinClient = prepareCrowdinClient;
181
+ function prepareIntegrationCredentials(config, integrationCredentials) {
182
+ var _a, _b, _c, _d, _e, _f, _g, _h;
183
+ return __awaiter(this, void 0, void 0, function* () {
184
+ const credentials = JSON.parse(decryptData(config.clientSecret, integrationCredentials.credentials));
185
+ if ((_a = config.oauthLogin) === null || _a === void 0 ? void 0 : _a.refresh) {
186
+ const oauthLogin = config.oauthLogin;
187
+ const { expireIn } = credentials;
188
+ //2 min as an extra buffer
189
+ const isExpired = expireIn + 120 < Date.now() / 1000;
190
+ if (isExpired) {
191
+ let newCredentials;
192
+ if (oauthLogin.performRefreshTokenRequest) {
193
+ newCredentials = yield oauthLogin.performRefreshTokenRequest(credentials);
194
+ }
195
+ else {
196
+ const url = oauthLogin.refreshTokenUrl || oauthLogin.accessTokenUrl;
197
+ const request = {};
198
+ request[((_b = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _b === void 0 ? void 0 : _b.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId;
199
+ request[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret;
200
+ request[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.refreshToken) || 'refresh_token'] = credentials.refreshToken;
201
+ if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraRefreshTokenParameters) {
202
+ Object.entries(oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraRefreshTokenParameters).forEach(([key, value]) => (request[key] = value));
203
+ }
204
+ newCredentials = (yield axios_1.default.post(url || '', request, {
205
+ headers: { Accept: 'application/json' },
206
+ })).data;
207
+ }
208
+ credentials.accessToken = newCredentials[((_e = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _e === void 0 ? void 0 : _e.accessToken) || 'access_token'];
209
+ credentials.expireIn =
210
+ Number(newCredentials[((_f = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _f === void 0 ? void 0 : _f.expiresIn) || 'expires_in']) + Date.now() / 1000;
211
+ if (newCredentials[((_g = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _g === void 0 ? void 0 : _g.refreshToken) || 'refresh_token']) {
212
+ credentials.refreshToken = newCredentials[((_h = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _h === void 0 ? void 0 : _h.refreshToken) || 'refresh_token'];
213
+ }
214
+ yield (0, storage_1.updateIntegrationCredentials)(integrationCredentials.id, encryptData(config.clientSecret, JSON.stringify(credentials)));
215
+ }
216
+ }
217
+ return credentials;
218
+ });
219
+ }
220
+ exports.prepareIntegrationCredentials = prepareIntegrationCredentials;
221
+ function runJob(config, job) {
222
+ return __awaiter(this, void 0, void 0, function* () {
223
+ const crowdinCredentialsList = yield (0, storage_1.getAllCrowdinCredentials)();
224
+ yield Promise.all(crowdinCredentialsList.map((crowdinCredentials) => __awaiter(this, void 0, void 0, function* () {
225
+ const crowdinClient = yield prepareCrowdinClient(config, crowdinCredentials);
226
+ const integrationCredentialsList = yield (0, storage_1.getAllIntegrationCredentials)(crowdinCredentials.id);
227
+ yield Promise.all(integrationCredentialsList.map((integrationCredentials) => __awaiter(this, void 0, void 0, function* () {
228
+ const projectId = crowdinAppFunctions.getProjectId(integrationCredentials.id);
229
+ const apiCredentials = yield prepareIntegrationCredentials(config, integrationCredentials);
230
+ const rootFolder = yield getRootFolder(config, crowdinClient, projectId);
231
+ const intConfig = integrationCredentials.config
232
+ ? JSON.parse(integrationCredentials.config)
233
+ : undefined;
234
+ yield job.task(projectId, crowdinClient, apiCredentials, rootFolder, intConfig);
235
+ })));
236
+ })));
237
+ });
238
+ }
239
+ exports.runJob = runJob;
@@ -5,13 +5,13 @@
5
5
  <body>
6
6
  <div class="i_w">
7
7
  <div class='top'>
8
+ {{#if infoModal}}
9
+ <crowdin-button icon-before="info" onclick="infoModal.open();">{{infoModal.title}}</crowdin-button>
10
+ {{/if}}
8
11
  {{#if configurationFields}}
9
12
  <crowdin-button icon-before="settings" onclick="settingsModal.open();fillSettingsForm();">Settings</crowdin-button>
10
13
  {{/if}}
11
14
  <crowdin-button icon-before="account_circle" onclick="integrationLogout()">Log out</crowdin-button>
12
- {{#if infoModal}}
13
- <crowdin-button icon-before="info" onclick="infoModal.open();">{{infoModal.title}}</crowdin-button>
14
- {{/if}}
15
15
  </div>
16
16
  <crowdin-simple-integration integration-name="{{name}}" integration-logo="logo.png">
17
17
  </crowdin-simple-integration>
@@ -35,6 +35,9 @@
35
35
  value="false"
36
36
  id="{{key}}-settings"
37
37
  key="{{key}}"
38
+ {{#if helpText}}
39
+ help-text="{{helpText}}"
40
+ {{/if}}
38
41
  >
39
42
  </crowdin-checkbox>
40
43
  {{/ifeq}}
@@ -47,6 +50,9 @@
47
50
  id="{{key}}-settings"
48
51
  key="{{key}}"
49
52
  label="{{label}}"
53
+ {{#if helpText}}
54
+ help-text="{{helpText}}"
55
+ {{/if}}
50
56
  >
51
57
  {{#each options}}
52
58
  <option value="{{value}}">{{label}}</option>
@@ -60,6 +66,9 @@
60
66
  value=""
61
67
  id="{{key}}-settings"
62
68
  key="{{key}}"
69
+ {{#if helpText}}
70
+ help-text="{{helpText}}"
71
+ {{/if}}
63
72
  >
64
73
  </crowdin-input>
65
74
  {{/ifeq}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.4.0",
3
+ "version": "0.6.2",
4
4
  "description": "Module that generates for you all common endpoints for serving standalone Crowdin App",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
@@ -12,10 +12,11 @@
12
12
  "test": "echo \"test not implemented\""
13
13
  },
14
14
  "dependencies": {
15
- "express": "4.17.1",
16
- "@crowdin/crowdin-apps-functions": "0.0.2",
15
+ "@crowdin/crowdin-apps-functions": "0.0.3",
17
16
  "crypto-js": "^4.0.0",
17
+ "express": "4.17.1",
18
18
  "express-handlebars": "^5.3.4",
19
+ "node-cron": "^3.0.0",
19
20
  "sqlite3": "^5.0.2"
20
21
  },
21
22
  "devDependencies": {
@@ -24,6 +25,7 @@
24
25
  "@types/express-handlebars": "^5.3.1",
25
26
  "@types/node": "^12.0.10",
26
27
  "@types/sqlite3": "^3.1.7",
28
+ "@types/node-cron": "^3.0.0",
27
29
  "@typescript-eslint/eslint-plugin": "^2.3.1",
28
30
  "@typescript-eslint/parser": "^2.3.1",
29
31
  "eslint": "^6.4.0",