From 06cac44d84e9f99769170daebf553e8f699e048f Mon Sep 17 00:00:00 2001 From: roo hutton Date: Mon, 22 Apr 2024 08:09:01 +0100 Subject: [PATCH] Merge pull request #18021 from overleaf/rh-mailchimp-api [web] Replace node-mailchimp with own MailChimpClient GitOrigin-RevId: 10207620c48f30ad29f4f0e7ea5193c11d256902 --- package-lock.json | 131 ------------------ .../Features/Newsletter/MailChimpClient.js | 60 ++++++++ .../Features/Newsletter/MailChimpProvider.js | 8 +- services/web/package.json | 1 - .../src/Newsletter/NewsletterManagerTests.js | 18 ++- 5 files changed, 78 insertions(+), 140 deletions(-) create mode 100644 services/web/app/src/Features/Newsletter/MailChimpClient.js diff --git a/package-lock.json b/package-lock.json index 254b100e43..d088ad9128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27908,78 +27908,6 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, - "node_modules/mailchimp-api-v3": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.15.0.tgz", - "integrity": "sha512-9TxCFG+VRpl14HOHgABHYmC5GvpCY7LYqyTefOXd4GtI07oXCiJ7W5fEvk3SJKBctlbjhKbzjB5qOZMQpacEUQ==", - "dependencies": { - "bluebird": "^3.4.0", - "lodash": "^4.17.14", - "request": "^2.88.0", - "tar": "^4.0.2" - } - }, - "node_modules/mailchimp-api-v3/node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/mailchimp-api-v3/node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/mailchimp-api-v3/node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/mailchimp-api-v3/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/mailchimp-api-v3/node_modules/tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" - } - }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -43585,7 +43513,6 @@ "jsonwebtoken": "^9.0.0", "lodash": "^4.17.19", "lru-cache": "^7.10.1", - "mailchimp-api-v3": "^1.12.0", "marked": "^4.1.0", "method-override": "^2.3.3", "minimatch": "^7.4.2", @@ -52263,7 +52190,6 @@ "less-loader": "^11.1.3", "lodash": "^4.17.19", "lru-cache": "^7.10.1", - "mailchimp-api-v3": "^1.12.0", "marked": "^4.1.0", "match-sorter": "^6.2.0", "mathjax": "^3.2.2", @@ -69281,63 +69207,6 @@ } } }, - "mailchimp-api-v3": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.15.0.tgz", - "integrity": "sha512-9TxCFG+VRpl14HOHgABHYmC5GvpCY7LYqyTefOXd4GtI07oXCiJ7W5fEvk3SJKBctlbjhKbzjB5qOZMQpacEUQ==", - "requires": { - "bluebird": "^3.4.0", - "lodash": "^4.17.14", - "request": "^2.88.0", - "tar": "^4.0.2" - }, - "dependencies": { - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - } - } - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", diff --git a/services/web/app/src/Features/Newsletter/MailChimpClient.js b/services/web/app/src/Features/Newsletter/MailChimpClient.js new file mode 100644 index 0000000000..abc539cb18 --- /dev/null +++ b/services/web/app/src/Features/Newsletter/MailChimpClient.js @@ -0,0 +1,60 @@ +const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils') + +class MailChimpClient { + constructor(apiKey) { + this.apiKey = apiKey + this.dc = apiKey.split('-')[1] + this.baseUrl = `https://${this.dc}.api.mailchimp.com/3.0/` + this.fetchOptions = { + method: 'GET', + basicAuth: { + user: 'any', + password: this.apiKey, + }, + } + } + + async request(path, options) { + try { + const requestUrl = `${this.baseUrl}${path}` + if (options.method === 'GET') { + return await fetchJson(requestUrl, options) + } + await fetchNothing(requestUrl, options) + } catch (err) { + // if there's a json body in the response, expose it in the error (for compatibility with node-mailchimp) + const errorBody = err.body ? JSON.parse(err.body) : {} + const errWithBody = Object.assign(err, errorBody) + throw errWithBody + } + } + + async get(path) { + return await this.request(path, this.fetchOptions) + } + + async put(path, body) { + const options = Object.assign({}, this.fetchOptions) + options.method = 'PUT' + options.json = body + + return await this.request(path, options) + } + + async delete(path) { + const options = Object.assign({}, this.fetchOptions) + options.method = 'DELETE' + + return await this.request(path, options) + } + + async patch(path, body) { + const options = Object.assign({}, this.fetchOptions) + options.method = 'PATCH' + options.json = body + + return await this.request(path, options) + } +} + +module.exports = MailChimpClient diff --git a/services/web/app/src/Features/Newsletter/MailChimpProvider.js b/services/web/app/src/Features/Newsletter/MailChimpProvider.js index 4c56a8ec60..c9378080fc 100644 --- a/services/web/app/src/Features/Newsletter/MailChimpProvider.js +++ b/services/web/app/src/Features/Newsletter/MailChimpProvider.js @@ -1,9 +1,9 @@ const logger = require('@overleaf/logger') const Settings = require('@overleaf/settings') const crypto = require('crypto') -const Mailchimp = require('mailchimp-api-v3') const OError = require('@overleaf/o-error') const { callbackify } = require('util') +const MailChimpClient = require('./MailChimpClient') function mailchimpIsConfigured() { return Settings.mailchimp != null && Settings.mailchimp.api_key != null @@ -38,7 +38,7 @@ class NonFatalEmailUpdateError extends OError { } function makeMailchimpProvider(listName, listId) { - const mailchimp = new Mailchimp(Settings.mailchimp.api_key) + const mailchimp = new MailChimpClient(Settings.mailchimp.api_key) const MAILCHIMP_LIST_ID = listId return { @@ -54,7 +54,7 @@ function makeMailchimpProvider(listName, listId) { const result = await mailchimp.get(path) return result?.status === 'subscribed' } catch (err) { - if (err.status === 404) { + if (err?.response?.status === 404) { return false } throw OError.tag(err, 'error getting newsletter subscriptions status', { @@ -101,7 +101,7 @@ function makeMailchimpProvider(listName, listId) { 'finished unsubscribing user from newsletter' ) } catch (err) { - if (err.status === 404 || err.status === 405) { + if ([404, 405].includes(err?.response?.status)) { // silently ignore users who were never subscribed (404) or previously deleted (405) return } diff --git a/services/web/package.json b/services/web/package.json index 1642c0515e..76fdb0787b 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -123,7 +123,6 @@ "jsonwebtoken": "^9.0.0", "lodash": "^4.17.19", "lru-cache": "^7.10.1", - "mailchimp-api-v3": "^1.12.0", "marked": "^4.1.0", "method-override": "^2.3.3", "minimatch": "^7.4.2", diff --git a/services/web/test/unit/src/Newsletter/NewsletterManagerTests.js b/services/web/test/unit/src/Newsletter/NewsletterManagerTests.js index 7d29c8707d..193f9624da 100644 --- a/services/web/test/unit/src/Newsletter/NewsletterManagerTests.js +++ b/services/web/test/unit/src/Newsletter/NewsletterManagerTests.js @@ -1,5 +1,6 @@ const { expect } = require('chai') const sinon = require('sinon') +const { RequestFailedError } = require('@overleaf/fetch-utils') const SandboxedModule = require('sandboxed-module') const MODULE_PATH = '../../../../app/src/Features/Newsletter/NewsletterManager' @@ -28,11 +29,15 @@ describe('NewsletterManager', function () { this.NewsletterManager = SandboxedModule.require(MODULE_PATH, { requires: { - 'mailchimp-api-v3': this.Mailchimp, + './MailChimpClient': this.Mailchimp, '@overleaf/settings': this.Settings, }, + globals: { AbortController }, }).promises + this.NewsletterManager.get = sinon.stub() + this.NewsletterManager.delete = sinon.stub() + this.user = { _id: 'user_id', email: 'overleaf.duck@example.com', @@ -59,9 +64,14 @@ describe('NewsletterManager', function () { }) it('returns false on 404', async function () { - const err = new Error() - err.status = 404 - this.mailchimp.get.rejects(err) + this.mailchimp.get.rejects( + new RequestFailedError( + 'http://some-url', + {}, + { status: 404 }, + 'Not found' + ) + ) const subscribed = await this.NewsletterManager.subscribed(this.user) expect(subscribed).to.be.false })