@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.
Files changed (253) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.env.example +29 -0
  3. package/.github/workflows/ci.yml +160 -0
  4. package/.github/workflows/publish.yml +126 -0
  5. package/.nvmrc +1 -0
  6. package/.prettierignore +2 -0
  7. package/.prettierrc +11 -0
  8. package/CHANGELOG.md +87 -0
  9. package/README.md +364 -0
  10. package/TESTING_AND_DOCUMENTATION_SETUP.md +348 -0
  11. package/dist/access/adminAccessOnly.d.ts +25 -0
  12. package/dist/access/adminAccessOnly.d.ts.map +1 -0
  13. package/dist/access/adminAccessOnly.js +11 -0
  14. package/dist/access/adminAccessOnly.js.map +1 -0
  15. package/dist/access/admins.d.ts +72 -0
  16. package/dist/access/admins.d.ts.map +1 -0
  17. package/dist/access/admins.js +76 -0
  18. package/dist/access/admins.js.map +1 -0
  19. package/dist/access/anyone.d.ts +35 -0
  20. package/dist/access/anyone.d.ts.map +1 -0
  21. package/dist/access/anyone.js +34 -0
  22. package/dist/access/anyone.js.map +1 -0
  23. package/dist/access/authenticated.d.ts +63 -0
  24. package/dist/access/authenticated.d.ts.map +1 -0
  25. package/dist/access/authenticated.js +68 -0
  26. package/dist/access/authenticated.js.map +1 -0
  27. package/dist/access/authenticatedAccessOnly.d.ts +13 -0
  28. package/dist/access/authenticatedAccessOnly.d.ts.map +1 -0
  29. package/dist/access/authenticatedAccessOnly.js +11 -0
  30. package/dist/access/authenticatedAccessOnly.js.map +1 -0
  31. package/dist/access/index.d.ts +7 -0
  32. package/dist/access/index.d.ts.map +1 -0
  33. package/dist/access/index.js +7 -0
  34. package/dist/access/index.js.map +1 -0
  35. package/dist/access/publicReadAuthenticatedAccess.d.ts +13 -0
  36. package/dist/access/publicReadAuthenticatedAccess.d.ts.map +1 -0
  37. package/dist/access/publicReadAuthenticatedAccess.js +12 -0
  38. package/dist/access/publicReadAuthenticatedAccess.js.map +1 -0
  39. package/dist/collections/ContactMessages/hooks/sendEmailNotification.d.ts +31 -0
  40. package/dist/collections/ContactMessages/hooks/sendEmailNotification.d.ts.map +1 -0
  41. package/dist/collections/ContactMessages/hooks/sendEmailNotification.js +29 -0
  42. package/dist/collections/ContactMessages/hooks/sendEmailNotification.js.map +1 -0
  43. package/dist/collections/ContactMessages/index.d.ts +27 -0
  44. package/dist/collections/ContactMessages/index.d.ts.map +1 -0
  45. package/dist/collections/ContactMessages/index.js +81 -0
  46. package/dist/collections/ContactMessages/index.js.map +1 -0
  47. package/dist/collections/EmailTemplates/index.d.ts +26 -0
  48. package/dist/collections/EmailTemplates/index.d.ts.map +1 -0
  49. package/dist/collections/EmailTemplates/index.js +74 -0
  50. package/dist/collections/EmailTemplates/index.js.map +1 -0
  51. package/dist/collections/Media/index.d.ts +3 -0
  52. package/dist/collections/Media/index.d.ts.map +1 -0
  53. package/dist/collections/Media/index.js +22 -0
  54. package/dist/collections/Media/index.js.map +1 -0
  55. package/dist/collections/QueuedEmails/components/HtmlViewer.d.ts +3 -0
  56. package/dist/collections/QueuedEmails/components/HtmlViewer.d.ts.map +1 -0
  57. package/dist/collections/QueuedEmails/components/HtmlViewer.js +11 -0
  58. package/dist/collections/QueuedEmails/components/HtmlViewer.js.map +1 -0
  59. package/dist/collections/QueuedEmails/components/RetryEmailButtons.d.ts +2 -0
  60. package/dist/collections/QueuedEmails/components/RetryEmailButtons.d.ts.map +1 -0
  61. package/dist/collections/QueuedEmails/components/RetryEmailButtons.js +79 -0
  62. package/dist/collections/QueuedEmails/components/RetryEmailButtons.js.map +1 -0
  63. package/dist/collections/QueuedEmails/index.d.ts +3 -0
  64. package/dist/collections/QueuedEmails/index.d.ts.map +1 -0
  65. package/dist/collections/QueuedEmails/index.js +245 -0
  66. package/dist/collections/QueuedEmails/index.js.map +1 -0
  67. package/dist/collections/Users/auth-emails/forgot-password-email.d.ts +51 -0
  68. package/dist/collections/Users/auth-emails/forgot-password-email.d.ts.map +1 -0
  69. package/dist/collections/Users/auth-emails/forgot-password-email.js +90 -0
  70. package/dist/collections/Users/auth-emails/forgot-password-email.js.map +1 -0
  71. package/dist/collections/Users/auth-emails/verify-email.d.ts +51 -0
  72. package/dist/collections/Users/auth-emails/verify-email.d.ts.map +1 -0
  73. package/dist/collections/Users/auth-emails/verify-email.js +80 -0
  74. package/dist/collections/Users/auth-emails/verify-email.js.map +1 -0
  75. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.d.ts +3 -0
  76. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.d.ts.map +1 -0
  77. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.js +19 -0
  78. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.js.map +1 -0
  79. package/dist/collections/Users/index.d.ts +76 -0
  80. package/dist/collections/Users/index.d.ts.map +1 -0
  81. package/dist/collections/Users/index.js +116 -0
  82. package/dist/collections/Users/index.js.map +1 -0
  83. package/dist/collections/index.d.ts +10 -0
  84. package/dist/collections/index.d.ts.map +1 -0
  85. package/dist/collections/index.js +14 -0
  86. package/dist/collections/index.js.map +1 -0
  87. package/dist/components/index.d.ts +3 -0
  88. package/dist/components/index.d.ts.map +1 -0
  89. package/dist/components/index.js +4 -0
  90. package/dist/components/index.js.map +1 -0
  91. package/dist/forms/states.d.ts +8 -0
  92. package/dist/forms/states.d.ts.map +1 -0
  93. package/dist/forms/states.js +16 -0
  94. package/dist/forms/states.js.map +1 -0
  95. package/dist/forms/translate-errors.d.ts +8 -0
  96. package/dist/forms/translate-errors.d.ts.map +1 -0
  97. package/dist/forms/translate-errors.js +11 -0
  98. package/dist/forms/translate-errors.js.map +1 -0
  99. package/dist/forms/validators.d.ts +10 -0
  100. package/dist/forms/validators.d.ts.map +1 -0
  101. package/dist/forms/validators.js +23 -0
  102. package/dist/forms/validators.js.map +1 -0
  103. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.d.ts +2 -0
  104. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.d.ts.map +1 -0
  105. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.js +5 -0
  106. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.js.map +1 -0
  107. package/dist/globals/PrivacyPolicy/index.d.ts +3 -0
  108. package/dist/globals/PrivacyPolicy/index.d.ts.map +1 -0
  109. package/dist/globals/PrivacyPolicy/index.js +31 -0
  110. package/dist/globals/PrivacyPolicy/index.js.map +1 -0
  111. package/dist/globals/TermsOfUse/hooks/revalidate-cache.d.ts +2 -0
  112. package/dist/globals/TermsOfUse/hooks/revalidate-cache.d.ts.map +1 -0
  113. package/dist/globals/TermsOfUse/hooks/revalidate-cache.js +5 -0
  114. package/dist/globals/TermsOfUse/hooks/revalidate-cache.js.map +1 -0
  115. package/dist/globals/TermsOfUse/index.d.ts +3 -0
  116. package/dist/globals/TermsOfUse/index.d.ts.map +1 -0
  117. package/dist/globals/TermsOfUse/index.js +31 -0
  118. package/dist/globals/TermsOfUse/index.js.map +1 -0
  119. package/dist/globals/index.d.ts +3 -0
  120. package/dist/globals/index.d.ts.map +1 -0
  121. package/dist/globals/index.js +3 -0
  122. package/dist/globals/index.js.map +1 -0
  123. package/dist/index.d.ts +13 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +20 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/tasks/index.d.ts +2 -0
  128. package/dist/tasks/index.d.ts.map +1 -0
  129. package/dist/tasks/index.js +2 -0
  130. package/dist/tasks/index.js.map +1 -0
  131. package/dist/tasks/process-email-queue.d.ts +46 -0
  132. package/dist/tasks/process-email-queue.d.ts.map +1 -0
  133. package/dist/tasks/process-email-queue.js +199 -0
  134. package/dist/tasks/process-email-queue.js.map +1 -0
  135. package/dist/types/index.d.ts +14 -0
  136. package/dist/types/index.d.ts.map +1 -0
  137. package/dist/types/index.js +2 -0
  138. package/dist/types/index.js.map +1 -0
  139. package/dist/types/slug.d.ts +2 -0
  140. package/dist/types/slug.d.ts.map +1 -0
  141. package/dist/types/slug.js +11 -0
  142. package/dist/types/slug.js.map +1 -0
  143. package/dist/utils/database-dates.d.ts +3 -0
  144. package/dist/utils/database-dates.d.ts.map +1 -0
  145. package/dist/utils/database-dates.js +6 -0
  146. package/dist/utils/database-dates.js.map +1 -0
  147. package/dist/utils/email/generate-email-html.d.ts +23 -0
  148. package/dist/utils/email/generate-email-html.d.ts.map +1 -0
  149. package/dist/utils/email/generate-email-html.js +42 -0
  150. package/dist/utils/email/generate-email-html.js.map +1 -0
  151. package/dist/utils/email/get-email-template.d.ts +8 -0
  152. package/dist/utils/email/get-email-template.d.ts.map +1 -0
  153. package/dist/utils/email/get-email-template.js +14 -0
  154. package/dist/utils/email/get-email-template.js.map +1 -0
  155. package/dist/utils/email/index.d.ts +4 -0
  156. package/dist/utils/email/index.d.ts.map +1 -0
  157. package/dist/utils/email/index.js +4 -0
  158. package/dist/utils/email/index.js.map +1 -0
  159. package/dist/utils/email/queue-email.d.ts +29 -0
  160. package/dist/utils/email/queue-email.d.ts.map +1 -0
  161. package/dist/utils/email/queue-email.js +79 -0
  162. package/dist/utils/email/queue-email.js.map +1 -0
  163. package/dist/utils/get-global.d.ts +6 -0
  164. package/dist/utils/get-global.d.ts.map +1 -0
  165. package/dist/utils/get-global.js +20 -0
  166. package/dist/utils/get-global.js.map +1 -0
  167. package/dist/utils/id-from-payload.d.ts +4 -0
  168. package/dist/utils/id-from-payload.d.ts.map +1 -0
  169. package/dist/utils/id-from-payload.js +10 -0
  170. package/dist/utils/id-from-payload.js.map +1 -0
  171. package/dist/utils/index.d.ts +4 -0
  172. package/dist/utils/index.d.ts.map +1 -0
  173. package/dist/utils/index.js +4 -0
  174. package/dist/utils/index.js.map +1 -0
  175. package/dist/utils/migrations.d.ts +2 -0
  176. package/dist/utils/migrations.d.ts.map +1 -0
  177. package/dist/utils/migrations.js +18 -0
  178. package/dist/utils/migrations.js.map +1 -0
  179. package/dist/utils/payload-client.d.ts +10 -0
  180. package/dist/utils/payload-client.d.ts.map +1 -0
  181. package/dist/utils/payload-client.js +14 -0
  182. package/dist/utils/payload-client.js.map +1 -0
  183. package/dist/utils/slugify.d.ts +8 -0
  184. package/dist/utils/slugify.d.ts.map +1 -0
  185. package/dist/utils/slugify.js +15 -0
  186. package/dist/utils/slugify.js.map +1 -0
  187. package/eslint.config.mjs +90 -0
  188. package/package.json +139 -0
  189. package/pnpm-workspace.yaml +4 -0
  190. package/src/access/adminAccessOnly.ts +13 -0
  191. package/src/access/admins.ts +78 -0
  192. package/src/access/anyone.ts +35 -0
  193. package/src/access/authenticated.ts +70 -0
  194. package/src/access/authenticatedAccessOnly.ts +13 -0
  195. package/src/access/index.ts +6 -0
  196. package/src/access/publicReadAuthenticatedAccess.ts +14 -0
  197. package/src/collections/ContactMessages/hooks/sendEmailNotification.ts +58 -0
  198. package/src/collections/ContactMessages/index.ts +100 -0
  199. package/src/collections/EmailTemplates/index.ts +89 -0
  200. package/src/collections/Media/index.ts +24 -0
  201. package/src/collections/QueuedEmails/components/HtmlViewer.tsx +16 -0
  202. package/src/collections/QueuedEmails/components/RetryEmailButtons.tsx +115 -0
  203. package/src/collections/QueuedEmails/index.ts +246 -0
  204. package/src/collections/Users/auth-emails/forgot-password-email.ts +135 -0
  205. package/src/collections/Users/auth-emails/verify-email.ts +123 -0
  206. package/src/collections/Users/hooks/ensureFirstUserIsAdmin.ts +22 -0
  207. package/src/collections/Users/index.ts +201 -0
  208. package/src/collections/index.ts +23 -0
  209. package/src/components/index.ts +3 -0
  210. package/src/forms/states.ts +23 -0
  211. package/src/forms/translate-errors.ts +13 -0
  212. package/src/forms/validators.ts +33 -0
  213. package/src/globals/PrivacyPolicy/hooks/revalidate-cache.ts +5 -0
  214. package/src/globals/PrivacyPolicy/index.ts +33 -0
  215. package/src/globals/TermsOfUse/hooks/revalidate-cache.ts +5 -0
  216. package/src/globals/TermsOfUse/index.ts +33 -0
  217. package/src/globals/index.ts +2 -0
  218. package/src/index.ts +26 -0
  219. package/src/tasks/index.ts +7 -0
  220. package/src/tasks/process-email-queue.ts +261 -0
  221. package/src/types/index.ts +15 -0
  222. package/src/types/slug.ts +11 -0
  223. package/src/utils/database-dates.ts +6 -0
  224. package/src/utils/email/generate-email-html.ts +63 -0
  225. package/src/utils/email/get-email-template.ts +18 -0
  226. package/src/utils/email/index.ts +3 -0
  227. package/src/utils/email/queue-email.ts +109 -0
  228. package/src/utils/get-global.ts +25 -0
  229. package/src/utils/id-from-payload.ts +11 -0
  230. package/src/utils/index.ts +3 -0
  231. package/src/utils/migrations.ts +18 -0
  232. package/src/utils/payload-client.ts +16 -0
  233. package/src/utils/slugify.ts +21 -0
  234. package/tests/fixtures/email-template.html +58 -0
  235. package/tests/fixtures/sample-data.ts +56 -0
  236. package/tests/helpers/create-test-user.ts +37 -0
  237. package/tests/helpers/init-payload.ts +59 -0
  238. package/tests/setup.integration.ts +9 -0
  239. package/tests/setup.ts +4 -0
  240. package/tests/unit/access/adminAccessOnly.spec.ts +117 -0
  241. package/tests/unit/access/admins.spec.ts +68 -0
  242. package/tests/unit/access/anyone.spec.ts +28 -0
  243. package/tests/unit/access/authenticated.spec.ts +53 -0
  244. package/tests/unit/access/authenticatedAccessOnly.spec.ts +112 -0
  245. package/tests/unit/access/publicReadAuthenticatedAccess.spec.ts +112 -0
  246. package/tests/unit/forms/validators.spec.ts +348 -0
  247. package/tests/unit/utils/database-dates.spec.ts +97 -0
  248. package/tests/unit/utils/id-from-payload.spec.ts +142 -0
  249. package/tests/unit/utils/slugify.spec.ts +185 -0
  250. package/tsconfig.json +31 -0
  251. package/typedoc.json +40 -0
  252. package/vitest.config.ts +31 -0
  253. package/vitest.integration.config.ts +27 -0
package/package.json ADDED
@@ -0,0 +1,139 @@
1
+ {
2
+ "name": "@latte-macchiat-io/latte-payload",
3
+ "version": "1.0.1",
4
+ "description": "Reusable Payload CMS collections, utilities, and components for Latte projects",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./collections": {
14
+ "import": "./dist/collections/index.js",
15
+ "types": "./dist/collections/index.d.ts"
16
+ },
17
+ "./globals": {
18
+ "import": "./dist/globals/index.js",
19
+ "types": "./dist/globals/index.d.ts"
20
+ },
21
+ "./access": {
22
+ "import": "./dist/access/index.js",
23
+ "types": "./dist/access/index.d.ts"
24
+ },
25
+ "./utils": {
26
+ "import": "./dist/utils/index.js",
27
+ "types": "./dist/utils/index.d.ts"
28
+ },
29
+ "./config": {
30
+ "import": "./dist/config/index.js",
31
+ "types": "./dist/config/index.d.ts"
32
+ },
33
+ "./tasks": {
34
+ "import": "./dist/tasks/index.js",
35
+ "types": "./dist/tasks/index.d.ts"
36
+ },
37
+ "./components": {
38
+ "import": "./dist/components/index.js",
39
+ "types": "./dist/components/index.d.ts"
40
+ },
41
+ "./types": {
42
+ "import": "./dist/types/index.js",
43
+ "types": "./dist/types/index.d.ts"
44
+ }
45
+ },
46
+ "scripts": {
47
+ "build": "tsc",
48
+ "dev": "tsc --watch",
49
+ "clean": "rm -rf dist",
50
+ "prepublishOnly": "pnpm clean && pnpm build",
51
+ "lint": "eslint",
52
+ "lint:fix": "eslint --fix",
53
+ "prettier": "prettier --check \"**/*.{js,jsx,ts,tsx}\"",
54
+ "prettier:fix": "prettier --write \"**/*.{js,jsx,ts,tsx}\"",
55
+ "test": "vitest",
56
+ "test:unit": "vitest --config vitest.config.ts --run",
57
+ "test:integration": "vitest --config vitest.integration.config.ts --run",
58
+ "test:all": "pnpm test:unit && pnpm test:integration",
59
+ "test:watch": "vitest --config vitest.config.ts",
60
+ "test:ui": "vitest --ui",
61
+ "test:coverage": "vitest --config vitest.config.ts --run --coverage",
62
+ "docs:generate": "typedoc --out docs src/index.ts",
63
+ "docs:serve": "npx http-server docs -p 8080"
64
+ },
65
+ "keywords": [
66
+ "payload",
67
+ "payloadcms",
68
+ "cms",
69
+ "latte",
70
+ "starter",
71
+ "template"
72
+ ],
73
+ "author": "Latte Macchiat.io",
74
+ "license": "MIT",
75
+ "dependencies": {
76
+ "@date-fns/tz": "^1.4.1",
77
+ "next-intl": "^4.6.1",
78
+ "slugify": "^1.6.6",
79
+ "zod": "^3.25.76"
80
+ },
81
+ "peerDependencies": {
82
+ "@payloadcms/db-postgres": "^3.64.0",
83
+ "@payloadcms/email-nodemailer": "^3.64.0",
84
+ "@payloadcms/email-resend": "^3.64.0",
85
+ "@payloadcms/next": "^3.64.0",
86
+ "@payloadcms/richtext-lexical": "^3.64.0",
87
+ "@payloadcms/storage-s3": "^3.64.0",
88
+ "@payloadcms/ui": "^3.64.0",
89
+ "next": "^15.0.0",
90
+ "payload": "^3.64.0",
91
+ "react": "^19.0.0",
92
+ "react-dom": "^19.0.0",
93
+ "sharp": "^0.34.0"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "@payloadcms/email-nodemailer": {
97
+ "optional": true
98
+ },
99
+ "@payloadcms/email-resend": {
100
+ "optional": true
101
+ },
102
+ "@payloadcms/storage-s3": {
103
+ "optional": true
104
+ }
105
+ },
106
+ "devDependencies": {
107
+ "@payloadcms/db-sqlite": "^3.64.0",
108
+ "@testing-library/jest-dom": "^6.6.3",
109
+ "@testing-library/react": "^16.1.0",
110
+ "@types/node": "^20.6.0",
111
+ "@types/react": "19.1.10",
112
+ "@types/react-dom": "19.1.7",
113
+ "@vitest/coverage-v8": "^2.1.8",
114
+ "@vitest/ui": "^2.1.8",
115
+ "dotenv": "^17.2.3",
116
+ "eslint": "^9.15.0",
117
+ "eslint-config-prettier": "^10.0.1",
118
+ "eslint-import-resolver-typescript": "^4.4.4",
119
+ "eslint-plugin-import": "^2.31.0",
120
+ "eslint-plugin-jsx-a11y": "^6.6.1",
121
+ "eslint-plugin-storybook": "^10.1.6",
122
+ "happy-dom": "^20.0.11",
123
+ "prettier": "^3.7.4",
124
+ "typedoc": "^0.27.5",
125
+ "typedoc-plugin-markdown": "^4.3.3",
126
+ "typescript": "^5.9.2",
127
+ "typescript-eslint": "^8.21.0",
128
+ "vitest": "^2.1.8"
129
+ },
130
+ "packageManager": "pnpm@10.9.0",
131
+ "engines": {
132
+ "node": ">=22.x",
133
+ "pnpm": "^10"
134
+ },
135
+ "repository": {
136
+ "type": "git",
137
+ "url": "https://github.com/latte-macchiat-io/latte-payload"
138
+ }
139
+ }
@@ -0,0 +1,4 @@
1
+ onlyBuiltDependencies:
2
+ - '@parcel/watcher'
3
+ - '@swc/core'
4
+ - unrs-resolver
@@ -0,0 +1,13 @@
1
+ import { PayloadRequest } from 'payload';
2
+
3
+ import { admins } from './admins';
4
+
5
+ export const adminAccessOnly = {
6
+ admin: ({ req }: { req: PayloadRequest }) => Boolean(req.user?.roles?.includes('admin')),
7
+ create: admins,
8
+ delete: admins,
9
+ read: admins,
10
+ readVersions: admins,
11
+ unlock: admins,
12
+ update: admins,
13
+ };
@@ -0,0 +1,78 @@
1
+ import type { FieldAccess, PayloadRequest } from 'payload';
2
+
3
+ /**
4
+ * Access control function that restricts access to admin users only.
5
+ *
6
+ * @description
7
+ * This function checks if the authenticated user has the 'admin' role in their roles array.
8
+ * Commonly used for collection-level or operation-level access control in PayloadCMS.
9
+ *
10
+ * @param {Object} params - Access control parameters
11
+ * @param {PayloadRequest} params.req - The Payload request object containing user information
12
+ *
13
+ * @returns {boolean} True if user has admin role, false otherwise
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Use in a collection config
18
+ * export const AdminOnlyCollection: CollectionConfig = {
19
+ * slug: 'admin-only',
20
+ * access: {
21
+ * create: admins,
22
+ * read: admins,
23
+ * update: admins,
24
+ * delete: admins,
25
+ * },
26
+ * fields: [...],
27
+ * };
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Use for specific operations
33
+ * if (admins({ req })) {
34
+ * // User is admin, allow operation
35
+ * }
36
+ * ```
37
+ *
38
+ * @see {@link adminsFieldLevel} for field-level admin access control
39
+ * @see {@link adminAccessOnly} for a complete access control object with all operations restricted to admins
40
+ */
41
+ export const admins = ({ req }: { req: PayloadRequest }) => {
42
+ // Return true or false based on if the user has an admin role
43
+ return Boolean(req.user?.roles?.includes('admin'));
44
+ };
45
+
46
+ /**
47
+ * Field-level access control function that restricts field access to admin users only.
48
+ *
49
+ * @description
50
+ * Similar to {@link admins} but designed for use with field-level access control.
51
+ * Checks if the user has the 'admin' role to determine field visibility/editability.
52
+ *
53
+ * @param {Object} params - Field access parameters
54
+ * @param {Object} params.req - The request object
55
+ * @param {Object} params.req.user - The authenticated user
56
+ *
57
+ * @returns {boolean} True if user has admin role, false otherwise
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * // Restrict field to admins only
62
+ * {
63
+ * name: 'sensitiveField',
64
+ * type: 'text',
65
+ * access: {
66
+ * read: adminsFieldLevel,
67
+ * update: adminsFieldLevel,
68
+ * },
69
+ * }
70
+ * ```
71
+ *
72
+ * @see {@link admins} for collection-level admin access control
73
+ */
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ export const adminsFieldLevel: FieldAccess<any, any> = ({ req: { user } }) => {
76
+ // Return true or false based on if the user has an admin role
77
+ return Boolean(user?.roles?.includes('admin'));
78
+ };
@@ -0,0 +1,35 @@
1
+ import type { Access } from 'payload';
2
+
3
+ /**
4
+ * Access control function that allows unrestricted public access.
5
+ *
6
+ * @description
7
+ * This function always returns true, allowing access to all users regardless of authentication status.
8
+ * Use this for truly public content like blog posts, landing pages, or public API endpoints.
9
+ *
10
+ * @returns {boolean} Always returns true
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Use for public content
15
+ * export const BlogPostsCollection: CollectionConfig = {
16
+ * slug: 'blog-posts',
17
+ * access: {
18
+ * read: anyone, // Anyone can read
19
+ * create: admins, // Only admins can create
20
+ * update: admins,
21
+ * delete: admins,
22
+ * },
23
+ * fields: [...],
24
+ * };
25
+ * ```
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Use with publicReadAuthenticatedAccess for read-only public access
30
+ * import { publicReadAuthenticatedAccess } from '@latte-macchiat-io/latte-payload/access';
31
+ * ```
32
+ *
33
+ * @see {@link publicReadAuthenticatedAccess} for public read with authenticated write pattern
34
+ */
35
+ export const anyone: Access = () => true;
@@ -0,0 +1,70 @@
1
+ import type { Access, FieldAccess } from 'payload';
2
+
3
+ /**
4
+ * Access control function that allows access to any authenticated user.
5
+ *
6
+ * @description
7
+ * This function checks if there is an authenticated user in the request, regardless of their role.
8
+ * Useful for content that should be accessible to all logged-in users but not to the public.
9
+ *
10
+ * @param {Object} params - Access control parameters
11
+ * @param {Object} params.req - The request object
12
+ * @param {Object} params.req.user - The authenticated user (if any)
13
+ *
14
+ * @returns {boolean} True if user is authenticated, false otherwise
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Use in a collection config
19
+ * export const UserContentCollection: CollectionConfig = {
20
+ * slug: 'user-content',
21
+ * access: {
22
+ * create: authenticated,
23
+ * read: authenticated,
24
+ * update: authenticated,
25
+ * delete: authenticated,
26
+ * },
27
+ * fields: [...],
28
+ * };
29
+ * ```
30
+ *
31
+ * @see {@link authenticatedFieldLevel} for field-level authenticated access control
32
+ * @see {@link authenticatedAccessOnly} for a complete access control object
33
+ */
34
+ export const authenticated: Access = ({ req: { user } }) => {
35
+ // Return true or false based on if the user is authenticated
36
+ return Boolean(!!user);
37
+ };
38
+
39
+ /**
40
+ * Field-level access control function that restricts field access to authenticated users.
41
+ *
42
+ * @description
43
+ * Similar to {@link authenticated} but designed for use with field-level access control.
44
+ * Checks if the user is authenticated to determine field visibility/editability.
45
+ *
46
+ * @param {Object} params - Field access parameters
47
+ * @param {Object} params.req - The request object
48
+ * @param {Object} params.req.user - The authenticated user (if any)
49
+ *
50
+ * @returns {boolean} True if user is authenticated, false otherwise
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * // Hide field from unauthenticated users
55
+ * {
56
+ * name: 'memberOnlyField',
57
+ * type: 'text',
58
+ * access: {
59
+ * read: authenticatedFieldLevel,
60
+ * update: authenticatedFieldLevel,
61
+ * },
62
+ * }
63
+ * ```
64
+ *
65
+ * @see {@link authenticated} for collection-level authenticated access control
66
+ */
67
+ export const authenticatedFieldLevel: FieldAccess = ({ req: { user } }) => {
68
+ // Return true or false based on if the user is authenticated
69
+ return Boolean(!!user);
70
+ };
@@ -0,0 +1,13 @@
1
+ import { PayloadRequest } from 'payload';
2
+
3
+ import { authenticated } from './authenticated';
4
+
5
+ export const authenticatedAccessOnly = {
6
+ admin: ({ req }: { req: PayloadRequest }) => !!req.user,
7
+ create: authenticated,
8
+ delete: authenticated,
9
+ read: authenticated,
10
+ readVersions: authenticated,
11
+ unlock: authenticated,
12
+ update: authenticated,
13
+ };
@@ -0,0 +1,6 @@
1
+ export { anyone } from './anyone';
2
+ export { authenticated, authenticatedFieldLevel } from './authenticated';
3
+ export { admins, adminsFieldLevel } from './admins';
4
+ export { adminAccessOnly } from './adminAccessOnly';
5
+ export { authenticatedAccessOnly } from './authenticatedAccessOnly';
6
+ export { publicReadAuthenticatedAccess } from './publicReadAuthenticatedAccess';
@@ -0,0 +1,14 @@
1
+ import { PayloadRequest } from 'payload';
2
+
3
+ import { anyone } from './anyone';
4
+ import { authenticated } from './authenticated';
5
+
6
+ export const publicReadAuthenticatedAccess = {
7
+ admin: ({ req }: { req: PayloadRequest }) => !!req.user,
8
+ create: authenticated,
9
+ delete: authenticated,
10
+ read: anyone,
11
+ readVersions: authenticated,
12
+ unlock: authenticated,
13
+ update: authenticated,
14
+ };
@@ -0,0 +1,58 @@
1
+ import { CollectionAfterChangeHook } from 'payload';
2
+
3
+ export interface SendEmailNotificationConfig {
4
+ /**
5
+ * Email address to send notifications to
6
+ * If not provided, uses process.env.EMAIL_DEFAULT_CONTACT_ADDRESS
7
+ */
8
+ contactEmail?: string;
9
+
10
+ /**
11
+ * Subject for the notification email
12
+ * Default: 'Nouveau message de contact'
13
+ */
14
+ subject?: string;
15
+
16
+ /**
17
+ * Custom email body generator
18
+ * @param doc - The contact message document
19
+ * @returns Email body text
20
+ */
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ generateEmailBody?: (doc: any) => string;
23
+ }
24
+
25
+ /**
26
+ * Create a sendEmailNotification hook with custom configuration
27
+ *
28
+ * @param config - Optional configuration for email notifications
29
+ * @returns CollectionAfterChangeHook
30
+ */
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ export function createSendEmailNotificationHook(config?: SendEmailNotificationConfig): CollectionAfterChangeHook<any> {
33
+ return async ({ req: { payload }, doc }) => {
34
+ const toEmail = config?.contactEmail || process.env.EMAIL_DEFAULT_CONTACT_ADDRESS;
35
+
36
+ if (!toEmail) {
37
+ payload.logger.error('No contact email address configured for sendEmailNotification hook');
38
+ return;
39
+ }
40
+
41
+ payload.logger.info('Sending email notification to the admin');
42
+
43
+ const emailBody =
44
+ config?.generateEmailBody?.(doc) ||
45
+ `Un nouveau message de contact a été reçu de la part de ${doc.name} (${doc.email}).\n\nSujet: ${doc.subject}\n\nMessage: ${doc.message}`;
46
+
47
+ await payload.sendEmail({
48
+ to: toEmail,
49
+ subject: config?.subject || 'Nouveau message de contact',
50
+ text: emailBody,
51
+ });
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Default sendEmailNotification hook
57
+ */
58
+ export const sendEmailNotification = createSendEmailNotificationHook();
@@ -0,0 +1,100 @@
1
+ import { CollectionConfig } from 'payload';
2
+
3
+ import { createSendEmailNotificationHook, sendEmailNotification, type SendEmailNotificationConfig } from './hooks/sendEmailNotification';
4
+ import { authenticated } from '../../access/authenticated';
5
+ import type { Locale } from '../../types';
6
+
7
+ export interface ContactMessagesConfig {
8
+ /**
9
+ * Allowed locales for contact messages
10
+ * @default ['en', 'fr', 'nl']
11
+ */
12
+ locales?: Locale[];
13
+
14
+ /**
15
+ * Configuration for email notifications
16
+ */
17
+ emailNotificationConfig?: SendEmailNotificationConfig;
18
+ }
19
+
20
+ /**
21
+ * Create ContactMessages collection with optional configuration
22
+ *
23
+ * @param config - Optional configuration for the ContactMessages collection
24
+ * @returns CollectionConfig for ContactMessages
25
+ */
26
+ export function createContactMessagesCollection(config?: ContactMessagesConfig): CollectionConfig {
27
+ const locales = config?.locales || (['en', 'fr', 'nl'] as Locale[]);
28
+
29
+ return {
30
+ slug: 'contact-messages',
31
+ labels: {
32
+ singular: { fr: 'Message reçu', nl: 'Ontvangen bericht', en: 'Received Message' },
33
+ plural: { fr: 'Messages reçus', nl: 'Ontvangen berichten', en: 'Received Messages' },
34
+ },
35
+ admin: {
36
+ description: {
37
+ fr: 'Messages reçus via le formulaire de contact',
38
+ nl: 'Berichten ontvangen via het contactformulier',
39
+ en: 'Messages received via the contact form',
40
+ },
41
+ group: { fr: '✉️ Communication', nl: '✉️ Communicatie', en: '✉️ Communication' },
42
+ useAsTitle: 'subject',
43
+ defaultColumns: ['name', 'email', 'subject', 'createdAt'],
44
+ hideAPIURL: true,
45
+ hidden: true, // Hide from the admin UI
46
+ },
47
+ access: {
48
+ create: () => false, // Not available
49
+ delete: authenticated,
50
+ read: authenticated,
51
+ update: () => false, // Not available
52
+ },
53
+ hooks: {
54
+ afterChange: [config?.emailNotificationConfig ? createSendEmailNotificationHook(config.emailNotificationConfig) : sendEmailNotification],
55
+ },
56
+ fields: [
57
+ {
58
+ name: 'name',
59
+ label: { fr: 'Nom', nl: 'Naam', en: 'Name' },
60
+ type: 'text',
61
+ required: true,
62
+ },
63
+ {
64
+ name: 'email',
65
+ label: { fr: 'Email', nl: 'E-mail', en: 'Email' },
66
+ type: 'email',
67
+ required: true,
68
+ },
69
+ {
70
+ name: 'subject',
71
+ label: { fr: 'Sujet', nl: 'Onderwerp', en: 'Subject' },
72
+ type: 'text',
73
+ required: true,
74
+ },
75
+ {
76
+ name: 'message',
77
+ label: { fr: 'Message', nl: 'Bericht', en: 'Message' },
78
+ type: 'textarea',
79
+ required: true,
80
+ },
81
+ {
82
+ name: 'locale',
83
+ label: { fr: 'Langue', nl: 'Taal', en: 'Language' },
84
+ type: 'text',
85
+ required: true,
86
+ validate: (value: string | string[] | null | undefined) => {
87
+ return locales.includes(value as Locale) || 'Invalid locale';
88
+ },
89
+ },
90
+ ],
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Default ContactMessages collection
96
+ */
97
+ export const ContactMessages = createContactMessagesCollection();
98
+
99
+ // Re-export for customization
100
+ export { createSendEmailNotificationHook, type SendEmailNotificationConfig };
@@ -0,0 +1,89 @@
1
+ import { CollectionConfig } from 'payload';
2
+ import { admins } from '../../access/admins';
3
+
4
+ /**
5
+ * Configuration options for EmailTemplates collection
6
+ */
7
+ export interface EmailTemplatesConfig {
8
+ /**
9
+ * Allowed email template codes
10
+ * Default: ['verify-email', 'password-lost']
11
+ */
12
+ emailCodes?: Array<{ label: string; value: string }>;
13
+ }
14
+
15
+ /**
16
+ * Create EmailTemplates collection with optional configuration
17
+ *
18
+ * @param config - Optional configuration for email codes
19
+ * @returns CollectionConfig for EmailTemplates
20
+ */
21
+ export function createEmailTemplatesCollection(config?: EmailTemplatesConfig): CollectionConfig {
22
+ const emailCodes = config?.emailCodes || [
23
+ { label: 'verify-email', value: 'verify-email' },
24
+ { label: 'password-lost', value: 'password-lost' },
25
+ ];
26
+
27
+ return {
28
+ slug: 'email-templates',
29
+ labels: {
30
+ singular: { fr: "Modèle d'email", nl: 'E-mail sjabloon', en: 'Email Template' },
31
+ plural: { fr: "Modèles d'emails", nl: 'E-mail sjablonen', en: 'Email Templates' },
32
+ },
33
+ admin: {
34
+ description: { fr: "Gérer les modèles d'emails", nl: 'Beheer e-mail sjablonen', en: 'Manage email templates' },
35
+ group: { fr: '✉️ Communication', nl: '✉️ Communicatie', en: '✉️ Communication' },
36
+ useAsTitle: 'name',
37
+ defaultColumns: ['code', 'name'],
38
+ hideAPIURL: true,
39
+ },
40
+ access: {
41
+ create: admins, // Not allowed to create new emails templates
42
+ delete: () => false, // Not allowed to delete emails templates
43
+ read: admins,
44
+ update: admins,
45
+ },
46
+ fields: [
47
+ {
48
+ type: 'row',
49
+ fields: [
50
+ {
51
+ name: 'name',
52
+ label: { fr: 'Nom (interne)', nl: 'Naam (intern)', en: 'Name (internal)' },
53
+ type: 'text',
54
+ required: true,
55
+ unique: true,
56
+ },
57
+
58
+ {
59
+ name: 'code',
60
+ label: { fr: 'Identifiant', nl: 'Identificatie', en: 'Identifier' },
61
+ type: 'select',
62
+ options: emailCodes,
63
+ required: true,
64
+ unique: true,
65
+ },
66
+ ],
67
+ },
68
+ {
69
+ name: 'subject',
70
+ label: { fr: "Sujet de l'email", nl: 'Onderwerp van de e-mail', en: 'Email subject' },
71
+ type: 'text',
72
+ required: true,
73
+ localized: true,
74
+ },
75
+ {
76
+ name: 'body',
77
+ label: { fr: "Corps de l'email", nl: 'Inhoud van de e-mail', en: 'Email body' },
78
+ type: 'richText',
79
+ required: true,
80
+ localized: true,
81
+ },
82
+ ],
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Default EmailTemplates collection with standard email codes
88
+ */
89
+ export const EmailTemplates = createEmailTemplatesCollection();
@@ -0,0 +1,24 @@
1
+ import type { CollectionConfig } from 'payload';
2
+
3
+ import { publicReadAuthenticatedAccess } from '../../access/publicReadAuthenticatedAccess';
4
+
5
+ export const Media: CollectionConfig = {
6
+ slug: 'media',
7
+ labels: {
8
+ singular: { fr: 'Upload', en: 'Upload', nl: 'Upload' },
9
+ plural: { fr: 'Uploads', en: 'Uploads', nl: 'Uploads' },
10
+ },
11
+ admin: {
12
+ hideAPIURL: true,
13
+ group: { fr: 'Uploads et médias', en: 'Uploads and Media', nl: 'Uploads en media' },
14
+ },
15
+ access: publicReadAuthenticatedAccess,
16
+ fields: [
17
+ {
18
+ name: 'alt',
19
+ type: 'text',
20
+ required: false,
21
+ },
22
+ ],
23
+ upload: true,
24
+ };
@@ -0,0 +1,16 @@
1
+ import type { TextareaFieldServerComponent } from 'payload';
2
+
3
+ export const HTMLViewer: TextareaFieldServerComponent = async ({ data }) => {
4
+ return (
5
+ <div
6
+ style={{
7
+ padding: '1rem',
8
+ border: '1px solid #ccc',
9
+ backgroundColor: '#f9f9f9',
10
+ borderRadius: '6px',
11
+ marginTop: '0.5rem',
12
+ }}
13
+ dangerouslySetInnerHTML={{ __html: data.htmlBody || '' }}
14
+ />
15
+ );
16
+ };