diff --git a/.github/workflows/scripts/mailchimp/htmlContent.js b/.github/workflows/scripts/mailchimp/htmlContent.js index d132c72f1aa0..8bcce8e59420 100644 --- a/.github/workflows/scripts/mailchimp/htmlContent.js +++ b/.github/workflows/scripts/mailchimp/htmlContent.js @@ -424,7 +424,7 @@ Topic: Cheers,
-AsyncAPI Initiative +AsyncAPI Initiative diff --git a/config/mailchimp-config.json b/config/mailchimp-config.json deleted file mode 100644 index 5f423956d390..000000000000 --- a/config/mailchimp-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "listId": "6e3e437abe", - "interests" : { - "Newsletter": "a7d6314955", - "Meetings": "3505cd49d1", - "TSC Voting": "f7204f9b90" - } -} \ No newline at end of file diff --git a/netlify/functions/newsletter_subscription.ts b/netlify/functions/newsletter_subscription.ts index d8bcea39ef70..699b4f3b5e6e 100644 --- a/netlify/functions/newsletter_subscription.ts +++ b/netlify/functions/newsletter_subscription.ts @@ -1,50 +1,153 @@ -import mailchimp from '@mailchimp/mailchimp_marketing'; import type { Handler, HandlerEvent } from '@netlify/functions'; -import md5 from 'md5'; -import config from '../../config/mailchimp-config.json'; +const KIT_BASE = 'https://api.kit.com/v4'; +const REQUEST_TIMEOUT_MS = 15_000; + +const INTEREST_TO_ENV = { + Newsletter: 'KIT_NEWSLETTER_TAG_ID', + Meetings: 'KIT_MEETINGS_TAG_ID', + 'TSC Voting': 'KIT_TSC_TAG_ID' +} as const; + +type ValidInterest = keyof typeof INTEREST_TO_ENV; + +function isValidInterest(s: string): s is ValidInterest { + return Object.hasOwn(INTEREST_TO_ENV, s); +} + +function parseTagId(raw: string | undefined): number | null { + if (raw == null || raw.trim() === '') { + return null; + } + const n = Number(raw.trim()); + if (!Number.isInteger(n) || n <= 0) { + return null; + } + return n; +} + +function isAbortError(err: unknown): boolean { + return err instanceof Error && err.name === 'AbortError'; +} + +async function kitFetch(url: string, init: RequestInit): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timeoutId); + } +} export const handler: Handler = async (event: HandlerEvent) => { - if (event.httpMethod === 'POST') { - const { listId } = config; - const { email, name, interest } = JSON.parse(event.body || ''); - - const subscriberHash = md5(email.toLowerCase()); - - try { - mailchimp.setConfig({ - apiKey: process.env.MAILCHIMP_API_KEY, - server: 'us12' - }); - - const response = await mailchimp.lists.setListMember(listId, - subscriberHash, - { - email_address: email, - merge_fields: { - FNAME: name - }, - status: 'subscribed', - interests: { - [config.interests[interest]]: true - } - }); + if (event.httpMethod !== 'POST') { + return { + statusCode: 405, + body: JSON.stringify({ message: 'The specified HTTP method is not allowed.' }) + }; + } + + let body: unknown; + try { + body = JSON.parse(event.body ?? '{}'); + } catch { + return { + statusCode: 400, + body: JSON.stringify({ message: 'Invalid request body.' }) + }; + } + + if (body === null || typeof body !== 'object' || Array.isArray(body)) { + return { + statusCode: 400, + body: JSON.stringify({ message: 'Invalid subscription request.' }) + }; + } + + const { email, name, interest } = body as Partial>; + + if (typeof email !== 'string' || typeof interest !== 'string' || !isValidInterest(interest)) { + return { + statusCode: 400, + body: JSON.stringify({ message: 'Invalid subscription request.' }) + }; + } + + if (!process.env.KIT_API_KEY) { + return { + statusCode: 503, + body: JSON.stringify({ message: 'Subscription is temporarily unavailable. Please try again later.' }) + }; + } + + const envVarName = INTEREST_TO_ENV[interest]; + const tagId = parseTagId(process.env[envVarName]); + + if (tagId == null) { + return { + statusCode: 503, + body: JSON.stringify({ message: 'Subscription is temporarily unavailable. Please try again later.' }) + }; + } + + const firstName = typeof name === 'string' ? name : ''; + const headers = { + 'X-Kit-Api-Key': process.env.KIT_API_KEY, + 'Content-Type': 'application/json' + }; + + try { + const subRes = await kitFetch(`${KIT_BASE}/subscribers`, { + method: 'POST', + headers, + body: JSON.stringify({ email_address: email, first_name: firstName, state: 'active' }) + }); + + if (!subRes.ok) { + await subRes.text().catch(() => undefined); return { - statusCode: 200, - body: JSON.stringify(response) + statusCode: 502, + body: JSON.stringify({ + message: 'Subscription could not be completed. Please try again later.' + }) }; - } catch (err) { + } + + const tagRes = await kitFetch(`${KIT_BASE}/tags/${tagId}/subscribers`, { + method: 'POST', + headers, + body: JSON.stringify({ email_address: email }) + }); + + if (!tagRes.ok) { + await tagRes.text().catch(() => undefined); + return { + statusCode: 502, + body: JSON.stringify({ + message: 'Subscription could not be completed. Please try again later.' + }) + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ message: 'Subscribed successfully.' }) + }; + } catch (err) { + if (isAbortError(err)) { return { - statusCode: err.status, - body: JSON.stringify(err) + statusCode: 504, + body: JSON.stringify({ + message: 'Subscription service timed out. Please try again later.' + }) }; } - } else { return { statusCode: 500, body: JSON.stringify({ - message: 'The specified HTTP method is not allowed.' + message: 'An unexpected error occurred. Please try again later.' }) }; } diff --git a/package-lock.json b/package-lock.json index 52f76d3bc94f..9fa1e8d41ff4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@fec/remark-a11y-emoji": "^4.0.2", "@floating-ui/react-dom-interactions": "^0.13.3", "@heroicons/react": "^1.0.6", - "@mailchimp/mailchimp_marketing": "^3.0.80", "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.1.0", @@ -50,7 +49,6 @@ "lodash": "^4.18.1", "markdown-to-txt": "^2.0.1", "markdown-toc": "^1.2.0", - "md5": "^2.3.0", "mermaid": "9.3.0", "next": "15.5.14", "next-i18next": "^15.3.0", @@ -4724,28 +4722,6 @@ "isomorphic-fetch": "^3.0.0" } }, - "node_modules/@mailchimp/mailchimp_marketing": { - "version": "3.0.80", - "resolved": "https://registry.npmjs.org/@mailchimp/mailchimp_marketing/-/mailchimp_marketing-3.0.80.tgz", - "integrity": "sha512-Cgz0xPb+1DUjmrl5whAsmqfAChBko+Wf4/PLQE4RvwfPlcq2agfHr1QFiXEhZ8e+GQwQ3hZQn9iLGXwIXwxUCg==", - "license": "Apache 2.0", - "dependencies": { - "dotenv": "^8.2.0", - "superagent": "3.8.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@mailchimp/mailchimp_marketing/node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, "node_modules/@mdx-js/loader": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-3.1.1.tgz", @@ -11190,15 +11166,6 @@ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -11830,15 +11797,6 @@ "dev": true, "license": "MIT" }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -11930,12 +11888,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "license": "MIT" - }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -12112,15 +12064,6 @@ "uncrypto": "^0.1.3" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/crypto-browserify": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", @@ -16050,16 +15993,6 @@ "node": ">=12.20.0" } }, - "node_modules/formidable": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -20618,17 +20551,6 @@ "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", "license": "MIT" }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -21186,15 +21108,6 @@ "uuid": "^9.0.0" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micro-memoize": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-4.2.0.tgz", @@ -21987,6 +21900,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", + "optional": true, "bin": { "mime": "cli.js" }, @@ -28906,54 +28820,6 @@ "node": ">= 6" } }, - "node_modules/superagent": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.1.tgz", - "integrity": "sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==", - "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", - "license": "MIT", - "dependencies": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.1.1", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/superagent/node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -29976,6 +29842,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29992,6 +29859,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30008,6 +29876,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30024,6 +29893,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30040,6 +29910,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30056,6 +29927,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30072,6 +29944,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30088,6 +29961,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30104,6 +29978,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30120,6 +29995,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30136,6 +30012,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30152,6 +30029,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30168,6 +30046,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30184,6 +30063,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30200,6 +30080,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30216,6 +30097,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30232,6 +30114,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30248,6 +30131,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30264,6 +30148,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30280,6 +30165,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30296,6 +30182,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30312,6 +30199,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30328,6 +30216,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30344,6 +30233,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30360,6 +30250,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -30376,6 +30267,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index f8c08fd78695..5adcd9911225 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "@fec/remark-a11y-emoji": "^4.0.2", "@floating-ui/react-dom-interactions": "^0.13.3", "@heroicons/react": "^1.0.6", - "@mailchimp/mailchimp_marketing": "^3.0.80", "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.1.0", @@ -88,7 +87,6 @@ "lodash": "^4.18.1", "markdown-to-txt": "^2.0.1", "markdown-toc": "^1.2.0", - "md5": "^2.3.0", "mermaid": "9.3.0", "next": "15.5.14", "next-i18next": "^15.3.0",