mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 16:23:23 +00:00
Enhance mailing plugin with collection overrides, detailed access controls, and improved rich text serialization logic
This commit is contained in:
119
README.md
119
README.md
@@ -359,14 +359,127 @@ EMAIL_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yoursite.com
|
||||
```
|
||||
|
||||
## Security and Access Control
|
||||
|
||||
### Collection Access Restrictions
|
||||
|
||||
By default, both email templates and emails collections allow full access (`read/create/update/delete: () => true`). For production use, you should configure proper access restrictions using collection overrides:
|
||||
|
||||
```typescript
|
||||
mailingPlugin({
|
||||
// ... other config
|
||||
collections: {
|
||||
templates: {
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:read')
|
||||
},
|
||||
create: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:create')
|
||||
},
|
||||
update: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:update')
|
||||
},
|
||||
delete: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin'
|
||||
},
|
||||
}
|
||||
},
|
||||
emails: {
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:read')
|
||||
},
|
||||
create: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:create')
|
||||
},
|
||||
update: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:update')
|
||||
},
|
||||
delete: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin'
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Collection Overrides
|
||||
|
||||
You can override any collection configuration using the `collections.templates` or `collections.emails` options. This includes:
|
||||
|
||||
- **Access controls** - Restrict who can read/create/update/delete
|
||||
- **Admin UI settings** - Customize admin interface appearance
|
||||
- **Field modifications** - Add custom fields or modify existing ones
|
||||
- **Hooks** - Add custom validation or processing logic
|
||||
|
||||
Example with additional custom fields:
|
||||
|
||||
```typescript
|
||||
mailingPlugin({
|
||||
// ... other config
|
||||
collections: {
|
||||
templates: {
|
||||
admin: {
|
||||
group: 'Custom Marketing',
|
||||
description: 'Custom email templates with enhanced features'
|
||||
},
|
||||
fields: [
|
||||
// Plugin's default fields are preserved
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Marketing', value: 'marketing' },
|
||||
{ label: 'Transactional', value: 'transactional' },
|
||||
{ label: 'System', value: 'system' }
|
||||
],
|
||||
admin: {
|
||||
position: 'sidebar'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'text',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: 'Tags for organizing templates'
|
||||
}
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data, req }) => {
|
||||
// Custom validation logic
|
||||
if (data.category === 'system' && req.user?.role !== 'admin') {
|
||||
throw new Error('Only admins can create system templates')
|
||||
}
|
||||
return data
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
The plugin includes full TypeScript definitions. Import types as needed:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
MailingPluginConfig,
|
||||
SendEmailOptions,
|
||||
import {
|
||||
MailingPluginConfig,
|
||||
SendEmailOptions,
|
||||
EmailTemplate,
|
||||
QueuedEmail,
|
||||
EmailObject,
|
||||
|
||||
@@ -146,6 +146,123 @@ const buildConfigWithMemoryDB = async () => {
|
||||
retryDelay: 60000, // 1 minute for dev
|
||||
queue: 'email-queue',
|
||||
|
||||
// Example: Collection overrides for customization
|
||||
// Uncomment and modify as needed for your use case
|
||||
/*
|
||||
collections: {
|
||||
templates: {
|
||||
// Custom access controls - restrict who can manage templates
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:read')
|
||||
},
|
||||
create: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:create')
|
||||
},
|
||||
update: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:update')
|
||||
},
|
||||
delete: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin'
|
||||
},
|
||||
},
|
||||
// Custom admin UI settings
|
||||
admin: {
|
||||
group: 'Marketing',
|
||||
description: 'Email templates with enhanced security and categorization'
|
||||
},
|
||||
// Add custom fields to templates
|
||||
fields: [
|
||||
// Default plugin fields are automatically included
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Marketing', value: 'marketing' },
|
||||
{ label: 'Transactional', value: 'transactional' },
|
||||
{ label: 'System Notifications', value: 'system' }
|
||||
],
|
||||
defaultValue: 'transactional',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Template category for organization'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'text',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Tags for easy template filtering'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Only active templates can be used'
|
||||
}
|
||||
}
|
||||
],
|
||||
// Custom validation hooks
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data, req }) => {
|
||||
// Example: Only admins can create system templates
|
||||
if (data.category === 'system' && req.user?.role !== 'admin') {
|
||||
throw new Error('Only administrators can create system notification templates')
|
||||
}
|
||||
|
||||
// Example: Auto-generate slug if not provided
|
||||
if (!data.slug && data.name) {
|
||||
data.slug = data.name.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emails: {
|
||||
// Restrict access to emails collection
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:read')
|
||||
},
|
||||
create: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:create')
|
||||
},
|
||||
update: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin' || user.permissions?.includes('mailing:update')
|
||||
},
|
||||
delete: ({ req: { user } }) => {
|
||||
if (!user) return false
|
||||
return user.role === 'admin'
|
||||
},
|
||||
},
|
||||
// Custom admin configuration for emails
|
||||
admin: {
|
||||
group: 'Marketing',
|
||||
description: 'Email delivery tracking and management',
|
||||
defaultColumns: ['subject', 'to', 'status', 'priority', 'scheduledAt'],
|
||||
}
|
||||
}
|
||||
},
|
||||
*/
|
||||
|
||||
// Optional: Custom rich text editor configuration
|
||||
// Comment out to use default lexical editor
|
||||
richTextEditor: lexicalEditor({
|
||||
|
||||
@@ -52,7 +52,10 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
|
||||
},
|
||||
// Update relationship fields to point to correct templates collection
|
||||
fields: (emailsOverrides.fields || Emails.fields).map((field: any) => {
|
||||
if (field.name === 'template' && field.type === 'relationship') {
|
||||
if (field &&
|
||||
typeof field === 'object' &&
|
||||
field.name === 'template' &&
|
||||
field.type === 'relationship') {
|
||||
return {
|
||||
...field,
|
||||
relationTo: templatesSlug,
|
||||
@@ -95,12 +98,10 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error processing email queue:', error)
|
||||
return {
|
||||
output: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
|
||||
// Properly fail the job by throwing the error
|
||||
throw new Error(`Email queue processing failed: ${errorMessage}`)
|
||||
}
|
||||
},
|
||||
interfaceName: 'ProcessEmailQueueJob',
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
// Using any type for now since Lexical types have import issues
|
||||
// import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
type SerializedEditorState = any
|
||||
// Proper type definitions for Lexical serialization
|
||||
interface SerializedEditorState {
|
||||
root: {
|
||||
children: SerializedLexicalNode[]
|
||||
}
|
||||
}
|
||||
|
||||
interface SerializedLexicalNode {
|
||||
type: string
|
||||
children?: SerializedLexicalNode[]
|
||||
text?: string
|
||||
format?: number
|
||||
tag?: string
|
||||
listType?: 'number' | 'bullet'
|
||||
url?: string
|
||||
newTab?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Lexical richtext content to HTML
|
||||
@@ -24,11 +38,11 @@ export function serializeRichTextToText(richTextData: SerializedEditorState): st
|
||||
return serializeNodesToText(richTextData.root.children)
|
||||
}
|
||||
|
||||
function serializeNodesToHTML(nodes: any[]): string {
|
||||
function serializeNodesToHTML(nodes: SerializedLexicalNode[]): string {
|
||||
return nodes.map(node => serializeNodeToHTML(node)).join('')
|
||||
}
|
||||
|
||||
function serializeNodeToHTML(node: any): string {
|
||||
function serializeNodeToHTML(node: SerializedLexicalNode): string {
|
||||
if (!node) return ''
|
||||
|
||||
switch (node.type) {
|
||||
@@ -43,16 +57,25 @@ function serializeNodeToHTML(node: any): string {
|
||||
|
||||
case 'text':
|
||||
let text = node.text || ''
|
||||
|
||||
// Apply text formatting
|
||||
|
||||
// Apply text formatting using proper nesting order
|
||||
if (node.format) {
|
||||
if (node.format & 1) text = `<strong>${text}</strong>` // Bold
|
||||
if (node.format & 2) text = `<em>${text}</em>` // Italic
|
||||
if (node.format & 4) text = `<s>${text}</s>` // Strikethrough
|
||||
if (node.format & 8) text = `<u>${text}</u>` // Underline
|
||||
if (node.format & 16) text = `<code>${text}</code>` // Code
|
||||
const formatFlags = {
|
||||
bold: (node.format & 1) !== 0,
|
||||
italic: (node.format & 2) !== 0,
|
||||
strikethrough: (node.format & 4) !== 0,
|
||||
underline: (node.format & 8) !== 0,
|
||||
code: (node.format & 16) !== 0,
|
||||
}
|
||||
|
||||
// Apply formatting in proper nesting order: code > bold > italic > underline > strikethrough
|
||||
if (formatFlags.code) text = `<code>${text}</code>`
|
||||
if (formatFlags.bold) text = `<strong>${text}</strong>`
|
||||
if (formatFlags.italic) text = `<em>${text}</em>`
|
||||
if (formatFlags.underline) text = `<u>${text}</u>`
|
||||
if (formatFlags.strikethrough) text = `<s>${text}</s>`
|
||||
}
|
||||
|
||||
|
||||
return text
|
||||
|
||||
case 'linebreak':
|
||||
@@ -89,11 +112,11 @@ function serializeNodeToHTML(node: any): string {
|
||||
}
|
||||
}
|
||||
|
||||
function serializeNodesToText(nodes: any[]): string {
|
||||
function serializeNodesToText(nodes: SerializedLexicalNode[]): string {
|
||||
return nodes.map(node => serializeNodeToText(node)).join('')
|
||||
}
|
||||
|
||||
function serializeNodeToText(node: any): string {
|
||||
function serializeNodeToText(node: SerializedLexicalNode): string {
|
||||
if (!node) return ''
|
||||
|
||||
switch (node.type) {
|
||||
|
||||
Reference in New Issue
Block a user