@latte-macchiat-io/latte-payload 1.0.1
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/.claude/settings.local.json +9 -0
- package/.env.example +29 -0
- package/.github/workflows/ci.yml +160 -0
- package/.github/workflows/publish.yml +126 -0
- package/.nvmrc +1 -0
- package/.prettierignore +2 -0
- package/.prettierrc +11 -0
- package/CHANGELOG.md +87 -0
- package/README.md +364 -0
- package/TESTING_AND_DOCUMENTATION_SETUP.md +348 -0
- package/dist/access/adminAccessOnly.d.ts +25 -0
- package/dist/access/adminAccessOnly.d.ts.map +1 -0
- package/dist/access/adminAccessOnly.js +11 -0
- package/dist/access/adminAccessOnly.js.map +1 -0
- package/dist/access/admins.d.ts +72 -0
- package/dist/access/admins.d.ts.map +1 -0
- package/dist/access/admins.js +76 -0
- package/dist/access/admins.js.map +1 -0
- package/dist/access/anyone.d.ts +35 -0
- package/dist/access/anyone.d.ts.map +1 -0
- package/dist/access/anyone.js +34 -0
- package/dist/access/anyone.js.map +1 -0
- package/dist/access/authenticated.d.ts +63 -0
- package/dist/access/authenticated.d.ts.map +1 -0
- package/dist/access/authenticated.js +68 -0
- package/dist/access/authenticated.js.map +1 -0
- package/dist/access/authenticatedAccessOnly.d.ts +13 -0
- package/dist/access/authenticatedAccessOnly.d.ts.map +1 -0
- package/dist/access/authenticatedAccessOnly.js +11 -0
- package/dist/access/authenticatedAccessOnly.js.map +1 -0
- package/dist/access/index.d.ts +7 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +7 -0
- package/dist/access/index.js.map +1 -0
- package/dist/access/publicReadAuthenticatedAccess.d.ts +13 -0
- package/dist/access/publicReadAuthenticatedAccess.d.ts.map +1 -0
- package/dist/access/publicReadAuthenticatedAccess.js +12 -0
- package/dist/access/publicReadAuthenticatedAccess.js.map +1 -0
- package/dist/collections/ContactMessages/hooks/sendEmailNotification.d.ts +31 -0
- package/dist/collections/ContactMessages/hooks/sendEmailNotification.d.ts.map +1 -0
- package/dist/collections/ContactMessages/hooks/sendEmailNotification.js +29 -0
- package/dist/collections/ContactMessages/hooks/sendEmailNotification.js.map +1 -0
- package/dist/collections/ContactMessages/index.d.ts +27 -0
- package/dist/collections/ContactMessages/index.d.ts.map +1 -0
- package/dist/collections/ContactMessages/index.js +81 -0
- package/dist/collections/ContactMessages/index.js.map +1 -0
- package/dist/collections/EmailTemplates/index.d.ts +26 -0
- package/dist/collections/EmailTemplates/index.d.ts.map +1 -0
- package/dist/collections/EmailTemplates/index.js +74 -0
- package/dist/collections/EmailTemplates/index.js.map +1 -0
- package/dist/collections/Media/index.d.ts +3 -0
- package/dist/collections/Media/index.d.ts.map +1 -0
- package/dist/collections/Media/index.js +22 -0
- package/dist/collections/Media/index.js.map +1 -0
- package/dist/collections/QueuedEmails/components/HtmlViewer.d.ts +3 -0
- package/dist/collections/QueuedEmails/components/HtmlViewer.d.ts.map +1 -0
- package/dist/collections/QueuedEmails/components/HtmlViewer.js +11 -0
- package/dist/collections/QueuedEmails/components/HtmlViewer.js.map +1 -0
- package/dist/collections/QueuedEmails/components/RetryEmailButtons.d.ts +2 -0
- package/dist/collections/QueuedEmails/components/RetryEmailButtons.d.ts.map +1 -0
- package/dist/collections/QueuedEmails/components/RetryEmailButtons.js +79 -0
- package/dist/collections/QueuedEmails/components/RetryEmailButtons.js.map +1 -0
- package/dist/collections/QueuedEmails/index.d.ts +3 -0
- package/dist/collections/QueuedEmails/index.d.ts.map +1 -0
- package/dist/collections/QueuedEmails/index.js +245 -0
- package/dist/collections/QueuedEmails/index.js.map +1 -0
- package/dist/collections/Users/auth-emails/forgot-password-email.d.ts +51 -0
- package/dist/collections/Users/auth-emails/forgot-password-email.d.ts.map +1 -0
- package/dist/collections/Users/auth-emails/forgot-password-email.js +90 -0
- package/dist/collections/Users/auth-emails/forgot-password-email.js.map +1 -0
- package/dist/collections/Users/auth-emails/verify-email.d.ts +51 -0
- package/dist/collections/Users/auth-emails/verify-email.d.ts.map +1 -0
- package/dist/collections/Users/auth-emails/verify-email.js +80 -0
- package/dist/collections/Users/auth-emails/verify-email.js.map +1 -0
- package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.d.ts +3 -0
- package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.d.ts.map +1 -0
- package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.js +19 -0
- package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.js.map +1 -0
- package/dist/collections/Users/index.d.ts +76 -0
- package/dist/collections/Users/index.d.ts.map +1 -0
- package/dist/collections/Users/index.js +116 -0
- package/dist/collections/Users/index.js.map +1 -0
- package/dist/collections/index.d.ts +10 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/collections/index.js +14 -0
- package/dist/collections/index.js.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/forms/states.d.ts +8 -0
- package/dist/forms/states.d.ts.map +1 -0
- package/dist/forms/states.js +16 -0
- package/dist/forms/states.js.map +1 -0
- package/dist/forms/translate-errors.d.ts +8 -0
- package/dist/forms/translate-errors.d.ts.map +1 -0
- package/dist/forms/translate-errors.js +11 -0
- package/dist/forms/translate-errors.js.map +1 -0
- package/dist/forms/validators.d.ts +10 -0
- package/dist/forms/validators.d.ts.map +1 -0
- package/dist/forms/validators.js +23 -0
- package/dist/forms/validators.js.map +1 -0
- package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.d.ts +2 -0
- package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.d.ts.map +1 -0
- package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.js +5 -0
- package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.js.map +1 -0
- package/dist/globals/PrivacyPolicy/index.d.ts +3 -0
- package/dist/globals/PrivacyPolicy/index.d.ts.map +1 -0
- package/dist/globals/PrivacyPolicy/index.js +31 -0
- package/dist/globals/PrivacyPolicy/index.js.map +1 -0
- package/dist/globals/TermsOfUse/hooks/revalidate-cache.d.ts +2 -0
- package/dist/globals/TermsOfUse/hooks/revalidate-cache.d.ts.map +1 -0
- package/dist/globals/TermsOfUse/hooks/revalidate-cache.js +5 -0
- package/dist/globals/TermsOfUse/hooks/revalidate-cache.js.map +1 -0
- package/dist/globals/TermsOfUse/index.d.ts +3 -0
- package/dist/globals/TermsOfUse/index.d.ts.map +1 -0
- package/dist/globals/TermsOfUse/index.js +31 -0
- package/dist/globals/TermsOfUse/index.js.map +1 -0
- package/dist/globals/index.d.ts +3 -0
- package/dist/globals/index.d.ts.map +1 -0
- package/dist/globals/index.js +3 -0
- package/dist/globals/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/process-email-queue.d.ts +46 -0
- package/dist/tasks/process-email-queue.d.ts.map +1 -0
- package/dist/tasks/process-email-queue.js +199 -0
- package/dist/tasks/process-email-queue.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/slug.d.ts +2 -0
- package/dist/types/slug.d.ts.map +1 -0
- package/dist/types/slug.js +11 -0
- package/dist/types/slug.js.map +1 -0
- package/dist/utils/database-dates.d.ts +3 -0
- package/dist/utils/database-dates.d.ts.map +1 -0
- package/dist/utils/database-dates.js +6 -0
- package/dist/utils/database-dates.js.map +1 -0
- package/dist/utils/email/generate-email-html.d.ts +23 -0
- package/dist/utils/email/generate-email-html.d.ts.map +1 -0
- package/dist/utils/email/generate-email-html.js +42 -0
- package/dist/utils/email/generate-email-html.js.map +1 -0
- package/dist/utils/email/get-email-template.d.ts +8 -0
- package/dist/utils/email/get-email-template.d.ts.map +1 -0
- package/dist/utils/email/get-email-template.js +14 -0
- package/dist/utils/email/get-email-template.js.map +1 -0
- package/dist/utils/email/index.d.ts +4 -0
- package/dist/utils/email/index.d.ts.map +1 -0
- package/dist/utils/email/index.js +4 -0
- package/dist/utils/email/index.js.map +1 -0
- package/dist/utils/email/queue-email.d.ts +29 -0
- package/dist/utils/email/queue-email.d.ts.map +1 -0
- package/dist/utils/email/queue-email.js +79 -0
- package/dist/utils/email/queue-email.js.map +1 -0
- package/dist/utils/get-global.d.ts +6 -0
- package/dist/utils/get-global.d.ts.map +1 -0
- package/dist/utils/get-global.js +20 -0
- package/dist/utils/get-global.js.map +1 -0
- package/dist/utils/id-from-payload.d.ts +4 -0
- package/dist/utils/id-from-payload.d.ts.map +1 -0
- package/dist/utils/id-from-payload.js +10 -0
- package/dist/utils/id-from-payload.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/migrations.d.ts +2 -0
- package/dist/utils/migrations.d.ts.map +1 -0
- package/dist/utils/migrations.js +18 -0
- package/dist/utils/migrations.js.map +1 -0
- package/dist/utils/payload-client.d.ts +10 -0
- package/dist/utils/payload-client.d.ts.map +1 -0
- package/dist/utils/payload-client.js +14 -0
- package/dist/utils/payload-client.js.map +1 -0
- package/dist/utils/slugify.d.ts +8 -0
- package/dist/utils/slugify.d.ts.map +1 -0
- package/dist/utils/slugify.js +15 -0
- package/dist/utils/slugify.js.map +1 -0
- package/eslint.config.mjs +90 -0
- package/package.json +139 -0
- package/pnpm-workspace.yaml +4 -0
- package/src/access/adminAccessOnly.ts +13 -0
- package/src/access/admins.ts +78 -0
- package/src/access/anyone.ts +35 -0
- package/src/access/authenticated.ts +70 -0
- package/src/access/authenticatedAccessOnly.ts +13 -0
- package/src/access/index.ts +6 -0
- package/src/access/publicReadAuthenticatedAccess.ts +14 -0
- package/src/collections/ContactMessages/hooks/sendEmailNotification.ts +58 -0
- package/src/collections/ContactMessages/index.ts +100 -0
- package/src/collections/EmailTemplates/index.ts +89 -0
- package/src/collections/Media/index.ts +24 -0
- package/src/collections/QueuedEmails/components/HtmlViewer.tsx +16 -0
- package/src/collections/QueuedEmails/components/RetryEmailButtons.tsx +115 -0
- package/src/collections/QueuedEmails/index.ts +246 -0
- package/src/collections/Users/auth-emails/forgot-password-email.ts +135 -0
- package/src/collections/Users/auth-emails/verify-email.ts +123 -0
- package/src/collections/Users/hooks/ensureFirstUserIsAdmin.ts +22 -0
- package/src/collections/Users/index.ts +201 -0
- package/src/collections/index.ts +23 -0
- package/src/components/index.ts +3 -0
- package/src/forms/states.ts +23 -0
- package/src/forms/translate-errors.ts +13 -0
- package/src/forms/validators.ts +33 -0
- package/src/globals/PrivacyPolicy/hooks/revalidate-cache.ts +5 -0
- package/src/globals/PrivacyPolicy/index.ts +33 -0
- package/src/globals/TermsOfUse/hooks/revalidate-cache.ts +5 -0
- package/src/globals/TermsOfUse/index.ts +33 -0
- package/src/globals/index.ts +2 -0
- package/src/index.ts +26 -0
- package/src/tasks/index.ts +7 -0
- package/src/tasks/process-email-queue.ts +261 -0
- package/src/types/index.ts +15 -0
- package/src/types/slug.ts +11 -0
- package/src/utils/database-dates.ts +6 -0
- package/src/utils/email/generate-email-html.ts +63 -0
- package/src/utils/email/get-email-template.ts +18 -0
- package/src/utils/email/index.ts +3 -0
- package/src/utils/email/queue-email.ts +109 -0
- package/src/utils/get-global.ts +25 -0
- package/src/utils/id-from-payload.ts +11 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/migrations.ts +18 -0
- package/src/utils/payload-client.ts +16 -0
- package/src/utils/slugify.ts +21 -0
- package/tests/fixtures/email-template.html +58 -0
- package/tests/fixtures/sample-data.ts +56 -0
- package/tests/helpers/create-test-user.ts +37 -0
- package/tests/helpers/init-payload.ts +59 -0
- package/tests/setup.integration.ts +9 -0
- package/tests/setup.ts +4 -0
- package/tests/unit/access/adminAccessOnly.spec.ts +117 -0
- package/tests/unit/access/admins.spec.ts +68 -0
- package/tests/unit/access/anyone.spec.ts +28 -0
- package/tests/unit/access/authenticated.spec.ts +53 -0
- package/tests/unit/access/authenticatedAccessOnly.spec.ts +112 -0
- package/tests/unit/access/publicReadAuthenticatedAccess.spec.ts +112 -0
- package/tests/unit/forms/validators.spec.ts +348 -0
- package/tests/unit/utils/database-dates.spec.ts +97 -0
- package/tests/unit/utils/id-from-payload.spec.ts +142 -0
- package/tests/unit/utils/slugify.spec.ts +185 -0
- package/tsconfig.json +31 -0
- package/typedoc.json +40 -0
- package/vitest.config.ts +31 -0
- package/vitest.integration.config.ts +27 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { CollectionConfig } from 'payload';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createForgotPasswordEmailGenerator,
|
|
5
|
+
createForgotPasswordEmailSubjectGenerator,
|
|
6
|
+
type ForgotPasswordEmailConfig,
|
|
7
|
+
generateForgotPasswordEmailHtmlFromTemplate,
|
|
8
|
+
generateForgotPasswordEmailSubjectFromTemplate,
|
|
9
|
+
} from './auth-emails/forgot-password-email';
|
|
10
|
+
import {
|
|
11
|
+
createVerifyEmailGenerator,
|
|
12
|
+
createVerifyEmailSubjectGenerator,
|
|
13
|
+
generateVerifyEmailHtmlFromTemplate,
|
|
14
|
+
generateVerifyEmailSubjectFromTemplate,
|
|
15
|
+
type VerifyEmailConfig,
|
|
16
|
+
} from './auth-emails/verify-email';
|
|
17
|
+
import { ensureFirstUserIsAdmin } from './hooks/ensureFirstUserIsAdmin';
|
|
18
|
+
import { adminsFieldLevel } from '../../access/admins';
|
|
19
|
+
import { authenticatedAccessOnly } from '../../access/authenticatedAccessOnly';
|
|
20
|
+
import type { Locale } from '../../types';
|
|
21
|
+
|
|
22
|
+
export interface UsersCollectionConfig {
|
|
23
|
+
/**
|
|
24
|
+
* User roles
|
|
25
|
+
* @default ['admin', 'user']
|
|
26
|
+
*/
|
|
27
|
+
roles?: Array<{ label: { fr: string; nl: string; en: string }; value: string }>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Available locales for users
|
|
31
|
+
* @default [{ label: { fr: 'Anglais', nl: 'Engels', en: 'English' }, value: 'en' }, ...]
|
|
32
|
+
*/
|
|
33
|
+
locales?: Array<{ label: { fr: string; nl: string; en: string }; value: Locale }>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default locale
|
|
37
|
+
* @default 'en'
|
|
38
|
+
*/
|
|
39
|
+
defaultLocale?: Locale;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Token expiration in seconds
|
|
43
|
+
* @default 60 * 60 * 24 * 365 (1 year)
|
|
44
|
+
*/
|
|
45
|
+
tokenExpiration?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Maximum login attempts before lockout
|
|
49
|
+
* @default 10
|
|
50
|
+
*/
|
|
51
|
+
maxLoginAttempts?: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Lockout time in milliseconds
|
|
55
|
+
* @default 1000 * 60 * 10 (10 minutes)
|
|
56
|
+
*/
|
|
57
|
+
lockTime?: number;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Forgot password expiration in milliseconds
|
|
61
|
+
* @default 1000 * 60 * 60 * 24 * 10 (10 days)
|
|
62
|
+
*/
|
|
63
|
+
forgotPasswordExpiration?: number;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Configuration for verify email
|
|
67
|
+
*/
|
|
68
|
+
verifyEmailConfig?: VerifyEmailConfig;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Configuration for forgot password email
|
|
72
|
+
*/
|
|
73
|
+
forgotPasswordEmailConfig?: ForgotPasswordEmailConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create Users collection with optional configuration
|
|
78
|
+
*
|
|
79
|
+
* @param config - Optional configuration for the Users collection
|
|
80
|
+
* @returns CollectionConfig for Users
|
|
81
|
+
*/
|
|
82
|
+
export function createUsersCollection(config?: UsersCollectionConfig): CollectionConfig {
|
|
83
|
+
const roles = config?.roles || [
|
|
84
|
+
{
|
|
85
|
+
label: { fr: 'Administrateur', nl: 'Beheerder', en: 'Admin' },
|
|
86
|
+
value: 'admin',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: { fr: 'User', nl: 'Gebruiker', en: 'User' },
|
|
90
|
+
value: 'user',
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const locales = config?.locales || [
|
|
95
|
+
{
|
|
96
|
+
label: { fr: 'Anglais', nl: 'Engels', en: 'English' },
|
|
97
|
+
value: 'en' as Locale,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: { fr: 'Français', nl: 'Frans', en: 'French' },
|
|
101
|
+
value: 'fr' as Locale,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: { fr: 'Néerlandais', nl: 'Nederlands', en: 'Dutch' },
|
|
105
|
+
value: 'nl' as Locale,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
slug: 'users',
|
|
111
|
+
labels: {
|
|
112
|
+
singular: { fr: 'Utilisateur', en: 'User', nl: 'Gebruiker' },
|
|
113
|
+
plural: { fr: 'Utilisateurs', en: 'Users', nl: 'Gebruikers' },
|
|
114
|
+
},
|
|
115
|
+
admin: {
|
|
116
|
+
useAsTitle: 'email',
|
|
117
|
+
hideAPIURL: true,
|
|
118
|
+
group: { fr: 'Utilisateurs', nl: 'Gebruikers', en: 'Users' },
|
|
119
|
+
},
|
|
120
|
+
access: authenticatedAccessOnly,
|
|
121
|
+
auth: {
|
|
122
|
+
tokenExpiration: config?.tokenExpiration ?? 60 * 60 * 24 * 365, // In seconds, 1 year
|
|
123
|
+
maxLoginAttempts: config?.maxLoginAttempts ?? 10,
|
|
124
|
+
lockTime: config?.lockTime ?? 1000 * 60 * 10, // In milliseconds, 10 minutes
|
|
125
|
+
|
|
126
|
+
verify: {
|
|
127
|
+
generateEmailHTML: config?.verifyEmailConfig ? createVerifyEmailGenerator(config.verifyEmailConfig) : generateVerifyEmailHtmlFromTemplate,
|
|
128
|
+
generateEmailSubject: config?.verifyEmailConfig
|
|
129
|
+
? createVerifyEmailSubjectGenerator(config.verifyEmailConfig)
|
|
130
|
+
: generateVerifyEmailSubjectFromTemplate,
|
|
131
|
+
},
|
|
132
|
+
forgotPassword: {
|
|
133
|
+
expiration: config?.forgotPasswordExpiration ?? 1000 * 60 * 60 * 24 * 10, // In milliseconds, 10 days
|
|
134
|
+
generateEmailHTML: config?.forgotPasswordEmailConfig
|
|
135
|
+
? createForgotPasswordEmailGenerator(config.forgotPasswordEmailConfig)
|
|
136
|
+
: generateForgotPasswordEmailHtmlFromTemplate,
|
|
137
|
+
generateEmailSubject: config?.forgotPasswordEmailConfig
|
|
138
|
+
? createForgotPasswordEmailSubjectGenerator(config.forgotPasswordEmailConfig)
|
|
139
|
+
: generateForgotPasswordEmailSubjectFromTemplate,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
fields: [
|
|
144
|
+
// Email added by default
|
|
145
|
+
// Add more fields as needed
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
type: 'row',
|
|
149
|
+
fields: [
|
|
150
|
+
{
|
|
151
|
+
name: 'name',
|
|
152
|
+
label: { fr: 'Nom', nl: 'Naam', en: 'Name' },
|
|
153
|
+
type: 'text',
|
|
154
|
+
required: false,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
name: 'locale',
|
|
159
|
+
label: { fr: 'Langue', nl: 'Taal', en: 'Language' },
|
|
160
|
+
type: 'select',
|
|
161
|
+
hasMany: false,
|
|
162
|
+
defaultValue: config?.defaultLocale || 'en',
|
|
163
|
+
options: locales,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
name: 'roles',
|
|
170
|
+
label: { fr: 'Rôles', nl: 'Rollen', en: 'Roles' },
|
|
171
|
+
type: 'select',
|
|
172
|
+
hasMany: true,
|
|
173
|
+
defaultValue: ['user'],
|
|
174
|
+
options: roles,
|
|
175
|
+
hooks: {
|
|
176
|
+
beforeChange: [ensureFirstUserIsAdmin],
|
|
177
|
+
},
|
|
178
|
+
access: {
|
|
179
|
+
read: adminsFieldLevel,
|
|
180
|
+
create: adminsFieldLevel,
|
|
181
|
+
update: adminsFieldLevel,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Default Users collection
|
|
190
|
+
*/
|
|
191
|
+
export const Users = createUsersCollection();
|
|
192
|
+
|
|
193
|
+
// Re-export auth email generators for customization
|
|
194
|
+
export {
|
|
195
|
+
createVerifyEmailGenerator,
|
|
196
|
+
createVerifyEmailSubjectGenerator,
|
|
197
|
+
createForgotPasswordEmailGenerator,
|
|
198
|
+
createForgotPasswordEmailSubjectGenerator,
|
|
199
|
+
type VerifyEmailConfig,
|
|
200
|
+
type ForgotPasswordEmailConfig,
|
|
201
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Collections
|
|
2
|
+
export { Users, createUsersCollection, type UsersCollectionConfig } from './Users';
|
|
3
|
+
export { Media } from './Media';
|
|
4
|
+
export { ContactMessages, createContactMessagesCollection, type ContactMessagesConfig } from './ContactMessages';
|
|
5
|
+
export { EmailTemplates, createEmailTemplatesCollection, type EmailTemplatesConfig } from './EmailTemplates';
|
|
6
|
+
export { QueuedEmails } from './QueuedEmails';
|
|
7
|
+
|
|
8
|
+
// Users auth emails
|
|
9
|
+
export {
|
|
10
|
+
createVerifyEmailGenerator,
|
|
11
|
+
createVerifyEmailSubjectGenerator,
|
|
12
|
+
createForgotPasswordEmailGenerator,
|
|
13
|
+
createForgotPasswordEmailSubjectGenerator,
|
|
14
|
+
type VerifyEmailConfig,
|
|
15
|
+
type ForgotPasswordEmailConfig,
|
|
16
|
+
} from './Users';
|
|
17
|
+
|
|
18
|
+
// ContactMessages hooks
|
|
19
|
+
export { createSendEmailNotificationHook, type SendEmailNotificationConfig } from './ContactMessages';
|
|
20
|
+
|
|
21
|
+
// QueuedEmails components
|
|
22
|
+
export { HTMLViewer } from './QueuedEmails/components/HtmlViewer';
|
|
23
|
+
export { RetryEmailButtons } from './QueuedEmails/components/RetryEmailButtons';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createSuccessFormState<T>(previousState: T): { success: true } & T {
|
|
2
|
+
return {
|
|
3
|
+
...previousState,
|
|
4
|
+
success: true,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// TODO createErrorFormState
|
|
9
|
+
|
|
10
|
+
export function extractFormDataToReturn<T extends Record<string, unknown>>({
|
|
11
|
+
formData,
|
|
12
|
+
excludes,
|
|
13
|
+
}: {
|
|
14
|
+
formData: FormData;
|
|
15
|
+
excludes?: (keyof T)[];
|
|
16
|
+
}): T {
|
|
17
|
+
const data = Object.fromEntries(formData.entries()) as T;
|
|
18
|
+
excludes?.forEach((field) => {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
(data as any)[field] = '';
|
|
21
|
+
});
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getTranslations } from 'next-intl/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// Utility to translate error keys using i18n
|
|
5
|
+
export async function translateFormErrors<T extends z.ZodTypeAny>(errors: z.inferFlattenedErrors<T>) {
|
|
6
|
+
const t = await getTranslations();
|
|
7
|
+
const translate = (messages?: string[]) => messages?.map((msg) => t(msg)) ?? [];
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
fieldErrors: Object.fromEntries(Object.entries(errors.fieldErrors).map(([field, msgs]) => [field, translate(msgs as string[])])),
|
|
11
|
+
formErrors: translate(errors.formErrors),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const requiredString = (message: string) =>
|
|
4
|
+
z
|
|
5
|
+
.string({ errorMap: () => ({ message }) }) // Same error message for all errors
|
|
6
|
+
.trim()
|
|
7
|
+
.nonempty();
|
|
8
|
+
|
|
9
|
+
export const requiredPasswordString = (minLength: number, requiredMessage: string, minLengthMessage: string) =>
|
|
10
|
+
z
|
|
11
|
+
.string({ errorMap: () => ({ message: requiredMessage }) }) // Same error message for all errors
|
|
12
|
+
.trim()
|
|
13
|
+
.nonempty()
|
|
14
|
+
.pipe(
|
|
15
|
+
z.string().min(minLength, { message: minLengthMessage }) // Ensure password is at least 12 characters long
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export const requiredEmailString = ({ required, email }: { required: string; email: string }) =>
|
|
19
|
+
z
|
|
20
|
+
.string() // Same error message for all errors
|
|
21
|
+
.trim()
|
|
22
|
+
.nonempty({ message: required })
|
|
23
|
+
.pipe(z.string().email({ message: email })); // “email” runs only if nonempty passed
|
|
24
|
+
|
|
25
|
+
export const validHoneyPot = (message: string) =>
|
|
26
|
+
z
|
|
27
|
+
.string({ errorMap: () => ({ message }) }) // Same error message for all errors
|
|
28
|
+
.refine((val) => val.length === 0); // Must be empty
|
|
29
|
+
|
|
30
|
+
export const requiredCheckbox = (message: string) =>
|
|
31
|
+
z
|
|
32
|
+
.boolean({ errorMap: () => ({ message }) }) // Same error message for all errors
|
|
33
|
+
.refine((val) => val); // Must be true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GlobalConfig } from 'payload';
|
|
2
|
+
|
|
3
|
+
import { revalidateCache } from './hooks/revalidate-cache';
|
|
4
|
+
import { authenticatedAccessOnly } from '../../access/authenticatedAccessOnly';
|
|
5
|
+
|
|
6
|
+
export const PrivacyPolicy: GlobalConfig = {
|
|
7
|
+
slug: 'privacy-policy',
|
|
8
|
+
label: { fr: 'Politique de confidentialité', nl: 'Privacybeleid', en: 'Privacy Policy' },
|
|
9
|
+
admin: {
|
|
10
|
+
group: { fr: 'Légal', nl: 'Juridisch', en: 'Legal' },
|
|
11
|
+
hideAPIURL: true,
|
|
12
|
+
},
|
|
13
|
+
access: authenticatedAccessOnly,
|
|
14
|
+
hooks: {
|
|
15
|
+
afterChange: [revalidateCache],
|
|
16
|
+
},
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'title',
|
|
20
|
+
label: { fr: 'Titre', nl: 'Titel', en: 'Title' },
|
|
21
|
+
type: 'text',
|
|
22
|
+
required: true,
|
|
23
|
+
localized: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'content',
|
|
27
|
+
label: { fr: 'Contenu', nl: 'Inhoud', en: 'Content' },
|
|
28
|
+
type: 'richText',
|
|
29
|
+
required: true,
|
|
30
|
+
localized: true,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GlobalConfig } from 'payload';
|
|
2
|
+
|
|
3
|
+
import { revalidateCache } from './hooks/revalidate-cache';
|
|
4
|
+
import { authenticatedAccessOnly } from '../../access/authenticatedAccessOnly';
|
|
5
|
+
|
|
6
|
+
export const TermsOfUse: GlobalConfig = {
|
|
7
|
+
slug: 'terms-of-use',
|
|
8
|
+
label: { fr: "Conditions d'utilisation", nl: 'Gebruiksvoorwaarden', en: 'Terms of Use' },
|
|
9
|
+
admin: {
|
|
10
|
+
group: { fr: 'Légal', nl: 'Juridisch', en: 'Legal' },
|
|
11
|
+
hideAPIURL: true,
|
|
12
|
+
},
|
|
13
|
+
access: authenticatedAccessOnly,
|
|
14
|
+
hooks: {
|
|
15
|
+
afterChange: [revalidateCache],
|
|
16
|
+
},
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'title',
|
|
20
|
+
label: { fr: 'Titre', nl: 'Titel', en: 'Title' },
|
|
21
|
+
type: 'text',
|
|
22
|
+
required: true,
|
|
23
|
+
localized: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'content',
|
|
27
|
+
label: { fr: 'Contenu', nl: 'Inhoud', en: 'Content' },
|
|
28
|
+
type: 'richText',
|
|
29
|
+
required: true,
|
|
30
|
+
localized: true,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @latte-macchiat-io/latte-payload
|
|
3
|
+
*
|
|
4
|
+
* Reusable Payload CMS collections, utilities, and components for Latte projects
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Collections
|
|
8
|
+
export * from './collections';
|
|
9
|
+
|
|
10
|
+
// Globals
|
|
11
|
+
export * from './globals';
|
|
12
|
+
|
|
13
|
+
// Access Control
|
|
14
|
+
export * from './access';
|
|
15
|
+
|
|
16
|
+
// Tasks
|
|
17
|
+
export * from './tasks';
|
|
18
|
+
|
|
19
|
+
// Utilities
|
|
20
|
+
export * from './utils';
|
|
21
|
+
|
|
22
|
+
// Components
|
|
23
|
+
export * from './components';
|
|
24
|
+
|
|
25
|
+
// Types
|
|
26
|
+
export * from './types';
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { TZDate } from '@date-fns/tz';
|
|
2
|
+
import { TaskConfig } from 'payload';
|
|
3
|
+
import { toDatabaseTZDate } from '../utils';
|
|
4
|
+
|
|
5
|
+
export const processEmailQueueName = 'process-email-queue';
|
|
6
|
+
|
|
7
|
+
export const processEmailQueueAutoRunConfig = {
|
|
8
|
+
queue: processEmailQueueName,
|
|
9
|
+
cron: '* * * * *', // Every minute
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Utility function to add delay between emails to respect rate limits
|
|
13
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
export interface ProcessEmailQueueConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Timezone for email timestamps
|
|
18
|
+
* @default 'Europe/Brussels'
|
|
19
|
+
*/
|
|
20
|
+
timezone?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maximum number of emails to process per run
|
|
24
|
+
* @default 100
|
|
25
|
+
*/
|
|
26
|
+
maxEmailsPerRun?: number;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Delay between sending emails in milliseconds (for rate limiting)
|
|
30
|
+
* @default 1000 (1 second - respects Resend's 2 requests/second limit)
|
|
31
|
+
*/
|
|
32
|
+
delayBetweenEmails?: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default "from" email address
|
|
36
|
+
* Falls back to process.env.SMTP_FROM_ADDRESS
|
|
37
|
+
*/
|
|
38
|
+
defaultFromAddress?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional error reporter (e.g., Sentry)
|
|
42
|
+
*/
|
|
43
|
+
errorReporter?: {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
captureException: (error: Error, context?: any) => void;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create process-email-queue task with custom configuration
|
|
51
|
+
*
|
|
52
|
+
* @param config - Optional configuration
|
|
53
|
+
* @returns TaskConfig for process-email-queue
|
|
54
|
+
*/
|
|
55
|
+
export function createProcessEmailQueueTask(config?: ProcessEmailQueueConfig): TaskConfig<'process-email-queue'> {
|
|
56
|
+
const timezone = config?.timezone || 'Europe/Brussels';
|
|
57
|
+
const maxEmailsPerRun = config?.maxEmailsPerRun || 100;
|
|
58
|
+
const delayBetweenEmails = config?.delayBetweenEmails || 1000;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
slug: 'process-email-queue',
|
|
62
|
+
label: 'Process Email Queue',
|
|
63
|
+
retries: {
|
|
64
|
+
attempts: 3,
|
|
65
|
+
backoff: { delay: 2000, type: 'exponential' },
|
|
66
|
+
},
|
|
67
|
+
inputSchema: undefined, // No input - auto-triggered by cron
|
|
68
|
+
outputSchema: [
|
|
69
|
+
{
|
|
70
|
+
name: 'processedCount',
|
|
71
|
+
type: 'number',
|
|
72
|
+
required: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'sentCount',
|
|
76
|
+
type: 'number',
|
|
77
|
+
required: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'failedCount',
|
|
81
|
+
type: 'number',
|
|
82
|
+
required: true,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
// Auto-enqueue to run following the cron schedule
|
|
87
|
+
schedule: [
|
|
88
|
+
{
|
|
89
|
+
queue: processEmailQueueAutoRunConfig.queue,
|
|
90
|
+
cron: processEmailQueueAutoRunConfig.cron,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
|
|
94
|
+
handler: async ({ req }) => {
|
|
95
|
+
const { payload } = req;
|
|
96
|
+
payload.logger.info(`[(Task)ProcessEmailQueue] Task started`);
|
|
97
|
+
|
|
98
|
+
let processedCount = 0;
|
|
99
|
+
let sentCount = 0;
|
|
100
|
+
let failedCount = 0;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Find all emails that need to be sent (pending or failed with retries remaining)
|
|
104
|
+
// Note: We can't directly compare attemptCount < maxAttempts in the query,
|
|
105
|
+
// so we fetch all pending/failed emails and filter them in code
|
|
106
|
+
const allPendingOrFailed = await payload.find({
|
|
107
|
+
collection: 'queued-emails',
|
|
108
|
+
where: {
|
|
109
|
+
or: [
|
|
110
|
+
{
|
|
111
|
+
status: { equals: 'pending' },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
status: { equals: 'failed' },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
sort: 'createdAt', // Process oldest emails first
|
|
119
|
+
limit: 200, // Fetch more to filter down to maxEmailsPerRun eligible
|
|
120
|
+
pagination: false,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Filter emails that still have retries remaining
|
|
124
|
+
const emailsToSend = {
|
|
125
|
+
docs: allPendingOrFailed.docs.filter((email) => email.attemptCount < email.maxAttempts).slice(0, maxEmailsPerRun),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
payload.logger.info(`[(Task)ProcessEmailQueue] Found ${emailsToSend.docs.length} emails to process`);
|
|
129
|
+
|
|
130
|
+
if (emailsToSend.docs.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
output: {
|
|
133
|
+
processedCount: 0,
|
|
134
|
+
sentCount: 0,
|
|
135
|
+
failedCount: 0,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Process each email
|
|
141
|
+
for (const email of emailsToSend.docs) {
|
|
142
|
+
processedCount++;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
payload.logger.info(
|
|
146
|
+
`[(Task)ProcessEmailQueue] Processing email ${email.id} (${email.code}) to ${email.recipient} (attempt ${email.attemptCount + 1}/${email.maxAttempts})`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Mark as sending to prevent duplicate processing
|
|
150
|
+
await payload.update({
|
|
151
|
+
collection: 'queued-emails',
|
|
152
|
+
id: email.id,
|
|
153
|
+
data: {
|
|
154
|
+
status: 'sending',
|
|
155
|
+
lastAttemptAt: toDatabaseTZDate(TZDate.tz(timezone)),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Parse CC if it exists
|
|
160
|
+
const ccArray = email.cc ? email.cc.split(',').map((e: string) => e.trim()) : undefined;
|
|
161
|
+
|
|
162
|
+
const fromAddress = config?.defaultFromAddress || process.env.SMTP_FROM_ADDRESS;
|
|
163
|
+
|
|
164
|
+
// Send the email using Payload's email service
|
|
165
|
+
await payload.sendEmail({
|
|
166
|
+
from: fromAddress,
|
|
167
|
+
to: email.recipient,
|
|
168
|
+
cc: ccArray,
|
|
169
|
+
subject: email.subject,
|
|
170
|
+
html: email.htmlBody,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Mark as sent
|
|
174
|
+
await payload.update({
|
|
175
|
+
collection: 'queued-emails',
|
|
176
|
+
id: email.id,
|
|
177
|
+
data: {
|
|
178
|
+
status: 'sent',
|
|
179
|
+
sentAt: toDatabaseTZDate(TZDate.tz(timezone)),
|
|
180
|
+
attemptCount: email.attemptCount + 1,
|
|
181
|
+
lastError: null,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
sentCount++;
|
|
186
|
+
payload.logger.info(`[(Task)ProcessEmailQueue] Successfully sent email ${email.id} to ${email.recipient}`);
|
|
187
|
+
|
|
188
|
+
// Add delay between emails to respect rate limits
|
|
189
|
+
// Only delay if there are more emails to process
|
|
190
|
+
if (processedCount < emailsToSend.docs.length) {
|
|
191
|
+
await sleep(delayBetweenEmails);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
failedCount++;
|
|
195
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
196
|
+
const now = toDatabaseTZDate(TZDate.tz(timezone));
|
|
197
|
+
const newAttemptCount = email.attemptCount + 1;
|
|
198
|
+
|
|
199
|
+
payload.logger.error(`[(Task)ProcessEmailQueue] Failed to send email ${email.id} to ${email.recipient}: ${errorMessage}`);
|
|
200
|
+
|
|
201
|
+
// Append to error history
|
|
202
|
+
const errorHistory = email.errorHistory || [];
|
|
203
|
+
errorHistory.push({
|
|
204
|
+
timestamp: now,
|
|
205
|
+
timestamp_tz: timezone,
|
|
206
|
+
attemptNumber: newAttemptCount,
|
|
207
|
+
error: errorMessage,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Update email status to failed with error message
|
|
211
|
+
await payload.update({
|
|
212
|
+
collection: 'queued-emails',
|
|
213
|
+
id: email.id,
|
|
214
|
+
data: {
|
|
215
|
+
status: 'failed',
|
|
216
|
+
attemptCount: newAttemptCount,
|
|
217
|
+
lastError: errorMessage,
|
|
218
|
+
lastAttemptAt: now,
|
|
219
|
+
errorHistory: errorHistory,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Report to error tracker if configured
|
|
224
|
+
if (config?.errorReporter && error instanceof Error) {
|
|
225
|
+
config.errorReporter.captureException(error, {
|
|
226
|
+
extra: {
|
|
227
|
+
emailId: email.id,
|
|
228
|
+
emailCode: email.code,
|
|
229
|
+
recipient: email.recipient,
|
|
230
|
+
attemptCount: newAttemptCount,
|
|
231
|
+
maxAttempts: email.maxAttempts,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
payload.logger.info(`[(Task)ProcessEmailQueue] Task completed - Processed: ${processedCount}, Sent: ${sentCount}, Failed: ${failedCount}`);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
output: {
|
|
242
|
+
processedCount,
|
|
243
|
+
sentCount,
|
|
244
|
+
failedCount,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
} catch (error) {
|
|
248
|
+
payload.logger.error(`[(Task)ProcessEmailQueue] Critical error: ${error}`);
|
|
249
|
+
if (config?.errorReporter && error instanceof Error) {
|
|
250
|
+
config.errorReporter.captureException(error);
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Default process-email-queue task
|
|
260
|
+
*/
|
|
261
|
+
export const processEmailQueueTaskConfig = createProcessEmailQueueTask();
|