diff --git a/package.json b/package.json index ec7f407518..604a4b50e9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "prepare": "husky install", "setup": "yarn && yarn clean && yarn build:ts && yarn build", "clean": "lerna run --stream clean --no-private", - "watch": "lerna run --stream watch --no-private", + "watch": "lerna run --stream watch --no-private --parallel", "build": "lerna run --stream build --no-private", "build:ts": "lerna run --stream build:ts --no-private", "generate": "plop --plopfile ./packages/generators/admin/plopfile.js", diff --git a/packages/core/admin/admin/src/pages/ProfilePage/index.js b/packages/core/admin/admin/src/pages/ProfilePage/index.js index a0c6e8ad3d..c98014ba6e 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage/index.js +++ b/packages/core/admin/admin/src/pages/ProfilePage/index.js @@ -458,7 +458,7 @@ const ProfilePage = () => { href="https://docs.strapi.io/developer-docs/latest/development/admin-customization.html#locales" > {formatMessage({ - id: 'Settings.profile.form.section.experience.documentation', + id: 'Settings.profile.form.section.experience.here', defaultMessage: 'here', })} diff --git a/packages/core/admin/admin/src/translations/ca.json b/packages/core/admin/admin/src/translations/ca.json index 235f729295..254cbd06bb 100644 --- a/packages/core/admin/admin/src/translations/ca.json +++ b/packages/core/admin/admin/src/translations/ca.json @@ -125,11 +125,10 @@ "Settings.permissions.users.tabs.label": "Permisos de pestanyes", "Settings.profile.form.notify.data.loaded": "S'han carregat les dades del vostre perfil", "Settings.profile.form.section.experience.clear.select": "Esborrar l'idioma d'interfície seleccionat", - "Settings.profile.form.section.experience.documentation": "documentació", - "Settings.profile.form.section.experience.here": "aquí", + "Settings.profile.form.section.experience.here": "documentació", "Settings.profile.form.section.experience.interfaceLanguage": "Idioma d'interfície", "Settings.profile.form.section.experience.interfaceLanguage.hint": "Això només mostrarà la vostra pròpia interfície en l'idioma escollit.", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "La selecció canviarà l'idioma de la interfície només per a vosaltres. Consulteu aquesta {documentation} perquè altres idiomes estiguin disponibles per al vostre ordinador.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "La selecció canviarà l'idioma de la interfície només per a vosaltres. Consulteu aquesta {here} perquè altres idiomes estiguin disponibles per al vostre ordinador.", "Settings.profile.form.section.experience.mode.hint": "Mostra la vostra interfície en el mode escollit.", "Settings.profile.form.section.experience.mode.label": "Mode d'interfície", "Settings.profile.form.section.experience.mode.option-label": "mode {nom}", diff --git a/packages/core/admin/admin/src/translations/dk.json b/packages/core/admin/admin/src/translations/dk.json index 0745a9199a..ceeb515517 100644 --- a/packages/core/admin/admin/src/translations/dk.json +++ b/packages/core/admin/admin/src/translations/dk.json @@ -123,10 +123,10 @@ "Settings.permissions.users.tabs.label": "Tabs Tilladelser", "Settings.profile.form.notify.data.loaded": "Dine profildata er blevet hentet", "Settings.profile.form.section.experience.clear.select": "Nulstil det valgte interface sprog", - "Settings.profile.form.section.experience.documentation": "dokumentation", + "Settings.profile.form.section.experience.here": "dokumentation", "Settings.profile.form.section.experience.interfaceLanguage": "Interface sprog", "Settings.profile.form.section.experience.interfaceLanguage.hint": "Dette vil kun vise dit eget interface i det valgte sprog.", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "Valget vil kun ændre sproget for dig. Referér venligst til dette {documentation} for at gøre andre sprog tilgængelige for dit hold.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "Valget vil kun ændre sproget for dig. Referér venligst til dette {here} for at gøre andre sprog tilgængelige for dit hold.", "Settings.profile.form.section.experience.title": "Oplevelse", "Settings.profile.form.section.helmet.title": "Bruger profil", "Settings.profile.form.section.profile.page.title": "Profil side", diff --git a/packages/core/admin/admin/src/translations/es.json b/packages/core/admin/admin/src/translations/es.json index 5af4f1031b..9809356695 100644 --- a/packages/core/admin/admin/src/translations/es.json +++ b/packages/core/admin/admin/src/translations/es.json @@ -123,10 +123,10 @@ "Settings.permissions.users.tabs.label": "Permisos de pestañas", "Settings.profile.form.notify.data.loaded": "Se han cargado los datos de tu perfil", "Settings.profile.form.section.experience.clear.select": "Borrar el idioma de interfaz seleccionado", - "Settings.profile.form.section.experience.documentation": "documentación", + "Settings.profile.form.section.experience.here": "documentación", "Settings.profile.form.section.experience.interfaceLanguage": "Idioma de interfaz", "Settings.profile.form.section.experience.interfaceLanguage.hint": "Esto solo mostrará su propia interfaz en el idioma elegido.", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "La selección cambiará el idioma de la interfaz solo para usted. Consulte esta {documentation} para que otros idiomas estén disponibles para su equipo.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "La selección cambiará el idioma de la interfaz solo para usted. Consulte esta {here} para que otros idiomas estén disponibles para su equipo.", "Settings.profile.form.section.experience.title": "Experiencia", "Settings.profile.form.section.helmet.title": "Perfil de usuario", "Settings.profile.form.section.profile.page.title": "Página de perfil", diff --git a/packages/core/admin/admin/src/translations/fr.json b/packages/core/admin/admin/src/translations/fr.json index 7e3b00a05b..9a9ff48c22 100644 --- a/packages/core/admin/admin/src/translations/fr.json +++ b/packages/core/admin/admin/src/translations/fr.json @@ -123,10 +123,10 @@ "Settings.permissions.users.tabs.label": "Onglet Autorisations", "Settings.profile.form.notify.data.loaded": "Les données de votre profil ont été chargées", "Settings.profile.form.section.experience.clear.select": "Vider la langue de l'interface sélectionnée", - "Settings.profile.form.section.experience.documentation": "documentation", + "Settings.profile.form.section.experience.here": "documentation", "Settings.profile.form.section.experience.interfaceLanguage": "Langue de l'interface", "Settings.profile.form.section.experience.interfaceLanguage.hint": "Cela affichera seulement votre propre interface dans la langue sélectionnée", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "La sélection changera la langue de l'interface uniquement pour vous. Veuillez vous référer à cette {documentation} pour rendre d'autres langues disponibles pour votre équipe.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "La sélection changera la langue de l'interface uniquement pour vous. Veuillez vous référer à cette {here} pour rendre d'autres langues disponibles pour votre équipe.", "Settings.profile.form.section.experience.title": "Expérience", "Settings.profile.form.section.helmet.title": "Profil utilisateur", "Settings.profile.form.section.profile.page.title": "Page de profil", diff --git a/packages/core/admin/admin/src/translations/hu.json b/packages/core/admin/admin/src/translations/hu.json index f8fff028b7..b02e1616b5 100644 --- a/packages/core/admin/admin/src/translations/hu.json +++ b/packages/core/admin/admin/src/translations/hu.json @@ -123,10 +123,10 @@ "Settings.permissions.users.tabs.label": "Hozzáférések Tab", "Settings.profile.form.notify.data.loaded": "Profiladatok betöltve", "Settings.profile.form.section.experience.clear.select": "A kiválasztott felület nyelvének törlése", - "Settings.profile.form.section.experience.documentation": "dokumentáció", + "Settings.profile.form.section.experience.here": "dokumentáció", "Settings.profile.form.section.experience.interfaceLanguage": "A felület nyelve", "Settings.profile.form.section.experience.interfaceLanguage.hint": "Ez csak a saját felületét jeleníti meg a kiválasztott nyelven.", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "A kiválasztás csak az Ön számára módosítja a felület nyelvét. Kérjük, olvassa el ezt a {document}, hogy más nyelveket a csapata számára is elérhetővé tehesse.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "A kiválasztás csak az Ön számára módosítja a felület nyelvét. Kérjük, olvassa el ezt a {here}, hogy más nyelveket a csapata számára is elérhetővé tehesse.", "Settings.profile.form.section.experience.title": "Tapasztalat", "Settings.profile.form.section.helmet.title": "Felhasználói profil", "Settings.profile.form.section.profile.page.title": "Profil oldal", diff --git a/packages/core/admin/admin/src/translations/ja.json b/packages/core/admin/admin/src/translations/ja.json index a86a96de21..4617aa3902 100644 --- a/packages/core/admin/admin/src/translations/ja.json +++ b/packages/core/admin/admin/src/translations/ja.json @@ -123,10 +123,10 @@ "Settings.permissions.users.tabs.label": "Tabs Permissions", "Settings.profile.form.notify.data.loaded": "Your profile data has been loaded", "Settings.profile.form.section.experience.clear.select": "Clear the interface language selected", - "Settings.profile.form.section.experience.documentation": "documentation", + "Settings.profile.form.section.experience.here": "documentation", "Settings.profile.form.section.experience.interfaceLanguage": "Interface language", "Settings.profile.form.section.experience.interfaceLanguage.hint": "This will only display your own interface in the chosen language.", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "Selection will change the interface language only for you. Please refer to this {documentation} to make other languages available for your team.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "Selection will change the interface language only for you. Please refer to this {here} to make other languages available for your team.", "Settings.profile.form.section.experience.title": "Experience", "Settings.profile.form.section.helmet.title": "ユーザープロフィール", "Settings.profile.form.section.profile.page.title": "プロフィールページ", diff --git a/packages/core/admin/admin/src/translations/nl.json b/packages/core/admin/admin/src/translations/nl.json index b9bed4ec89..dac74ff76e 100644 --- a/packages/core/admin/admin/src/translations/nl.json +++ b/packages/core/admin/admin/src/translations/nl.json @@ -177,10 +177,10 @@ "Settings.permissions.users.strapi-author": "Auteur", "Settings.profile.form.notify.data.loaded": "Je profielgegevens zijn geladen", "Settings.profile.form.section.experience.clear.select": "Wis de geselecteerde interfacetaal", - "Settings.profile.form.section.experience.documentation": "documentatie", + "Settings.profile.form.section.experience.here": "documentatie", "Settings.profile.form.section.experience.interfaceLanguage": "Interfacetaal", "Settings.profile.form.section.experience.interfaceLanguage.hint": "Hierdoor wordt alleen je eigen interface in de gekozen taal weergegeven.", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "Selectie zal de interfacetaal alleen voor jou veranderen. Raadpleeg deze {documentation} om andere talen beschikbaar te maken voor uw team.", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "Selectie zal de interfacetaal alleen voor jou veranderen. Raadpleeg deze {here} om andere talen beschikbaar te maken voor uw team.", "Settings.profile.form.section.experience.mode.label": "Interface modus", "Settings.profile.form.section.experience.mode.hint": "Toont uw interface in de gekozen modus.", "Settings.profile.form.section.experience.mode.option-label": "{name} modus", diff --git a/packages/core/admin/admin/src/translations/zh-Hans.json b/packages/core/admin/admin/src/translations/zh-Hans.json index a818662771..dac2cdae7a 100644 --- a/packages/core/admin/admin/src/translations/zh-Hans.json +++ b/packages/core/admin/admin/src/translations/zh-Hans.json @@ -132,7 +132,7 @@ "Settings.permissions.users.strapi-author": "作者", "Settings.profile.form.notify.data.loaded": "你的个人数据已经加载完成", "Settings.profile.form.section.experience.clear.select": "清除已选择的界面语言", - "Settings.profile.form.section.experience.documentation": "文档", + "Settings.profile.form.section.experience.here": "文档", "Settings.profile.form.section.experience.interfaceLanguage": "界面语言", "Settings.profile.form.section.experience.interfaceLanguage.hint": "将会用所选择的语言显示你的界面", "Settings.profile.form.section.experience.interfaceLanguageHelp": "当前的语言选择只会更改你当前帐号界面语言。 请参考此 {here} 为你的团队提供其他语言。", diff --git a/packages/core/admin/admin/src/translations/zh.json b/packages/core/admin/admin/src/translations/zh.json index aac52e453a..c8c28f7084 100644 --- a/packages/core/admin/admin/src/translations/zh.json +++ b/packages/core/admin/admin/src/translations/zh.json @@ -177,10 +177,10 @@ "Settings.permissions.users.strapi-author": "作者", "Settings.profile.form.notify.data.loaded": "您的個人檔案資料已經載入", "Settings.profile.form.section.experience.clear.select": "清除已選的介面語言", - "Settings.profile.form.section.experience.here": "here", + "Settings.profile.form.section.experience.here": "此文檔", "Settings.profile.form.section.experience.interfaceLanguage": "介面語言", "Settings.profile.form.section.experience.interfaceLanguage.hint": "將會用所選擇的語言顯示您的介面", - "Settings.profile.form.section.experience.interfaceLanguageHelp": "只有您的介面會變為所選擇的語言。如果要為您的團隊提供其他語言,請參考此 {documentation}。", + "Settings.profile.form.section.experience.interfaceLanguageHelp": "只有您的介面會變為所選擇的語言。如果要為您的團隊提供其他語言,請參考{here}。", "Settings.profile.form.section.experience.mode.label": "介面模式", "Settings.profile.form.section.experience.mode.hint": "在選擇的模式中顯示您的介面。", "Settings.profile.form.section.experience.mode.option-label": "{name} 模式", diff --git a/packages/core/admin/package.json b/packages/core/admin/package.json index 081d480904..3a643ecd79 100644 --- a/packages/core/admin/package.json +++ b/packages/core/admin/package.json @@ -82,6 +82,7 @@ "jsonwebtoken": "8.5.1", "koa-compose": "4.1.0", "koa-passport": "5.0.0", + "koa2-ratelimit": "^1.1.2", "koa-static": "5.0.0", "lodash": "4.17.21", "markdown-it": "^12.3.2", diff --git a/packages/core/admin/server/index.js b/packages/core/admin/server/index.js index d0aad2e764..3a8e78b32c 100644 --- a/packages/core/admin/server/index.js +++ b/packages/core/admin/server/index.js @@ -10,6 +10,7 @@ const routes = require('./routes'); const services = require('./services'); const controllers = require('./controllers'); const contentTypes = require('./content-types'); +const middlewares = require('./middlewares'); module.exports = { register, @@ -21,4 +22,5 @@ module.exports = { services, controllers, contentTypes, + middlewares, }; diff --git a/packages/core/admin/server/middlewares/index.js b/packages/core/admin/server/middlewares/index.js new file mode 100644 index 0000000000..2abf96cbb1 --- /dev/null +++ b/packages/core/admin/server/middlewares/index.js @@ -0,0 +1,7 @@ +'use strict'; + +const rateLimit = require('./rateLimit'); + +module.exports = { + rateLimit, +}; diff --git a/packages/core/admin/server/middlewares/rateLimit.js b/packages/core/admin/server/middlewares/rateLimit.js new file mode 100644 index 0000000000..9c80034567 --- /dev/null +++ b/packages/core/admin/server/middlewares/rateLimit.js @@ -0,0 +1,43 @@ +'use strict'; + +const utils = require('@strapi/utils'); +const { has, toLower } = require('lodash/fp'); + +const { RateLimitError } = utils.errors; + +module.exports = + (config, { strapi }) => + async (ctx, next) => { + let rateLimitConfig = strapi.config.get('admin.rateLimit'); + + if (!rateLimitConfig) { + rateLimitConfig = { + enabled: true, + }; + } + + if (!has('enabled', rateLimitConfig)) { + rateLimitConfig.enabled = true; + } + + if (rateLimitConfig.enabled === true) { + const rateLimit = require('koa2-ratelimit').RateLimit; + + const userEmail = toLower(ctx.request.body.email) || 'unknownEmail'; + + const loadConfig = { + interval: { min: 5 }, + max: 5, + prefixKey: `${userEmail}:${ctx.request.path}:${ctx.request.ip}`, + handler() { + throw new RateLimitError(); + }, + ...rateLimitConfig, + ...config, + }; + + return rateLimit.middleware(loadConfig)(ctx, next); + } + + return next(); + }; diff --git a/packages/core/admin/server/routes/authentication.js b/packages/core/admin/server/routes/authentication.js index 9b031e07ca..35c4dc02f3 100644 --- a/packages/core/admin/server/routes/authentication.js +++ b/packages/core/admin/server/routes/authentication.js @@ -5,7 +5,10 @@ module.exports = [ method: 'POST', path: '/login', handler: 'authentication.login', - config: { auth: false }, + config: { + auth: false, + middlewares: ['admin::rateLimit'], + }, }, { method: 'POST', diff --git a/packages/core/data-transfer/lib/engine/index.ts b/packages/core/data-transfer/lib/engine/index.ts index f40c7b61fb..a8b4e4d1de 100644 --- a/packages/core/data-transfer/lib/engine/index.ts +++ b/packages/core/data-transfer/lib/engine/index.ts @@ -159,7 +159,7 @@ class TransferEngine< }); } - #emitTransferUpdate(type: 'start' | 'finish' | 'error', payload?: object) { + #emitTransferUpdate(type: 'init' | 'start' | 'finish' | 'error', payload?: object) { this.progress.stream.emit(`transfer::${type}`, payload); } @@ -336,9 +336,8 @@ class TransferEngine< // reset data between transfers this.progress.data = {}; - this.#emitTransferUpdate('start'); - try { + this.#emitTransferUpdate('init'); await this.bootstrap(); await this.init(); @@ -351,6 +350,8 @@ class TransferEngine< ); } + this.#emitTransferUpdate('start'); + await this.beforeTransfer(); // Run the transfer stages diff --git a/packages/core/data-transfer/lib/providers/local-file-source-provider/index.ts b/packages/core/data-transfer/lib/providers/local-file-source-provider/index.ts index 8904817982..ff078043dd 100644 --- a/packages/core/data-transfer/lib/providers/local-file-source-provider/index.ts +++ b/packages/core/data-transfer/lib/providers/local-file-source-provider/index.ts @@ -49,6 +49,8 @@ class LocalFileSourceProvider implements ISourceProvider { options: ILocalFileSourceProviderOptions; + #metadata?: IMetadata; + constructor(options: ILocalFileSourceProviderOptions) { this.options = options; @@ -60,15 +62,16 @@ class LocalFileSourceProvider implements ISourceProvider { } /** - * Pre flight checks regarding the provided options (making sure that the provided path is correct, etc...) + * Pre flight checks regarding the provided options, making sure that the file can be opened (decrypted, decompressed), etc. */ async bootstrap() { const { path: filePath } = this.options.file; + try { - // This is only to show a nicer error, it doesn't ensure the file will still exist when we try to open it later - await fs.access(filePath, fs.constants.R_OK); + // Read the metadata to ensure the file can be parsed + this.#metadata = await this.getMetadata(); } catch (e) { - throw new Error(`Can't access file "${filePath}".`); + throw new Error(`Can't read file "${filePath}".`); } } diff --git a/packages/core/data-transfer/package.json b/packages/core/data-transfer/package.json index a4233d66f8..0ae176a828 100644 --- a/packages/core/data-transfer/package.json +++ b/packages/core/data-transfer/package.json @@ -31,7 +31,7 @@ "build": "yarn build:ts", "build:ts": "tsc -p tsconfig.json", "build:clean": "yarn clean && yarn build", - "watch": "yarn build:ts -w", + "watch": "yarn build:ts -w --preserveWatchOutput", "test:unit": "jest --verbose", "prepublishOnly": "yarn build:clean" }, diff --git a/packages/core/strapi/lib/commands/__tests__/export.test.js b/packages/core/strapi/lib/commands/__tests__/export.test.js index bf30ebeeda..6a57e78533 100644 --- a/packages/core/strapi/lib/commands/__tests__/export.test.js +++ b/packages/core/strapi/lib/commands/__tests__/export.test.js @@ -1,98 +1,146 @@ 'use strict'; -const utils = require('../transfer/utils'); - -const mockDataTransfer = { - createLocalFileDestinationProvider: jest.fn(), - createLocalStrapiSourceProvider: jest.fn(), - createTransferEngine: jest.fn().mockReturnValue({ - transfer: jest.fn().mockReturnValue(Promise.resolve({})), - }), -}; - -jest.mock( - '@strapi/data-transfer', - () => { - return mockDataTransfer; - }, - { virtual: true } -); - -const exportCommand = require('../transfer/export'); - -const exit = jest.spyOn(process, 'exit').mockImplementation(() => {}); -jest.spyOn(console, 'error').mockImplementation(() => {}); - -jest.mock('../transfer/utils'); - -const defaultFileName = 'defaultFilename'; - describe('export', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); + const defaultFileName = 'defaultFilename'; + + // mock @strapi/data-transfer + const mockDataTransfer = { + createLocalFileDestinationProvider: jest.fn().mockReturnValue({ name: 'testDest' }), + createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testSource' }), + createTransferEngine() { + return { + transfer: jest.fn().mockReturnValue(Promise.resolve({})), + progress: { + on: jest.fn(), + stream: { + on: jest.fn(), + }, + }, + sourceProvider: { name: 'testSource' }, + destinationProvider: { name: 'testDestination' }, + }; + }, + }; + jest.mock( + '@strapi/data-transfer', + () => { + return mockDataTransfer; + }, + { virtual: true } + ); + + // mock utils + const mockUtils = { + createStrapiInstance() { + return { + telemetry: { + send: jest.fn(), + }, + }; + }, + getDefaultExportName: jest.fn(() => defaultFileName), + }; + jest.mock( + '../transfer/utils', + () => { + return mockUtils; + }, + { virtual: true } + ); + + // other spies= + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Now that everything is mocked, import export command + const exportCommand = require('../transfer/export'); + + const expectExit = async (code, fn) => { + const exit = jest.spyOn(process, 'exit').mockImplementation((number) => { + throw new Error(`process.exit: ${number}`); + }); + await expect(async () => { + await fn(); + }).rejects.toThrow(); + expect(exit).toHaveBeenCalledWith(code); + exit.mockRestore(); + }; + + beforeEach(() => {}); it('uses path provided by user', async () => { - const filename = 'testfile'; + const filename = 'test'; - await exportCommand({ file: filename }); + await expectExit(1, async () => { + await exportCommand({ file: filename }); + }); expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith( expect.objectContaining({ file: { path: filename }, }) ); - expect(utils.getDefaultExportName).not.toHaveBeenCalled(); - expect(exit).toHaveBeenCalled(); + expect(mockUtils.getDefaultExportName).not.toHaveBeenCalled(); }); it('uses default path if not provided by user', async () => { - utils.getDefaultExportName.mockReturnValue(defaultFileName); - - await exportCommand({}); + await expectExit(1, async () => { + await exportCommand({}); + }); + expect(mockUtils.getDefaultExportName).toHaveBeenCalledTimes(1); expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith( expect.objectContaining({ file: { path: defaultFileName }, }) ); - - expect(utils.getDefaultExportName).toHaveBeenCalled(); - expect(exit).toHaveBeenCalled(); }); it('encrypts the output file if specified', async () => { const encrypt = true; - await exportCommand({ encrypt }); + await expectExit(1, async () => { + await exportCommand({ encrypt }); + }); + expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith( expect.objectContaining({ encryption: { enabled: encrypt }, }) ); - expect(exit).toHaveBeenCalled(); }); it('encrypts the output file with the given key', async () => { const key = 'secret-key'; const encrypt = true; + await expectExit(1, async () => { + await exportCommand({ encrypt, key }); + }); - await exportCommand({ encrypt, key }); expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith( expect.objectContaining({ encryption: { enabled: encrypt, key }, }) ); - expect(exit).toHaveBeenCalled(); }); - it('compresses the output file if specified', async () => { - const compress = true; - await exportCommand({ compress }); + it('uses compress option', async () => { + await expectExit(1, async () => { + await exportCommand({ compress: false }); + }); + expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith( expect.objectContaining({ - compression: { enabled: compress }, + compression: { enabled: false }, + }) + ); + await expectExit(1, async () => { + await exportCommand({ compress: true }); + }); + expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith( + expect.objectContaining({ + compression: { enabled: true }, }) ); - expect(exit).toHaveBeenCalled(); }); }); diff --git a/packages/core/strapi/lib/commands/transfer/export.js b/packages/core/strapi/lib/commands/transfer/export.js index de288b840b..795bb713f7 100644 --- a/packages/core/strapi/lib/commands/transfer/export.js +++ b/packages/core/strapi/lib/commands/transfer/export.js @@ -72,32 +72,23 @@ module.exports = async (opts) => { }, }); - try { - logger.log(`Starting export...`); + const progress = engine.progress.stream; - const progress = engine.progress.stream; - - const telemetryPayload = (/* payload */) => { - return { - eventProperties: { - source: engine.sourceProvider.name, - destination: engine.destinationProvider.name, - }, - }; + const getTelemetryPayload = (/* payload */) => { + return { + eventProperties: { + source: engine.sourceProvider.name, + destination: engine.destinationProvider.name, + }, }; + }; - progress.on('transfer::start', (payload) => { - strapi.telemetry.send('didDEITSProcessStart', telemetryPayload(payload)); - }); - - progress.on('transfer::finish', (payload) => { - strapi.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload)); - }); - - progress.on('transfer::error', (payload) => { - strapi.telemetry.send('didDEITSProcessFail', telemetryPayload(payload)); - }); + progress.on('transfer::start', async () => { + logger.log(`Starting export...`); + await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload()); + }); + try { const results = await engine.transfer(); const outFile = results.destination.file.path; @@ -111,11 +102,15 @@ module.exports = async (opts) => { logger.log(`${chalk.bold('Export process has been completed successfully!')}`); logger.log(`Export archive is in ${chalk.green(outFile)}`); - process.exit(0); } catch (e) { + await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload()); logger.error('Export process failed unexpectedly:', e.toString()); process.exit(1); } + + // Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send + await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload()); + process.exit(0); }; /** diff --git a/packages/core/strapi/lib/commands/transfer/import.js b/packages/core/strapi/lib/commands/transfer/import.js index 67ad73012d..83ecbb7ee7 100644 --- a/packages/core/strapi/lib/commands/transfer/import.js +++ b/packages/core/strapi/lib/commands/transfer/import.js @@ -75,44 +75,40 @@ module.exports = async (opts) => { ], }, }; + const engine = createTransferEngine(source, destination, engineOptions); - try { - logger.info('Starting import...'); - - const progress = engine.progress.stream; - const telemetryPayload = (/* payload */) => { - return { - eventProperties: { - source: engine.sourceProvider.name, - destination: engine.destinationProvider.name, - }, - }; + const progress = engine.progress.stream; + const getTelemetryPayload = () => { + return { + eventProperties: { + source: engine.sourceProvider.name, + destination: engine.destinationProvider.name, + }, }; + }; - progress.on('transfer::start', (payload) => { - strapiInstance.telemetry.send('didDEITSProcessStart', telemetryPayload(payload)); - }); - - progress.on('transfer::finish', (payload) => { - strapiInstance.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload)); - }); - - progress.on('transfer::error', (payload) => { - strapiInstance.telemetry.send('didDEITSProcessFail', telemetryPayload(payload)); - }); + progress.on('transfer::start', async () => { + logger.info('Starting import...'); + await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload()); + }); + try { const results = await engine.transfer(); const table = buildTransferTable(results.engine); logger.info(table.toString()); logger.info('Import process has been completed successfully!'); - process.exit(0); } catch (e) { + await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload()); logger.error('Import process failed unexpectedly:'); logger.error(e); process.exit(1); } + + // Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send + await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload()); + process.exit(0); }; /** diff --git a/packages/core/strapi/lib/services/errors.js b/packages/core/strapi/lib/services/errors.js index 0efba67a5d..536529b781 100644 --- a/packages/core/strapi/lib/services/errors.js +++ b/packages/core/strapi/lib/services/errors.js @@ -1,7 +1,7 @@ 'use strict'; const createError = require('http-errors'); -const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError } = +const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError, RateLimitError } = require('@strapi/utils').errors; const mapErrorsAndStatus = [ @@ -21,6 +21,10 @@ const mapErrorsAndStatus = [ classError: PayloadTooLargeError, status: 413, }, + { + classError: RateLimitError, + status: 429, + }, ]; const formatApplicationError = (error) => { diff --git a/packages/core/utils/lib/errors.js b/packages/core/utils/lib/errors.js index 34cb3c919d..3ccf1975e9 100644 --- a/packages/core/utils/lib/errors.js +++ b/packages/core/utils/lib/errors.js @@ -55,14 +55,6 @@ class ForbiddenError extends ApplicationError { } } -class PayloadTooLargeError extends ApplicationError { - constructor(message, details) { - super(message, details); - this.name = 'PayloadTooLargeError'; - this.message = message || 'Entity too large'; - } -} - class UnauthorizedError extends ApplicationError { constructor(message, details) { super(message, details); @@ -71,6 +63,23 @@ class UnauthorizedError extends ApplicationError { } } +class RateLimitError extends ApplicationError { + constructor(message, details) { + super(message, details); + this.name = 'RateLimitError'; + this.message = message || 'Too many requests, please try again later.'; + this.details = details || {}; + } +} + +class PayloadTooLargeError extends ApplicationError { + constructor(message, details) { + super(message, details); + this.name = 'PayloadTooLargeError'; + this.message = message || 'Entity too large'; + } +} + class PolicyError extends ForbiddenError { constructor(message, details) { super(message, details); @@ -88,7 +97,8 @@ module.exports = { PaginationError, NotFoundError, ForbiddenError, - PayloadTooLargeError, UnauthorizedError, + RateLimitError, + PayloadTooLargeError, PolicyError, }; diff --git a/packages/plugins/documentation/admin/src/translations/dk.json b/packages/plugins/documentation/admin/src/translations/dk.json index 294e23b612..16280f0131 100755 --- a/packages/plugins/documentation/admin/src/translations/dk.json +++ b/packages/plugins/documentation/admin/src/translations/dk.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "Regenerér {target}", "pages.PluginPage.table.icon.show": "Åben {target}", "pages.PluginPage.table.version": "Version", - "pages.SettingsPage.Button.description": "Konfigurér dokumentations pluginnet", + "pages.SettingsPage.header.description": "Konfigurér dokumentations pluginnet", "pages.SettingsPage.header.save": "Gem", "pages.SettingsPage.toggle.hint": "Gør dokumentationens endpoint privat", "pages.SettingsPage.toggle.label": "Begrænset adgang", diff --git a/packages/plugins/documentation/admin/src/translations/en.json b/packages/plugins/documentation/admin/src/translations/en.json index 89330197da..cad45850d0 100755 --- a/packages/plugins/documentation/admin/src/translations/en.json +++ b/packages/plugins/documentation/admin/src/translations/en.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "Regenerate {target}", "pages.PluginPage.table.icon.show": "Open {target}", "pages.PluginPage.table.version": "Version", - "pages.SettingsPage.Button.description": "Configure the documentation plugin", + "pages.SettingsPage.header.description": "Configure the documentation plugin", "pages.SettingsPage.header.save": "Save", "pages.SettingsPage.toggle.hint": "Make the documentation endpoint private", "pages.SettingsPage.toggle.label": "Restricted Access", diff --git a/packages/plugins/documentation/admin/src/translations/es.json b/packages/plugins/documentation/admin/src/translations/es.json index 87b9d6ef68..ac911c5e4e 100755 --- a/packages/plugins/documentation/admin/src/translations/es.json +++ b/packages/plugins/documentation/admin/src/translations/es.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "Regenerar {target}", "pages.PluginPage.table.icon.show": "Abrir {target}", "pages.PluginPage.table.version": "Versión", - "pages.SettingsPage.Button.description": "Configura el plugin de documentación", + "pages.SettingsPage.header.description": "Configura el plugin de documentación", "pages.SettingsPage.header.save": "Guardar", "pages.SettingsPage.toggle.hint": "Hacer que la documentación sea privada", "pages.SettingsPage.toggle.label": "Acceso restringido", diff --git a/packages/plugins/documentation/admin/src/translations/ko.json b/packages/plugins/documentation/admin/src/translations/ko.json index b84cb40f2d..455d5fbd65 100755 --- a/packages/plugins/documentation/admin/src/translations/ko.json +++ b/packages/plugins/documentation/admin/src/translations/ko.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "{target} 재생성", "pages.PluginPage.table.icon.show": "{target} 열기", "pages.PluginPage.table.version": "버전", - "pages.SettingsPage.Button.description": "도큐멘테이션 플러그인 설정", + "pages.SettingsPage.header.description": "도큐멘테이션 플러그인 설정", "pages.SettingsPage.header.save": "저장", "pages.SettingsPage.toggle.hint": "도큐멘테이션 엔드포인트를 비공개로 설정합니다.", "pages.SettingsPage.toggle.label": "액세스 제한", diff --git a/packages/plugins/documentation/admin/src/translations/pl.json b/packages/plugins/documentation/admin/src/translations/pl.json index a546792fa5..5c7f166921 100755 --- a/packages/plugins/documentation/admin/src/translations/pl.json +++ b/packages/plugins/documentation/admin/src/translations/pl.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "Wygeneruj ponownie {target}", "pages.PluginPage.table.icon.show": "Otwórz {target}", "pages.PluginPage.table.version": "Wersja", - "pages.SettingsPage.Button.description": "Skonfiguruj plugin dokumentacji", + "pages.SettingsPage.header.description": "Skonfiguruj plugin dokumentacji", "pages.SettingsPage.header.save": "Zapisz", "pages.SettingsPage.toggle.hint": "Ustaw endpoint dokumentacji na prywatny", "pages.SettingsPage.toggle.label": "Dostęp ograniczony", diff --git a/packages/plugins/documentation/admin/src/translations/sv.json b/packages/plugins/documentation/admin/src/translations/sv.json index 3c282fc021..23a54ebb75 100644 --- a/packages/plugins/documentation/admin/src/translations/sv.json +++ b/packages/plugins/documentation/admin/src/translations/sv.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "Återskapa {target}", "pages.PluginPage.table.icon.show": "Öppna {target}", "pages.PluginPage.table.version": "Version", - "pages.SettingsPage.Button.description": "Konfigurera dokumentationspluginet", + "pages.SettingsPage.header.description": "Konfigurera dokumentationspluginet", "pages.SettingsPage.header.save": "Spara", "pages.SettingsPage.toggle.hint": "Gör dokumentationensrutten privat", "pages.SettingsPage.toggle.label": "Begränsad åtkomst", diff --git a/packages/plugins/documentation/admin/src/translations/tr.json b/packages/plugins/documentation/admin/src/translations/tr.json index 913e69e6cc..0911625251 100755 --- a/packages/plugins/documentation/admin/src/translations/tr.json +++ b/packages/plugins/documentation/admin/src/translations/tr.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "Yeniden üret: {target}", "pages.PluginPage.table.icon.show": "Aç: {target}", "pages.PluginPage.table.version": "Versiyon", - "pages.SettingsPage.Button.description": "Dokümantasyon eklentisini ayarla", + "pages.SettingsPage.header.description": "Dokümantasyon eklentisini ayarla", "pages.SettingsPage.header.save": "Kaydet", "pages.SettingsPage.toggle.hint": "Dokümantasyon uç noktasını gizli yap", "pages.SettingsPage.toggle.label": "Kısıtlı Erişim", diff --git a/packages/plugins/documentation/admin/src/translations/zh.json b/packages/plugins/documentation/admin/src/translations/zh.json index 16f546c658..1476433abd 100755 --- a/packages/plugins/documentation/admin/src/translations/zh.json +++ b/packages/plugins/documentation/admin/src/translations/zh.json @@ -29,7 +29,7 @@ "pages.PluginPage.table.icon.regenerate": "重新產生 {target}", "pages.PluginPage.table.icon.show": "開啟 {target}", "pages.PluginPage.table.version": "版本", - "pages.SettingsPage.Button.description": "設定說明文件外掛程式", + "pages.SettingsPage.header.description": "設定說明文件外掛程式", "pages.SettingsPage.header.save": "儲存", "pages.SettingsPage.toggle.hint": "將說明文件端點設為私人", "pages.SettingsPage.toggle.label": "受限存取",