@localazy/strapi-plugin 0.1.12 → 0.1.13-rc.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.
Files changed (29) hide show
  1. package/admin/src/index.js +26 -1
  2. package/admin/src/modules/@common/components/LanguagesSelector/index.js +69 -0
  3. package/admin/src/modules/@common/locale/en.js +1 -0
  4. package/admin/src/modules/localazy-download/locale/en.js +3 -0
  5. package/admin/src/modules/plugin-settings/components/ContentTransferSetup/index.js +1 -2
  6. package/admin/src/modules/plugin-settings/components/GlobalSettings/index.js +269 -0
  7. package/admin/src/modules/plugin-settings/locale/en.js +38 -3
  8. package/admin/src/modules/plugin-settings/services/strapi-users-service.js +26 -0
  9. package/admin/src/pages/Download/index.js +49 -2
  10. package/admin/src/pages/PluginSettings/index.js +27 -11
  11. package/admin/src/utils/createAxiosInstance.js +2 -0
  12. package/package.json +2 -2
  13. package/server/bootstrap.js +50 -9
  14. package/server/controllers/localazy-transfer-controller.js +42 -4
  15. package/server/controllers/strapi-controller.js +3 -0
  16. package/server/lifecycles/deep-populate-hook.js +11 -0
  17. package/server/lifecycles/deprecate-event-entry-in-localazy-hook.js +69 -0
  18. package/server/lifecycles/upload-event-entry-to-localazy-hook.js +31 -0
  19. package/server/middlewares/index.js +5 -1
  20. package/server/middlewares/verify-webhook.js +77 -0
  21. package/server/routes/index.js +2 -0
  22. package/server/routes/localazy-public-transfer.js +20 -0
  23. package/server/routes/strapi.js +8 -0
  24. package/server/services/helpers/plugin-settings-service-helper.js +52 -0
  25. package/server/services/localazy-pubapi-service.js +6 -0
  26. package/server/services/plugin-settings-service.js +7 -1
  27. package/server/services/strapi-i18n-service.js +1 -1
  28. package/server/utils/get-picked-flatten-keys-for-hook-entry.js +270 -0
  29. package/server/utils/request-initiator-helper.js +36 -0
@@ -1,19 +1,60 @@
1
1
  "use strict";
2
2
 
3
- const getFullPopulateObject = require('./utils/get-full-populate-object')
3
+ const deepPopulateHook = require('./lifecycles/deep-populate-hook');
4
+ const uploadEventEntryToLocalazyHook = require('./lifecycles/upload-event-entry-to-localazy-hook');
5
+ const deprecateEventEntryInLocalazyHook = require('./lifecycles/deprecate-event-entry-in-localazy-hook');
6
+ const RequestInitiatorHelper = require('./utils/request-initiator-helper');
4
7
 
5
8
  module.exports = ({ strapi }) => {
6
9
  // bootstrap phase
7
- // Subscribe to the lifecycles that we are intrested in.
8
- strapi.db.lifecycles.subscribe((event) => {
9
- if (event.action === 'beforeFindMany' || event.action === 'beforeFindOne') {
10
- const populate = event.params?.populate;
10
+ // Subscribe to the lifecycles that we are interested in.
11
+ strapi.db.lifecycles.subscribe(async (event) => {
12
+ const requestInitiatorHelper = new RequestInitiatorHelper(strapi);
13
+ const action = event.action;
14
+ switch (action) {
15
+ case 'beforeFindMany':
16
+ case 'beforeFindOne': {
17
+ deepPopulateHook(event);
18
+ break;
19
+ }
20
+ case 'afterCreate':
21
+ case 'afterUpdate': {
22
+ try {
23
+ if (requestInitiatorHelper.isInitiatedByLocalazyWebhook() || requestInitiatorHelper.isInitiatedByLocalazyPluginUI()) {
24
+ break;
25
+ }
26
+
27
+ uploadEventEntryToLocalazyHook(event).then((result) => {
28
+ if (typeof result !== 'undefined') {
29
+ strapi.log.info(`${action} hook result: ${JSON.stringify(result)}`);
30
+ }
31
+ });
32
+ } catch (e) {
33
+ strapi.log.error(e);
34
+ } finally {
35
+ break;
36
+ }
37
+ }
38
+ case 'beforeDeleteMany':
39
+ case 'beforeDelete': {
40
+ try {
41
+ if (requestInitiatorHelper.isInitiatedByLocalazyWebhook() || requestInitiatorHelper.isInitiatedByLocalazyPluginUI()) {
42
+ break;
43
+ }
11
44
 
12
- if (populate && populate[0] === 'deep') {
13
- const depth = populate[1] ?? 5
14
- const modelObject = getFullPopulateObject(event.model.uid, depth);
15
- event.params.populate = modelObject.populate
45
+ // have to await here
46
+ const result = await deprecateEventEntryInLocalazyHook(event);
47
+ if (typeof result !== 'undefined') {
48
+ strapi.log.info(`${action} hook result: ${JSON.stringify(result)}`);
49
+ }
50
+ } catch (e) {
51
+ strapi.log.error(e);
52
+ } finally {
53
+ break;
54
+ }
16
55
  }
56
+ default:
57
+ break;
17
58
  }
18
59
  });
19
60
  };
@@ -18,6 +18,31 @@ const shouldSetDownloadedProperty = require("../functions/should-set-downloaded-
18
18
  const set = require("lodash/set");
19
19
  const isEmpty = require("lodash/isEmpty");
20
20
  const omitDeep = require("../utils/omit-deep");
21
+ const RequestInitiatorHelper = require('../utils/request-initiator-helper');
22
+ const PluginSettingsServiceHelper = require('../services/helpers/plugin-settings-service-helper');
23
+
24
+ const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
25
+ const pluginSettingsServiceHelper = new PluginSettingsServiceHelper(strapi);
26
+ const requestInitiatorHelper = new RequestInitiatorHelper(strapi);
27
+ await pluginSettingsServiceHelper.setup();
28
+ let localLanguagesCodes;
29
+ if (requestInitiatorHelper.isInitiatedByLocalazyWebhook()) {
30
+ // called by a webhook
31
+ localLanguagesCodes = pluginSettingsServiceHelper.getWebhookLanguagesCodes();
32
+ } else if (requestInitiatorHelper.isInitiatedByLocalazyPluginUI()) {
33
+ // called by a user from the UI
34
+ localLanguagesCodes = pluginSettingsServiceHelper.getUiLanguagesCodes();
35
+ } else {
36
+ strapi.log.warn("Called by an unknown initiator.");
37
+ return languagesCodes;
38
+ }
39
+
40
+ if (isEmpty(localLanguagesCodes)) {
41
+ return languagesCodes;
42
+ }
43
+
44
+ return languagesCodes.filter((code) => localLanguagesCodes.includes(code));
45
+ };
21
46
 
22
47
  module.exports = {
23
48
  async upload(ctx) {
@@ -152,7 +177,11 @@ module.exports = {
152
177
  locale,
153
178
  chunks
154
179
  );
155
- await LocalazyUploadService.upload(importFile);
180
+ await LocalazyUploadService.upload(
181
+ importFile,
182
+ {
183
+ deprecate: "file"
184
+ });
156
185
 
157
186
  ctx.body = {
158
187
  success,
@@ -205,8 +234,15 @@ module.exports = {
205
234
  user.project.id
206
235
  );
207
236
  if (!strapiFile) {
208
- strapi.log.error(`File ${config.LOCALAZY_DEFAULT_FILE_NAME} not found`);
209
- ctx.throw(400, `File ${config.LOCALAZY_DEFAULT_FILE_NAME} not found`);
237
+ success = false;
238
+ const message = `File ${config.LOCALAZY_DEFAULT_FILE_NAME} not found`;
239
+ strapi.log.error(message);
240
+ messageReport.push(message);
241
+ ctx.body = {
242
+ success,
243
+ report: messageReport,
244
+ };
245
+ return;
210
246
  }
211
247
 
212
248
  /**
@@ -269,9 +305,11 @@ module.exports = {
269
305
  /**
270
306
  * Get project languages codes
271
307
  */
272
- const languagesCodes = projectLanguagesWithoutSourceLanguage.map(
308
+ let languagesCodes = projectLanguagesWithoutSourceLanguage.map(
273
309
  (language) => language.code
274
310
  );
311
+ // process filtered languages only / keep all of empty!
312
+ languagesCodes = await getFilteredLanguagesCodesForDownload(languagesCodes);
275
313
 
276
314
  /**
277
315
  * Iterate over languages and create the ones that are not present in Strapi
@@ -13,4 +13,7 @@ module.exports = {
13
13
  .service("strapiService")
14
14
  .getLocalizableModels();
15
15
  },
16
+ async postLifecycleLocalazyWebhooks(ctx) {
17
+ strapi.log("done");
18
+ },
16
19
  };
@@ -0,0 +1,11 @@
1
+ const getFullPopulateObject = require('../utils/get-full-populate-object');
2
+
3
+ module.exports = (event) => {
4
+ const populate = event.params?.populate;
5
+
6
+ if (populate && populate[0] === 'deep') {
7
+ const depth = populate[1] ?? 5
8
+ const modelObject = getFullPopulateObject(event.model.uid, depth);
9
+ event.params.populate = modelObject.populate
10
+ }
11
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ const getPickedFlattenKeysForHookEntry = require('../utils/get-picked-flatten-keys-for-hook-entry');
4
+ const config = require("../config").default;
5
+ const getLocalazyApi = require("../utils/get-localazy-api");
6
+ const delay = require("../utils/delay");
7
+
8
+ module.exports = async (event) => {
9
+ const pickedFlattedResult = await getPickedFlattenKeysForHookEntry(event);
10
+
11
+ if (typeof pickedFlattedResult === "undefined") {
12
+ return;
13
+ }
14
+
15
+ const { pickedFlatten, eventEntryLocale } = pickedFlattedResult;
16
+
17
+ const LocalazyUserService = strapi
18
+ .plugin("localazy")
19
+ .service("localazyUserService");
20
+ const LocalazyPubApiService = strapi
21
+ .plugin("localazy")
22
+ .service("localazyPubApiService");
23
+ const LocalazyDownloadService = strapi
24
+ .plugin("localazy")
25
+ .service("localazyDownloadService");
26
+
27
+ const user = await LocalazyUserService.getUser();
28
+
29
+ /**
30
+ * Get Strapi File
31
+ */
32
+ const strapiFile = await LocalazyPubApiService.getStrapiFile(
33
+ user.project.id
34
+ );
35
+ if (!strapiFile) {
36
+ strapi.log.error(`File ${config.LOCALAZY_DEFAULT_FILE_NAME} not found`);
37
+ return;
38
+ }
39
+
40
+ const projectKeys = await LocalazyDownloadService.download({
41
+ projectId: user.project.id,
42
+ fileId: strapiFile.id,
43
+ lang: eventEntryLocale,
44
+ });
45
+
46
+ const pickedFlattenKeys = Object.keys(pickedFlatten);
47
+ const filteredProjectKeysIds = projectKeys.data
48
+ .filter((key) => pickedFlattenKeys.includes(key.key[0]))
49
+ .map((key) => key.id);
50
+
51
+ const LocalazyApi = await getLocalazyApi();
52
+ // call in a loop in a sequence, to avoid hitting PubAPI limits
53
+ for (const filteredProjectKeysId of filteredProjectKeysIds) {
54
+ try {
55
+ await LocalazyApi.updateKey({
56
+ projectId: user.project.id,
57
+ keyId: filteredProjectKeysId,
58
+ deprecated: 0,
59
+ });
60
+ await delay();
61
+ } catch (e) {
62
+ strapi.log.error(e.message);
63
+ }
64
+ }
65
+
66
+ return {
67
+ success: true,
68
+ };
69
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ const getPickedFlattenKeysForHookEntry = require('../utils/get-picked-flatten-keys-for-hook-entry');
4
+ const config = require("../config").default;
5
+
6
+ module.exports = async (event) => {
7
+ const pickedFlattedResult = await getPickedFlattenKeysForHookEntry(event);
8
+
9
+ if (typeof pickedFlattedResult === "undefined") {
10
+ return;
11
+ }
12
+
13
+ const { pickedFlatten, eventEntryLocale } = pickedFlattedResult;
14
+
15
+ // Localazy Upload Service
16
+ const LocalazyUploadService = strapi
17
+ .plugin("localazy")
18
+ .service("localazyUploadService");
19
+
20
+ const chunks = LocalazyUploadService.splitToChunks(pickedFlatten);
21
+ const importFile = LocalazyUploadService.createImportFileRepresentation(
22
+ config.LOCALAZY_DEFAULT_FILE_NAME,
23
+ config.LOCALAZY_DEFAULT_FILE_PATH,
24
+ config.LOCALAZY_DEFAULT_FILE_EXTENSION,
25
+ eventEntryLocale,
26
+ chunks
27
+ );
28
+
29
+ const result = await LocalazyUploadService.upload(importFile);
30
+ return result;
31
+ }
@@ -1,3 +1,7 @@
1
1
  "use strict";
2
2
 
3
- module.exports = {};
3
+ const verifyWebhook = require("./verify-webhook");
4
+
5
+ module.exports = {
6
+ verifyWebhook
7
+ };
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const PluginSettingsServiceHelper = require('../services/helpers/plugin-settings-service-helper');
5
+
6
+ module.exports = (config, { strapi }) => {
7
+ return async (ctx, next) => {
8
+ try {
9
+ const pluginSettingsServiceHelper = new PluginSettingsServiceHelper(strapi);
10
+ await pluginSettingsServiceHelper.setup();
11
+
12
+ if (!pluginSettingsServiceHelper.shouldAllowWebhookDownloadProcess()) {
13
+ throw new Error("Localazy Plugin: Webhook download process is disabled; terminating execution");
14
+ }
15
+
16
+ const requestBody = ctx.request.body;
17
+ strapi.log.info(`Localazy Plugin: Webhook of type '${requestBody.type}' procedure started`);
18
+
19
+ const LocalazyPubApiService = strapi
20
+ .plugin("localazy")
21
+ .service("localazyPubApiService");
22
+ const LocalazyUserService = strapi
23
+ .plugin("localazy")
24
+ .service("localazyUserService");
25
+ const user = await LocalazyUserService.getUser();
26
+
27
+ const result = await LocalazyPubApiService.getWebhooksSecret(user.project.id);
28
+
29
+ const { secret } = result;
30
+ const xLocalazyHmac = ctx.request.header["x-localazy-hmac"];
31
+ const xLocalazyTimestamp = ctx.request.header["x-localazy-timestamp"];
32
+
33
+ // if older than 15 mins - do not process
34
+ const THRESHOLD = 900000; // 15 mins = 900000ms
35
+
36
+ const intXLocalazyTimestamp = parseInt(xLocalazyTimestamp);
37
+ if (Number.isNaN(intXLocalazyTimestamp)) {
38
+ throw new Error("Localazy Plugin: Webhook request invalid timestamp provided; terminating execution");
39
+ }
40
+
41
+ if ((intXLocalazyTimestamp * 1000 + THRESHOLD) < Date.now()) {
42
+ throw new Error("Localazy Plugin: Webhook request is older than threshold; terminating execution");
43
+ }
44
+
45
+ const strapiHmac = crypto.createHmac("sha256", secret);
46
+ const signedMessage = strapiHmac.update(`${xLocalazyTimestamp}-${JSON.stringify(requestBody)}`).digest("hex");
47
+
48
+ if (xLocalazyHmac !== signedMessage) {
49
+ throw new Error("Localazy Plugin: Webhook verification did not pass; terminating execution");
50
+ }
51
+
52
+ /**
53
+ * When further actions are called by a webhook, then there's no user assigned to the operation
54
+ * It's needed to assign the user ID to the operation (requested e.g. for adding a new locale)
55
+ */
56
+ if (!ctx.state.user) {
57
+ const webhookAuthorId = pluginSettingsServiceHelper.getWebhookAuthorId();
58
+ if (Number.isNaN(webhookAuthorId)) {
59
+ throw new Error("Localazy Plugin: Webhook author is not set; terminating execution");
60
+ }
61
+ ctx.state.user = {
62
+ id: webhookAuthorId,
63
+ };
64
+ }
65
+
66
+ await next(); // proceed with the request; await for the response
67
+
68
+ strapi.log.info(`Localazy Plugin: Webhook of type '${requestBody.type}' procedure finished`);
69
+ } catch (e) {
70
+ strapi.log.warn(e.message);
71
+ ctx.body = {
72
+ success: false,
73
+ message: e.message,
74
+ };
75
+ }
76
+ }
77
+ }
@@ -6,6 +6,7 @@ const pluginSettingsRoutes = require("./plugin-settings");
6
6
  const strapiRoutes = require("./strapi");
7
7
  const localazyTransferRoutes = require("./localazy-transfer");
8
8
  const localazyProjectRoutes = require("./localazy-project");
9
+ const localazyPublicTransferRoutes = require("./localazy-public-transfer");
9
10
 
10
11
  module.exports = {
11
12
  "content-api": {
@@ -23,4 +24,5 @@ module.exports = {
23
24
  ...localazyProjectRoutes,
24
25
  ],
25
26
  },
27
+ localazyPublicTransferRoutes,
26
28
  };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ const ROUTE_PREFIX = "/public/transfer";
4
+
5
+ module.exports = {
6
+ routes: [
7
+ {
8
+ method: "POST",
9
+ path: `${ROUTE_PREFIX}/download`,
10
+ handler: "localazyTransferController.download",
11
+ config: {
12
+ policies: [],
13
+ auth: false, // make the route public
14
+ middlewares: [
15
+ "plugin::localazy.verifyWebhook"
16
+ ]
17
+ },
18
+ },
19
+ ]
20
+ };
@@ -19,4 +19,12 @@ module.exports = [
19
19
  policies: [],
20
20
  },
21
21
  },
22
+ {
23
+ method: "POST",
24
+ path: `${ROUTE_PREFIX}/lifecycle/localazy-webhooks`,
25
+ handler: "strapiController.postLifecycleLocalazyWebhooks",
26
+ config: {
27
+ policies: [],
28
+ },
29
+ },
22
30
  ];
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ class PluginSettingsServiceHelper {
4
+ constructor(strapi) {
5
+ this.strapi = strapi;
6
+ this.pluginSettingsService = strapi.plugin("localazy").service("pluginSettingsService");
7
+ }
8
+
9
+ async setup() {
10
+ this.pluginSettings = await this.pluginSettingsService.getPluginSettings();
11
+ }
12
+
13
+ shouldAllowAutomatedUpload() {
14
+ return typeof this.pluginSettings?.upload?.allowAutomated === "boolean" ? this.pluginSettings.upload.allowAutomated : false;
15
+ }
16
+
17
+ shouldAllowAutomatedCreatedTrigger() {
18
+ const automatedTriggers = this.getAutomatedUploadTriggers();
19
+ return this.shouldAllowAutomatedUpload() && automatedTriggers.includes("created");
20
+ }
21
+
22
+ shouldAllowAutomatedUpdatedTrigger() {
23
+ const automatedTriggers = this.getAutomatedUploadTriggers();
24
+ return this.shouldAllowAutomatedUpload() && automatedTriggers.includes("updated");
25
+ }
26
+
27
+ getAutomatedUploadTriggers() {
28
+ return this.pluginSettings?.upload?.automatedTriggers || [];
29
+ }
30
+
31
+ shouldAllowDeprecateOnDeletion() {
32
+ return typeof this.pluginSettings?.upload?.allowDeprecate === "boolean" ? this.pluginSettings.upload.allowDeprecate : false;
33
+ }
34
+
35
+ shouldAllowWebhookDownloadProcess() {
36
+ return typeof this.pluginSettings?.download?.processDownloadWebhook === "boolean" ? this.pluginSettings.download.processDownloadWebhook : true;
37
+ }
38
+
39
+ getWebhookAuthorId() {
40
+ return this.pluginSettings?.download?.webhookAuthorId || null;
41
+ }
42
+
43
+ getWebhookLanguagesCodes() {
44
+ return this.pluginSettings?.download?.webhookLanguages || [];
45
+ }
46
+
47
+ getUiLanguagesCodes() {
48
+ return this.pluginSettings?.download?.uiLanguages || [];
49
+ }
50
+ }
51
+
52
+ module.exports = PluginSettingsServiceHelper;
@@ -44,4 +44,10 @@ module.exports = ({ strapi }) => ({
44
44
 
45
45
  return projects.find((project) => project.id === projectId);
46
46
  },
47
+ async getWebhooksSecret(projectId) {
48
+ const LocalazyApi = await getLocalazyApi();
49
+ const result = await LocalazyApi.getWebhooksSecret({ projectId });
50
+
51
+ return result;
52
+ }
47
53
  });
@@ -39,7 +39,13 @@ module.exports = ({ strapi }) => ({
39
39
  },
40
40
 
41
41
  async updatePluginSettings(settings) {
42
- const newSettings = settings;
42
+ const currentSettings = await this.getPluginSettings();
43
+
44
+ // keep the current settings if the new settings are not provided
45
+ const newSettings = {
46
+ ...currentSettings,
47
+ ...settings,
48
+ };
43
49
  const pluginStore = getStrapiStore(strapi);
44
50
  await pluginStore.set({
45
51
  key: pluginSettingsModel.KEY,
@@ -7,7 +7,7 @@ const cloneDeep = require("lodash/cloneDeep");
7
7
  const omitDeep = require("../utils/omit-deep.js");
8
8
 
9
9
  module.exports = ({ strapi }) => ({
10
- async getLocales(ctx) {
10
+ async getLocales(ctx = {}) {
11
11
  await strapi.controller("plugin::i18n.locales").listLocales(ctx);
12
12
  return ctx.body;
13
13
  },