diff --git a/.changeset/weak-monkeys-grab.md b/.changeset/weak-monkeys-grab.md new file mode 100644 index 0000000000..2989ca349d --- /dev/null +++ b/.changeset/weak-monkeys-grab.md @@ -0,0 +1,5 @@ +--- +"strapi-cms": minor +--- + +Add Active Campaign integration to create and delete lists when creating and deleting webinars diff --git a/apps/strapi-cms/.env.default b/apps/strapi-cms/.env.default index 9c70738f7f..f513d49b2b 100644 --- a/apps/strapi-cms/.env.default +++ b/apps/strapi-cms/.env.default @@ -55,3 +55,9 @@ GOOGLE_OAUTH_CLIENT_ID= GOOGLE_OAUTH_CLIENT_SECRET= GOOGLE_OAUTH_REDIRECT_URI= GOOGLE_GSUITE_HD= + +# ACTIVE CAMPAIGN +ACTIVE_CAMPAIGN_INTEGRATION_IS_ENABLED=True +AC_BASE_URL=https://.activehosted.com +AC_API_KEY=your_api_key +SENDER_URL=your_server_url diff --git a/apps/strapi-cms/package.json b/apps/strapi-cms/package.json index cbbce99ac5..792c028112 100644 --- a/apps/strapi-cms/package.json +++ b/apps/strapi-cms/package.json @@ -20,6 +20,7 @@ "@strapi/plugin-users-permissions": "4.24.2", "@strapi/provider-upload-aws-s3": "^4.24.2", "@strapi/strapi": "4.24.2", + "axios": "^1.7.8", "better-sqlite3": "8.6.0", "pg": "^8.11.5", "react": "^18.2.0", diff --git a/apps/strapi-cms/src/api/webinar/content-types/webinar/lifecycles.ts b/apps/strapi-cms/src/api/webinar/content-types/webinar/lifecycles.ts index 26f5bad5ed..cc0af4ca3e 100644 --- a/apps/strapi-cms/src/api/webinar/content-types/webinar/lifecycles.ts +++ b/apps/strapi-cms/src/api/webinar/content-types/webinar/lifecycles.ts @@ -1,10 +1,37 @@ -import { errors } from '@strapi/utils'; +import { errors, env } from '@strapi/utils'; +import axios from 'axios'; + +interface IActiveCampaignListPayload { + readonly list: { + readonly name: string; + readonly stringid: string; + readonly sender_url: string; + readonly sender_reminder: string; + readonly subscription_notify?: string; + readonly unsubscription_notify?: string; + }; +} + +const activeCampaignIntegrationIsEnabled = + env('ACTIVE_CAMPAIGN_INTEGRATION_IS_ENABLED', 'False') === 'True'; + +function getHeaders() { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Api-Token': env('AC_API_KEY', ''), + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }; +} interface IWebinar { + readonly id?: string; readonly slug?: string; + readonly title?: string; readonly locale?: string; readonly startDatetime?: string; readonly endDatetime?: string; + readonly publishedAt?: string; } interface IWebinarEvent { @@ -14,6 +41,7 @@ interface IWebinarEvent { readonly id?: string; }; }; + readonly result: IWebinar; } const validateDates = (event: IWebinarEvent): boolean => { @@ -36,11 +64,142 @@ const validateDates = (event: IWebinarEvent): boolean => { return true; }; +const validateSlug = async (event: IWebinarEvent): Promise => { + const { id } = event.params.data; + if (!id) { + throw new errors.ApplicationError('Webinar id not found'); + } + const previousWebinar = await strapi.db + .query('api::webinar.webinar') + .findOne({ + select: ['slug'], + where: { id }, + }); + if (previousWebinar && previousWebinar.slug !== event.params.data.slug) { + throw new errors.ApplicationError( + 'The slug of a webinar cannot be changed' + ); + } + return true; +}; + +const activeCampaignError = (message: string) => { + throw new errors.ApplicationError( + `Something went wrong during Active Campaign ${message}` + ); +}; + +const createActiveCampaignList = async ( + event: IWebinarEvent +): Promise => { + if ( + !activeCampaignIntegrationIsEnabled || + !event.result?.slug || + !event.result?.title + ) { + return true; + } + + const { slug: name, title: stringid } = event.result; + + const payload: IActiveCampaignListPayload = { + list: { + name, + sender_reminder: '', + sender_url: `${env( + 'SENDER_URL', + 'http://localhost:3000/' + )}/webinars/${name}`, + stringid, + subscription_notify: '', + unsubscription_notify: '', + }, + }; + + const response = await axios + .post(`${env('AC_BASE_URL')}/api/3/lists`, payload, { + headers: getHeaders(), + }) + .catch((_) => { + activeCampaignError('list creation'); + }); + + if (response?.status !== 201) { + activeCampaignError('list creation'); + } + + return response?.status === 201; +}; + +const getListIdByName = async (name: string): Promise => { + const response = await axios.get<{ + readonly lists: ReadonlyArray<{ readonly id: number }>; + }>( + `${env('AC_BASE_URL')}/api/3/lists`, + // eslint-disable-next-line @typescript-eslint/naming-convention + { headers: getHeaders(), params: { 'filters[name][eq]': name } } + ); + return response?.data.lists[0]?.id; +}; + +const deleteActiveCampaignList = async ( + event: IWebinarEvent +): Promise => { + if ( + !activeCampaignIntegrationIsEnabled || + !event?.params.where || + !event.params.where.id + ) { + return true; + } + const webinar = await strapi.db + .query('api::webinar.webinar') + .findOne({ where: { id: event.params.where.id } }); + + if (!webinar?.slug) { + activeCampaignError('list deletion: webinar slug is missing'); + } + // Get list ID using the slug (name) + const listId = await getListIdByName(webinar.slug); + + if (!listId) { + return false; + } + + const response = await axios + .delete(`${env('AC_BASE_URL')}/api/3/lists/${listId}`, { + headers: getHeaders(), + }) + .catch((_) => { + activeCampaignError('list deletion'); + }); + + if (response?.status !== 200) { + activeCampaignError('list deletion'); + } + + return response?.status === 200; +}; + module.exports = { + async afterCreate(event: IWebinarEvent) { + await createActiveCampaignList(event); + }, beforeCreate(event: IWebinarEvent) { validateDates(event); }, - beforeUpdate(event: IWebinarEvent) { + async beforeDelete(event: IWebinarEvent) { + await deleteActiveCampaignList(event); + }, + beforeDeleteMany() { + if (activeCampaignIntegrationIsEnabled) { + throw new errors.ApplicationError( + 'Bulk deletion is not allowed for webinars if Active Campaign integration is enabled' + ); + } + }, + async beforeUpdate(event: IWebinarEvent) { validateDates(event); + await validateSlug(event); }, }; diff --git a/package-lock.json b/package-lock.json index 5ef4494002..5e054c02bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -423,6 +423,7 @@ "@strapi/plugin-users-permissions": "4.24.2", "@strapi/provider-upload-aws-s3": "^4.24.2", "@strapi/strapi": "4.24.2", + "axios": "^1.7.8", "better-sqlite3": "8.6.0", "pg": "^8.11.5", "react": "^18.2.0", @@ -443,6 +444,17 @@ "npm": ">=6.0.0" } }, + "apps/strapi-cms/node_modules/axios": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "apps/strapi-cms/node_modules/eslint": { "version": "8.50.0", "dev": true,