@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 +104 -1
- package/out/handlers/install.js +2 -0
- package/out/handlers/oauth-login.js +17 -10
- package/out/index.js +6 -0
- package/out/middlewares/crowdin-client.js +1 -25
- package/out/middlewares/integration-credentials.js +6 -33
- package/out/models/index.d.ts +22 -0
- package/out/models/index.js +6 -0
- package/out/static/js/main.js +4 -0
- package/out/storage/index.d.ts +2 -0
- package/out/storage/index.js +33 -9
- package/out/util/index.d.ts +8 -1
- package/out/util/index.js +127 -3
- package/out/views/main.handlebars +12 -3
- package/package.json +5 -3
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.
|
package/out/handlers/install.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
}
|
package/out/models/index.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/out/models/index.js
CHANGED
|
@@ -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 = {}));
|
package/out/static/js/main.js
CHANGED
package/out/storage/index.d.ts
CHANGED
|
@@ -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>;
|
package/out/storage/index.js
CHANGED
|
@@ -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
|
}
|
package/out/util/index.d.ts
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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",
|