domma-cms 0.3.0 → 0.5.2

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 (150) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +167 -23
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/navigation.json +5 -15
  69. package/config/plugins.json +19 -29
  70. package/config/server.json +6 -6
  71. package/config/site.json +16 -6
  72. package/package.json +25 -10
  73. package/plugins/example-analytics/stats.json +17 -12
  74. package/plugins/form-builder/data/forms/contacts.json +62 -62
  75. package/plugins/form-builder/data/forms/enquiries.json +103 -0
  76. package/plugins/form-builder/data/forms/feedback.json +17 -16
  77. package/plugins/form-builder/data/forms/notes.json +79 -0
  78. package/plugins/form-builder/data/forms/to-do.json +100 -0
  79. package/plugins/form-builder/data/submissions/contacts.json +1 -26
  80. package/plugins/form-builder/data/submissions/notes.json +1 -0
  81. package/plugins/form-builder/data/submissions/to-do.json +1 -0
  82. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  83. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  84. package/plugins/theme-roller/config.js +1 -0
  85. package/plugins/theme-roller/plugin.js +233 -0
  86. package/plugins/theme-roller/plugin.json +31 -0
  87. package/plugins/theme-roller/public/active-theme.css +0 -0
  88. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  89. package/public/css/forms.css +1 -0
  90. package/public/css/site.css +1 -1
  91. package/public/js/forms.js +1 -0
  92. package/public/js/site.js +1 -1
  93. package/scripts/build.js +194 -129
  94. package/scripts/pro.js +254 -0
  95. package/scripts/reset.js +33 -8
  96. package/scripts/seed.js +677 -128
  97. package/scripts/setup.js +1 -0
  98. package/server/middleware/auth.js +136 -120
  99. package/server/routes/api/actions.js +200 -0
  100. package/server/routes/api/auth.js +292 -146
  101. package/server/routes/api/blocks.js +84 -0
  102. package/server/routes/api/collections.js +79 -27
  103. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
  104. package/server/routes/api/layouts.js +49 -39
  105. package/server/routes/api/media.js +118 -92
  106. package/server/routes/api/navigation.js +40 -36
  107. package/server/routes/api/pages.js +132 -118
  108. package/server/routes/api/plugins.js +6 -3
  109. package/server/routes/api/settings.js +104 -88
  110. package/server/routes/api/users.js +27 -19
  111. package/server/routes/api/views.js +148 -0
  112. package/server/routes/public.js +124 -108
  113. package/server/server.js +269 -181
  114. package/server/services/actions.js +387 -0
  115. package/server/services/adapterRegistry.js +98 -0
  116. package/server/services/adapters/FileAdapter.js +192 -0
  117. package/server/services/adapters/MongoAdapter.js +220 -0
  118. package/server/services/blocks.js +162 -0
  119. package/server/services/collections.js +74 -86
  120. package/server/services/connectionManager.js +102 -0
  121. package/server/services/content.js +312 -307
  122. package/server/services/email.js +126 -0
  123. package/server/services/forms.js +173 -0
  124. package/server/services/markdown.js +1378 -747
  125. package/server/services/permissionRegistry.js +173 -0
  126. package/server/services/presetCollections.js +251 -0
  127. package/server/services/renderer.js +98 -2
  128. package/server/services/roles.js +227 -0
  129. package/server/services/rowAccess.js +104 -0
  130. package/server/services/userProfiles.js +199 -0
  131. package/server/services/users.js +281 -212
  132. package/server/services/views.js +280 -0
  133. package/server/templates/page.html +124 -113
  134. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  135. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  136. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  137. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  138. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  139. package/plugins/form-builder/config.js +0 -9
  140. package/plugins/form-builder/data/forms/consent.json +0 -104
  141. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  142. package/plugins/form-builder/data/submissions/consent.json +0 -13
  143. package/plugins/form-builder/plugin.json +0 -52
  144. package/plugins/form-builder/public/inject-body.html +0 -352
  145. package/plugins/form-builder/public/inject-head.html +0 -58
  146. package/plugins/form-builder/public/package.json +0 -1
  147. package/scripts/copy-domma.js +0 -48
  148. package/server/services/userTypes.js +0 -167
  149. /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
  150. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -0,0 +1,1411 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="code"></span> API Reference</h1>
3
+ </div>
4
+
5
+ <style>
6
+ .method-badge {
7
+ display: inline-block;
8
+ font-size: 11px;
9
+ font-weight: 700;
10
+ letter-spacing: 0.5px;
11
+ padding: 2px 7px;
12
+ border-radius: 4px;
13
+ font-family: var(--dm-font-mono, monospace);
14
+ margin-right: 6px;
15
+ vertical-align: middle;
16
+ }
17
+
18
+ .method-get {
19
+ background: #d1fae5;
20
+ color: #065f46;
21
+ }
22
+
23
+ .method-post {
24
+ background: #dbeafe;
25
+ color: #1e40af;
26
+ }
27
+
28
+ .method-put {
29
+ background: #fef3c7;
30
+ color: #92400e;
31
+ }
32
+
33
+ .method-patch {
34
+ background: #ccfbf1;
35
+ color: #134e4a;
36
+ }
37
+
38
+ .method-delete {
39
+ background: #fee2e2;
40
+ color: #991b1b;
41
+ }
42
+
43
+ .endpoint-path {
44
+ font-family: var(--dm-font-mono, monospace);
45
+ font-size: 14px;
46
+ }
47
+
48
+ .auth-note {
49
+ font-size: 12px;
50
+ color: var(--dm-color-text-muted, #6b7280);
51
+ margin: 4px 0 12px;
52
+ }
53
+
54
+ .auth-note code {
55
+ font-size: 11px;
56
+ }
57
+
58
+ .docs-body h3 {
59
+ margin-top: 24px;
60
+ margin-bottom: 8px;
61
+ }
62
+
63
+ .docs-body h3:first-child {
64
+ margin-top: 0;
65
+ }
66
+ </style>
67
+
68
+ <div class="row">
69
+ <div class="col-12">
70
+
71
+ <div class="card mb-4">
72
+ <div class="card-body docs-body">
73
+ <p>
74
+ All admin endpoints are prefixed with <code>/api</code> and require a Bearer token in the
75
+ <code>Authorization</code> header unless noted otherwise. Content type for request bodies is
76
+ <code>application/json</code>. Tokens are obtained via <code>POST /api/auth/login</code>.
77
+ </p>
78
+ <p>
79
+ Base URL: <code>http://your-domain/api</code>
80
+ </p>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- ─── Authentication ─────────────────────────────────────────── -->
85
+ <div class="card card-collapsible mb-4">
86
+ <div class="card-header" role="button" tabindex="0">
87
+ <div class="card-header-content"><h2><span data-icon="lock"></span> Authentication</h2></div>
88
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
89
+ </div>
90
+ <div class="card-body docs-body">
91
+
92
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/auth/setup-status</span>
93
+ </h3>
94
+ <p class="auth-note">No authentication required.</p>
95
+ <p>Check whether the CMS has been set up. Returns <code>{ needsSetup: true }</code> when no users exist.</p>
96
+ <pre class="code-block"><code>// Response
97
+ { "needsSetup": false }</code></pre>
98
+
99
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/setup</span></h3>
100
+ <p class="auth-note">No authentication required. Only succeeds when zero users exist.</p>
101
+ <p>Create the initial admin account. Blocked once any user exists (returns 403).</p>
102
+ <table class="table table-sm">
103
+ <thead>
104
+ <tr>
105
+ <th>Field</th>
106
+ <th>Type</th>
107
+ <th>Description</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ <tr>
112
+ <td><code>name</code></td>
113
+ <td>string</td>
114
+ <td>Display name</td>
115
+ </tr>
116
+ <tr>
117
+ <td><code>email</code></td>
118
+ <td>string</td>
119
+ <td>Email address</td>
120
+ </tr>
121
+ <tr>
122
+ <td><code>password</code></td>
123
+ <td>string</td>
124
+ <td>Minimum 8 characters</td>
125
+ </tr>
126
+ </tbody>
127
+ </table>
128
+ <pre class="code-block"><code>// Response 201
129
+ { "token": "eyJ...", "refreshToken": "eyJ...", "user": { "id": "...", "name": "...", "email": "...", "role": "admin" } }</code></pre>
130
+
131
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/login</span></h3>
132
+ <p class="auth-note">No authentication required.</p>
133
+ <p>Authenticate with email and password. Returns access and refresh tokens.</p>
134
+ <table class="table table-sm">
135
+ <thead>
136
+ <tr>
137
+ <th>Field</th>
138
+ <th>Type</th>
139
+ <th>Description</th>
140
+ </tr>
141
+ </thead>
142
+ <tbody>
143
+ <tr>
144
+ <td><code>email</code></td>
145
+ <td>string</td>
146
+ <td>User email</td>
147
+ </tr>
148
+ <tr>
149
+ <td><code>password</code></td>
150
+ <td>string</td>
151
+ <td>User password</td>
152
+ </tr>
153
+ </tbody>
154
+ </table>
155
+ <pre class="code-block"><code>// Response 200
156
+ { "token": "eyJ...", "refreshToken": "eyJ...", "user": { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin" } }
157
+
158
+ // Error 401
159
+ { "error": "Invalid credentials" }</code></pre>
160
+
161
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/auth/me</span></h3>
162
+ <p class="auth-note">Requires Bearer token.</p>
163
+ <p>Return the authenticated user's profile.</p>
164
+ <pre class="code-block"><code>// Response 200
165
+ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "isActive": true }</code></pre>
166
+
167
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/logout</span></h3>
168
+ <p class="auth-note">No authentication required. Safe to call without a token.</p>
169
+ <p>Blacklists the provided refresh token. The in-memory blacklist is cleared on server restart.</p>
170
+ <table class="table table-sm">
171
+ <thead>
172
+ <tr>
173
+ <th>Field</th>
174
+ <th>Type</th>
175
+ <th>Description</th>
176
+ </tr>
177
+ </thead>
178
+ <tbody>
179
+ <tr>
180
+ <td><code>refreshToken</code></td>
181
+ <td>string</td>
182
+ <td>The refresh token to revoke (optional)</td>
183
+ </tr>
184
+ </tbody>
185
+ </table>
186
+ <pre class="code-block"><code>// Response 200
187
+ { "ok": true }</code></pre>
188
+
189
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/refresh</span></h3>
190
+ <p class="auth-note">No authentication required. Provide a valid refresh token.</p>
191
+ <p>Exchange a refresh token for a new access token.</p>
192
+ <table class="table table-sm">
193
+ <thead>
194
+ <tr>
195
+ <th>Field</th>
196
+ <th>Type</th>
197
+ <th>Description</th>
198
+ </tr>
199
+ </thead>
200
+ <tbody>
201
+ <tr>
202
+ <td><code>refreshToken</code></td>
203
+ <td>string</td>
204
+ <td>A valid, non-revoked refresh token</td>
205
+ </tr>
206
+ </tbody>
207
+ </table>
208
+ <pre class="code-block"><code>// Response 200
209
+ { "token": "eyJ..." }
210
+
211
+ // Error 401
212
+ { "error": "Invalid or expired refresh token" }</code></pre>
213
+
214
+ </div>
215
+ </div>
216
+
217
+ <!-- ─── Pages ──────────────────────────────────────────────────── -->
218
+ <div class="card card-collapsible mb-4">
219
+ <div class="card-header" role="button" tabindex="0">
220
+ <div class="card-header-content"><h2><span data-icon="file-text"></span> Pages</h2></div>
221
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
222
+ </div>
223
+ <div class="card-body docs-body">
224
+
225
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/pages/preview</span></h3>
226
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission.</p>
227
+ <p>Render Markdown to HTML (shortcodes processed, no frontmatter). Useful for live editor previews.</p>
228
+ <table class="table table-sm">
229
+ <thead>
230
+ <tr>
231
+ <th>Field</th>
232
+ <th>Type</th>
233
+ <th>Description</th>
234
+ </tr>
235
+ </thead>
236
+ <tbody>
237
+ <tr>
238
+ <td><code>markdown</code></td>
239
+ <td>string</td>
240
+ <td>Markdown string to render</td>
241
+ </tr>
242
+ </tbody>
243
+ </table>
244
+ <pre class="code-block"><code>// Response 200
245
+ { "html": "&lt;p&gt;Hello &lt;strong&gt;world&lt;/strong&gt;&lt;/p&gt;" }</code></pre>
246
+
247
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/pages/tags</span></h3>
248
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission.</p>
249
+ <p>Aggregate all unique tags across every page, sorted alphabetically.</p>
250
+ <pre class="code-block"><code>// Response 200
251
+ { "tags": ["guide", "news", "tutorial"] }</code></pre>
252
+
253
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/pages</span></h3>
254
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission.</p>
255
+ <p>List all pages with their metadata. Body content is excluded.</p>
256
+ <pre class="code-block"><code>// Response 200
257
+ [
258
+ {
259
+ "urlPath": "/about",
260
+ "title": "About Us",
261
+ "slug": "about",
262
+ "status": "published",
263
+ "layout": "default",
264
+ "showInNav": true,
265
+ "sortOrder": 1,
266
+ "category": null,
267
+ "visibility": "public",
268
+ "tags": [],
269
+ "updatedAt": "2024-01-15T10:30:00.000Z",
270
+ "createdAt": "2024-01-01T09:00:00.000Z"
271
+ }
272
+ ]</code></pre>
273
+
274
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/pages/*</span></h3>
275
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission. The <code>*</code> is the URL path,
276
+ e.g. <code>/api/pages/about</code> or <code>/api/pages/blog/post-1</code>.</p>
277
+ <p>Retrieve a single page including its frontmatter and full body content.</p>
278
+ <pre class="code-block"><code>// Response 200
279
+ {
280
+ "urlPath": "/about",
281
+ "title": "About Us",
282
+ "body": "## Our Story\n\nWe started...",
283
+ "status": "published",
284
+ ...
285
+ }
286
+
287
+ // Error 404
288
+ { "error": "Page not found" }</code></pre>
289
+
290
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/pages</span></h3>
291
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission.</p>
292
+ <p>Create a new page. Fails with 409 if a page already exists at the given path.</p>
293
+ <table class="table table-sm">
294
+ <thead>
295
+ <tr>
296
+ <th>Field</th>
297
+ <th>Type</th>
298
+ <th>Description</th>
299
+ </tr>
300
+ </thead>
301
+ <tbody>
302
+ <tr>
303
+ <td><code>urlPath</code></td>
304
+ <td>string</td>
305
+ <td>Required. Public URL path e.g. <code>/about</code></td>
306
+ </tr>
307
+ <tr>
308
+ <td><code>frontmatter</code></td>
309
+ <td>object</td>
310
+ <td>YAML frontmatter fields (title, status, etc.)</td>
311
+ </tr>
312
+ <tr>
313
+ <td><code>body</code></td>
314
+ <td>string</td>
315
+ <td>Markdown content</td>
316
+ </tr>
317
+ </tbody>
318
+ </table>
319
+ <pre class="code-block"><code>// Response 201 — returns the created page object</code></pre>
320
+
321
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/pages/*</span></h3>
322
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission.</p>
323
+ <p>Update an existing page. Optionally rename it to a new URL path (navigation links are rewritten
324
+ automatically).</p>
325
+ <table class="table table-sm">
326
+ <thead>
327
+ <tr>
328
+ <th>Field</th>
329
+ <th>Type</th>
330
+ <th>Description</th>
331
+ </tr>
332
+ </thead>
333
+ <tbody>
334
+ <tr>
335
+ <td><code>frontmatter</code></td>
336
+ <td>object</td>
337
+ <td>Updated frontmatter fields</td>
338
+ </tr>
339
+ <tr>
340
+ <td><code>body</code></td>
341
+ <td>string</td>
342
+ <td>Updated Markdown content</td>
343
+ </tr>
344
+ <tr>
345
+ <td><code>newUrlPath</code></td>
346
+ <td>string</td>
347
+ <td>Optional. Rename the page to a different URL path</td>
348
+ </tr>
349
+ </tbody>
350
+ </table>
351
+ <pre class="code-block"><code>// Response 200 — returns the updated page object
352
+
353
+ // Error 404
354
+ { "error": "Page not found" }
355
+
356
+ // Error 409
357
+ { "error": "A page already exists at that path" }</code></pre>
358
+
359
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/pages/*</span></h3>
360
+ <p class="auth-note">Requires Bearer token + <code>pages</code> permission.</p>
361
+ <p>Delete a page and its source file.</p>
362
+ <pre class="code-block"><code>// Response 200
363
+ { "success": true }
364
+
365
+ // Error 404
366
+ { "error": "Page not found" }</code></pre>
367
+
368
+ </div>
369
+ </div>
370
+
371
+ <!-- ─── Settings ───────────────────────────────────────────────── -->
372
+ <div class="card card-collapsible mb-4">
373
+ <div class="card-header" role="button" tabindex="0">
374
+ <div class="card-header-content"><h2><span data-icon="settings"></span> Settings</h2></div>
375
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
376
+ </div>
377
+ <div class="card-body docs-body">
378
+
379
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/settings</span></h3>
380
+ <p class="auth-note">Requires Bearer token + <code>settings</code> permission.</p>
381
+ <p>Return the full site settings object from <code>config/site.json</code>.</p>
382
+ <pre class="code-block"><code>// Response 200
383
+ {
384
+ "siteName": "My Site",
385
+ "adminTheme": "charcoal-dark",
386
+ "smtp": { "host": "smtp.example.com", "port": 587, ... },
387
+ ...
388
+ }</code></pre>
389
+
390
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/settings</span></h3>
391
+ <p class="auth-note">Requires Bearer token + <code>settings</code> permission.</p>
392
+ <p>Replace the site settings object. Send the full merged object — partial updates overwrite the entire
393
+ config.</p>
394
+ <pre class="code-block"><code>// Request body — full site settings object
395
+ { "siteName": "My Site", "adminTheme": "ocean-dark", ... }
396
+
397
+ // Response 200
398
+ { "success": true }</code></pre>
399
+
400
+ <h3><span class="method-badge method-post">POST</span><span
401
+ class="endpoint-path">/api/settings/test-email</span></h3>
402
+ <p class="auth-note">Requires Bearer token + <code>settings</code> permission.</p>
403
+ <p>Send a test email using the stored SMTP configuration. Fails if SMTP host is not configured.</p>
404
+ <table class="table table-sm">
405
+ <thead>
406
+ <tr>
407
+ <th>Field</th>
408
+ <th>Type</th>
409
+ <th>Description</th>
410
+ </tr>
411
+ </thead>
412
+ <tbody>
413
+ <tr>
414
+ <td><code>to</code></td>
415
+ <td>string</td>
416
+ <td>Optional. Recipient address. Defaults to the configured From Address.</td>
417
+ </tr>
418
+ </tbody>
419
+ </table>
420
+ <pre class="code-block"><code>// Response 200
421
+ { "success": true, "message": "Test email sent to alice@example.com" }
422
+
423
+ // Error 400
424
+ { "error": "SMTP is not configured. Save your SMTP settings first." }</code></pre>
425
+
426
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/settings/custom-css</span>
427
+ </h3>
428
+ <p class="auth-note">Requires Bearer token + <code>settings</code> permission.</p>
429
+ <p>Return the current custom CSS from <code>content/custom.css</code>. Returns an empty string if the file does
430
+ not exist.</p>
431
+ <pre class="code-block"><code>// Response 200
432
+ { "css": "body { font-family: sans-serif; }" }</code></pre>
433
+
434
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/settings/custom-css</span>
435
+ </h3>
436
+ <p class="auth-note">Requires Bearer token + <code>settings</code> permission.</p>
437
+ <p>Write CSS to <code>content/custom.css</code>. Maximum size is 100 KB.</p>
438
+ <table class="table table-sm">
439
+ <thead>
440
+ <tr>
441
+ <th>Field</th>
442
+ <th>Type</th>
443
+ <th>Description</th>
444
+ </tr>
445
+ </thead>
446
+ <tbody>
447
+ <tr>
448
+ <td><code>css</code></td>
449
+ <td>string</td>
450
+ <td>CSS string (max 100 KB)</td>
451
+ </tr>
452
+ </tbody>
453
+ </table>
454
+ <pre class="code-block"><code>// Response 200
455
+ { "success": true }</code></pre>
456
+
457
+ </div>
458
+ </div>
459
+
460
+ <!-- ─── Layouts ────────────────────────────────────────────────── -->
461
+ <div class="card card-collapsible mb-4">
462
+ <div class="card-header" role="button" tabindex="0">
463
+ <div class="card-header-content"><h2><span data-icon="layout"></span> Layouts</h2></div>
464
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
465
+ </div>
466
+ <div class="card-body docs-body">
467
+
468
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/layouts</span></h3>
469
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
470
+ <p>Return all layout presets from <code>config/presets.json</code>.</p>
471
+ <pre class="code-block"><code>// Response 200
472
+ {
473
+ "default": { "label": "Default", "sections": [...] },
474
+ "full-width": { "label": "Full Width", "sections": [...] }
475
+ }</code></pre>
476
+
477
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/layouts</span></h3>
478
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
479
+ <p>Replace the entire layout presets object.</p>
480
+ <pre class="code-block"><code>// Response 200
481
+ { "success": true }</code></pre>
482
+
483
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/layouts/options</span></h3>
484
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
485
+ <p>Return layout display options (e.g. spacer size) stored in <code>config/site.json</code> under <code>layoutOptions</code>.
486
+ </p>
487
+ <pre class="code-block"><code>// Response 200
488
+ { "spacerSize": 8 }</code></pre>
489
+
490
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/layouts/options</span></h3>
491
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
492
+ <p>Merge layout option updates into the existing options. Existing keys not included in the request are
493
+ preserved.</p>
494
+ <table class="table table-sm">
495
+ <thead>
496
+ <tr>
497
+ <th>Field</th>
498
+ <th>Type</th>
499
+ <th>Description</th>
500
+ </tr>
501
+ </thead>
502
+ <tbody>
503
+ <tr>
504
+ <td><code>spacerSize</code></td>
505
+ <td>number</td>
506
+ <td>Default spacer block size in pixels</td>
507
+ </tr>
508
+ </tbody>
509
+ </table>
510
+ <pre class="code-block"><code>// Response 200
511
+ { "success": true }</code></pre>
512
+
513
+ </div>
514
+ </div>
515
+
516
+ <!-- ─── Navigation ─────────────────────────────────────────────── -->
517
+ <div class="card card-collapsible mb-4">
518
+ <div class="card-header" role="button" tabindex="0">
519
+ <div class="card-header-content"><h2><span data-icon="menu"></span> Navigation</h2></div>
520
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
521
+ </div>
522
+ <div class="card-body docs-body">
523
+
524
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/navigation</span></h3>
525
+ <p class="auth-note">Requires Bearer token + <code>navigation</code> permission.</p>
526
+ <p>Return the navigation configuration from <code>config/navigation.json</code>.</p>
527
+ <pre class="code-block"><code>// Response 200
528
+ {
529
+ "items": [
530
+ { "label": "Home", "url": "/" },
531
+ { "label": "About", "url": "/about" },
532
+ {
533
+ "label": "Resources",
534
+ "items": [
535
+ { "label": "Blog", "url": "/blog" }
536
+ ]
537
+ }
538
+ ]
539
+ }</code></pre>
540
+
541
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/navigation</span></h3>
542
+ <p class="auth-note">Requires Bearer token + <code>navigation</code> permission.</p>
543
+ <p>Replace the navigation config. Sub-items must use the <code>items</code> key (the server normalises <code>children</code>
544
+ to <code>items</code> automatically).</p>
545
+ <pre class="code-block"><code>// Request body
546
+ {
547
+ "items": [
548
+ { "label": "Home", "url": "/" },
549
+ { "label": "About", "url": "/about" }
550
+ ]
551
+ }
552
+
553
+ // Response 200
554
+ { "success": true }</code></pre>
555
+
556
+ </div>
557
+ </div>
558
+
559
+ <!-- ─── Media ──────────────────────────────────────────────────── -->
560
+ <div class="card card-collapsible mb-4">
561
+ <div class="card-header" role="button" tabindex="0">
562
+ <div class="card-header-content"><h2><span data-icon="image"></span> Media</h2></div>
563
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
564
+ </div>
565
+ <div class="card-body docs-body">
566
+
567
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/media</span></h3>
568
+ <p class="auth-note">Requires Bearer token + <code>media</code> permission.</p>
569
+ <p>List all media files in the uploads directory.</p>
570
+ <pre class="code-block"><code>// Response 200
571
+ [
572
+ { "name": "hero.jpg", "url": "/media/hero.jpg", "size": 204800, "mime": "image/jpeg" }
573
+ ]</code></pre>
574
+
575
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/media</span></h3>
576
+ <p class="auth-note">Requires Bearer token + <code>media</code> permission. Content-Type: <code>multipart/form-data</code>.
577
+ </p>
578
+ <p>Upload one or more files. Filenames are sanitised (only alphanumeric, dot, underscore, hyphen allowed).
579
+ Returns a single object for one file, or an array for multiple.</p>
580
+ <pre class="code-block"><code>// Response 201 (single file)
581
+ { "name": "photo.jpg", "url": "/media/photo.jpg", "size": 98304, "mime": "image/jpeg" }
582
+
583
+ // Response 201 (multiple files)
584
+ [
585
+ { "name": "photo1.jpg", "url": "/media/photo1.jpg", ... },
586
+ { "name": "photo2.jpg", "url": "/media/photo2.jpg", ... }
587
+ ]</code></pre>
588
+
589
+ <h3><span class="method-badge method-patch">PATCH</span><span class="endpoint-path">/api/media/:name</span></h3>
590
+ <p class="auth-note">Requires Bearer token + <code>media</code> permission.</p>
591
+ <p>Rename a media file. Returns the updated file info. Fails with 409 if the new name already exists.</p>
592
+ <table class="table table-sm">
593
+ <thead>
594
+ <tr>
595
+ <th>Field</th>
596
+ <th>Type</th>
597
+ <th>Description</th>
598
+ </tr>
599
+ </thead>
600
+ <tbody>
601
+ <tr>
602
+ <td><code>newName</code></td>
603
+ <td>string</td>
604
+ <td>New filename (will be sanitised)</td>
605
+ </tr>
606
+ </tbody>
607
+ </table>
608
+ <pre class="code-block"><code>// Response 200
609
+ { "name": "new-photo.jpg", "url": "/media/new-photo.jpg", ... }</code></pre>
610
+
611
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/media/:name</span>
612
+ </h3>
613
+ <p class="auth-note">Requires Bearer token + <code>media</code> permission.</p>
614
+ <p>Delete a media file from the uploads directory.</p>
615
+ <pre class="code-block"><code>// Response 200
616
+ { "success": true }</code></pre>
617
+
618
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/media/:name/info</span>
619
+ </h3>
620
+ <p class="auth-note">Requires Bearer token + <code>media</code> permission.</p>
621
+ <p>Return image metadata (dimensions, format, file size) for editable image formats (JPEG, PNG, WebP, GIF,
622
+ TIFF).</p>
623
+ <pre class="code-block"><code>// Response 200
624
+ { "width": 1920, "height": 1080, "format": "jpeg", "size": 204800 }
625
+
626
+ // Error 400
627
+ { "error": "Not an editable image format" }</code></pre>
628
+
629
+ <h3><span class="method-badge method-post">POST</span><span
630
+ class="endpoint-path">/api/media/:name/transform</span></h3>
631
+ <p class="auth-note">Requires Bearer token + <code>media</code> permission.</p>
632
+ <p>Apply image transformations (resize, crop, rotate, watermark, etc.) and optionally save to a new
633
+ filename.</p>
634
+ <table class="table table-sm">
635
+ <thead>
636
+ <tr>
637
+ <th>Field</th>
638
+ <th>Type</th>
639
+ <th>Description</th>
640
+ </tr>
641
+ </thead>
642
+ <tbody>
643
+ <tr>
644
+ <td><code>operations</code></td>
645
+ <td>object</td>
646
+ <td>Transformation operations to apply</td>
647
+ </tr>
648
+ <tr>
649
+ <td><code>saveAs</code></td>
650
+ <td>string</td>
651
+ <td>Optional. Output filename. Defaults to overwriting the source.</td>
652
+ </tr>
653
+ </tbody>
654
+ </table>
655
+ <pre class="code-block"><code>// Request body example
656
+ {
657
+ "operations": { "resize": { "width": 800, "height": 600 }, "format": "webp" },
658
+ "saveAs": "hero-800.webp"
659
+ }
660
+
661
+ // Response 200
662
+ { "name": "hero-800.webp", "url": "/media/hero-800.webp", ... }</code></pre>
663
+
664
+ </div>
665
+ </div>
666
+
667
+ <!-- ─── Users ──────────────────────────────────────────────────── -->
668
+ <div class="card card-collapsible mb-4">
669
+ <div class="card-header" role="button" tabindex="0">
670
+ <div class="card-header-content"><h2><span data-icon="users"></span> Users</h2></div>
671
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
672
+ </div>
673
+ <div class="card-body docs-body">
674
+
675
+ <p>Role hierarchy governs which users can manage other users. A manager cannot create, edit, or delete an admin.
676
+ Self-deletion is always blocked.</p>
677
+
678
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/users</span></h3>
679
+ <p class="auth-note">Requires Bearer token + <code>users</code> permission (admin or manager).</p>
680
+ <p>Return all users. Passwords are stripped from the response.</p>
681
+ <pre class="code-block"><code>// Response 200
682
+ [
683
+ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "isActive": true }
684
+ ]</code></pre>
685
+
686
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/users/:id</span></h3>
687
+ <p class="auth-note">Requires Bearer token. Accessible to the user themselves, or a user with <code>users</code>
688
+ permission.</p>
689
+ <p>Return a single user by ID.</p>
690
+ <pre class="code-block"><code>// Response 200
691
+ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "isActive": true }
692
+
693
+ // Error 404
694
+ { "error": "User not found" }</code></pre>
695
+
696
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/users</span></h3>
697
+ <p class="auth-note">Requires Bearer token + <code>users</code> permission.</p>
698
+ <p>Create a new user. The actor cannot assign a role higher than their own level.</p>
699
+ <table class="table table-sm">
700
+ <thead>
701
+ <tr>
702
+ <th>Field</th>
703
+ <th>Type</th>
704
+ <th>Description</th>
705
+ </tr>
706
+ </thead>
707
+ <tbody>
708
+ <tr>
709
+ <td><code>name</code></td>
710
+ <td>string</td>
711
+ <td>Display name</td>
712
+ </tr>
713
+ <tr>
714
+ <td><code>email</code></td>
715
+ <td>string</td>
716
+ <td>Unique email address</td>
717
+ </tr>
718
+ <tr>
719
+ <td><code>password</code></td>
720
+ <td>string</td>
721
+ <td>Minimum 8 characters</td>
722
+ </tr>
723
+ <tr>
724
+ <td><code>role</code></td>
725
+ <td>string</td>
726
+ <td>Optional. Defaults to <code>editor</code>.</td>
727
+ </tr>
728
+ </tbody>
729
+ </table>
730
+ <pre class="code-block"><code>// Response 201
731
+ { "id": "uuid", "name": "Bob", "email": "bob@example.com", "role": "editor", "isActive": true }
732
+
733
+ // Error 409
734
+ { "error": "Email already in use" }</code></pre>
735
+
736
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/users/:id</span></h3>
737
+ <p class="auth-note">Requires Bearer token + <code>users</code> permission.</p>
738
+ <p>Update a user's details. Managers cannot edit admins. Role escalation beyond the actor's own level is
739
+ blocked.</p>
740
+ <table class="table table-sm">
741
+ <thead>
742
+ <tr>
743
+ <th>Field</th>
744
+ <th>Type</th>
745
+ <th>Description</th>
746
+ </tr>
747
+ </thead>
748
+ <tbody>
749
+ <tr>
750
+ <td><code>name</code></td>
751
+ <td>string</td>
752
+ <td>New display name</td>
753
+ </tr>
754
+ <tr>
755
+ <td><code>email</code></td>
756
+ <td>string</td>
757
+ <td>New email address</td>
758
+ </tr>
759
+ <tr>
760
+ <td><code>password</code></td>
761
+ <td>string</td>
762
+ <td>New password (min 8 chars)</td>
763
+ </tr>
764
+ <tr>
765
+ <td><code>role</code></td>
766
+ <td>string</td>
767
+ <td>New role</td>
768
+ </tr>
769
+ <tr>
770
+ <td><code>isActive</code></td>
771
+ <td>boolean</td>
772
+ <td>Enable or disable the account</td>
773
+ </tr>
774
+ </tbody>
775
+ </table>
776
+ <pre class="code-block"><code>// Response 200 — returns the updated user object</code></pre>
777
+
778
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/users/:id</span></h3>
779
+ <p class="auth-note">Requires Bearer token + <code>users</code> permission.</p>
780
+ <p>Delete a user. Cannot delete your own account or a user with a higher role level.</p>
781
+ <pre class="code-block"><code>// Response 200
782
+ { "success": true }
783
+
784
+ // Error 403
785
+ { "error": "You cannot delete your own account" }</code></pre>
786
+
787
+ </div>
788
+ </div>
789
+
790
+ <!-- ─── Plugins ────────────────────────────────────────────────── -->
791
+ <div class="card card-collapsible mb-4">
792
+ <div class="card-header" role="button" tabindex="0">
793
+ <div class="card-header-content"><h2><span data-icon="package"></span> Plugins</h2></div>
794
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
795
+ </div>
796
+ <div class="card-body docs-body">
797
+
798
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/plugins</span></h3>
799
+ <p class="auth-note">Requires Bearer token + admin role.</p>
800
+ <p>List all discovered plugins with their current enabled state and settings.</p>
801
+ <pre class="code-block"><code>// Response 200
802
+ [
803
+ {
804
+ "name": "form-builder",
805
+ "displayName": "Form Builder",
806
+ "version": "1.0.0",
807
+ "description": "Build and manage contact forms.",
808
+ "author": "Domma Team",
809
+ "date": "2024-01-01",
810
+ "icon": "clipboard",
811
+ "enabled": true,
812
+ "settings": {}
813
+ }
814
+ ]</code></pre>
815
+
816
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/plugins/:name</span></h3>
817
+ <p class="auth-note">Requires Bearer token + admin role.</p>
818
+ <p>Enable or disable a plugin, or update its settings. Plugin must exist in the <code>plugins/</code> directory.
819
+ </p>
820
+ <table class="table table-sm">
821
+ <thead>
822
+ <tr>
823
+ <th>Field</th>
824
+ <th>Type</th>
825
+ <th>Description</th>
826
+ </tr>
827
+ </thead>
828
+ <tbody>
829
+ <tr>
830
+ <td><code>enabled</code></td>
831
+ <td>boolean</td>
832
+ <td>Whether the plugin is active</td>
833
+ </tr>
834
+ <tr>
835
+ <td><code>settings</code></td>
836
+ <td>object</td>
837
+ <td>Plugin-specific settings object</td>
838
+ </tr>
839
+ </tbody>
840
+ </table>
841
+ <pre class="code-block"><code>// Response 200
842
+ { "success": true }
843
+
844
+ // Error 404
845
+ { "error": "Plugin not found" }</code></pre>
846
+
847
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/plugins/admin-config</span>
848
+ </h3>
849
+ <p class="auth-note">Requires Bearer token (any role).</p>
850
+ <p>Return the merged admin configuration for all enabled plugins — sidebar items, SPA routes, and view entry
851
+ points. Used by the admin SPA on startup to inject plugin UI.</p>
852
+ <pre class="code-block"><code>// Response 200
853
+ {
854
+ "sidebar": [
855
+ { "id": "form-builder", "text": "Form Settings", "icon": "clipboard", "url": "#/form-settings" }
856
+ ],
857
+ "routes": [
858
+ { "path": "/form-settings", "view": "formSettings", "title": "Form Settings - Domma CMS" }
859
+ ],
860
+ "views": {
861
+ "formSettings": { "entry": "form-builder/admin/views/settings.js", "exportName": "formSettingsView" }
862
+ }
863
+ }</code></pre>
864
+
865
+ </div>
866
+ </div>
867
+
868
+ <!-- ─── Collections ────────────────────────────────────────────── -->
869
+ <div class="card card-collapsible mb-4">
870
+ <div class="card-header" role="button" tabindex="0">
871
+ <div class="card-header-content"><h2><span data-icon="database"></span> Collections</h2></div>
872
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
873
+ </div>
874
+ <div class="card-body docs-body">
875
+
876
+ <p>Collections have two access planes: <strong>admin endpoints</strong> (authenticated, role-gated) and <strong>public
877
+ endpoints</strong> (access level configured per collection).</p>
878
+
879
+ <h3 style="margin-top:16px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">Schema
880
+ Management</h3>
881
+
882
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/collections</span></h3>
883
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
884
+ <p>List all collection schemas (metadata only, no entries).</p>
885
+ <pre class="code-block"><code>// Response 200
886
+ [
887
+ { "slug": "blog", "title": "Blog Posts", "description": "...", "fields": [...] }
888
+ ]</code></pre>
889
+
890
+ <h3><span class="method-badge method-get">GET</span><span
891
+ class="endpoint-path">/api/collections/pro-status</span></h3>
892
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
893
+ <p>Check whether the Pro (MongoDB) storage adapter is available. Returns named connections if configured.</p>
894
+ <pre class="code-block"><code>// Response 200 (free)
895
+ { "pro": false, "connections": [] }
896
+
897
+ // Response 200 (pro)
898
+ { "pro": true, "connections": ["default", "analytics"] }</code></pre>
899
+
900
+ <h3><span class="method-badge method-get">GET</span><span
901
+ class="endpoint-path">/api/collections/connections</span></h3>
902
+ <p class="auth-note">Requires Bearer token + admin role.</p>
903
+ <p>Return configured MongoDB connections from <code>config/connections.json</code>.</p>
904
+ <pre class="code-block"><code>// Response 200
905
+ {
906
+ "default": { "type": "mongodb", "uri": "mongodb://localhost:27017", "database": "my_cms" }
907
+ }</code></pre>
908
+
909
+ <h3><span class="method-badge method-put">PUT</span><span
910
+ class="endpoint-path">/api/collections/connections</span></h3>
911
+ <p class="auth-note">Requires Bearer token + admin role.</p>
912
+ <p>Save MongoDB connection definitions. Each connection requires <code>type</code>, <code>uri</code>, and <code>database</code>.
913
+ </p>
914
+ <table class="table table-sm">
915
+ <thead>
916
+ <tr>
917
+ <th>Field</th>
918
+ <th>Type</th>
919
+ <th>Description</th>
920
+ </tr>
921
+ </thead>
922
+ <tbody>
923
+ <tr>
924
+ <td><code>{name}</code></td>
925
+ <td>object</td>
926
+ <td>Named connection with <code>type</code>, <code>uri</code>, <code>database</code></td>
927
+ </tr>
928
+ </tbody>
929
+ </table>
930
+ <pre class="code-block"><code>// Response 200
931
+ { "success": true }
932
+
933
+ // Error 400
934
+ { "error": "Connection \"default\" requires type, uri, and database" }</code></pre>
935
+
936
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/collections</span></h3>
937
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
938
+ <p>Create a new collection. A <code>slug</code> is auto-generated from the title if not provided.</p>
939
+ <table class="table table-sm">
940
+ <thead>
941
+ <tr>
942
+ <th>Field</th>
943
+ <th>Type</th>
944
+ <th>Description</th>
945
+ </tr>
946
+ </thead>
947
+ <tbody>
948
+ <tr>
949
+ <td><code>title</code></td>
950
+ <td>string</td>
951
+ <td>Required. Human-readable collection name</td>
952
+ </tr>
953
+ <tr>
954
+ <td><code>slug</code></td>
955
+ <td>string</td>
956
+ <td>Optional. URL-safe identifier. Auto-generated if omitted.</td>
957
+ </tr>
958
+ <tr>
959
+ <td><code>description</code></td>
960
+ <td>string</td>
961
+ <td>Optional description</td>
962
+ </tr>
963
+ <tr>
964
+ <td><code>fields</code></td>
965
+ <td>array</td>
966
+ <td>Field definitions</td>
967
+ </tr>
968
+ <tr>
969
+ <td><code>api</code></td>
970
+ <td>object</td>
971
+ <td>Public API access config per operation</td>
972
+ </tr>
973
+ <tr>
974
+ <td><code>storage</code></td>
975
+ <td>object</td>
976
+ <td>Optional. Pro: <code>{ "adapter": "mongodb", "connection": "default" }</code></td>
977
+ </tr>
978
+ </tbody>
979
+ </table>
980
+ <pre class="code-block"><code>// Response 201 — returns the created schema object
981
+
982
+ // Error 409
983
+ { "error": "A collection with that slug already exists" }</code></pre>
984
+
985
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/collections/:slug</span>
986
+ </h3>
987
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
988
+ <p>Return the schema for a single collection by slug.</p>
989
+ <pre class="code-block"><code>// Response 200
990
+ {
991
+ "slug": "blog",
992
+ "title": "Blog Posts",
993
+ "fields": [
994
+ { "name": "title", "type": "text", "required": true },
995
+ { "name": "body", "type": "richtext" }
996
+ ],
997
+ "api": { "read": { "enabled": true, "access": "public" }, ... }
998
+ }
999
+
1000
+ // Error 404
1001
+ { "error": "Collection not found" }</code></pre>
1002
+
1003
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/collections/:slug</span>
1004
+ </h3>
1005
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1006
+ <p>Update a collection schema.</p>
1007
+ <pre class="code-block"><code>// Response 200 — returns the updated schema
1008
+
1009
+ // Error 404
1010
+ { "error": "Collection not found" }</code></pre>
1011
+
1012
+ <h3><span class="method-badge method-delete">DELETE</span><span
1013
+ class="endpoint-path">/api/collections/:slug</span></h3>
1014
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1015
+ <p>Delete a collection and all its entries. Preset collections (e.g. <code>roles</code>) cannot be deleted.
1016
+ </p>
1017
+ <pre class="code-block"><code>// Response 200
1018
+ { "success": true }
1019
+
1020
+ // Error 403
1021
+ { "error": "Cannot delete a preset collection" }</code></pre>
1022
+
1023
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">Admin Entry
1024
+ CRUD</h3>
1025
+
1026
+ <h3><span class="method-badge method-get">GET</span><span
1027
+ class="endpoint-path">/api/collections/:slug/entries</span></h3>
1028
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1029
+ <p>List entries with pagination, sorting, and full-text search.</p>
1030
+ <table class="table table-sm">
1031
+ <thead>
1032
+ <tr>
1033
+ <th>Query param</th>
1034
+ <th>Default</th>
1035
+ <th>Description</th>
1036
+ </tr>
1037
+ </thead>
1038
+ <tbody>
1039
+ <tr>
1040
+ <td><code>page</code></td>
1041
+ <td>1</td>
1042
+ <td>Page number</td>
1043
+ </tr>
1044
+ <tr>
1045
+ <td><code>limit</code></td>
1046
+ <td>50</td>
1047
+ <td>Entries per page</td>
1048
+ </tr>
1049
+ <tr>
1050
+ <td><code>sort</code></td>
1051
+ <td>createdAt</td>
1052
+ <td>Field to sort by</td>
1053
+ </tr>
1054
+ <tr>
1055
+ <td><code>order</code></td>
1056
+ <td>desc</td>
1057
+ <td><code>asc</code> or <code>desc</code></td>
1058
+ </tr>
1059
+ <tr>
1060
+ <td><code>search</code></td>
1061
+ <td>—</td>
1062
+ <td>Full-text search query</td>
1063
+ </tr>
1064
+ </tbody>
1065
+ </table>
1066
+ <pre class="code-block"><code>// Response 200
1067
+ {
1068
+ "entries": [ { "id": "uuid", "data": { ... }, "createdAt": "...", "updatedAt": "..." } ],
1069
+ "total": 42,
1070
+ "page": 1,
1071
+ "limit": 50
1072
+ }</code></pre>
1073
+
1074
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/collections/:slug/entries/:id</span>
1075
+ </h3>
1076
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1077
+ <p>Return a single entry by ID.</p>
1078
+ <pre class="code-block"><code>// Response 200
1079
+ { "id": "uuid", "data": { "title": "Hello World", "body": "..." }, "createdAt": "...", "updatedAt": "..." }
1080
+
1081
+ // Error 404
1082
+ { "error": "Entry not found" }</code></pre>
1083
+
1084
+ <h3><span class="method-badge method-post">POST</span><span
1085
+ class="endpoint-path">/api/collections/:slug/entries</span></h3>
1086
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1087
+ <p>Create a new entry. Data is validated against the collection schema.</p>
1088
+ <table class="table table-sm">
1089
+ <thead>
1090
+ <tr>
1091
+ <th>Field</th>
1092
+ <th>Type</th>
1093
+ <th>Description</th>
1094
+ </tr>
1095
+ </thead>
1096
+ <tbody>
1097
+ <tr>
1098
+ <td><code>data</code></td>
1099
+ <td>object</td>
1100
+ <td>Entry field values keyed by field name</td>
1101
+ </tr>
1102
+ </tbody>
1103
+ </table>
1104
+ <pre class="code-block"><code>// Response 201 — returns the created entry</code></pre>
1105
+
1106
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/collections/:slug/entries/:id</span>
1107
+ </h3>
1108
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1109
+ <p>Update an entry. Data is validated against the schema.</p>
1110
+ <pre class="code-block"><code>// Response 200 — returns the updated entry
1111
+
1112
+ // Error 404
1113
+ { "error": "Entry not found" }</code></pre>
1114
+
1115
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/collections/:slug/entries/:id</span>
1116
+ </h3>
1117
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1118
+ <p>Delete a single entry. Deleting the root admin role from <code>roles</code> is blocked.</p>
1119
+ <pre class="code-block"><code>// Response 200
1120
+ { "success": true }</code></pre>
1121
+
1122
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/collections/:slug/entries</span>
1123
+ </h3>
1124
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1125
+ <p>Clear all entries from a collection. Irreversible.</p>
1126
+ <pre class="code-block"><code>// Response 200
1127
+ { "success": true }</code></pre>
1128
+
1129
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">Export &amp;
1130
+ Import</h3>
1131
+
1132
+ <h3><span class="method-badge method-get">GET</span><span
1133
+ class="endpoint-path">/api/collections/:slug/export</span></h3>
1134
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1135
+ <p>Download all entries as a file attachment.</p>
1136
+ <table class="table table-sm">
1137
+ <thead>
1138
+ <tr>
1139
+ <th>Query param</th>
1140
+ <th>Values</th>
1141
+ <th>Description</th>
1142
+ </tr>
1143
+ </thead>
1144
+ <tbody>
1145
+ <tr>
1146
+ <td><code>format</code></td>
1147
+ <td><code>json</code> (default), <code>csv</code></td>
1148
+ <td>Export format</td>
1149
+ </tr>
1150
+ </tbody>
1151
+ </table>
1152
+ <pre class="code-block"><code>// Response 200 — file download
1153
+ // Content-Disposition: attachment; filename="blog-entries.json"</code></pre>
1154
+
1155
+ <h3><span class="method-badge method-post">POST</span><span
1156
+ class="endpoint-path">/api/collections/:slug/import</span></h3>
1157
+ <p class="auth-note">Requires Bearer token + <code>collections</code> permission.</p>
1158
+ <p>Bulk-import entries from a JSON array. Existing entries are not removed.</p>
1159
+ <table class="table table-sm">
1160
+ <thead>
1161
+ <tr>
1162
+ <th>Field</th>
1163
+ <th>Type</th>
1164
+ <th>Description</th>
1165
+ </tr>
1166
+ </thead>
1167
+ <tbody>
1168
+ <tr>
1169
+ <td><code>entries</code></td>
1170
+ <td>array</td>
1171
+ <td>Array of entry objects with a <code>data</code> field each</td>
1172
+ </tr>
1173
+ </tbody>
1174
+ </table>
1175
+ <pre class="code-block"><code>// Request body
1176
+ { "entries": [ { "data": { "title": "Post 1" } }, { "data": { "title": "Post 2" } } ] }
1177
+
1178
+ // Response 201
1179
+ { "imported": 2, "skipped": 0 }</code></pre>
1180
+
1181
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">Public
1182
+ Access</h3>
1183
+
1184
+ <p>Public endpoints respect the per-collection <code>api</code> config. Each operation (<code>read</code>,
1185
+ <code>create</code>, <code>update</code>, <code>delete</code>) can be <strong>disabled</strong>, <strong>public</strong>
1186
+ (no auth), or restricted to a minimum role level.</p>
1187
+
1188
+ <h3><span class="method-badge method-get">GET</span><span
1189
+ class="endpoint-path">/api/collections/:slug/public</span></h3>
1190
+ <p class="auth-note">Access level: per collection <code>api.read</code> config.</p>
1191
+ <p>List entries publicly. Supports the same pagination and search query params as the admin endpoint.</p>
1192
+
1193
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/collections/:slug/public/:id</span>
1194
+ </h3>
1195
+ <p class="auth-note">Access level: per collection <code>api.read</code> config.</p>
1196
+ <p>Return a single entry publicly by ID.</p>
1197
+
1198
+ <h3><span class="method-badge method-post">POST</span><span
1199
+ class="endpoint-path">/api/collections/:slug/public</span></h3>
1200
+ <p class="auth-note">Access level: per collection <code>api.create</code> config.</p>
1201
+ <p>Create an entry publicly (e.g. form submissions). Entry is tagged with <code>source: "api"</code>.</p>
1202
+
1203
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/collections/:slug/public/:id</span>
1204
+ </h3>
1205
+ <p class="auth-note">Access level: per collection <code>api.update</code> config.</p>
1206
+ <p>Update an entry publicly.</p>
1207
+
1208
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/collections/:slug/public/:id</span>
1209
+ </h3>
1210
+ <p class="auth-note">Access level: per collection <code>api.delete</code> config.</p>
1211
+ <p>Delete an entry publicly.</p>
1212
+
1213
+ </div>
1214
+ </div>
1215
+
1216
+ <!-- Views API -->
1217
+ <div class="card card-collapsible mb-4">
1218
+ <div class="card-header" role="button" tabindex="0">
1219
+ <div class="card-header-content">
1220
+ <h2><span data-icon="eye"></span> Views API
1221
+ <span class="badge badge-warning" style="font-size:.7rem;margin-left:.4rem;">Pro</span>
1222
+ </h2>
1223
+ </div>
1224
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
1225
+ </div>
1226
+ <div class="card-body docs-body">
1227
+ <p>Views require a MongoDB connection. All admin endpoints require authentication and the
1228
+ <code>views</code> permission. View configs are stored in the <code>cms__views</code> MongoDB collection
1229
+ on the <code>default</code> connection.</p>
1230
+
1231
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
1232
+ Admin Endpoints</h3>
1233
+
1234
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views</span></h3>
1235
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1236
+ <p>List all view configs, sorted by creation date descending.</p>
1237
+
1238
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/views</span></h3>
1239
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1240
+ <p>Create a new view config. Returns <code>201</code> on success.</p>
1241
+ <pre class="code-block"><code>{
1242
+ "title": "Active Premium Users",
1243
+ "slug": "active-premium-users", // optional — auto-derived from title
1244
+ "description": "...",
1245
+ "connection": "default",
1246
+ "pipeline": {
1247
+ "source": "users", // CMS collection slug
1248
+ "stages": [
1249
+ { "type": "$match", "config": { "data.status": "active" } },
1250
+ { "type": "$sort", "config": { "meta.createdAt": -1 } },
1251
+ { "type": "$project", "config": { "data.name": 1, "data.email": 1 } }
1252
+ ]
1253
+ },
1254
+ "display": {
1255
+ "mode": "table", // "table" | "list"
1256
+ "columns": [
1257
+ { "key": "data.name", "label": "Name" },
1258
+ { "key": "data.email", "label": "Email" }
1259
+ ],
1260
+ "pageSize": 25
1261
+ },
1262
+ "access": {
1263
+ "roles": ["admin", "manager"],
1264
+ "public": false
1265
+ }
1266
+ }</code></pre>
1267
+
1268
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug</span></h3>
1269
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1270
+ <p>Return a single view config by slug.</p>
1271
+
1272
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/views/:slug</span></h3>
1273
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1274
+ <p>Update a view config. Accepts the same body shape as POST; all fields are optional.</p>
1275
+
1276
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/views/:slug</span></h3>
1277
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1278
+ <p>Delete a view config.</p>
1279
+
1280
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug/execute</span></h3>
1281
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1282
+ <p>Execute the view's aggregation pipeline and return paginated results.</p>
1283
+ <p><strong>Query params:</strong> <code>page</code> (default 1), <code>limit</code> (default 25).</p>
1284
+ <pre class="code-block"><code>// Response
1285
+ {
1286
+ "results": [ { "data": { "name": "Alice" }, ... }, ... ],
1287
+ "total": 142,
1288
+ "page": 1,
1289
+ "limit": 25
1290
+ }</code></pre>
1291
+
1292
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/collection/:slug</span></h3>
1293
+ <p class="auth-note">Requires: <code>views</code> permission</p>
1294
+ <p>List all view configs whose <code>pipeline.source</code> matches the given collection slug.</p>
1295
+
1296
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
1297
+ Public Endpoint</h3>
1298
+
1299
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug/public</span></h3>
1300
+ <p class="auth-note">Access level: per view <code>access</code> config</p>
1301
+ <p>Execute the view publicly. If <code>access.public</code> is <code>false</code>, a valid JWT and
1302
+ a role listed in <code>access.roles</code> is required. Same query params and response shape as
1303
+ <code>/execute</code>.</p>
1304
+
1305
+ </div>
1306
+ </div>
1307
+
1308
+ <!-- Actions API -->
1309
+ <div class="card card-collapsible mb-4">
1310
+ <div class="card-header" role="button" tabindex="0">
1311
+ <div class="card-header-content">
1312
+ <h2><span data-icon="zap"></span> Actions API
1313
+ <span class="badge badge-warning" style="font-size:.7rem;margin-left:.4rem;">Pro</span>
1314
+ </h2>
1315
+ </div>
1316
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
1317
+ </div>
1318
+ <div class="card-body docs-body">
1319
+ <p>Actions require a MongoDB connection. All admin endpoints require authentication and the
1320
+ <code>actions</code> permission. Action configs are stored in <code>cms__actions</code>.</p>
1321
+
1322
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
1323
+ Admin Endpoints</h3>
1324
+
1325
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions</span></h3>
1326
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1327
+ <p>List all action configs.</p>
1328
+
1329
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions</span></h3>
1330
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1331
+ <p>Create a new action config. Returns <code>201</code> on success.</p>
1332
+ <pre class="code-block"><code>{
1333
+ "title": "Approve Application",
1334
+ "slug": "approve-application", // optional
1335
+ "description": "...",
1336
+ "collection": "applications",
1337
+ "trigger": {
1338
+ "type": "manual",
1339
+ "label": "Approve",
1340
+ "icon": "check-circle",
1341
+ "confirmMessage": "Approve this application?" // null to skip confirmation
1342
+ },
1343
+ "steps": [
1344
+ { "type": "updateField", "config": { "field": "status", "value": "approved" } },
1345
+ { "type": "updateField", "config": { "field": "approvedAt", "value": "{{now}}" } },
1346
+ { "type": "webhook", "config": { "url": "https://hooks.example.com/approved", "method": "POST",
1347
+ "body": { "email": "{{entry.data.email}}" } } },
1348
+ { "type": "email", "config": { "to": "{{entry.data.email}}",
1349
+ "subject": "Application approved",
1350
+ "template": "Hi {{entry.data.name}}, your application is approved." } }
1351
+ ],
1352
+ "access": { "roles": ["admin", "manager"] }
1353
+ }</code></pre>
1354
+
1355
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions/:slug</span></h3>
1356
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1357
+ <p>Return a single action config by slug.</p>
1358
+
1359
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/actions/:slug</span></h3>
1360
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1361
+ <p>Update an action config.</p>
1362
+
1363
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/actions/:slug</span></h3>
1364
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1365
+ <p>Delete an action config.</p>
1366
+
1367
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/execute</span></h3>
1368
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1369
+ <p>Execute an action against a specific entry.</p>
1370
+ <pre class="code-block"><code>// Request body
1371
+ { "entryId": "uuid-of-the-entry" }
1372
+
1373
+ // Response
1374
+ {
1375
+ "success": true,
1376
+ "stepsCompleted": 4,
1377
+ "results": [
1378
+ { "type": "updateField", "success": true, "result": { "field": "status", "value": "approved" } },
1379
+ { "type": "email", "success": true, "result": { "to": "user@example.com" } }
1380
+ ]
1381
+ }
1382
+
1383
+ // Partial failure response
1384
+ {
1385
+ "success": false,
1386
+ "stepsCompleted": 2,
1387
+ "results": [
1388
+ { "type": "updateField", "success": true, "result": { "field": "status", "value": "approved" } },
1389
+ { "type": "webhook", "success": false, "error": "Webhook returned HTTP 500" }
1390
+ ]
1391
+ }</code></pre>
1392
+
1393
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions/collection/:slug</span></h3>
1394
+ <p class="auth-note">Requires: <code>actions</code> permission</p>
1395
+ <p>List all action configs targeting a given collection slug. Used by the entry list view to
1396
+ populate per-row trigger buttons.</p>
1397
+
1398
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
1399
+ Public Endpoint</h3>
1400
+
1401
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/public</span></h3>
1402
+ <p class="auth-note">Requires: JWT + role in <code>access.roles</code></p>
1403
+ <p>Execute an action publicly. Always requires a valid JWT — the role is checked against
1404
+ <code>access.roles</code>. Request body and response shape are identical to the admin
1405
+ <code>/execute</code> endpoint.</p>
1406
+
1407
+ </div>
1408
+ </div>
1409
+
1410
+ </div>
1411
+ </div>