payload-plugin-newsletter 0.3.1 → 0.4.4
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.
- package/CHANGELOG.md +65 -0
- package/CLAUDE.md +122 -0
- package/README.md +3 -3
- package/TEST_SUMMARY.md +152 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/endpoints/subscribe.d.ts.map +1 -1
- package/dist/src/collections/NewsletterSettings.js +2 -2
- package/dist/src/collections/NewsletterSettings.js.map +1 -1
- package/dist/src/collections/Subscribers.js +8 -8
- package/dist/src/collections/Subscribers.js.map +1 -1
- package/dist/src/endpoints/subscribe.js +26 -15
- package/dist/src/endpoints/subscribe.js.map +1 -1
- package/dist/src/fields/newsletterScheduling.js +1 -2
- package/dist/src/fields/newsletterScheduling.js.map +1 -1
- package/dist/src/utils/access.js +28 -4
- package/dist/src/utils/access.js.map +1 -1
- package/dist/src/utils/rate-limiter.js +43 -0
- package/dist/src/utils/rate-limiter.js.map +1 -0
- package/dist/src/utils/validation.js +88 -6
- package/dist/src/utils/validation.js.map +1 -1
- package/dist/utils/access.d.ts.map +1 -1
- package/dist/utils/rate-limiter.d.ts +15 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/package.json +23 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/endpoints/subscribe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAQtD,eAAO,MAAM,uBAAuB,GAClC,QAAQ,sBAAsB,KAC7B,
|
|
1
|
+
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/endpoints/subscribe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAQtD,eAAO,MAAM,uBAAuB,GAClC,QAAQ,sBAAsB,KAC7B,QAyLF,CAAA"}
|
|
@@ -369,8 +369,8 @@ export const createNewsletterSettingsCollection = (pluginConfig)=>{
|
|
|
369
369
|
try {
|
|
370
370
|
// TODO: Implement email service reinitialization
|
|
371
371
|
console.warn('Newsletter settings updated, reinitializing service...');
|
|
372
|
-
} catch
|
|
373
|
-
|
|
372
|
+
} catch {
|
|
373
|
+
// Failed to reinitialize email service
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
return doc;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/collections/NewsletterSettings.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\nimport { adminOnly } from '../utils/access'\n\nexport const createNewsletterSettingsCollection = (\n pluginConfig: NewsletterPluginConfig\n): CollectionConfig => {\n const slug = pluginConfig.settingsSlug || 'newsletter-settings'\n \n return {\n slug,\n labels: {\n singular: 'Newsletter Setting',\n plural: 'Newsletter Settings',\n },\n admin: {\n useAsTitle: 'name',\n defaultColumns: ['name', 'provider', 'active', 'updatedAt'],\n group: 'Newsletter',\n description: 'Configure email provider settings and templates',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Configuration Name',\n required: true,\n admin: {\n description: 'A descriptive name for this configuration (e.g., \"Production\", \"Development\", \"Marketing Emails\")',\n },\n },\n {\n name: 'active',\n type: 'checkbox',\n label: 'Active',\n defaultValue: false,\n admin: {\n description: 'Only one configuration can be active at a time',\n },\n },\n {\n type: 'tabs',\n tabs: [\n {\n label: 'Provider Settings',\n fields: [\n {\n name: 'provider',\n type: 'select',\n label: 'Email Provider',\n required: true,\n options: [\n { label: 'Resend', value: 'resend' },\n { label: 'Broadcast (Self-Hosted)', value: 'broadcast' },\n ],\n defaultValue: pluginConfig.providers.default,\n admin: {\n description: 'Choose which email service to use',\n },\n },\n {\n name: 'resendSettings',\n type: 'group',\n label: 'Resend Settings',\n admin: {\n condition: (data) => data?.provider === 'resend',\n },\n fields: [\n {\n name: 'apiKey',\n type: 'text',\n label: 'API Key',\n required: true,\n admin: {\n description: 'Your Resend API key',\n },\n },\n {\n name: 'audienceIds',\n type: 'array',\n label: 'Audience IDs by Locale',\n fields: [\n {\n name: 'locale',\n type: 'select',\n label: 'Locale',\n required: true,\n options: pluginConfig.i18n?.locales?.map(locale => ({\n label: locale.toUpperCase(),\n value: locale,\n })) || [\n { label: 'EN', value: 'en' },\n ],\n },\n {\n name: 'production',\n type: 'text',\n label: 'Production Audience ID',\n },\n {\n name: 'development',\n type: 'text',\n label: 'Development Audience ID',\n },\n ],\n },\n ],\n },\n {\n name: 'broadcastSettings',\n type: 'group',\n label: 'Broadcast Settings',\n admin: {\n condition: (data) => data?.provider === 'broadcast',\n },\n fields: [\n {\n name: 'apiUrl',\n type: 'text',\n label: 'API URL',\n required: true,\n admin: {\n description: 'Your Broadcast instance URL',\n },\n },\n {\n name: 'productionToken',\n type: 'text',\n label: 'Production Token',\n admin: {\n description: 'Token for production environment',\n },\n },\n {\n name: 'developmentToken',\n type: 'text',\n label: 'Development Token',\n admin: {\n description: 'Token for development environment',\n },\n },\n ],\n },\n {\n name: 'fromAddress',\n type: 'email',\n label: 'From Address',\n required: true,\n admin: {\n description: 'Default sender email address',\n },\n },\n {\n name: 'fromName',\n type: 'text',\n label: 'From Name',\n required: true,\n admin: {\n description: 'Default sender name',\n },\n },\n {\n name: 'replyTo',\n type: 'email',\n label: 'Reply-To Address',\n admin: {\n description: 'Optional reply-to email address',\n },\n },\n ],\n },\n {\n label: 'Email Templates',\n fields: [\n {\n name: 'emailTemplates',\n type: 'group',\n label: 'Email Templates',\n fields: [\n {\n name: 'welcome',\n type: 'group',\n label: 'Welcome Email',\n fields: [\n {\n name: 'enabled',\n type: 'checkbox',\n label: 'Send Welcome Email',\n defaultValue: true,\n },\n {\n name: 'subject',\n type: 'text',\n label: 'Subject Line',\n defaultValue: 'Welcome to {{fromName}}!',\n admin: {\n condition: (data) => data?.emailTemplates?.welcome?.enabled,\n },\n },\n {\n name: 'preheader',\n type: 'text',\n label: 'Preheader Text',\n admin: {\n condition: (data) => data?.emailTemplates?.welcome?.enabled,\n },\n },\n ],\n },\n {\n name: 'magicLink',\n type: 'group',\n label: 'Magic Link Email',\n fields: [\n {\n name: 'subject',\n type: 'text',\n label: 'Subject Line',\n defaultValue: 'Sign in to {{fromName}}',\n },\n {\n name: 'preheader',\n type: 'text',\n label: 'Preheader Text',\n defaultValue: 'Click the link to access your preferences',\n },\n {\n name: 'expirationTime',\n type: 'select',\n label: 'Link Expiration',\n defaultValue: '7d',\n options: [\n { label: '1 hour', value: '1h' },\n { label: '24 hours', value: '24h' },\n { label: '7 days', value: '7d' },\n { label: '30 days', value: '30d' },\n ],\n },\n ],\n },\n ],\n },\n ],\n },\n {\n label: 'Subscription Settings',\n fields: [\n {\n name: 'subscriptionSettings',\n type: 'group',\n label: 'Subscription Settings',\n fields: [\n {\n name: 'requireDoubleOptIn',\n type: 'checkbox',\n label: 'Require Double Opt-In',\n defaultValue: false,\n admin: {\n description: 'Require email confirmation before activating subscriptions',\n },\n },\n {\n name: 'allowedDomains',\n type: 'array',\n label: 'Allowed Email Domains',\n admin: {\n description: 'Leave empty to allow all domains',\n },\n fields: [\n {\n name: 'domain',\n type: 'text',\n label: 'Domain',\n required: true,\n admin: {\n placeholder: 'example.com',\n },\n },\n ],\n },\n {\n name: 'maxSubscribersPerIP',\n type: 'number',\n label: 'Max Subscribers per IP',\n defaultValue: 10,\n min: 1,\n admin: {\n description: 'Maximum number of subscriptions allowed from a single IP address',\n },\n },\n ],\n },\n ],\n },\n ],\n },\n ],\n hooks: {\n beforeChange: [\n async ({ data, req, operation }) => {\n // Verify admin access for settings changes\n if (!req.user || req.user.collection !== 'users') {\n throw new Error('Only administrators can modify newsletter settings')\n }\n \n // If setting this config as active, deactivate all others\n if (data?.active && operation !== 'create') {\n await req.payload.update({\n collection: slug,\n where: {\n id: {\n not_equals: data.id,\n },\n },\n data: {\n active: false,\n },\n // Keep overrideAccess: true for admin operations after verification\n })\n }\n \n // For new configs, ensure only one is active\n if (operation === 'create' && data?.active) {\n const existingActive = await req.payload.find({\n collection: slug,\n where: {\n active: {\n equals: true,\n },\n },\n // Keep overrideAccess: true for admin operations\n })\n \n if (existingActive.docs.length > 0) {\n // Deactivate existing active configs\n for (const doc of existingActive.docs) {\n await req.payload.update({\n collection: slug,\n id: doc.id,\n data: {\n active: false,\n },\n // Keep overrideAccess: true for admin operations\n })\n }\n }\n }\n \n return data\n },\n ],\n afterChange: [\n async ({ doc, req }) => {\n // Reinitialize email service when settings change\n if ((req.payload as any).newsletterEmailService && doc.active) {\n try {\n // TODO: Implement email service reinitialization\n console.warn('Newsletter settings updated, reinitializing service...')\n } catch (error) {\n console.error('Failed to reinitialize email service:', error)\n }\n }\n \n return doc\n },\n ],\n },\n access: {\n read: () => true, // Settings can be read publicly for validation\n create: adminOnly(pluginConfig),\n update: adminOnly(pluginConfig),\n delete: adminOnly(pluginConfig),\n },\n timestamps: true,\n }\n}"],"names":["adminOnly","createNewsletterSettingsCollection","pluginConfig","slug","settingsSlug","labels","singular","plural","admin","useAsTitle","defaultColumns","group","description","fields","name","type","label","required","defaultValue","tabs","options","value","providers","default","condition","data","provider","i18n","locales","map","locale","toUpperCase","emailTemplates","welcome","enabled","placeholder","min","hooks","beforeChange","req","operation","user","collection","Error","active","payload","update","where","id","not_equals","existingActive","find","equals","docs","length","doc","afterChange","newsletterEmailService","console","warn","error","access","read","create","delete","timestamps"],"mappings":"AAEA,SAASA,SAAS,QAAQ,kBAAiB;AAE3C,OAAO,MAAMC,qCAAqC,CAChDC;IAEA,MAAMC,OAAOD,aAAaE,YAAY,IAAI;IAE1C,OAAO;QACLD;QACAE,QAAQ;YACNC,UAAU;YACVC,QAAQ;QACV;QACAC,OAAO;YACLC,YAAY;YACZC,gBAAgB;gBAAC;gBAAQ;gBAAY;gBAAU;aAAY;YAC3DC,OAAO;YACPC,aAAa;QACf;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,OAAO;gBACPC,UAAU;gBACVT,OAAO;oBACLI,aAAa;gBACf;YACF;YACA;gBACEE,MAAM;gBACNC,MAAM;gBACNC,OAAO;gBACPE,cAAc;gBACdV,OAAO;oBACLI,aAAa;gBACf;YACF;YACA;gBACEG,MAAM;gBACNI,MAAM;oBACJ;wBACEH,OAAO;wBACPH,QAAQ;4BACN;gCACEC,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPC,UAAU;gCACVG,SAAS;oCACP;wCAAEJ,OAAO;wCAAUK,OAAO;oCAAS;oCACnC;wCAAEL,OAAO;wCAA2BK,OAAO;oCAAY;iCACxD;gCACDH,cAAchB,aAAaoB,SAAS,CAACC,OAAO;gCAC5Cf,OAAO;oCACLI,aAAa;gCACf;4BACF;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPR,OAAO;oCACLgB,WAAW,CAACC,OAASA,MAAMC,aAAa;gCAC1C;gCACAb,QAAQ;oCACN;wCACEC,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPC,UAAU;wCACVT,OAAO;4CACLI,aAAa;wCACf;oCACF;oCACA;wCACEE,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPH,QAAQ;4CACN;gDACEC,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPC,UAAU;gDACVG,SAASlB,aAAayB,IAAI,EAAEC,SAASC,IAAIC,CAAAA,SAAW,CAAA;wDAClDd,OAAOc,OAAOC,WAAW;wDACzBV,OAAOS;oDACT,CAAA,MAAO;oDACL;wDAAEd,OAAO;wDAAMK,OAAO;oDAAK;iDAC5B;4CACH;4CACA;gDACEP,MAAM;gDACNC,MAAM;gDACNC,OAAO;4CACT;4CACA;gDACEF,MAAM;gDACNC,MAAM;gDACNC,OAAO;4CACT;yCACD;oCACH;iCACD;4BACH;4BACA;gCACEF,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPR,OAAO;oCACLgB,WAAW,CAACC,OAASA,MAAMC,aAAa;gCAC1C;gCACAb,QAAQ;oCACN;wCACEC,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPC,UAAU;wCACVT,OAAO;4CACLI,aAAa;wCACf;oCACF;oCACA;wCACEE,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPR,OAAO;4CACLI,aAAa;wCACf;oCACF;oCACA;wCACEE,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPR,OAAO;4CACLI,aAAa;wCACf;oCACF;iCACD;4BACH;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPC,UAAU;gCACVT,OAAO;oCACLI,aAAa;gCACf;4BACF;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPC,UAAU;gCACVT,OAAO;oCACLI,aAAa;gCACf;4BACF;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPR,OAAO;oCACLI,aAAa;gCACf;4BACF;yBACD;oBACH;oBACA;wBACEI,OAAO;wBACPH,QAAQ;4BACN;gCACEC,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPH,QAAQ;oCACN;wCACEC,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPH,QAAQ;4CACN;gDACEC,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPE,cAAc;4CAChB;4CACA;gDACEJ,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPE,cAAc;gDACdV,OAAO;oDACLgB,WAAW,CAACC,OAASA,MAAMO,gBAAgBC,SAASC;gDACtD;4CACF;4CACA;gDACEpB,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPR,OAAO;oDACLgB,WAAW,CAACC,OAASA,MAAMO,gBAAgBC,SAASC;gDACtD;4CACF;yCACD;oCACH;oCACA;wCACEpB,MAAM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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/collections/NewsletterSettings.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\nimport { adminOnly } from '../utils/access'\n\nexport const createNewsletterSettingsCollection = (\n pluginConfig: NewsletterPluginConfig\n): CollectionConfig => {\n const slug = pluginConfig.settingsSlug || 'newsletter-settings'\n \n return {\n slug,\n labels: {\n singular: 'Newsletter Setting',\n plural: 'Newsletter Settings',\n },\n admin: {\n useAsTitle: 'name',\n defaultColumns: ['name', 'provider', 'active', 'updatedAt'],\n group: 'Newsletter',\n description: 'Configure email provider settings and templates',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Configuration Name',\n required: true,\n admin: {\n description: 'A descriptive name for this configuration (e.g., \"Production\", \"Development\", \"Marketing Emails\")',\n },\n },\n {\n name: 'active',\n type: 'checkbox',\n label: 'Active',\n defaultValue: false,\n admin: {\n description: 'Only one configuration can be active at a time',\n },\n },\n {\n type: 'tabs',\n tabs: [\n {\n label: 'Provider Settings',\n fields: [\n {\n name: 'provider',\n type: 'select',\n label: 'Email Provider',\n required: true,\n options: [\n { label: 'Resend', value: 'resend' },\n { label: 'Broadcast (Self-Hosted)', value: 'broadcast' },\n ],\n defaultValue: pluginConfig.providers.default,\n admin: {\n description: 'Choose which email service to use',\n },\n },\n {\n name: 'resendSettings',\n type: 'group',\n label: 'Resend Settings',\n admin: {\n condition: (data) => data?.provider === 'resend',\n },\n fields: [\n {\n name: 'apiKey',\n type: 'text',\n label: 'API Key',\n required: true,\n admin: {\n description: 'Your Resend API key',\n },\n },\n {\n name: 'audienceIds',\n type: 'array',\n label: 'Audience IDs by Locale',\n fields: [\n {\n name: 'locale',\n type: 'select',\n label: 'Locale',\n required: true,\n options: pluginConfig.i18n?.locales?.map(locale => ({\n label: locale.toUpperCase(),\n value: locale,\n })) || [\n { label: 'EN', value: 'en' },\n ],\n },\n {\n name: 'production',\n type: 'text',\n label: 'Production Audience ID',\n },\n {\n name: 'development',\n type: 'text',\n label: 'Development Audience ID',\n },\n ],\n },\n ],\n },\n {\n name: 'broadcastSettings',\n type: 'group',\n label: 'Broadcast Settings',\n admin: {\n condition: (data) => data?.provider === 'broadcast',\n },\n fields: [\n {\n name: 'apiUrl',\n type: 'text',\n label: 'API URL',\n required: true,\n admin: {\n description: 'Your Broadcast instance URL',\n },\n },\n {\n name: 'productionToken',\n type: 'text',\n label: 'Production Token',\n admin: {\n description: 'Token for production environment',\n },\n },\n {\n name: 'developmentToken',\n type: 'text',\n label: 'Development Token',\n admin: {\n description: 'Token for development environment',\n },\n },\n ],\n },\n {\n name: 'fromAddress',\n type: 'email',\n label: 'From Address',\n required: true,\n admin: {\n description: 'Default sender email address',\n },\n },\n {\n name: 'fromName',\n type: 'text',\n label: 'From Name',\n required: true,\n admin: {\n description: 'Default sender name',\n },\n },\n {\n name: 'replyTo',\n type: 'email',\n label: 'Reply-To Address',\n admin: {\n description: 'Optional reply-to email address',\n },\n },\n ],\n },\n {\n label: 'Email Templates',\n fields: [\n {\n name: 'emailTemplates',\n type: 'group',\n label: 'Email Templates',\n fields: [\n {\n name: 'welcome',\n type: 'group',\n label: 'Welcome Email',\n fields: [\n {\n name: 'enabled',\n type: 'checkbox',\n label: 'Send Welcome Email',\n defaultValue: true,\n },\n {\n name: 'subject',\n type: 'text',\n label: 'Subject Line',\n defaultValue: 'Welcome to {{fromName}}!',\n admin: {\n condition: (data) => data?.emailTemplates?.welcome?.enabled,\n },\n },\n {\n name: 'preheader',\n type: 'text',\n label: 'Preheader Text',\n admin: {\n condition: (data) => data?.emailTemplates?.welcome?.enabled,\n },\n },\n ],\n },\n {\n name: 'magicLink',\n type: 'group',\n label: 'Magic Link Email',\n fields: [\n {\n name: 'subject',\n type: 'text',\n label: 'Subject Line',\n defaultValue: 'Sign in to {{fromName}}',\n },\n {\n name: 'preheader',\n type: 'text',\n label: 'Preheader Text',\n defaultValue: 'Click the link to access your preferences',\n },\n {\n name: 'expirationTime',\n type: 'select',\n label: 'Link Expiration',\n defaultValue: '7d',\n options: [\n { label: '1 hour', value: '1h' },\n { label: '24 hours', value: '24h' },\n { label: '7 days', value: '7d' },\n { label: '30 days', value: '30d' },\n ],\n },\n ],\n },\n ],\n },\n ],\n },\n {\n label: 'Subscription Settings',\n fields: [\n {\n name: 'subscriptionSettings',\n type: 'group',\n label: 'Subscription Settings',\n fields: [\n {\n name: 'requireDoubleOptIn',\n type: 'checkbox',\n label: 'Require Double Opt-In',\n defaultValue: false,\n admin: {\n description: 'Require email confirmation before activating subscriptions',\n },\n },\n {\n name: 'allowedDomains',\n type: 'array',\n label: 'Allowed Email Domains',\n admin: {\n description: 'Leave empty to allow all domains',\n },\n fields: [\n {\n name: 'domain',\n type: 'text',\n label: 'Domain',\n required: true,\n admin: {\n placeholder: 'example.com',\n },\n },\n ],\n },\n {\n name: 'maxSubscribersPerIP',\n type: 'number',\n label: 'Max Subscribers per IP',\n defaultValue: 10,\n min: 1,\n admin: {\n description: 'Maximum number of subscriptions allowed from a single IP address',\n },\n },\n ],\n },\n ],\n },\n ],\n },\n ],\n hooks: {\n beforeChange: [\n async ({ data, req, operation }) => {\n // Verify admin access for settings changes\n if (!req.user || req.user.collection !== 'users') {\n throw new Error('Only administrators can modify newsletter settings')\n }\n \n // If setting this config as active, deactivate all others\n if (data?.active && operation !== 'create') {\n await req.payload.update({\n collection: slug,\n where: {\n id: {\n not_equals: data.id,\n },\n },\n data: {\n active: false,\n },\n // Keep overrideAccess: true for admin operations after verification\n })\n }\n \n // For new configs, ensure only one is active\n if (operation === 'create' && data?.active) {\n const existingActive = await req.payload.find({\n collection: slug,\n where: {\n active: {\n equals: true,\n },\n },\n // Keep overrideAccess: true for admin operations\n })\n \n if (existingActive.docs.length > 0) {\n // Deactivate existing active configs\n for (const doc of existingActive.docs) {\n await req.payload.update({\n collection: slug,\n id: doc.id,\n data: {\n active: false,\n },\n // Keep overrideAccess: true for admin operations\n })\n }\n }\n }\n \n return data\n },\n ],\n afterChange: [\n async ({ doc, req }) => {\n // Reinitialize email service when settings change\n if ((req.payload as any).newsletterEmailService && doc.active) {\n try {\n // TODO: Implement email service reinitialization\n console.warn('Newsletter settings updated, reinitializing service...')\n } catch {\n // Failed to reinitialize email service\n }\n }\n \n return doc\n },\n ],\n },\n access: {\n read: () => true, // Settings can be read publicly for validation\n create: adminOnly(pluginConfig),\n update: adminOnly(pluginConfig),\n delete: adminOnly(pluginConfig),\n },\n timestamps: true,\n }\n}"],"names":["adminOnly","createNewsletterSettingsCollection","pluginConfig","slug","settingsSlug","labels","singular","plural","admin","useAsTitle","defaultColumns","group","description","fields","name","type","label","required","defaultValue","tabs","options","value","providers","default","condition","data","provider","i18n","locales","map","locale","toUpperCase","emailTemplates","welcome","enabled","placeholder","min","hooks","beforeChange","req","operation","user","collection","Error","active","payload","update","where","id","not_equals","existingActive","find","equals","docs","length","doc","afterChange","newsletterEmailService","console","warn","access","read","create","delete","timestamps"],"mappings":"AAEA,SAASA,SAAS,QAAQ,kBAAiB;AAE3C,OAAO,MAAMC,qCAAqC,CAChDC;IAEA,MAAMC,OAAOD,aAAaE,YAAY,IAAI;IAE1C,OAAO;QACLD;QACAE,QAAQ;YACNC,UAAU;YACVC,QAAQ;QACV;QACAC,OAAO;YACLC,YAAY;YACZC,gBAAgB;gBAAC;gBAAQ;gBAAY;gBAAU;aAAY;YAC3DC,OAAO;YACPC,aAAa;QACf;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,OAAO;gBACPC,UAAU;gBACVT,OAAO;oBACLI,aAAa;gBACf;YACF;YACA;gBACEE,MAAM;gBACNC,MAAM;gBACNC,OAAO;gBACPE,cAAc;gBACdV,OAAO;oBACLI,aAAa;gBACf;YACF;YACA;gBACEG,MAAM;gBACNI,MAAM;oBACJ;wBACEH,OAAO;wBACPH,QAAQ;4BACN;gCACEC,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPC,UAAU;gCACVG,SAAS;oCACP;wCAAEJ,OAAO;wCAAUK,OAAO;oCAAS;oCACnC;wCAAEL,OAAO;wCAA2BK,OAAO;oCAAY;iCACxD;gCACDH,cAAchB,aAAaoB,SAAS,CAACC,OAAO;gCAC5Cf,OAAO;oCACLI,aAAa;gCACf;4BACF;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPR,OAAO;oCACLgB,WAAW,CAACC,OAASA,MAAMC,aAAa;gCAC1C;gCACAb,QAAQ;oCACN;wCACEC,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPC,UAAU;wCACVT,OAAO;4CACLI,aAAa;wCACf;oCACF;oCACA;wCACEE,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPH,QAAQ;4CACN;gDACEC,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPC,UAAU;gDACVG,SAASlB,aAAayB,IAAI,EAAEC,SAASC,IAAIC,CAAAA,SAAW,CAAA;wDAClDd,OAAOc,OAAOC,WAAW;wDACzBV,OAAOS;oDACT,CAAA,MAAO;oDACL;wDAAEd,OAAO;wDAAMK,OAAO;oDAAK;iDAC5B;4CACH;4CACA;gDACEP,MAAM;gDACNC,MAAM;gDACNC,OAAO;4CACT;4CACA;gDACEF,MAAM;gDACNC,MAAM;gDACNC,OAAO;4CACT;yCACD;oCACH;iCACD;4BACH;4BACA;gCACEF,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPR,OAAO;oCACLgB,WAAW,CAACC,OAASA,MAAMC,aAAa;gCAC1C;gCACAb,QAAQ;oCACN;wCACEC,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPC,UAAU;wCACVT,OAAO;4CACLI,aAAa;wCACf;oCACF;oCACA;wCACEE,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPR,OAAO;4CACLI,aAAa;wCACf;oCACF;oCACA;wCACEE,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPR,OAAO;4CACLI,aAAa;wCACf;oCACF;iCACD;4BACH;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPC,UAAU;gCACVT,OAAO;oCACLI,aAAa;gCACf;4BACF;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPC,UAAU;gCACVT,OAAO;oCACLI,aAAa;gCACf;4BACF;4BACA;gCACEE,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPR,OAAO;oCACLI,aAAa;gCACf;4BACF;yBACD;oBACH;oBACA;wBACEI,OAAO;wBACPH,QAAQ;4BACN;gCACEC,MAAM;gCACNC,MAAM;gCACNC,OAAO;gCACPH,QAAQ;oCACN;wCACEC,MAAM;wCACNC,MAAM;wCACNC,OAAO;wCACPH,QAAQ;4CACN;gDACEC,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPE,cAAc;4CAChB;4CACA;gDACEJ,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPE,cAAc;gDACdV,OAAO;oDACLgB,WAAW,CAACC,OAASA,MAAMO,gBAAgBC,SAASC;gDACtD;4CACF;4CACA;gDACEpB,MAAM;gDACNC,MAAM;gDACNC,OAAO;gDACPR,OAAO;oDACLgB,WAAW,CAACC,OAASA,MAAMO,gBAAgBC,SAASC;gDACtD;4CACF;yCACD;oCACH;oCACA;wCACEpB,MAAM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"}
|
|
@@ -231,16 +231,16 @@ export const createSubscribersCollection = (pluginConfig)=>{
|
|
|
231
231
|
if (emailService) {
|
|
232
232
|
try {
|
|
233
233
|
await emailService.addContact(doc);
|
|
234
|
-
} catch
|
|
235
|
-
|
|
234
|
+
} catch {
|
|
235
|
+
// Failed to add contact to email service
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
// Send welcome email if active
|
|
239
239
|
if (doc.subscriptionStatus === 'active' && emailService) {
|
|
240
240
|
try {
|
|
241
241
|
// TODO: Send welcome email
|
|
242
|
-
} catch
|
|
243
|
-
|
|
242
|
+
} catch {
|
|
243
|
+
// Failed to send welcome email
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
// Custom after subscribe hook
|
|
@@ -258,8 +258,8 @@ export const createSubscribersCollection = (pluginConfig)=>{
|
|
|
258
258
|
if (doc.subscriptionStatus !== previousDoc.subscriptionStatus && emailService) {
|
|
259
259
|
try {
|
|
260
260
|
await emailService.updateContact(doc);
|
|
261
|
-
} catch
|
|
262
|
-
|
|
261
|
+
} catch {
|
|
262
|
+
// Failed to update contact in email service
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
// Handle unsubscribe
|
|
@@ -288,8 +288,8 @@ export const createSubscribersCollection = (pluginConfig)=>{
|
|
|
288
288
|
id
|
|
289
289
|
});
|
|
290
290
|
await emailService.removeContact(doc.email);
|
|
291
|
-
} catch
|
|
292
|
-
|
|
291
|
+
} catch {
|
|
292
|
+
// Failed to remove contact from email service
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/collections/Subscribers.ts"],"sourcesContent":["import type { CollectionConfig, Field, CollectionAfterChangeHook, CollectionBeforeDeleteHook } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\nimport { adminOnly, adminOrSelf } from '../utils/access'\n\nexport const createSubscribersCollection = (\n pluginConfig: NewsletterPluginConfig\n): CollectionConfig => {\n const slug = pluginConfig.subscribersSlug || 'subscribers'\n \n // Default fields for the subscribers collection\n const defaultFields: Field[] = [\n // Core fields\n {\n name: 'email',\n type: 'email',\n required: true,\n unique: true,\n admin: {\n description: 'Subscriber email address',\n },\n },\n {\n name: 'name',\n type: 'text',\n admin: {\n description: 'Subscriber full name',\n },\n },\n {\n name: 'locale',\n type: 'select',\n options: pluginConfig.i18n?.locales?.map(locale => ({\n label: locale.toUpperCase(),\n value: locale,\n })) || [\n { label: 'EN', value: 'en' },\n ],\n defaultValue: pluginConfig.i18n?.defaultLocale || 'en',\n admin: {\n description: 'Preferred language for communications',\n },\n },\n \n // Authentication fields (hidden from admin UI)\n {\n name: 'magicLinkToken',\n type: 'text',\n hidden: true,\n },\n {\n name: 'magicLinkTokenExpiry',\n type: 'date',\n hidden: true,\n },\n \n // Subscription status\n {\n name: 'subscriptionStatus',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Unsubscribed', value: 'unsubscribed' },\n { label: 'Pending', value: 'pending' },\n ],\n defaultValue: 'pending',\n required: true,\n admin: {\n description: 'Current subscription status',\n },\n },\n {\n name: 'unsubscribedAt',\n type: 'date',\n admin: {\n condition: (data) => data?.subscriptionStatus === 'unsubscribed',\n description: 'When the user unsubscribed',\n readOnly: true,\n },\n },\n \n // Email preferences\n {\n name: 'emailPreferences',\n type: 'group',\n fields: [\n {\n name: 'newsletter',\n type: 'checkbox',\n defaultValue: true,\n label: 'Newsletter',\n admin: {\n description: 'Receive regular newsletter updates',\n },\n },\n {\n name: 'announcements',\n type: 'checkbox',\n defaultValue: true,\n label: 'Announcements',\n admin: {\n description: 'Receive important announcements',\n },\n },\n ],\n admin: {\n description: 'Email communication preferences',\n },\n },\n \n // Source tracking\n {\n name: 'source',\n type: 'text',\n admin: {\n description: 'Where the subscriber signed up from',\n },\n },\n ]\n\n // Add UTM tracking fields if enabled\n if (pluginConfig.features?.utmTracking?.enabled) {\n const utmFields = pluginConfig.features.utmTracking.fields || [\n 'source',\n 'medium',\n 'campaign',\n 'content',\n 'term',\n ]\n \n defaultFields.push({\n name: 'utmParameters',\n type: 'group',\n fields: utmFields.map(field => ({\n name: field,\n type: 'text',\n admin: {\n description: `UTM ${field} parameter`,\n },\n })),\n admin: {\n description: 'UTM tracking parameters',\n },\n })\n }\n\n // Add signup metadata\n defaultFields.push({\n name: 'signupMetadata',\n type: 'group',\n fields: [\n {\n name: 'ipAddress',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'userAgent',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'referrer',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'signupPage',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n ],\n admin: {\n description: 'Technical information about signup',\n },\n })\n\n // Add lead magnet field if enabled\n if (pluginConfig.features?.leadMagnets?.enabled) {\n defaultFields.push({\n name: 'leadMagnet',\n type: 'relationship',\n relationTo: pluginConfig.features.leadMagnets.collection || 'media',\n admin: {\n description: 'Lead magnet downloaded at signup',\n },\n })\n }\n\n // Allow field customization\n let fields = defaultFields\n if (pluginConfig.fields?.overrides) {\n fields = pluginConfig.fields.overrides({ defaultFields })\n }\n if (pluginConfig.fields?.additional) {\n fields = [...fields, ...pluginConfig.fields.additional]\n }\n\n const subscribersCollection: CollectionConfig = {\n slug,\n labels: {\n singular: 'Subscriber',\n plural: 'Subscribers',\n },\n admin: {\n useAsTitle: 'email',\n defaultColumns: ['email', 'name', 'subscriptionStatus', 'createdAt'],\n group: 'Newsletter',\n },\n fields,\n hooks: {\n afterChange: [\n async ({ doc, req, operation, previousDoc }) => {\n // After create logic\n if (operation === 'create') {\n // Add to email service\n const emailService = (req.payload as any).newsletterEmailService\n if (emailService) {\n try {\n await emailService.addContact(doc)\n } catch (error) {\n console.error('Failed to add contact to email service:', error)\n }\n }\n\n // Send welcome email if active\n if (doc.subscriptionStatus === 'active' && emailService) {\n try {\n // TODO: Send welcome email\n } catch (error) {\n console.error('Failed to send welcome email:', error)\n }\n }\n\n // Custom after subscribe hook\n if (pluginConfig.hooks?.afterSubscribe) {\n await pluginConfig.hooks.afterSubscribe({ doc, req })\n }\n }\n \n // After update logic\n if (operation === 'update' && previousDoc) {\n // Update email service if status changed\n const emailService = (req.payload as any).newsletterEmailService\n if (\n doc.subscriptionStatus !== previousDoc.subscriptionStatus &&\n emailService\n ) {\n try {\n await emailService.updateContact(doc)\n } catch (error) {\n console.error('Failed to update contact in email service:', error)\n }\n }\n\n // Handle unsubscribe\n if (\n doc.subscriptionStatus === 'unsubscribed' &&\n previousDoc.subscriptionStatus !== 'unsubscribed'\n ) {\n // Set unsubscribed timestamp\n doc.unsubscribedAt = new Date().toISOString()\n \n // Custom after unsubscribe hook\n if (pluginConfig.hooks?.afterUnsubscribe) {\n await pluginConfig.hooks.afterUnsubscribe({ doc, req })\n }\n }\n }\n },\n ] as CollectionAfterChangeHook[],\n beforeDelete: [\n async ({ id, req }) => {\n // Remove from email service\n const emailService = (req.payload as any).newsletterEmailService\n if (emailService) {\n try {\n const doc = await req.payload.findByID({\n collection: slug,\n id,\n })\n await emailService.removeContact(doc.email)\n } catch (error) {\n console.error('Failed to remove contact from email service:', error)\n }\n }\n },\n ] as CollectionBeforeDeleteHook[],\n },\n access: {\n create: () => true, // Public can subscribe\n read: adminOrSelf(pluginConfig),\n update: adminOrSelf(pluginConfig),\n delete: adminOnly(pluginConfig),\n },\n timestamps: true,\n }\n\n return subscribersCollection\n}"],"names":["adminOnly","adminOrSelf","createSubscribersCollection","pluginConfig","slug","subscribersSlug","defaultFields","name","type","required","unique","admin","description","options","i18n","locales","map","locale","label","toUpperCase","value","defaultValue","defaultLocale","hidden","condition","data","subscriptionStatus","readOnly","fields","features","utmTracking","enabled","utmFields","push","field","leadMagnets","relationTo","collection","overrides","additional","subscribersCollection","labels","singular","plural","useAsTitle","defaultColumns","group","hooks","afterChange","doc","req","operation","previousDoc","emailService","payload","newsletterEmailService","addContact","error","console","afterSubscribe","updateContact","unsubscribedAt","Date","toISOString","afterUnsubscribe","beforeDelete","id","findByID","removeContact","email","access","create","read","update","delete","timestamps"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/collections/Subscribers.ts"],"sourcesContent":["import type { CollectionConfig, Field, CollectionAfterChangeHook, CollectionBeforeDeleteHook } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\nimport { adminOnly, adminOrSelf } from '../utils/access'\n\nexport const createSubscribersCollection = (\n pluginConfig: NewsletterPluginConfig\n): CollectionConfig => {\n const slug = pluginConfig.subscribersSlug || 'subscribers'\n \n // Default fields for the subscribers collection\n const defaultFields: Field[] = [\n // Core fields\n {\n name: 'email',\n type: 'email',\n required: true,\n unique: true,\n admin: {\n description: 'Subscriber email address',\n },\n },\n {\n name: 'name',\n type: 'text',\n admin: {\n description: 'Subscriber full name',\n },\n },\n {\n name: 'locale',\n type: 'select',\n options: pluginConfig.i18n?.locales?.map(locale => ({\n label: locale.toUpperCase(),\n value: locale,\n })) || [\n { label: 'EN', value: 'en' },\n ],\n defaultValue: pluginConfig.i18n?.defaultLocale || 'en',\n admin: {\n description: 'Preferred language for communications',\n },\n },\n \n // Authentication fields (hidden from admin UI)\n {\n name: 'magicLinkToken',\n type: 'text',\n hidden: true,\n },\n {\n name: 'magicLinkTokenExpiry',\n type: 'date',\n hidden: true,\n },\n \n // Subscription status\n {\n name: 'subscriptionStatus',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Unsubscribed', value: 'unsubscribed' },\n { label: 'Pending', value: 'pending' },\n ],\n defaultValue: 'pending',\n required: true,\n admin: {\n description: 'Current subscription status',\n },\n },\n {\n name: 'unsubscribedAt',\n type: 'date',\n admin: {\n condition: (data) => data?.subscriptionStatus === 'unsubscribed',\n description: 'When the user unsubscribed',\n readOnly: true,\n },\n },\n \n // Email preferences\n {\n name: 'emailPreferences',\n type: 'group',\n fields: [\n {\n name: 'newsletter',\n type: 'checkbox',\n defaultValue: true,\n label: 'Newsletter',\n admin: {\n description: 'Receive regular newsletter updates',\n },\n },\n {\n name: 'announcements',\n type: 'checkbox',\n defaultValue: true,\n label: 'Announcements',\n admin: {\n description: 'Receive important announcements',\n },\n },\n ],\n admin: {\n description: 'Email communication preferences',\n },\n },\n \n // Source tracking\n {\n name: 'source',\n type: 'text',\n admin: {\n description: 'Where the subscriber signed up from',\n },\n },\n ]\n\n // Add UTM tracking fields if enabled\n if (pluginConfig.features?.utmTracking?.enabled) {\n const utmFields = pluginConfig.features.utmTracking.fields || [\n 'source',\n 'medium',\n 'campaign',\n 'content',\n 'term',\n ]\n \n defaultFields.push({\n name: 'utmParameters',\n type: 'group',\n fields: utmFields.map(field => ({\n name: field,\n type: 'text',\n admin: {\n description: `UTM ${field} parameter`,\n },\n })),\n admin: {\n description: 'UTM tracking parameters',\n },\n })\n }\n\n // Add signup metadata\n defaultFields.push({\n name: 'signupMetadata',\n type: 'group',\n fields: [\n {\n name: 'ipAddress',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'userAgent',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'referrer',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'signupPage',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n ],\n admin: {\n description: 'Technical information about signup',\n },\n })\n\n // Add lead magnet field if enabled\n if (pluginConfig.features?.leadMagnets?.enabled) {\n defaultFields.push({\n name: 'leadMagnet',\n type: 'relationship',\n relationTo: pluginConfig.features.leadMagnets.collection || 'media',\n admin: {\n description: 'Lead magnet downloaded at signup',\n },\n })\n }\n\n // Allow field customization\n let fields = defaultFields\n if (pluginConfig.fields?.overrides) {\n fields = pluginConfig.fields.overrides({ defaultFields })\n }\n if (pluginConfig.fields?.additional) {\n fields = [...fields, ...pluginConfig.fields.additional]\n }\n\n const subscribersCollection: CollectionConfig = {\n slug,\n labels: {\n singular: 'Subscriber',\n plural: 'Subscribers',\n },\n admin: {\n useAsTitle: 'email',\n defaultColumns: ['email', 'name', 'subscriptionStatus', 'createdAt'],\n group: 'Newsletter',\n },\n fields,\n hooks: {\n afterChange: [\n async ({ doc, req, operation, previousDoc }) => {\n // After create logic\n if (operation === 'create') {\n // Add to email service\n const emailService = (req.payload as any).newsletterEmailService\n if (emailService) {\n try {\n await emailService.addContact(doc)\n } catch {\n // Failed to add contact to email service\n }\n }\n\n // Send welcome email if active\n if (doc.subscriptionStatus === 'active' && emailService) {\n try {\n // TODO: Send welcome email\n } catch {\n // Failed to send welcome email\n }\n }\n\n // Custom after subscribe hook\n if (pluginConfig.hooks?.afterSubscribe) {\n await pluginConfig.hooks.afterSubscribe({ doc, req })\n }\n }\n \n // After update logic\n if (operation === 'update' && previousDoc) {\n // Update email service if status changed\n const emailService = (req.payload as any).newsletterEmailService\n if (\n doc.subscriptionStatus !== previousDoc.subscriptionStatus &&\n emailService\n ) {\n try {\n await emailService.updateContact(doc)\n } catch {\n // Failed to update contact in email service\n }\n }\n\n // Handle unsubscribe\n if (\n doc.subscriptionStatus === 'unsubscribed' &&\n previousDoc.subscriptionStatus !== 'unsubscribed'\n ) {\n // Set unsubscribed timestamp\n doc.unsubscribedAt = new Date().toISOString()\n \n // Custom after unsubscribe hook\n if (pluginConfig.hooks?.afterUnsubscribe) {\n await pluginConfig.hooks.afterUnsubscribe({ doc, req })\n }\n }\n }\n },\n ] as CollectionAfterChangeHook[],\n beforeDelete: [\n async ({ id, req }) => {\n // Remove from email service\n const emailService = (req.payload as any).newsletterEmailService\n if (emailService) {\n try {\n const doc = await req.payload.findByID({\n collection: slug,\n id,\n })\n await emailService.removeContact(doc.email)\n } catch {\n // Failed to remove contact from email service\n }\n }\n },\n ] as CollectionBeforeDeleteHook[],\n },\n access: {\n create: () => true, // Public can subscribe\n read: adminOrSelf(pluginConfig),\n update: adminOrSelf(pluginConfig),\n delete: adminOnly(pluginConfig),\n },\n timestamps: true,\n }\n\n return subscribersCollection\n}"],"names":["adminOnly","adminOrSelf","createSubscribersCollection","pluginConfig","slug","subscribersSlug","defaultFields","name","type","required","unique","admin","description","options","i18n","locales","map","locale","label","toUpperCase","value","defaultValue","defaultLocale","hidden","condition","data","subscriptionStatus","readOnly","fields","features","utmTracking","enabled","utmFields","push","field","leadMagnets","relationTo","collection","overrides","additional","subscribersCollection","labels","singular","plural","useAsTitle","defaultColumns","group","hooks","afterChange","doc","req","operation","previousDoc","emailService","payload","newsletterEmailService","addContact","afterSubscribe","updateContact","unsubscribedAt","Date","toISOString","afterUnsubscribe","beforeDelete","id","findByID","removeContact","email","access","create","read","update","delete","timestamps"],"mappings":"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"}
|
|
@@ -6,9 +6,11 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
6
6
|
handler: async (req, res)=>{
|
|
7
7
|
try {
|
|
8
8
|
const { email, name, source, preferences, leadMagnet, surveyResponses, metadata = {} } = req.body;
|
|
9
|
+
// Trim email before validation
|
|
10
|
+
const trimmedEmail = email?.trim();
|
|
9
11
|
// Validate input
|
|
10
12
|
const validation = validateSubscriberData({
|
|
11
|
-
email,
|
|
13
|
+
email: trimmedEmail,
|
|
12
14
|
name,
|
|
13
15
|
source
|
|
14
16
|
});
|
|
@@ -31,8 +33,8 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
31
33
|
overrideAccess: false
|
|
32
34
|
});
|
|
33
35
|
const settings = settingsResult.docs[0];
|
|
34
|
-
const allowedDomains = settings?.allowedDomains?.map((d)=>d.domain) || [];
|
|
35
|
-
if (!isDomainAllowed(
|
|
36
|
+
const allowedDomains = settings?.subscriptionSettings?.allowedDomains?.map((d)=>d.domain) || [];
|
|
37
|
+
if (!isDomainAllowed(trimmedEmail, allowedDomains)) {
|
|
36
38
|
return res.status(400).json({
|
|
37
39
|
success: false,
|
|
38
40
|
error: 'Email domain not allowed'
|
|
@@ -44,9 +46,10 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
44
46
|
collection: config.subscribersSlug || 'subscribers',
|
|
45
47
|
where: {
|
|
46
48
|
email: {
|
|
47
|
-
equals:
|
|
49
|
+
equals: trimmedEmail.toLowerCase()
|
|
48
50
|
}
|
|
49
|
-
}
|
|
51
|
+
},
|
|
52
|
+
overrideAccess: true
|
|
50
53
|
});
|
|
51
54
|
if (existing.docs.length > 0) {
|
|
52
55
|
const subscriber = existing.docs[0];
|
|
@@ -69,14 +72,15 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
69
72
|
}
|
|
70
73
|
// Check IP rate limiting
|
|
71
74
|
const ipAddress = req.ip || req.connection.remoteAddress;
|
|
72
|
-
const maxPerIP = settings?.maxSubscribersPerIP || 10;
|
|
75
|
+
const maxPerIP = settings?.subscriptionSettings?.maxSubscribersPerIP || 10;
|
|
73
76
|
const ipSubscribers = await req.payload.find({
|
|
74
77
|
collection: config.subscribersSlug || 'subscribers',
|
|
75
78
|
where: {
|
|
76
79
|
'signupMetadata.ipAddress': {
|
|
77
80
|
equals: ipAddress
|
|
78
81
|
}
|
|
79
|
-
}
|
|
82
|
+
},
|
|
83
|
+
overrideAccess: true
|
|
80
84
|
});
|
|
81
85
|
if (ipSubscribers.docs.length >= maxPerIP) {
|
|
82
86
|
return res.status(429).json({
|
|
@@ -86,13 +90,20 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
86
90
|
}
|
|
87
91
|
// Extract UTM parameters
|
|
88
92
|
const referer = req.headers.referer || req.headers.referrer || '';
|
|
89
|
-
|
|
93
|
+
let utmParams = {};
|
|
94
|
+
if (referer) {
|
|
95
|
+
try {
|
|
96
|
+
utmParams = extractUTMParams(new URL(referer).searchParams);
|
|
97
|
+
} catch {
|
|
98
|
+
// Invalid URL, ignore UTM params
|
|
99
|
+
}
|
|
100
|
+
}
|
|
90
101
|
// Prepare subscriber data
|
|
91
102
|
const subscriberData = {
|
|
92
|
-
email:
|
|
103
|
+
email: trimmedEmail.toLowerCase(),
|
|
93
104
|
name: name ? sanitizeInput(name) : undefined,
|
|
94
105
|
locale: metadata.locale || config.i18n?.defaultLocale || 'en',
|
|
95
|
-
subscriptionStatus: settings?.requireDoubleOptIn ? 'pending' : 'active',
|
|
106
|
+
subscriptionStatus: settings?.subscriptionSettings?.requireDoubleOptIn ? 'pending' : 'active',
|
|
96
107
|
source: source || 'api',
|
|
97
108
|
emailPreferences: {
|
|
98
109
|
newsletter: true,
|
|
@@ -118,14 +129,15 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
118
129
|
// Public endpoint needs to create subscribers
|
|
119
130
|
const subscriber = await req.payload.create({
|
|
120
131
|
collection: config.subscribersSlug || 'subscribers',
|
|
121
|
-
data: subscriberData
|
|
132
|
+
data: subscriberData,
|
|
133
|
+
overrideAccess: true
|
|
122
134
|
});
|
|
123
135
|
// Handle survey responses if provided
|
|
124
136
|
if (config.features?.surveys?.enabled && surveyResponses) {
|
|
125
137
|
// TODO: Store survey responses
|
|
126
138
|
}
|
|
127
139
|
// Send confirmation email if double opt-in
|
|
128
|
-
if (settings?.requireDoubleOptIn) {
|
|
140
|
+
if (settings?.subscriptionSettings?.requireDoubleOptIn) {
|
|
129
141
|
// TODO: Send confirmation email with magic link
|
|
130
142
|
}
|
|
131
143
|
res.json({
|
|
@@ -135,10 +147,9 @@ export const createSubscribeEndpoint = (config)=>{
|
|
|
135
147
|
email: subscriber.email,
|
|
136
148
|
subscriptionStatus: subscriber.subscriptionStatus
|
|
137
149
|
},
|
|
138
|
-
message: settings?.requireDoubleOptIn ? 'Please check your email to confirm your subscription' : 'Successfully subscribed'
|
|
150
|
+
message: settings?.subscriptionSettings?.requireDoubleOptIn ? 'Please check your email to confirm your subscription' : 'Successfully subscribed'
|
|
139
151
|
});
|
|
140
|
-
} catch
|
|
141
|
-
console.error('Subscribe endpoint error:', error);
|
|
152
|
+
} catch {
|
|
142
153
|
res.status(500).json({
|
|
143
154
|
success: false,
|
|
144
155
|
error: 'Failed to subscribe. Please try again.'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/endpoints/subscribe.ts"],"sourcesContent":["import type { Endpoint, PayloadHandler } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\nimport { \n isDomainAllowed, \n sanitizeInput, \n validateSubscriberData,\n extractUTMParams \n} from '../utils/validation'\n\nexport const createSubscribeEndpoint = (\n config: NewsletterPluginConfig\n): Endpoint => {\n return {\n path: '/newsletter/subscribe',\n method: 'post',\n handler: (async (req: any, res: any) => {\n try {\n const { \n email, \n name, \n source,\n preferences,\n leadMagnet,\n surveyResponses,\n metadata = {}\n } = req.body\n\n // Validate input\n const validation = validateSubscriberData({ email, name, source })\n if (!validation.valid) {\n return res.status(400).json({\n success: false,\n errors: validation.errors,\n })\n }\n\n // Check domain restrictions from active settings\n // Settings are public info needed for validation, but we can still respect access control\n const settingsResult = await req.payload.find({\n collection: config.settingsSlug || 'newsletter-settings',\n where: {\n active: {\n equals: true,\n },\n },\n limit: 1,\n overrideAccess: false,\n // No user context for public endpoint\n })\n \n const settings = settingsResult.docs[0]\n\n const allowedDomains = settings?.allowedDomains?.map((d: any) => d.domain) || []\n if (!isDomainAllowed(email, allowedDomains)) {\n return res.status(400).json({\n success: false,\n error: 'Email domain not allowed',\n })\n }\n\n // Check if already subscribed\n // This needs admin access to check for existing email\n const existing = await req.payload.find({\n collection: config.subscribersSlug || 'subscribers',\n where: {\n email: {\n equals: email.toLowerCase(),\n },\n },\n // Keep overrideAccess: true for public subscription check\n })\n\n if (existing.docs.length > 0) {\n const subscriber = existing.docs[0]\n \n // If unsubscribed, don't allow resubscription via API\n if (subscriber.subscriptionStatus === 'unsubscribed') {\n return res.status(400).json({\n success: false,\n error: 'This email has been unsubscribed. Please contact support to resubscribe.',\n })\n }\n\n return res.status(400).json({\n success: false,\n error: 'Already subscribed',\n subscriber: {\n id: subscriber.id,\n email: subscriber.email,\n subscriptionStatus: subscriber.subscriptionStatus,\n },\n })\n }\n\n // Check IP rate limiting\n const ipAddress = req.ip || req.connection.remoteAddress\n const maxPerIP = settings?.maxSubscribersPerIP || 10\n\n const ipSubscribers = await req.payload.find({\n collection: config.subscribersSlug || 'subscribers',\n where: {\n 'signupMetadata.ipAddress': {\n equals: ipAddress,\n },\n },\n // Keep overrideAccess: true for rate limiting check\n })\n\n if (ipSubscribers.docs.length >= maxPerIP) {\n return res.status(429).json({\n success: false,\n error: 'Too many subscriptions from this IP address',\n })\n }\n\n // Extract UTM parameters\n const referer = req.headers.referer || req.headers.referrer || ''\n const utmParams = extractUTMParams(new URL(referer).searchParams)\n\n // Prepare subscriber data\n const subscriberData: any = {\n email: email.toLowerCase(),\n name: name ? sanitizeInput(name) : undefined,\n locale: metadata.locale || config.i18n?.defaultLocale || 'en',\n subscriptionStatus: settings?.requireDoubleOptIn ? 'pending' : 'active',\n source: source || 'api',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n ...(preferences || {}),\n },\n signupMetadata: {\n ipAddress,\n userAgent: req.headers['user-agent'],\n referrer: referer,\n signupPage: metadata.signupPage || referer,\n },\n }\n\n // Add UTM parameters if tracking is enabled\n if (config.features?.utmTracking?.enabled && Object.keys(utmParams).length > 0) {\n subscriberData.utmParameters = utmParams\n }\n\n // Add lead magnet if provided\n if (config.features?.leadMagnets?.enabled && leadMagnet) {\n subscriberData.leadMagnet = leadMagnet\n }\n\n // Create subscriber\n // Public endpoint needs to create subscribers\n const subscriber = await req.payload.create({\n collection: config.subscribersSlug || 'subscribers',\n data: subscriberData,\n // Keep overrideAccess: true for public subscription\n })\n\n // Handle survey responses if provided\n if (config.features?.surveys?.enabled && surveyResponses) {\n // TODO: Store survey responses\n }\n\n // Send confirmation email if double opt-in\n if (settings?.requireDoubleOptIn) {\n // TODO: Send confirmation email with magic link\n }\n\n res.json({\n success: true,\n subscriber: {\n id: subscriber.id,\n email: subscriber.email,\n subscriptionStatus: subscriber.subscriptionStatus,\n },\n message: settings?.requireDoubleOptIn \n ? 'Please check your email to confirm your subscription'\n : 'Successfully subscribed',\n })\n } catch (error) {\n console.error('Subscribe endpoint error:', error)\n res.status(500).json({\n success: false,\n error: 'Failed to subscribe. Please try again.',\n })\n }\n }) as PayloadHandler,\n }\n}"],"names":["isDomainAllowed","sanitizeInput","validateSubscriberData","extractUTMParams","createSubscribeEndpoint","config","path","method","handler","req","res","email","name","source","preferences","leadMagnet","surveyResponses","metadata","body","validation","valid","status","json","success","errors","settingsResult","payload","find","collection","settingsSlug","where","active","equals","limit","overrideAccess","settings","docs","allowedDomains","map","d","domain","error","existing","subscribersSlug","toLowerCase","length","subscriber","subscriptionStatus","id","ipAddress","ip","connection","remoteAddress","maxPerIP","maxSubscribersPerIP","ipSubscribers","referer","headers","referrer","utmParams","URL","searchParams","subscriberData","undefined","locale","i18n","defaultLocale","requireDoubleOptIn","emailPreferences","newsletter","announcements","signupMetadata","userAgent","signupPage","features","utmTracking","enabled","Object","keys","utmParameters","leadMagnets","create","data","surveys","message","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/endpoints/subscribe.ts"],"sourcesContent":["import type { Endpoint, PayloadHandler } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\nimport { \n isDomainAllowed, \n sanitizeInput, \n validateSubscriberData,\n extractUTMParams \n} from '../utils/validation'\n\nexport const createSubscribeEndpoint = (\n config: NewsletterPluginConfig\n): Endpoint => {\n return {\n path: '/newsletter/subscribe',\n method: 'post',\n handler: (async (req: any, res: any) => {\n try {\n const { \n email, \n name, \n source,\n preferences,\n leadMagnet,\n surveyResponses,\n metadata = {}\n } = req.body\n\n // Trim email before validation\n const trimmedEmail = email?.trim()\n\n // Validate input\n const validation = validateSubscriberData({ email: trimmedEmail, name, source })\n if (!validation.valid) {\n return res.status(400).json({\n success: false,\n errors: validation.errors,\n })\n }\n\n // Check domain restrictions from active settings\n // Settings are public info needed for validation, but we can still respect access control\n const settingsResult = await req.payload.find({\n collection: config.settingsSlug || 'newsletter-settings',\n where: {\n active: {\n equals: true,\n },\n },\n limit: 1,\n overrideAccess: false,\n // No user context for public endpoint\n })\n \n const settings = settingsResult.docs[0]\n\n const allowedDomains = settings?.subscriptionSettings?.allowedDomains?.map((d: any) => d.domain) || []\n if (!isDomainAllowed(trimmedEmail, allowedDomains)) {\n return res.status(400).json({\n success: false,\n error: 'Email domain not allowed',\n })\n }\n\n // Check if already subscribed\n // This needs admin access to check for existing email\n const existing = await req.payload.find({\n collection: config.subscribersSlug || 'subscribers',\n where: {\n email: {\n equals: trimmedEmail.toLowerCase(),\n },\n },\n overrideAccess: true, // Need to check for duplicates in public endpoint\n })\n\n if (existing.docs.length > 0) {\n const subscriber = existing.docs[0]\n \n // If unsubscribed, don't allow resubscription via API\n if (subscriber.subscriptionStatus === 'unsubscribed') {\n return res.status(400).json({\n success: false,\n error: 'This email has been unsubscribed. Please contact support to resubscribe.',\n })\n }\n\n return res.status(400).json({\n success: false,\n error: 'Already subscribed',\n subscriber: {\n id: subscriber.id,\n email: subscriber.email,\n subscriptionStatus: subscriber.subscriptionStatus,\n },\n })\n }\n\n // Check IP rate limiting\n const ipAddress = req.ip || req.connection.remoteAddress\n const maxPerIP = settings?.subscriptionSettings?.maxSubscribersPerIP || 10\n\n const ipSubscribers = await req.payload.find({\n collection: config.subscribersSlug || 'subscribers',\n where: {\n 'signupMetadata.ipAddress': {\n equals: ipAddress,\n },\n },\n overrideAccess: true, // Need to check IP limits in public endpoint\n })\n\n if (ipSubscribers.docs.length >= maxPerIP) {\n return res.status(429).json({\n success: false,\n error: 'Too many subscriptions from this IP address',\n })\n }\n\n // Extract UTM parameters\n const referer = req.headers.referer || req.headers.referrer || ''\n let utmParams = {}\n if (referer) {\n try {\n utmParams = extractUTMParams(new URL(referer).searchParams)\n } catch {\n // Invalid URL, ignore UTM params\n }\n }\n\n // Prepare subscriber data\n const subscriberData: any = {\n email: trimmedEmail.toLowerCase(),\n name: name ? sanitizeInput(name) : undefined,\n locale: metadata.locale || config.i18n?.defaultLocale || 'en',\n subscriptionStatus: settings?.subscriptionSettings?.requireDoubleOptIn ? 'pending' : 'active',\n source: source || 'api',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n ...(preferences || {}),\n },\n signupMetadata: {\n ipAddress,\n userAgent: req.headers['user-agent'],\n referrer: referer,\n signupPage: metadata.signupPage || referer,\n },\n }\n\n // Add UTM parameters if tracking is enabled\n if (config.features?.utmTracking?.enabled && Object.keys(utmParams).length > 0) {\n subscriberData.utmParameters = utmParams\n }\n\n // Add lead magnet if provided\n if (config.features?.leadMagnets?.enabled && leadMagnet) {\n subscriberData.leadMagnet = leadMagnet\n }\n\n // Create subscriber\n // Public endpoint needs to create subscribers\n const subscriber = await req.payload.create({\n collection: config.subscribersSlug || 'subscribers',\n data: subscriberData,\n overrideAccess: true, // Public endpoint needs to create subscribers\n })\n\n // Handle survey responses if provided\n if (config.features?.surveys?.enabled && surveyResponses) {\n // TODO: Store survey responses\n }\n\n // Send confirmation email if double opt-in\n if (settings?.subscriptionSettings?.requireDoubleOptIn) {\n // TODO: Send confirmation email with magic link\n }\n\n res.json({\n success: true,\n subscriber: {\n id: subscriber.id,\n email: subscriber.email,\n subscriptionStatus: subscriber.subscriptionStatus,\n },\n message: settings?.subscriptionSettings?.requireDoubleOptIn \n ? 'Please check your email to confirm your subscription'\n : 'Successfully subscribed',\n })\n } catch {\n res.status(500).json({\n success: false,\n error: 'Failed to subscribe. Please try again.',\n })\n }\n }) as PayloadHandler,\n }\n}"],"names":["isDomainAllowed","sanitizeInput","validateSubscriberData","extractUTMParams","createSubscribeEndpoint","config","path","method","handler","req","res","email","name","source","preferences","leadMagnet","surveyResponses","metadata","body","trimmedEmail","trim","validation","valid","status","json","success","errors","settingsResult","payload","find","collection","settingsSlug","where","active","equals","limit","overrideAccess","settings","docs","allowedDomains","subscriptionSettings","map","d","domain","error","existing","subscribersSlug","toLowerCase","length","subscriber","subscriptionStatus","id","ipAddress","ip","connection","remoteAddress","maxPerIP","maxSubscribersPerIP","ipSubscribers","referer","headers","referrer","utmParams","URL","searchParams","subscriberData","undefined","locale","i18n","defaultLocale","requireDoubleOptIn","emailPreferences","newsletter","announcements","signupMetadata","userAgent","signupPage","features","utmTracking","enabled","Object","keys","utmParameters","leadMagnets","create","data","surveys","message"],"mappings":"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"}
|
|
@@ -174,8 +174,7 @@ export function createNewsletterSchedulingFields(config) {
|
|
|
174
174
|
return convertLexicalToMarkdown({
|
|
175
175
|
data: data[config.richTextField]
|
|
176
176
|
});
|
|
177
|
-
} catch
|
|
178
|
-
console.error('Failed to convert rich text to markdown:', error);
|
|
177
|
+
} catch {
|
|
179
178
|
return '';
|
|
180
179
|
}
|
|
181
180
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/fields/newsletterScheduling.ts"],"sourcesContent":["import type { Field } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\n\nexport function createNewsletterSchedulingFields(\n config: NewsletterPluginConfig\n): Field[] {\n const groupName = config.features?.newsletterScheduling?.fields?.groupName || 'newsletterScheduling'\n const contentField = config.features?.newsletterScheduling?.fields?.contentField || 'content'\n const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false\n\n const fields: Field[] = [\n {\n name: groupName,\n type: 'group',\n label: 'Newsletter Scheduling',\n admin: {\n condition: (data, { user }) => user?.collection === 'users', // Only show for admin users\n },\n fields: [\n {\n name: 'scheduled',\n type: 'checkbox',\n label: 'Schedule for Newsletter',\n defaultValue: false,\n admin: {\n description: 'Schedule this content to be sent as a newsletter',\n },\n },\n {\n name: 'scheduledDate',\n type: 'date',\n label: 'Send Date',\n required: true,\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'When to send this newsletter',\n },\n },\n {\n name: 'sentDate',\n type: 'date',\n label: 'Sent Date',\n admin: {\n readOnly: true,\n condition: (data) => data?.[groupName]?.sendStatus === 'sent',\n description: 'When this newsletter was sent',\n },\n },\n {\n name: 'sendStatus',\n type: 'select',\n label: 'Status',\n options: [\n { label: 'Draft', value: 'draft' },\n { label: 'Scheduled', value: 'scheduled' },\n { label: 'Sending', value: 'sending' },\n { label: 'Sent', value: 'sent' },\n { label: 'Failed', value: 'failed' },\n ],\n defaultValue: 'draft',\n admin: {\n readOnly: true,\n description: 'Current send status',\n },\n },\n {\n name: 'emailSubject',\n type: 'text',\n label: 'Email Subject',\n required: true,\n admin: {\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'Subject line for the newsletter email',\n },\n },\n {\n name: 'preheader',\n type: 'text',\n label: 'Email Preheader',\n admin: {\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'Preview text that appears after the subject line',\n },\n },\n {\n name: 'segments',\n type: 'select',\n label: 'Target Segments',\n hasMany: true,\n options: [\n { label: 'All Subscribers', value: 'all' },\n ...(config.i18n?.locales?.map(locale => ({\n label: `${locale.toUpperCase()} Subscribers`,\n value: locale,\n })) || []),\n ],\n defaultValue: ['all'],\n admin: {\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'Which subscriber segments to send to',\n },\n },\n {\n name: 'testEmails',\n type: 'array',\n label: 'Test Email Recipients',\n admin: {\n condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === 'draft',\n description: 'Send test emails before scheduling',\n },\n fields: [\n {\n name: 'email',\n type: 'email',\n required: true,\n },\n ],\n },\n ],\n },\n ]\n\n // Add markdown companion field if requested\n if (createMarkdownField) {\n fields.push(createMarkdownFieldInternal({\n name: `${contentField}Markdown`,\n richTextField: contentField,\n label: 'Email Content (Markdown)',\n admin: {\n position: 'sidebar',\n condition: (data: any) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),\n description: 'Markdown version for email rendering',\n readOnly: true,\n },\n }))\n }\n\n return fields\n}\n\n/**\n * Create a markdown companion field for rich text\n * This creates a virtual field that converts rich text to markdown\n */\nfunction createMarkdownFieldInternal(config: {\n name: string\n richTextField: string\n label?: string\n admin?: any\n}): Field {\n return {\n name: config.name,\n type: 'textarea',\n label: config.label || 'Markdown',\n admin: {\n ...config.admin,\n description: config.admin?.description || 'Auto-generated from rich text content',\n },\n hooks: {\n afterRead: [\n async ({ data }) => {\n // Convert rich text to markdown on read\n if (data?.[config.richTextField]) {\n try {\n const { convertLexicalToMarkdown } = await import('@payloadcms/richtext-lexical')\n return convertLexicalToMarkdown({\n data: data[config.richTextField],\n } as any)\n } catch
|
|
1
|
+
{"version":3,"sources":["../../../src/fields/newsletterScheduling.ts"],"sourcesContent":["import type { Field } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\n\nexport function createNewsletterSchedulingFields(\n config: NewsletterPluginConfig\n): Field[] {\n const groupName = config.features?.newsletterScheduling?.fields?.groupName || 'newsletterScheduling'\n const contentField = config.features?.newsletterScheduling?.fields?.contentField || 'content'\n const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false\n\n const fields: Field[] = [\n {\n name: groupName,\n type: 'group',\n label: 'Newsletter Scheduling',\n admin: {\n condition: (data, { user }) => user?.collection === 'users', // Only show for admin users\n },\n fields: [\n {\n name: 'scheduled',\n type: 'checkbox',\n label: 'Schedule for Newsletter',\n defaultValue: false,\n admin: {\n description: 'Schedule this content to be sent as a newsletter',\n },\n },\n {\n name: 'scheduledDate',\n type: 'date',\n label: 'Send Date',\n required: true,\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'When to send this newsletter',\n },\n },\n {\n name: 'sentDate',\n type: 'date',\n label: 'Sent Date',\n admin: {\n readOnly: true,\n condition: (data) => data?.[groupName]?.sendStatus === 'sent',\n description: 'When this newsletter was sent',\n },\n },\n {\n name: 'sendStatus',\n type: 'select',\n label: 'Status',\n options: [\n { label: 'Draft', value: 'draft' },\n { label: 'Scheduled', value: 'scheduled' },\n { label: 'Sending', value: 'sending' },\n { label: 'Sent', value: 'sent' },\n { label: 'Failed', value: 'failed' },\n ],\n defaultValue: 'draft',\n admin: {\n readOnly: true,\n description: 'Current send status',\n },\n },\n {\n name: 'emailSubject',\n type: 'text',\n label: 'Email Subject',\n required: true,\n admin: {\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'Subject line for the newsletter email',\n },\n },\n {\n name: 'preheader',\n type: 'text',\n label: 'Email Preheader',\n admin: {\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'Preview text that appears after the subject line',\n },\n },\n {\n name: 'segments',\n type: 'select',\n label: 'Target Segments',\n hasMany: true,\n options: [\n { label: 'All Subscribers', value: 'all' },\n ...(config.i18n?.locales?.map(locale => ({\n label: `${locale.toUpperCase()} Subscribers`,\n value: locale,\n })) || []),\n ],\n defaultValue: ['all'],\n admin: {\n condition: (data) => data?.[groupName]?.scheduled,\n description: 'Which subscriber segments to send to',\n },\n },\n {\n name: 'testEmails',\n type: 'array',\n label: 'Test Email Recipients',\n admin: {\n condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === 'draft',\n description: 'Send test emails before scheduling',\n },\n fields: [\n {\n name: 'email',\n type: 'email',\n required: true,\n },\n ],\n },\n ],\n },\n ]\n\n // Add markdown companion field if requested\n if (createMarkdownField) {\n fields.push(createMarkdownFieldInternal({\n name: `${contentField}Markdown`,\n richTextField: contentField,\n label: 'Email Content (Markdown)',\n admin: {\n position: 'sidebar',\n condition: (data: any) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),\n description: 'Markdown version for email rendering',\n readOnly: true,\n },\n }))\n }\n\n return fields\n}\n\n/**\n * Create a markdown companion field for rich text\n * This creates a virtual field that converts rich text to markdown\n */\nfunction createMarkdownFieldInternal(config: {\n name: string\n richTextField: string\n label?: string\n admin?: any\n}): Field {\n return {\n name: config.name,\n type: 'textarea',\n label: config.label || 'Markdown',\n admin: {\n ...config.admin,\n description: config.admin?.description || 'Auto-generated from rich text content',\n },\n hooks: {\n afterRead: [\n async ({ data }) => {\n // Convert rich text to markdown on read\n if (data?.[config.richTextField]) {\n try {\n const { convertLexicalToMarkdown } = await import('@payloadcms/richtext-lexical')\n return convertLexicalToMarkdown({\n data: data[config.richTextField],\n } as any)\n } catch {\n return ''\n }\n }\n return ''\n },\n ],\n beforeChange: [\n () => {\n // Don't save markdown to database\n return null\n },\n ],\n },\n }\n}"],"names":["createNewsletterSchedulingFields","config","groupName","features","newsletterScheduling","fields","contentField","createMarkdownField","name","type","label","admin","condition","data","user","collection","defaultValue","description","required","date","pickerAppearance","scheduled","readOnly","sendStatus","options","value","hasMany","i18n","locales","map","locale","toUpperCase","push","createMarkdownFieldInternal","richTextField","position","Boolean","hooks","afterRead","convertLexicalToMarkdown","beforeChange"],"mappings":"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"}
|
package/dist/src/utils/access.js
CHANGED
|
@@ -37,16 +37,40 @@
|
|
|
37
37
|
* Create admin or owner access control
|
|
38
38
|
*/ export const adminOrSelf = (config)=>({ req, id })=>{
|
|
39
39
|
const user = req.user;
|
|
40
|
+
// No user = no access
|
|
41
|
+
if (!user) {
|
|
42
|
+
// For list operations without ID, return impossible condition
|
|
43
|
+
if (!id) {
|
|
44
|
+
return {
|
|
45
|
+
id: {
|
|
46
|
+
equals: 'unauthorized-no-access'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
40
52
|
// Admins can access everything
|
|
41
53
|
if (isAdmin(user, config)) {
|
|
42
54
|
return true;
|
|
43
55
|
}
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
// Synthetic users (subscribers from magic link) can access their own data
|
|
57
|
+
if (user.collection === 'subscribers') {
|
|
58
|
+
// For list operations, scope to their own data
|
|
59
|
+
if (!id) {
|
|
60
|
+
return {
|
|
61
|
+
id: {
|
|
62
|
+
equals: user.id
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// For specific document access, check if it's their own
|
|
67
|
+
return id === user.id;
|
|
68
|
+
}
|
|
69
|
+
// Regular users cannot access subscriber data
|
|
70
|
+
if (!id) {
|
|
47
71
|
return {
|
|
48
72
|
id: {
|
|
49
|
-
equals:
|
|
73
|
+
equals: 'unauthorized-no-access'
|
|
50
74
|
}
|
|
51
75
|
};
|
|
52
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/access.ts"],"sourcesContent":["import type { Access, AccessArgs } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\n\n/**\n * Check if a user is an admin based on the plugin configuration\n */\nexport const isAdmin = (user: any, config?: NewsletterPluginConfig): boolean => {\n if (!user || user.collection !== 'users') {\n return false\n }\n\n // If custom admin check is provided, use it\n if (config?.access?.isAdmin) {\n return config.access.isAdmin(user)\n }\n\n // Default checks for common admin patterns\n // 1. Check for admin role\n if (user.roles?.includes('admin')) {\n return true\n }\n\n // 2. Check for isAdmin boolean field\n if (user.isAdmin === true) {\n return true\n }\n\n // 3. Check for role field with admin value\n if (user.role === 'admin') {\n return true\n }\n\n // 4. Check for admin collection relationship\n if (user.admin === true) {\n return true\n }\n\n return false\n}\n\n/**\n * Create admin-only access control\n */\nexport const adminOnly = (config?: NewsletterPluginConfig): Access => \n ({ req }: AccessArgs) => {\n const user = req.user\n return isAdmin(user, config)\n }\n\n/**\n * Create admin or owner access control\n */\nexport const adminOrSelf = (config?: NewsletterPluginConfig): Access => \n ({ req, id }: AccessArgs) => {\n const user = req.user\n \n // Admins can access everything\n if (isAdmin(user, config)) {\n return true\n }\n \n //
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/access.ts"],"sourcesContent":["import type { Access, AccessArgs } from 'payload'\nimport type { NewsletterPluginConfig } from '../types'\n\n/**\n * Check if a user is an admin based on the plugin configuration\n */\nexport const isAdmin = (user: any, config?: NewsletterPluginConfig): boolean => {\n if (!user || user.collection !== 'users') {\n return false\n }\n\n // If custom admin check is provided, use it\n if (config?.access?.isAdmin) {\n return config.access.isAdmin(user)\n }\n\n // Default checks for common admin patterns\n // 1. Check for admin role\n if (user.roles?.includes('admin')) {\n return true\n }\n\n // 2. Check for isAdmin boolean field\n if (user.isAdmin === true) {\n return true\n }\n\n // 3. Check for role field with admin value\n if (user.role === 'admin') {\n return true\n }\n\n // 4. Check for admin collection relationship\n if (user.admin === true) {\n return true\n }\n\n return false\n}\n\n/**\n * Create admin-only access control\n */\nexport const adminOnly = (config?: NewsletterPluginConfig): Access => \n ({ req }: AccessArgs) => {\n const user = req.user\n return isAdmin(user, config)\n }\n\n/**\n * Create admin or owner access control\n */\nexport const adminOrSelf = (config?: NewsletterPluginConfig): Access => \n ({ req, id }: AccessArgs) => {\n const user = req.user\n \n // No user = no access\n if (!user) {\n // For list operations without ID, return impossible condition\n if (!id) {\n return {\n id: {\n equals: 'unauthorized-no-access',\n },\n }\n }\n return false\n }\n \n // Admins can access everything\n if (isAdmin(user, config)) {\n return true\n }\n \n // Synthetic users (subscribers from magic link) can access their own data\n if (user.collection === 'subscribers') {\n // For list operations, scope to their own data\n if (!id) {\n return {\n id: {\n equals: user.id,\n },\n }\n }\n // For specific document access, check if it's their own\n return id === user.id\n }\n \n // Regular users cannot access subscriber data\n if (!id) {\n return {\n id: {\n equals: 'unauthorized-no-access',\n },\n }\n }\n return false\n }"],"names":["isAdmin","user","config","collection","access","roles","includes","role","admin","adminOnly","req","adminOrSelf","id","equals"],"mappings":"AAGA;;CAEC,GACD,OAAO,MAAMA,UAAU,CAACC,MAAWC;IACjC,IAAI,CAACD,QAAQA,KAAKE,UAAU,KAAK,SAAS;QACxC,OAAO;IACT;IAEA,4CAA4C;IAC5C,IAAID,QAAQE,QAAQJ,SAAS;QAC3B,OAAOE,OAAOE,MAAM,CAACJ,OAAO,CAACC;IAC/B;IAEA,2CAA2C;IAC3C,0BAA0B;IAC1B,IAAIA,KAAKI,KAAK,EAAEC,SAAS,UAAU;QACjC,OAAO;IACT;IAEA,qCAAqC;IACrC,IAAIL,KAAKD,OAAO,KAAK,MAAM;QACzB,OAAO;IACT;IAEA,2CAA2C;IAC3C,IAAIC,KAAKM,IAAI,KAAK,SAAS;QACzB,OAAO;IACT;IAEA,6CAA6C;IAC7C,IAAIN,KAAKO,KAAK,KAAK,MAAM;QACvB,OAAO;IACT;IAEA,OAAO;AACT,EAAC;AAED;;CAEC,GACD,OAAO,MAAMC,YAAY,CAACP,SACxB,CAAC,EAAEQ,GAAG,EAAc;QAClB,MAAMT,OAAOS,IAAIT,IAAI;QACrB,OAAOD,QAAQC,MAAMC;IACvB,EAAC;AAEH;;CAEC,GACD,OAAO,MAAMS,cAAc,CAACT,SAC1B,CAAC,EAAEQ,GAAG,EAAEE,EAAE,EAAc;QACtB,MAAMX,OAAOS,IAAIT,IAAI;QAErB,sBAAsB;QACtB,IAAI,CAACA,MAAM;YACT,8DAA8D;YAC9D,IAAI,CAACW,IAAI;gBACP,OAAO;oBACLA,IAAI;wBACFC,QAAQ;oBACV;gBACF;YACF;YACA,OAAO;QACT;QAEA,+BAA+B;QAC/B,IAAIb,QAAQC,MAAMC,SAAS;YACzB,OAAO;QACT;QAEA,0EAA0E;QAC1E,IAAID,KAAKE,UAAU,KAAK,eAAe;YACrC,+CAA+C;YAC/C,IAAI,CAACS,IAAI;gBACP,OAAO;oBACLA,IAAI;wBACFC,QAAQZ,KAAKW,EAAE;oBACjB;gBACF;YACF;YACA,wDAAwD;YACxD,OAAOA,OAAOX,KAAKW,EAAE;QACvB;QAEA,8CAA8C;QAC9C,IAAI,CAACA,IAAI;YACP,OAAO;gBACLA,IAAI;oBACFC,QAAQ;gBACV;YACF;QACF;QACA,OAAO;IACT,EAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class RateLimiter {
|
|
2
|
+
attempts = new Map();
|
|
3
|
+
options;
|
|
4
|
+
constructor(options){
|
|
5
|
+
this.options = options;
|
|
6
|
+
}
|
|
7
|
+
async checkLimit(key) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const record = this.attempts.get(key);
|
|
10
|
+
if (!record || record.resetTime < now) {
|
|
11
|
+
this.attempts.set(key, {
|
|
12
|
+
count: 1,
|
|
13
|
+
resetTime: now + this.options.windowMs
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (record.count >= this.options.maxAttempts) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
record.count++;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
async incrementAttempt(key) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const record = this.attempts.get(key);
|
|
26
|
+
if (!record || record.resetTime < now) {
|
|
27
|
+
this.attempts.set(key, {
|
|
28
|
+
count: 1,
|
|
29
|
+
resetTime: now + this.options.windowMs
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
record.count++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async reset(key) {
|
|
36
|
+
this.attempts.delete(key);
|
|
37
|
+
}
|
|
38
|
+
async resetAll() {
|
|
39
|
+
this.attempts.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/rate-limiter.ts"],"sourcesContent":["export interface RateLimiterOptions {\n maxAttempts: number\n windowMs: number\n prefix?: string\n}\n\nexport class RateLimiter {\n private attempts: Map<string, { count: number; resetTime: number }> = new Map()\n private options: RateLimiterOptions\n\n constructor(options: RateLimiterOptions) {\n this.options = options\n }\n\n async checkLimit(key: string): Promise<boolean> {\n const now = Date.now()\n const record = this.attempts.get(key)\n\n if (!record || record.resetTime < now) {\n this.attempts.set(key, {\n count: 1,\n resetTime: now + this.options.windowMs\n })\n return true\n }\n\n if (record.count >= this.options.maxAttempts) {\n return false\n }\n\n record.count++\n return true\n }\n\n async incrementAttempt(key: string): Promise<void> {\n const now = Date.now()\n const record = this.attempts.get(key)\n\n if (!record || record.resetTime < now) {\n this.attempts.set(key, {\n count: 1,\n resetTime: now + this.options.windowMs\n })\n } else {\n record.count++\n }\n }\n\n async reset(key: string): Promise<void> {\n this.attempts.delete(key)\n }\n\n async resetAll(): Promise<void> {\n this.attempts.clear()\n }\n}"],"names":["RateLimiter","attempts","Map","options","checkLimit","key","now","Date","record","get","resetTime","set","count","windowMs","maxAttempts","incrementAttempt","reset","delete","resetAll","clear"],"mappings":"AAMA,OAAO,MAAMA;IACHC,WAA8D,IAAIC,MAAK;IACvEC,QAA2B;IAEnC,YAAYA,OAA2B,CAAE;QACvC,IAAI,CAACA,OAAO,GAAGA;IACjB;IAEA,MAAMC,WAAWC,GAAW,EAAoB;QAC9C,MAAMC,MAAMC,KAAKD,GAAG;QACpB,MAAME,SAAS,IAAI,CAACP,QAAQ,CAACQ,GAAG,CAACJ;QAEjC,IAAI,CAACG,UAAUA,OAAOE,SAAS,GAAGJ,KAAK;YACrC,IAAI,CAACL,QAAQ,CAACU,GAAG,CAACN,KAAK;gBACrBO,OAAO;gBACPF,WAAWJ,MAAM,IAAI,CAACH,OAAO,CAACU,QAAQ;YACxC;YACA,OAAO;QACT;QAEA,IAAIL,OAAOI,KAAK,IAAI,IAAI,CAACT,OAAO,CAACW,WAAW,EAAE;YAC5C,OAAO;QACT;QAEAN,OAAOI,KAAK;QACZ,OAAO;IACT;IAEA,MAAMG,iBAAiBV,GAAW,EAAiB;QACjD,MAAMC,MAAMC,KAAKD,GAAG;QACpB,MAAME,SAAS,IAAI,CAACP,QAAQ,CAACQ,GAAG,CAACJ;QAEjC,IAAI,CAACG,UAAUA,OAAOE,SAAS,GAAGJ,KAAK;YACrC,IAAI,CAACL,QAAQ,CAACU,GAAG,CAACN,KAAK;gBACrBO,OAAO;gBACPF,WAAWJ,MAAM,IAAI,CAACH,OAAO,CAACU,QAAQ;YACxC;QACF,OAAO;YACLL,OAAOI,KAAK;QACd;IACF;IAEA,MAAMI,MAAMX,GAAW,EAAiB;QACtC,IAAI,CAACJ,QAAQ,CAACgB,MAAM,CAACZ;IACvB;IAEA,MAAMa,WAA0B;QAC9B,IAAI,CAACjB,QAAQ,CAACkB,KAAK;IACrB;AACF"}
|