@crowdin/app-project-module 3.0.1 → 3.2.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.
- package/out/middlewares/crowdin-client.d.ts +4 -2
- package/out/middlewares/crowdin-client.js +51 -14
- package/out/middlewares/integration-credentials.js +34 -12
- package/out/modules/integration/handlers/crowdin-files.js +34 -1
- package/out/modules/integration/handlers/user-error-detail.d.ts +2 -0
- package/out/modules/integration/handlers/user-error-detail.js +48 -0
- package/out/modules/integration/handlers/user-errors.d.ts +6 -0
- package/out/modules/integration/handlers/user-errors.js +39 -17
- package/out/modules/integration/index.js +8 -0
- package/out/modules/integration/types.d.ts +1 -0
- package/out/static/ui/main.bundle.js +116 -55
- package/out/static/ui/main.bundle.js.map +1 -1
- package/out/storage/drizzle/d1/index.js +2 -1
- package/out/storage/drizzle/d1/migrations/user-errors-indexes.d.ts +7 -0
- package/out/storage/drizzle/d1/migrations/user-errors-indexes.js +27 -0
- package/out/storage/drizzle/index.d.ts +8 -1
- package/out/storage/drizzle/index.js +75 -4
- package/out/storage/drizzle/postgresql/migrations/0003_user_errors_pagination_index.sql +5 -0
- package/out/storage/drizzle/postgresql/migrations/meta/0003_snapshot.json +646 -0
- package/out/storage/drizzle/postgresql/migrations/meta/_journal.json +7 -0
- package/out/storage/drizzle/sqlite/migrations/0002_user_errors_indexes.sql +4 -0
- package/out/storage/drizzle/sqlite/migrations/meta/0002_snapshot.json +687 -0
- package/out/storage/drizzle/sqlite/migrations/meta/_journal.json +7 -0
- package/out/storage/types.d.ts +30 -0
- package/out/types.d.ts +1 -0
- package/out/util/connection.d.ts +5 -0
- package/out/util/connection.js +7 -0
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import Crowdin from '@crowdin/crowdin-api-client';
|
|
1
|
+
import type Crowdin from '@crowdin/crowdin-api-client';
|
|
2
2
|
import { Response } from 'express';
|
|
3
3
|
import { Config, CrowdinClientRequest, CrowdinContextInfo, SubscriptionInfo } from '../types';
|
|
4
4
|
import { LogErrorFunction, LogFunction } from '../util/logger';
|
|
5
|
-
export declare function prepareCrowdinRequest({ jwtToken, config, optional, checkSubscriptionExpiration, moduleKey, }: {
|
|
5
|
+
export declare function prepareCrowdinRequest({ jwtToken, config, optional, checkSubscriptionExpiration, moduleKey, crowdinToken, projectId, }: {
|
|
6
6
|
jwtToken: string;
|
|
7
7
|
config: Config;
|
|
8
8
|
optional: boolean;
|
|
9
9
|
checkSubscriptionExpiration: boolean;
|
|
10
10
|
moduleKey: string[] | string | undefined;
|
|
11
|
+
crowdinToken?: string;
|
|
12
|
+
projectId?: number;
|
|
11
13
|
}): Promise<{
|
|
12
14
|
context: CrowdinContextInfo;
|
|
13
15
|
logInfo: LogFunction;
|
|
@@ -20,8 +20,17 @@ const logger_1 = require("../util/logger");
|
|
|
20
20
|
const subscription_1 = require("../util/subscription");
|
|
21
21
|
const integration_access_denied_1 = require("./integration-access-denied");
|
|
22
22
|
const token_1 = require("../util/app-functions/token");
|
|
23
|
+
function assertModuleKey(moduleKey, jwtModule) {
|
|
24
|
+
if (!moduleKey || !jwtModule) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const matches = typeof moduleKey === 'string' ? moduleKey === jwtModule : moduleKey.includes(jwtModule);
|
|
28
|
+
if (!matches) {
|
|
29
|
+
throw new Error("Module key doesn't match");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
23
32
|
function prepareCrowdinRequest(_a) {
|
|
24
|
-
return __awaiter(this, arguments, void 0, function* ({ jwtToken, config, optional = false, checkSubscriptionExpiration = true, moduleKey, }) {
|
|
33
|
+
return __awaiter(this, arguments, void 0, function* ({ jwtToken, config, optional = false, checkSubscriptionExpiration = true, moduleKey, crowdinToken, projectId, }) {
|
|
25
34
|
(0, logger_1.log)('Validating jwt token from incoming request');
|
|
26
35
|
const jwtPayload = yield (0, token_1.validateJwtToken)(jwtToken, config.clientSecret, config.jwtValidationOptions);
|
|
27
36
|
const context = {
|
|
@@ -32,6 +41,29 @@ function prepareCrowdinRequest(_a) {
|
|
|
32
41
|
};
|
|
33
42
|
const logInfo = (0, logger_1.withContext)(context);
|
|
34
43
|
const logError = (0, logger_1.withContextError)(context);
|
|
44
|
+
if (crowdinToken) {
|
|
45
|
+
logInfo('Using caller-supplied Crowdin token');
|
|
46
|
+
assertModuleKey(moduleKey, jwtPayload.module);
|
|
47
|
+
const client = yield (0, connection_1.buildCrowdinClientFromPat)({
|
|
48
|
+
config,
|
|
49
|
+
token: crowdinToken,
|
|
50
|
+
// For enterprise the org-specific URL implicitly restricts the token to this org.
|
|
51
|
+
// For Crowdin.com (no domain) we fall back to project-level access validation below.
|
|
52
|
+
organization: jwtPayload.domain || undefined,
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
yield client.usersApi.getAuthenticatedUser();
|
|
56
|
+
// For non-enterprise (no domain) there is no org-scoped URL, so validate by
|
|
57
|
+
// checking that the PAT has access to the project named in the JWT / request.
|
|
58
|
+
if (!jwtPayload.domain && projectId) {
|
|
59
|
+
yield client.projectsGroupsApi.getProject(projectId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (_b) {
|
|
63
|
+
throw new util_1.CodeError('Supplied crowdinToken is invalid or does not belong to this organization', 403);
|
|
64
|
+
}
|
|
65
|
+
return { context, client, logInfo, logError };
|
|
66
|
+
}
|
|
35
67
|
logInfo('Loading crowdin credentials');
|
|
36
68
|
const credentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(context.crowdinId);
|
|
37
69
|
if (!credentials) {
|
|
@@ -40,14 +72,7 @@ function prepareCrowdinRequest(_a) {
|
|
|
40
72
|
}
|
|
41
73
|
throw new Error("Can't find organization by id");
|
|
42
74
|
}
|
|
43
|
-
|
|
44
|
-
if (typeof moduleKey === 'string' && moduleKey !== jwtPayload.module) {
|
|
45
|
-
throw new Error("Module key doesn't match");
|
|
46
|
-
}
|
|
47
|
-
if (Array.isArray(moduleKey) && !moduleKey.includes(jwtPayload.module)) {
|
|
48
|
-
throw new Error("Module key doesn't match");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
75
|
+
assertModuleKey(moduleKey, jwtPayload.module);
|
|
51
76
|
logInfo('Building crowdin client instance');
|
|
52
77
|
const { client, token } = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew: true, context });
|
|
53
78
|
let subscriptionInfo;
|
|
@@ -67,22 +92,25 @@ function prepareCrowdinRequest(_a) {
|
|
|
67
92
|
}
|
|
68
93
|
function handle({ config, optional = false, checkSubscriptionExpiration = true, moduleKey, isIntegration = false, isLogout = false, }) {
|
|
69
94
|
return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
70
|
-
var _a, _b;
|
|
95
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
71
96
|
const jwtToken = getToken(req);
|
|
72
97
|
if (!jwtToken) {
|
|
73
98
|
(0, logger_1.temporaryErrorDebug)('Access denied: crowdin-client', req);
|
|
74
99
|
return res.status(403).send({ error: 'Access denied' });
|
|
75
100
|
}
|
|
76
101
|
try {
|
|
102
|
+
const isAgentCall = req.isApiCall && !!((_a = req.body) === null || _a === void 0 ? void 0 : _a.crowdinToken);
|
|
77
103
|
const data = yield prepareCrowdinRequest({
|
|
78
104
|
jwtToken,
|
|
79
105
|
config,
|
|
80
106
|
optional,
|
|
81
107
|
checkSubscriptionExpiration,
|
|
82
108
|
moduleKey,
|
|
109
|
+
crowdinToken: isAgentCall ? req.body.crowdinToken : undefined,
|
|
110
|
+
projectId: isAgentCall ? (_b = req.body) === null || _b === void 0 ? void 0 : _b.projectId : undefined,
|
|
83
111
|
});
|
|
84
|
-
if ((
|
|
85
|
-
if (req.isApiCall && !((
|
|
112
|
+
if ((_c = config.api) === null || _c === void 0 ? void 0 : _c.default) {
|
|
113
|
+
if (req.isApiCall && !((_d = req.body) === null || _d === void 0 ? void 0 : _d.projectId)) {
|
|
86
114
|
return res.status(400).json({
|
|
87
115
|
error: {
|
|
88
116
|
message: 'Missing required parameter: projectId',
|
|
@@ -91,7 +119,10 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
|
|
|
91
119
|
}
|
|
92
120
|
data.context = (0, api_1.updateCrowdinContext)(req, data.context);
|
|
93
121
|
}
|
|
94
|
-
|
|
122
|
+
// Skip integration access check when the caller supplies their own credentials —
|
|
123
|
+
// integration-credentials middleware handles those via the injected path.
|
|
124
|
+
const hasInjectedIntegrationCreds = req.isApiCall && !!((_e = req.body) === null || _e === void 0 ? void 0 : _e.integrationCredentials);
|
|
125
|
+
if (isIntegration && data.client && !hasInjectedIntegrationCreds) {
|
|
95
126
|
const result = yield (0, connection_1.resolveIntegrationAccess)(data.context.clientId);
|
|
96
127
|
if (!result.integrationCredentials && !isLogout) {
|
|
97
128
|
const owners = yield (0, integration_access_denied_1.getIntegrationManagedBy)({
|
|
@@ -121,12 +152,18 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
|
|
|
121
152
|
req.subscriptionInfo = data.subscriptionInfo;
|
|
122
153
|
req.logInfo = data.logInfo;
|
|
123
154
|
req.logError = data.logError;
|
|
155
|
+
// Strip secrets from req.body so no downstream handler ever sees raw tokens.
|
|
156
|
+
// crowdinToken has been consumed; move integrationCredentials to a typed slot
|
|
157
|
+
// so the integration-credentials middleware can still pick it up.
|
|
158
|
+
req.injectedIntegrationCredentials = (_f = req.body) === null || _f === void 0 ? void 0 : _f.integrationCredentials;
|
|
159
|
+
(_g = req.body) === null || _g === void 0 ? true : delete _g.crowdinToken;
|
|
160
|
+
(_h = req.body) === null || _h === void 0 ? true : delete _h.integrationCredentials;
|
|
124
161
|
next();
|
|
125
162
|
}
|
|
126
163
|
catch (e) {
|
|
127
164
|
const errorMessage = (0, logger_1.getErrorMessage)(e);
|
|
128
165
|
if (e instanceof util_1.CodeError) {
|
|
129
|
-
return res.status(402).send({ message: errorMessage || 'Error', code: e.code });
|
|
166
|
+
return res.status((_j = e.code) !== null && _j !== void 0 ? _j : 402).send({ message: errorMessage || 'Error', code: e.code });
|
|
130
167
|
}
|
|
131
168
|
(0, logger_1.logError)(e);
|
|
132
169
|
return res.status(403).send({ error: errorMessage || 'Error' });
|
|
@@ -15,9 +15,42 @@ const storage_1 = require("../storage");
|
|
|
15
15
|
const util_1 = require("../util");
|
|
16
16
|
const connection_1 = require("../util/connection");
|
|
17
17
|
const integration_access_denied_1 = require("./integration-access-denied");
|
|
18
|
+
function loadIntegrationSettings(clientId, integration, req) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
|
|
21
|
+
let settings = (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) ? JSON.parse(integrationConfig.config) || {} : {};
|
|
22
|
+
if (integration.normalizeSettings) {
|
|
23
|
+
settings = yield integration.normalizeSettings({
|
|
24
|
+
settings,
|
|
25
|
+
credentials: req.integrationCredentials,
|
|
26
|
+
client: req.crowdinApiClient,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return settings;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
18
32
|
function handle({ config, integration, optional = false, isLogout = false, }) {
|
|
19
33
|
return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
20
34
|
const isApiCall = req === null || req === void 0 ? void 0 : req.isApiCall;
|
|
35
|
+
// crowdin-client middleware moves integrationCredentials out of req.body before next(),
|
|
36
|
+
// so secrets are never visible to handlers. Read from the typed slot it left behind.
|
|
37
|
+
const rawInjected = req.injectedIntegrationCredentials;
|
|
38
|
+
if (isApiCall && rawInjected) {
|
|
39
|
+
delete req.injectedIntegrationCredentials;
|
|
40
|
+
req.logInfo('Using caller-supplied integration credentials');
|
|
41
|
+
req.integrationCredentials = typeof rawInjected === 'string' ? { accessToken: rawInjected } : rawInjected;
|
|
42
|
+
if (integration.checkConnection) {
|
|
43
|
+
try {
|
|
44
|
+
yield integration.checkConnection(req.integrationCredentials);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return res.status(401).json({ error: { message: 'Supplied integrationCredentials are invalid' } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const clientId = req.crowdinContext.clientId;
|
|
51
|
+
req.integrationSettings = yield loadIntegrationSettings(clientId, integration, req);
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
21
54
|
req.logInfo(`Loading integration credentials for client ${req.crowdinContext.clientId}`);
|
|
22
55
|
const { integrationCredentials: resolvedIntegrationCredentials, projectIntegrationCredentials, projectId, clientId: resolvedClientId, ownerIds, } = yield (0, connection_1.resolveIntegrationAccess)(req.crowdinContext.clientId);
|
|
23
56
|
let integrationCredentials = resolvedIntegrationCredentials;
|
|
@@ -79,18 +112,7 @@ function handle({ config, integration, optional = false, isLogout = false, }) {
|
|
|
79
112
|
throw new util_1.CodeError(message, 401);
|
|
80
113
|
}
|
|
81
114
|
}
|
|
82
|
-
|
|
83
|
-
if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) {
|
|
84
|
-
let integrationSettings = JSON.parse(integrationConfig.config) || {};
|
|
85
|
-
if (integration.normalizeSettings) {
|
|
86
|
-
integrationSettings = yield integration.normalizeSettings({
|
|
87
|
-
settings: integrationSettings,
|
|
88
|
-
credentials: req.integrationCredentials,
|
|
89
|
-
client: req.crowdinApiClient,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
req.integrationSettings = integrationSettings;
|
|
93
|
-
}
|
|
115
|
+
req.integrationSettings = yield loadIntegrationSettings(clientId, integration, req);
|
|
94
116
|
next();
|
|
95
117
|
}));
|
|
96
118
|
}
|
|
@@ -13,11 +13,24 @@ exports.default = handle;
|
|
|
13
13
|
const util_1 = require("../../../util");
|
|
14
14
|
const defaults_1 = require("../util/defaults");
|
|
15
15
|
const logger_1 = require("../../../util/logger");
|
|
16
|
+
const types_1 = require("../types");
|
|
16
17
|
const files_1 = require("../util/files");
|
|
18
|
+
const storage_1 = require("../../../storage");
|
|
17
19
|
function handle(config, integration) {
|
|
18
20
|
return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
19
|
-
var _a;
|
|
21
|
+
var _a, _b;
|
|
20
22
|
const mode = (_a = req.query) === null || _a === void 0 ? void 0 : _a.mode;
|
|
23
|
+
const externalId = (_b = req.query) === null || _b === void 0 ? void 0 : _b.externalId;
|
|
24
|
+
// Reverse lookup: given an integration file ID, return the mapped Crowdin file entry.
|
|
25
|
+
if (externalId) {
|
|
26
|
+
const syncedData = yield (0, storage_1.getStorage)().getSyncedData(req.crowdinContext.clientId, req.crowdinContext.crowdinId, types_1.Provider.INTEGRATION);
|
|
27
|
+
const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
|
|
28
|
+
const entry = syncedFiles.find((f) => f.id === externalId);
|
|
29
|
+
if (!(entry === null || entry === void 0 ? void 0 : entry.crowdinFileId)) {
|
|
30
|
+
return res.status(404).json({ error: { message: 'No Crowdin file mapped to this external ID' } });
|
|
31
|
+
}
|
|
32
|
+
return res.json({ id: String(entry.crowdinFileId), externalFileId: externalId });
|
|
33
|
+
}
|
|
21
34
|
req.logInfo(`Loading crowdin files${mode ? ` (mode: ${mode})` : ''}`);
|
|
22
35
|
if (integration.getCrowdinFiles) {
|
|
23
36
|
const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, req.crowdinContext.jwtPayload.context.project_id);
|
|
@@ -41,6 +54,26 @@ function handle(config, integration) {
|
|
|
41
54
|
files,
|
|
42
55
|
});
|
|
43
56
|
}
|
|
57
|
+
// Path B: populate externalFileId from stored integrationFileId→crowdinFileId mapping
|
|
58
|
+
// for files where the app developer hasn't supplied it directly (Path A).
|
|
59
|
+
// A leaf is either a typed file or explicitly flagged with nodeType '1';
|
|
60
|
+
// folders (nodeType '0') and branches (nodeType '2') without a type are skipped.
|
|
61
|
+
const isFileLeaf = (f) => f.type !== undefined || f.nodeType === '1';
|
|
62
|
+
const needsMapping = files.some((f) => isFileLeaf(f) && !f.externalFileId);
|
|
63
|
+
if (needsMapping) {
|
|
64
|
+
const syncedData = yield (0, storage_1.getStorage)().getSyncedData(req.crowdinContext.clientId, req.crowdinContext.crowdinId, types_1.Provider.INTEGRATION);
|
|
65
|
+
if (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) {
|
|
66
|
+
const syncedFiles = JSON.parse(syncedData.files);
|
|
67
|
+
const byCrowdinFileId = new Map(syncedFiles
|
|
68
|
+
.filter((e) => e.crowdinFileId !== null && e.crowdinFileId !== undefined)
|
|
69
|
+
.map((e) => [String(e.crowdinFileId), e.id]));
|
|
70
|
+
if (byCrowdinFileId.size > 0) {
|
|
71
|
+
files = files.map((f) => !isFileLeaf(f) || f.externalFileId || !byCrowdinFileId.has(f.id)
|
|
72
|
+
? f
|
|
73
|
+
: Object.assign(Object.assign({}, f), { externalFileId: byCrowdinFileId.get(f.id) }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
44
77
|
req.logInfo(`Returning ${files.length} items`);
|
|
45
78
|
res.send(files);
|
|
46
79
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.default = handle;
|
|
13
|
+
const storage_1 = require("../../../storage");
|
|
14
|
+
const util_1 = require("../../../util");
|
|
15
|
+
const logger_1 = require("../../../util/logger");
|
|
16
|
+
const INVALID_DATA = JSON.stringify({
|
|
17
|
+
appData: { Warning: 'Invalid response data. Please contact the Crowdin support manager.' },
|
|
18
|
+
});
|
|
19
|
+
function handle() {
|
|
20
|
+
return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
const id = Number.parseInt(`${req.params.id}`, 10);
|
|
22
|
+
if (!Number.isInteger(id)) {
|
|
23
|
+
return res.status(400).json({
|
|
24
|
+
error: {
|
|
25
|
+
message: 'Invalid error log id',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const userError = yield (0, storage_1.getStorage)().getUserError({
|
|
30
|
+
id,
|
|
31
|
+
crowdinId: req.crowdinContext.crowdinId,
|
|
32
|
+
integrationId: req.crowdinContext.clientId,
|
|
33
|
+
});
|
|
34
|
+
if (!userError) {
|
|
35
|
+
req.logInfo(`Error log ${id} not found`);
|
|
36
|
+
return res.status(404).json({
|
|
37
|
+
error: {
|
|
38
|
+
message: `Error log with ID ${id} not found`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!userError.data || !(0, util_1.isJson)(userError.data)) {
|
|
43
|
+
(0, logger_1.logError)(userError);
|
|
44
|
+
return res.send({ data: INVALID_DATA });
|
|
45
|
+
}
|
|
46
|
+
res.send({ data: userError.data });
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import { Response } from 'express';
|
|
2
|
+
export declare function parseUserErrorsQuery(query?: any): {
|
|
3
|
+
limit: number;
|
|
4
|
+
offset: number;
|
|
5
|
+
orderDirection: "asc" | "desc";
|
|
6
|
+
search: any;
|
|
7
|
+
};
|
|
2
8
|
export default function handle(): (req: import("express").Request | import("../../..").CrowdinClientRequest, res: Response, next: Function) => void;
|
|
@@ -9,27 +9,49 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseUserErrorsQuery = parseUserErrorsQuery;
|
|
12
13
|
exports.default = handle;
|
|
13
14
|
const storage_1 = require("../../../storage");
|
|
14
15
|
const util_1 = require("../../../util");
|
|
15
|
-
const
|
|
16
|
+
const DEFAULT_LIMIT = 10;
|
|
17
|
+
const MAX_LIMIT = 100;
|
|
18
|
+
const MAX_SEARCH_LENGTH = 255;
|
|
19
|
+
function parseInteger(value, fallback) {
|
|
20
|
+
const parsed = Number.parseInt(`${value}`, 10);
|
|
21
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
22
|
+
}
|
|
23
|
+
function parseUserErrorsQuery(query = {}) {
|
|
24
|
+
const search = typeof query.search === 'string' ? query.search.trim().slice(0, MAX_SEARCH_LENGTH) : '';
|
|
25
|
+
return {
|
|
26
|
+
limit: Math.min(Math.max(parseInteger(query.limit, DEFAULT_LIMIT), 1), MAX_LIMIT),
|
|
27
|
+
offset: Math.max(parseInteger(query.offset, 0), 0),
|
|
28
|
+
orderDirection: (query.order === 'asc' ? 'asc' : 'desc'),
|
|
29
|
+
search: search || undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
16
32
|
function handle() {
|
|
17
33
|
return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const { limit, offset, orderDirection, search } = parseUserErrorsQuery(req.query);
|
|
35
|
+
const filter = {
|
|
36
|
+
crowdinId: req.crowdinContext.crowdinId,
|
|
37
|
+
integrationId: req.crowdinContext.clientId,
|
|
38
|
+
search,
|
|
39
|
+
};
|
|
40
|
+
req.logInfo(`Loading user errors for crowdinId ${req.crowdinContext.crowdinId} and integrationId ${req.crowdinContext.clientId} (limit ${limit}, offset ${offset})`);
|
|
41
|
+
const [userTimezone, userErrors, total] = yield Promise.all([
|
|
42
|
+
req.crowdinApiClient.usersApi
|
|
43
|
+
.getAuthenticatedUser()
|
|
44
|
+
.then(({ data }) => data.timezone)
|
|
45
|
+
.catch((e) => {
|
|
46
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
47
|
+
req.logError(`Can't load user timezone, falling back to server timezone: ${errorMessage}`);
|
|
48
|
+
return undefined;
|
|
49
|
+
}),
|
|
50
|
+
(0, storage_1.getStorage)().getUserErrors(Object.assign(Object.assign({}, filter), { limit, offset, orderDirection })),
|
|
51
|
+
(0, storage_1.getStorage)().countUserErrors(filter),
|
|
52
|
+
]);
|
|
53
|
+
const data = userErrors.map((userError) => (Object.assign(Object.assign({}, userError), { formattedDate: (0, util_1.getFormattedDate)({ date: new Date(+userError.createdAt), userTimezone }) })));
|
|
54
|
+
req.logInfo(`Returning ${data.length} of ${total} user errors`);
|
|
55
|
+
res.send({ data, total });
|
|
34
56
|
}));
|
|
35
57
|
}
|
|
@@ -43,6 +43,7 @@ const settings_save_1 = __importDefault(require("./handlers/settings-save"));
|
|
|
43
43
|
const subscription_info_1 = __importDefault(require("./handlers/subscription-info"));
|
|
44
44
|
const sync_settings_1 = __importDefault(require("./handlers/sync-settings"));
|
|
45
45
|
const sync_settings_save_1 = __importDefault(require("./handlers/sync-settings-save"));
|
|
46
|
+
const user_error_detail_1 = __importDefault(require("./handlers/user-error-detail"));
|
|
46
47
|
const user_errors_1 = __importDefault(require("./handlers/user-errors"));
|
|
47
48
|
const users_1 = __importDefault(require("./handlers/users"));
|
|
48
49
|
const invite_users_1 = __importDefault(require("./handlers/invite-users"));
|
|
@@ -240,6 +241,13 @@ function register({ config, app }) {
|
|
|
240
241
|
moduleKey: integrationLogic.key,
|
|
241
242
|
isIntegration: true,
|
|
242
243
|
}), (0, integration_credentials_1.default)({ config, integration: integrationLogic }), (0, user_errors_1.default)());
|
|
244
|
+
app.get('/api/user-errors/:id', json_response_1.handle, (0, crowdin_client_1.handle)({
|
|
245
|
+
config,
|
|
246
|
+
optional: false,
|
|
247
|
+
checkSubscriptionExpiration: true,
|
|
248
|
+
moduleKey: integrationLogic.key,
|
|
249
|
+
isIntegration: true,
|
|
250
|
+
}), (0, integration_credentials_1.default)({ config, integration: integrationLogic }), (0, user_error_detail_1.default)());
|
|
243
251
|
app.get('/api/users', json_response_1.handle, (0, crowdin_client_1.handle)({
|
|
244
252
|
config,
|
|
245
253
|
optional: false,
|