domma-cms 0.9.5 → 0.9.10

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