domma-cms 0.93.0 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/admin/css/admin.css +1 -1
  2. package/admin/js/app.js +2 -2
  3. package/admin/js/templates/docs/api-actions.html +86 -64
  4. package/admin/js/templates/docs/api-authentication.html +159 -123
  5. package/admin/js/templates/docs/api-builder.html +197 -0
  6. package/admin/js/templates/docs/api-collections.html +199 -259
  7. package/admin/js/templates/docs/api-external.html +225 -0
  8. package/admin/js/templates/docs/api-forms.html +268 -0
  9. package/admin/js/templates/docs/api-layouts.html +70 -45
  10. package/admin/js/templates/docs/api-media.html +57 -80
  11. package/admin/js/templates/docs/api-navigation.html +66 -22
  12. package/admin/js/templates/docs/api-pages.html +109 -129
  13. package/admin/js/templates/docs/api-plugins.html +123 -61
  14. package/admin/js/templates/docs/api-scaffold.html +185 -0
  15. package/admin/js/templates/docs/api-settings.html +72 -64
  16. package/admin/js/templates/docs/api-users.html +74 -107
  17. package/admin/js/templates/docs/api-views.html +68 -54
  18. package/admin/js/templates/docs/components-howto.html +20 -17
  19. package/admin/js/templates/docs/components-reference.html +13 -16
  20. package/admin/js/templates/docs/components-rules.html +7 -6
  21. package/admin/js/templates/docs/components-walkthrough.html +19 -19
  22. package/admin/js/templates/docs/tutorial-crud.html +68 -38
  23. package/admin/js/templates/docs/tutorial-forms.html +51 -35
  24. package/admin/js/templates/docs/tutorial-plugin.html +132 -56
  25. package/admin/js/templates/docs/usage-actions.html +55 -14
  26. package/admin/js/templates/docs/usage-collections.html +108 -0
  27. package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
  28. package/admin/js/templates/docs/usage-dconfig.html +0 -3
  29. package/admin/js/templates/docs/usage-editions.html +213 -0
  30. package/admin/js/templates/docs/usage-media.html +22 -6
  31. package/admin/js/templates/docs/usage-navigation.html +74 -18
  32. package/admin/js/templates/docs/usage-pages.html +60 -20
  33. package/admin/js/templates/docs/usage-plugins.html +89 -17
  34. package/admin/js/templates/docs/usage-shortcodes.html +123 -70
  35. package/admin/js/templates/docs/usage-site-settings.html +50 -18
  36. package/admin/js/templates/docs/usage-tools.html +73 -0
  37. package/admin/js/templates/docs/usage-users-roles.html +99 -20
  38. package/admin/js/templates/docs/usage-views.html +36 -19
  39. package/admin/js/templates/documentation.html +153 -32
  40. package/admin/js/templates/plugin-guide.html +15 -0
  41. package/admin/js/templates/plugin-guides.html +21 -0
  42. package/admin/js/templates/pro-docs.html +53 -234
  43. package/admin/js/templates/tutorials.html +5 -4
  44. package/admin/js/views/doc-pages.js +1 -1
  45. package/admin/js/views/index.js +1 -1
  46. package/admin/js/views/plugin-guides.js +5 -0
  47. package/bin/cli.js +6 -6
  48. package/package.json +1 -1
  49. package/plugins/blog/docs/guide.md +205 -0
  50. package/plugins/blog/plugin.json +1 -1
  51. package/plugins/feedback/docs/guide.md +95 -0
  52. package/plugins/feedback/plugin.json +1 -1
  53. package/plugins/free-tier.lock.json +16 -11
  54. package/plugins/mail-reader/docs/guide.md +147 -0
  55. package/plugins/mail-reader/plugin.json +1 -1
  56. package/plugins/security/docs/guide.md +170 -0
  57. package/plugins/security/plugin.json +1 -1
  58. package/plugins/shopping-cart/docs/guide.md +191 -0
  59. package/plugins/shopping-cart/plugin.json +1 -1
  60. package/server/routes/api/documentation.js +42 -0
  61. package/server/server.js +12 -0
  62. package/server/services/docs.js +13 -2
  63. package/server/services/pluginGuides.js +255 -0
  64. package/server/services/plugins.js +8 -0
@@ -0,0 +1,225 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="code"></span> API · External API</h1>
3
+ <a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
4
+ </div>
5
+ <style>
6
+ .method-badge {
7
+ display: inline-block;
8
+ font-size: 11px;
9
+ font-weight: 700;
10
+ letter-spacing: 0.5px;
11
+ padding: 2px 7px;
12
+ border-radius: 4px;
13
+ font-family: var(--dm-font-mono, monospace);
14
+ margin-right: 6px;
15
+ vertical-align: middle;
16
+ }
17
+
18
+ .method-get {
19
+ background: color-mix(in srgb, var(--dm-success) 15%, transparent);
20
+ color: var(--dm-success);
21
+ }
22
+
23
+ .method-post {
24
+ background: color-mix(in srgb, var(--dm-primary) 15%, transparent);
25
+ color: var(--dm-primary);
26
+ }
27
+
28
+ .method-put {
29
+ background: color-mix(in srgb, var(--dm-warning) 15%, transparent);
30
+ color: var(--dm-warning-dark);
31
+ }
32
+
33
+ .method-patch {
34
+ background: color-mix(in srgb, var(--dm-info) 15%, transparent);
35
+ color: var(--dm-info);
36
+ }
37
+
38
+ .method-delete {
39
+ background: color-mix(in srgb, var(--dm-danger) 15%, transparent);
40
+ color: var(--dm-danger);
41
+ }
42
+
43
+ .endpoint-path {
44
+ font-family: var(--dm-font-mono, monospace);
45
+ font-size: 14px;
46
+ }
47
+
48
+ .auth-note {
49
+ font-size: 12px;
50
+ color: var(--dm-text-muted, #6b7280);
51
+ margin: 4px 0 12px;
52
+ }
53
+
54
+ .auth-note code {
55
+ font-size: 11px;
56
+ }
57
+
58
+ .docs-body h3 {
59
+ margin-top: 24px;
60
+ margin-bottom: 8px;
61
+ }
62
+
63
+ .docs-body h3:first-child {
64
+ margin-top: 0;
65
+ }
66
+ </style>
67
+
68
+
69
+ <div class="row">
70
+ <div class="col-12">
71
+ <div class="docs-body">
72
+
73
+ <p>The external API is the stable surface for other systems: <code>/api/v1/:slug</code> reads and writes a
74
+ collection's entries under that collection's own access rules. It shares its handlers with
75
+ <code>/api/collections/:slug/public</code>, so both behave identically. For fixed, named queries at clean
76
+ URLs see <a href="#/docs/api/builder">API Builder</a>.</p>
77
+
78
+ <h2>Endpoints</h2>
79
+ <table class="table table-sm">
80
+ <thead>
81
+ <tr><th>Method</th><th>Path</th><th>Verb checked</th><th>Success</th></tr>
82
+ </thead>
83
+ <tbody>
84
+ <tr><td><code>GET</code></td><td><code>/api/v1/:slug</code></td><td><code>read</code></td><td><code>{ entries, total, page, limit }</code></td></tr>
85
+ <tr><td><code>GET</code></td><td><code>/api/v1/:slug/:id</code></td><td><code>read</code></td><td>The entry</td></tr>
86
+ <tr><td><code>POST</code></td><td><code>/api/v1/:slug</code></td><td><code>create</code></td><td><code>201</code> + the entry</td></tr>
87
+ <tr><td><code>PUT</code></td><td><code>/api/v1/:slug/:id</code></td><td><code>update</code></td><td>The entry</td></tr>
88
+ <tr><td><code>DELETE</code></td><td><code>/api/v1/:slug/:id</code></td><td><code>delete</code></td><td><code>{ "success": true }</code></td></tr>
89
+ </tbody>
90
+ </table>
91
+ <p>Write bodies are <code>{ "data": { ... } }</code>. <strong>PUT replaces</strong> the entry's <code>data</code>
92
+ wholesale - send every field, not just the changed ones. Entries are validated against the collection's fields
93
+ (<code>400</code> with the reason); number fields sent as decimal text are stored as numbers.</p>
94
+
95
+ <h3>List query parameters</h3>
96
+ <table class="table table-sm">
97
+ <thead>
98
+ <tr><th>Parameter</th><th>Default</th><th>Description</th></tr>
99
+ </thead>
100
+ <tbody>
101
+ <tr><td><code>page</code></td><td><code>1</code></td><td>Page number</td></tr>
102
+ <tr><td><code>limit</code></td><td><code>50</code></td><td>Page size. <code>0</code> is treated as the default, not as "everything"; ask for a larger number instead.</td></tr>
103
+ <tr><td><code>sort</code> / <code>order</code></td><td><code>createdAt</code> / <code>desc</code></td><td>Sort field and direction (<code>asc</code> or <code>desc</code>)</td></tr>
104
+ <tr><td><code>search</code></td><td>-</td><td>Substring match across all field values</td></tr>
105
+ <tr><td><code>filter[&lt;field&gt;]</code></td><td>-</td><td>Equality filter; add an operator suffix: <code>_ne</code>, <code>_gt</code>, <code>_gte</code>, <code>_lt</code>, <code>_lte</code>, <code>_in</code>, <code>_nin</code>, <code>_contains</code>, <code>_starts</code>, <code>_ends</code>, <code>_exists</code>. Filters AND together; dot paths reach nested data.</td></tr>
106
+ <tr><td><code>resolveRefs</code></td><td>-</td><td><code>true</code> or <code>1</code> adds referenced entries under <code>_refs</code> (list only)</td></tr>
107
+ <tr><td><code>scope=mine</code></td><td>-</td><td>Only the caller's own entries. Needs a JWT and skips the <code>read</code> access rule.</td></tr>
108
+ </tbody>
109
+ </table>
110
+ <pre class="code-block"><code class="language-bash">curl 'https://example.com/api/v1/jobs?filter[status]=open&amp;filter[salary_gte]=40000&amp;sort=postedAt&amp;order=desc&amp;limit=20'</code></pre>
111
+
112
+ <h2>Access rules</h2>
113
+ <p>Each verb is set on the collection under <strong>API &amp; Export</strong> in the collection editor, stored as
114
+ <code>schema.api.&lt;verb&gt;</code>:</p>
115
+ <pre class="code-block"><code class="language-json">"api": {
116
+ "read": { "enabled": true, "access": "public", "fields": ["title", "location", "salary"] },
117
+ "create": { "enabled": true, "access": "token" },
118
+ "update": { "enabled": true, "access": "admin" },
119
+ "delete": { "enabled": false }
120
+ }</code></pre>
121
+ <table class="table table-sm">
122
+ <thead>
123
+ <tr><th><code>access</code></th><th>Who may call</th></tr>
124
+ </thead>
125
+ <tbody>
126
+ <tr><td><code>public</code></td><td>Anyone, no credentials.</td></tr>
127
+ <tr><td><code>token</code></td><td>Only a valid project API token (below). A JWT is refused.</td></tr>
128
+ <tr><td>a role name</td><td>A signed-in user (JWT) holding that role or a more senior one, across all their roles. A name that is not a role on the site admits only the level-0 role. An API token is refused.</td></tr>
129
+ </tbody>
130
+ </table>
131
+ <ul>
132
+ <li>A verb that is not <code>enabled</code> answers <code>403</code>.</li>
133
+ <li><code>read.fields</code> is an allowlist of <code>data</code> fields returned by external reads; empty
134
+ means every field. Admin endpoints are unaffected. <code>_refs</code> is not filtered.</li>
135
+ <li>If the collection's project is switched off, every <code>/api/v1</code> call on it answers
136
+ <code>404</code>.</li>
137
+ <li>Role and token modes grant the verb on every entry - there is no per-row ownership check on update or
138
+ delete.</li>
139
+ <li>Entries created here get <code>meta.source: "api"</code> and <code>meta.createdBy</code> set to the user's
140
+ id, <code>token:&lt;token id&gt;</code> for a token, or <code>null</code>.</li>
141
+ </ul>
142
+
143
+ <h2>API tokens</h2>
144
+ <p>Tokens are machine credentials, created under <strong>System &gt; API Tokens</strong> (permission family
145
+ <code>api-tokens.*</code>) or by a scaffolder recipe. They are stored as entries in the file-based
146
+ <code>api-tokens</code> preset collection; only a SHA-256 hash and the last four characters are kept.</p>
147
+ <ol>
148
+ <li>Open System &gt; API Tokens and choose <strong>New token</strong>.</li>
149
+ <li>Give it a <strong>name</strong> and pick its <strong>project</strong>. The project cannot be changed
150
+ later.</li>
151
+ <li>Optionally add <strong>scopes</strong>, one line per collection: <code>jobs: read</code>,
152
+ <code>enquiries: create, read</code>. No lines means every collection in the project; a collection with no
153
+ verbs means all four.</li>
154
+ <li>Optionally set <strong>expires</strong>. From then on every call with it is refused; the sidebar badge warns
155
+ 14 days ahead and turns red once one has expired.</li>
156
+ <li>Save and <strong>copy the token now</strong> - <code>dcms_</code> followed by 64 hex characters. It is never
157
+ shown again; if it is lost, revoke it and make another.</li>
158
+ </ol>
159
+ <p>From the list you can switch a token off (refused until switched back on), edit its name, scopes and expiry, or
160
+ revoke it (deleted, refused at once). <code>lastUsedAt</code> is updated at most once a minute.</p>
161
+ <p>A call with a token is accepted only when all of these hold, otherwise it is refused:</p>
162
+ <table class="table table-sm">
163
+ <thead>
164
+ <tr><th>Check</th><th>Refusal</th></tr>
165
+ </thead>
166
+ <tbody>
167
+ <tr><td>The verb's <code>access</code> is <code>token</code></td><td>role mode: <code>401</code> (a token is not a JWT); public mode ignores it</td></tr>
168
+ <tr><td>Header is exactly <code>Authorization: Bearer dcms_&lt;64 lower-case hex&gt;</code></td><td><code>401</code> <code>API token required</code></td></tr>
169
+ <tr><td>Token exists, is switched on and has not expired</td><td><code>401</code> <code>Invalid, disabled or expired API token</code></td></tr>
170
+ <tr><td>Token's project is the collection's project (untagged collections are <code>core</code>)</td><td><code>403</code> <code>Token is not valid for this collection's project</code></td></tr>
171
+ <tr><td>Scopes list this collection and verb (or the token has no scopes)</td><td><code>403</code> <code>Token scope does not permit this operation</code></td></tr>
172
+ </tbody>
173
+ </table>
174
+
175
+ <h2>Sending a token</h2>
176
+ <pre class="code-block"><code class="language-bash"># Read (collection "jobs" has read.access = "token")
177
+ curl -H 'Authorization: Bearer dcms_0123...cdef' \
178
+ 'https://example.com/api/v1/jobs?filter[status]=open'
179
+ # Create
180
+ curl -X POST https://example.com/api/v1/enquiries \
181
+ -H 'Authorization: Bearer dcms_0123...cdef' \
182
+ -H 'Content-Type: application/json' \
183
+ -d '{"data":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'
184
+ # Replace an entry (send every field)
185
+ curl -X PUT https://example.com/api/v1/jobs/3f2c... \
186
+ -H 'Authorization: Bearer dcms_0123...cdef' \
187
+ -H 'Content-Type: application/json' \
188
+ -d '{"data":{"title":"Engineer","status":"closed"}}'
189
+ # Delete
190
+ curl -X DELETE https://example.com/api/v1/jobs/3f2c... \
191
+ -H 'Authorization: Bearer dcms_0123...cdef'</code></pre>
192
+ <p>A role-mode verb takes a user's access token instead (from <code>POST /api/auth/login</code>). Keep tokens on the
193
+ server: anything shipped to a browser can be read by its user.</p>
194
+
195
+ <h2>Managing tokens over HTTP</h2>
196
+ <p>All need a JWT; project scope applies (a user confined to projects sees and manages only their tokens, and gets
197
+ <code>403</code> for others).</p>
198
+ <table class="table table-sm">
199
+ <thead>
200
+ <tr><th>Method</th><th>Path</th><th>Permission</th><th>Description</th></tr>
201
+ </thead>
202
+ <tbody>
203
+ <tr><td><code>GET</code></td><td><code>/api/api-tokens</code></td><td><code>api-tokens.read</code></td><td>List tokens (never the hash)</td></tr>
204
+ <tr><td><code>POST</code></td><td><code>/api/api-tokens</code></td><td><code>api-tokens.create</code></td><td>Create; <code>201</code> with <code>{ token, plaintext }</code></td></tr>
205
+ <tr><td><code>PUT</code></td><td><code>/api/api-tokens/:id</code></td><td><code>api-tokens.update</code></td><td>Change <code>name</code>, <code>enabled</code>, <code>scopes</code>, <code>expiresAt</code></td></tr>
206
+ <tr><td><code>DELETE</code></td><td><code>/api/api-tokens/:id</code></td><td><code>api-tokens.delete</code></td><td>Revoke</td></tr>
207
+ </tbody>
208
+ </table>
209
+ <pre class="code-block"><code class="language-json">// POST /api/api-tokens
210
+ { "name": "mobile-app", "project": "core",
211
+ "scopes": [{ "collection": "jobs", "verbs": ["read", "create"] }],
212
+ "expiresAt": "2027-01-01T00:00:00.000Z" }
213
+ // Response 201 - plaintext appears here and nowhere else
214
+ { "token": { "id": "uuid", "name": "mobile-app", "project": "core", "tokenHint": "cdef",
215
+ "scopes": [...], "enabled": true, "expiresAt": "2027-01-01T00:00:00.000Z",
216
+ "lastUsedAt": null, "createdBy": "Alice", "meta": {...} },
217
+ "plaintext": "dcms_..." }</code></pre>
218
+ <p>The base <code>super-admin</code> and <code>admin</code> roles hold <code>api-tokens.*</code>. Custom roles are
219
+ not given it automatically - grant it in the role editor.</p>
220
+ <p>Every route shares the site-wide rate limit of 500 requests a minute per IP. Browser calls from another origin
221
+ also need that origin allowed in <code>cors</code> in <code>config/server.json</code>.</p>
222
+
223
+ </div>
224
+ </div>
225
+ </div>
@@ -0,0 +1,268 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="code"></span> API · Forms API</h1>
3
+ <a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
4
+ </div>
5
+ <style>
6
+ .method-badge {
7
+ display: inline-block;
8
+ font-size: 11px;
9
+ font-weight: 700;
10
+ letter-spacing: 0.5px;
11
+ padding: 2px 7px;
12
+ border-radius: 4px;
13
+ font-family: var(--dm-font-mono, monospace);
14
+ margin-right: 6px;
15
+ vertical-align: middle;
16
+ }
17
+
18
+ .method-get {
19
+ background: color-mix(in srgb, var(--dm-success) 15%, transparent);
20
+ color: var(--dm-success);
21
+ }
22
+
23
+ .method-post {
24
+ background: color-mix(in srgb, var(--dm-primary) 15%, transparent);
25
+ color: var(--dm-primary);
26
+ }
27
+
28
+ .method-put {
29
+ background: color-mix(in srgb, var(--dm-warning) 15%, transparent);
30
+ color: var(--dm-warning-dark);
31
+ }
32
+
33
+ .method-patch {
34
+ background: color-mix(in srgb, var(--dm-info) 15%, transparent);
35
+ color: var(--dm-info);
36
+ }
37
+
38
+ .method-delete {
39
+ background: color-mix(in srgb, var(--dm-danger) 15%, transparent);
40
+ color: var(--dm-danger);
41
+ }
42
+
43
+ .endpoint-path {
44
+ font-family: var(--dm-font-mono, monospace);
45
+ font-size: 14px;
46
+ }
47
+
48
+ .auth-note {
49
+ font-size: 12px;
50
+ color: var(--dm-text-muted, #6b7280);
51
+ margin: 4px 0 12px;
52
+ }
53
+
54
+ .auth-note code {
55
+ font-size: 11px;
56
+ }
57
+
58
+ .docs-body h3 {
59
+ margin-top: 24px;
60
+ margin-bottom: 8px;
61
+ }
62
+
63
+ .docs-body h3:first-child {
64
+ margin-top: 0;
65
+ }
66
+ </style>
67
+
68
+ <div class="row">
69
+ <div class="col-12">
70
+ <div class="docs-body">
71
+
72
+ <p>Forms are JSON definitions stored in <code>content/forms/</code>. Every submission is stored as an entry in a
73
+ collection - the form's collection action target when that is switched on, otherwise a collection with the
74
+ form's own slug. The admin endpoints below use the <strong>collections</strong> permission family (there is no
75
+ separate forms permission); the submit endpoint is public.</p>
76
+
77
+ <h2>Form definitions</h2>
78
+
79
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/forms</span></h3>
80
+ <p class="auth-note">Requires: <code>collections.read</code></p>
81
+ <p>List form definitions the caller's project scope allows, each with <code>submissionCount</code>,
82
+ <code>submissionsThisWeek</code> and <code>lastSubmissionAt</code>.</p>
83
+
84
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/forms</span></h3>
85
+ <p class="auth-note">Requires: <code>collections.create</code></p>
86
+ <p>Create a form. <code>title</code> is required; <code>slug</code> is derived from it when omitted. Returns
87
+ <code>201</code>, or <code>409</code> when the slug is taken. A collection with the same slug is created
88
+ alongside it (admin-only API access) unless one already exists.</p>
89
+ <table class="table table-sm">
90
+ <thead>
91
+ <tr><th>Field</th><th>Type</th><th>Description</th></tr>
92
+ </thead>
93
+ <tbody>
94
+ <tr><td><code>title</code></td><td>string</td><td>Required</td></tr>
95
+ <tr><td><code>slug</code></td><td>string</td><td>Optional; slugified</td></tr>
96
+ <tr><td><code>description</code></td><td>string</td><td>Optional</td></tr>
97
+ <tr><td><code>fields</code></td><td>array</td><td>Field definitions (<code>name</code>, <code>label</code>, <code>type</code>, <code>required</code>, <code>logic</code>, <code>triggers</code>, <code>file</code> ...)</td></tr>
98
+ <tr><td><code>settings</code></td><td>object</td><td>Merged over the defaults below</td></tr>
99
+ <tr><td><code>actions</code></td><td>object</td><td><code>email</code>, <code>webhook</code> and <code>collection</code> blocks</td></tr>
100
+ </tbody>
101
+ </table>
102
+ <pre class="code-block"><code class="language-json">{
103
+ "title": "Contact",
104
+ "fields": [
105
+ { "name": "name", "label": "Name", "type": "text", "required": true },
106
+ { "name": "email", "label": "Email", "type": "email", "required": true },
107
+ { "name": "message", "label": "Message", "type": "textarea" }
108
+ ],
109
+ "settings": {
110
+ "submitText": "Submit",
111
+ "successMessage": "Thank you for your submission.",
112
+ "successRedirect": "/thanks?id=&#123;&#123;entryId&#125;&#125;",
113
+ "layout": "grid",
114
+ "columns": 2,
115
+ "honeypot": true,
116
+ "rateLimitPerMinute": 3,
117
+ "actionSlug": ""
118
+ },
119
+ "actions": {
120
+ "email": { "enabled": true, "recipients": "office@example.com", "subjectPrefix": "[Contact]" },
121
+ "webhook": { "enabled": false, "url": "", "method": "POST" },
122
+ "collection": { "enabled": true, "slug": "contact" }
123
+ }
124
+ }</code></pre>
125
+ <p>The create and update responses carry a <code>warnings</code> array when the form and its target collection
126
+ disagree - the collection is missing, requires a field the form does not collect, or lacks a field the form
127
+ sends. Saving still succeeds.</p>
128
+
129
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/forms/:slug</span></h3>
130
+ <p class="auth-note">Requires: <code>collections.read</code></p>
131
+ <p>The full definition, including <code>actions</code>. <code>403</code> when the form belongs to a project outside
132
+ the caller's scope.</p>
133
+
134
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/forms/:slug/public</span></h3>
135
+ <p class="auth-note">No authentication required.</p>
136
+ <p>The definition without its <code>actions</code> block (recipients and webhook URLs stay private). This is what the
137
+ public <code>[form]</code> embed renders from.</p>
138
+
139
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/forms/:slug</span></h3>
140
+ <p class="auth-note">Requires: <code>collections.update</code></p>
141
+ <p>Shallow-merges the body over the stored definition (send whole <code>fields</code>, <code>settings</code> and
142
+ <code>actions</code> objects). <code>slug</code> and <code>createdAt</code> cannot be changed.</p>
143
+
144
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/forms/:slug</span></h3>
145
+ <p class="auth-note">Requires: <code>collections.delete</code></p>
146
+ <p>Deletes the definition. Its submissions collection is left in place. Returns <code>{ "ok": true }</code>.</p>
147
+
148
+ <h2>Submitting a form</h2>
149
+
150
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/forms/submit/:slug</span></h3>
151
+ <p class="auth-note">No authentication required. A valid access token, when sent, is recorded as the submitter.</p>
152
+ <p>Accepts <code>application/json</code> (field names as keys) or <code>multipart/form-data</code> when the form
153
+ has file fields. With a JWT the entry's <code>meta.createdBy</code> is the user's id and actions receive the user
154
+ as <code>&#123;&#123;user.*&#125;&#125;</code>; without one the submission is anonymous.</p>
155
+ <pre class="code-block"><code class="language-bash">curl -X POST https://example.com/api/forms/submit/contact \
156
+ -H 'Content-Type: application/json' \
157
+ -d '{"name":"Ada","email":"ada@example.com","message":"Hello"}'</code></pre>
158
+ <pre class="code-block"><code class="language-json">// Response 200
159
+ { "ok": true, "entryId": "uuid", "ended": false,
160
+ "message": "Thank you for your submission.", "redirect": "/thanks?id=uuid" }
161
+ // Error 400 - missing or invalid answers, or a trigger blocked the submit
162
+ { "error": "Required fields missing: Email." }
163
+ // Error 404
164
+ { "error": "Form not found." }
165
+ // Error 429
166
+ { "error": "Too many submissions. Please try again later." }</code></pre>
167
+ <p>What happens, in order:</p>
168
+ <ol>
169
+ <li>Spam checks (honeypot and timing - see Spam protection and limits).</li>
170
+ <li>Triggers are resolved from the answers; a <code>block-submit</code> in force answers <code>400</code>
171
+ with its message.</li>
172
+ <li>Each field that is visible (field logic) and not hidden, disabled or masked by a trigger is checked for
173
+ required and validation rules. Trigger-required fields count as required.</li>
174
+ <li>The per-IP rate limit is applied.</li>
175
+ <li>The visible answers are stored in the target collection with <code>meta.source: "form:&lt;slug&gt;"</code>.
176
+ A missing target collection is created from the form's fields rather than losing the submission. A
177
+ collection validation failure answers <code>400</code>.</li>
178
+ <li>Email, webhook (<code>{ "form": slug, "data": {...} }</code> as JSON) and the <code>settings.actionSlug</code>
179
+ Action run. Their failures are logged and raised as admin notifications; they never fail the
180
+ submission.</li>
181
+ <li>Trigger events run: <code>run-action</code>, <code>notify</code> and <code>redirect</code> (the first
182
+ redirect wins over <code>settings.successRedirect</code>). <code>&#123;&#123;entryId&#125;&#125;</code> in the redirect is
183
+ replaced with the new entry's id.</li>
184
+ </ol>
185
+
186
+ <h3>File uploads</h3>
187
+ <p>Send <code>multipart/form-data</code>. A file part whose name matches a <code>type: "file"</code> field is checked
188
+ against that field's <code>file.maxSize</code> (bytes, default 5 MB) and <code>file.accept</code> (comma-separated
189
+ MIME types, wildcards such as <code>image/*</code> allowed), saved to the media library under a prefixed safe name,
190
+ and stored on the entry as <code>{ "url", "name", "size", "mime" }</code>. A file part for an unknown field is
191
+ ignored; an empty file input is skipped. The server-wide upload limit (<code>uploads.maxFileSize</code> in
192
+ <code>config/server.json</code>) still applies.</p>
193
+ <pre class="code-block"><code class="language-bash">curl -X POST https://example.com/api/forms/submit/apply \
194
+ -F name='Ada Lovelace' \
195
+ -F email=ada@example.com \
196
+ -F 'cv=@cv.pdf;type=application/pdf'</code></pre>
197
+
198
+ <h2>Spam protection and limits</h2>
199
+ <table class="table table-sm">
200
+ <thead>
201
+ <tr><th>Check</th><th>Setting</th><th>Behaviour</th></tr>
202
+ </thead>
203
+ <tbody>
204
+ <tr><td>Honeypot</td><td><code>settings.honeypot</code> (default on)</td><td>A non-empty <code>_hp</code> field is answered with an ordinary success and nothing is stored.</td></tr>
205
+ <tr><td>Timing</td><td><code>settings.honeypot</code></td><td><code>_t</code> is the time (ms since epoch) the form was rendered. A submit under 2 seconds later is answered with success and nothing is stored.</td></tr>
206
+ <tr><td>Rate limit</td><td><code>settings.rateLimitPerMinute</code> (default 3)</td><td>Per form and IP address, over a rolling minute. Counted only for submissions that pass validation. <code>429</code> when exceeded. Kept in memory, so a restart resets it.</td></tr>
207
+ <tr><td>Global limit</td><td>-</td><td>Every route shares the site-wide limit of 500 requests a minute per IP.</td></tr>
208
+ </tbody>
209
+ </table>
210
+ <p>There is no CAPTCHA in core.</p>
211
+
212
+ <h2>Triggers on the server</h2>
213
+ <p>Field triggers (<code>field.triggers[]</code>) run in the browser, but anything they decide about submission is
214
+ decided again on the server from the same engine (<code>public/js/form-logic-engine.js</code>), so disabling a
215
+ button in the browser changes nothing:</p>
216
+ <ul>
217
+ <li><strong>block-submit</strong> - refused with <code>400</code> and the trigger's message.</li>
218
+ <li><strong>hide-fields / disable-fields</strong> - those fields are excused from required checks and any value posted for them is
219
+ dropped.</li>
220
+ <li><strong>require-fields</strong> - those fields become required.</li>
221
+ <li><strong>end-form</strong> - the answers so far are the submission; masked fields are excused (also in the
222
+ collection's own required check) and the entry gets <code>meta.outcome</code>. With
223
+ <code>record: false</code> nothing is stored and email, webhook and Actions do not run. The response has
224
+ <code>ended: true</code> and the trigger's done message.</li>
225
+ <li><strong>run-action</strong>, <strong>notify</strong>, <strong>redirect</strong> - performed after the entry is
226
+ stored. <code>notify</code> exists only on the server (notification source
227
+ <code>core:form-triggers</code>).</li>
228
+ </ul>
229
+
230
+ <h2>Submissions</h2>
231
+ <p>All submissions routes read and write the form's target collection, and only this form's entries in it (in a
232
+ shared collection, the ones stamped <code>meta.source: "form:&lt;slug&gt;"</code>). A form in a project outside
233
+ the caller's scope answers <code>404</code>.</p>
234
+
235
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/forms/:slug/submissions</span></h3>
236
+ <p class="auth-note">Requires: <code>collections.read</code></p>
237
+ <p>An array of entries, newest first. <code>[]</code> when the collection does not exist yet.</p>
238
+
239
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/forms/:slug/submissions/export</span></h3>
240
+ <p class="auth-note">Requires: <code>collections.read</code></p>
241
+ <p>CSV download (<code>&lt;slug&gt;-submissions.csv</code>): one column per field label plus <code>Date</code>.</p>
242
+
243
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/forms/:slug/submissions/export/json</span></h3>
244
+ <p class="auth-note">Requires: <code>collections.read</code></p>
245
+ <p>JSON download of the raw entries.</p>
246
+
247
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/forms/:slug/submissions</span></h3>
248
+ <p class="auth-note">Requires: <code>collections.delete</code></p>
249
+ <p>Delete every submission of this form. A shared collection keeps other forms' entries.</p>
250
+
251
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/forms/:slug/submissions/:id</span></h3>
252
+ <p class="auth-note">Requires: <code>collections.delete</code></p>
253
+ <p>Delete one submission. <code>404</code> when the entry is not this form's.</p>
254
+
255
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/forms/:slug/submissions/:id/spam</span></h3>
256
+ <p class="auth-note">Requires: <code>collections.update</code></p>
257
+ <p>Flag (<code>{ "spam": true }</code>, the default) or unflag (<code>{ "spam": false }</code>) a submission. Sets
258
+ <code>data.spam</code> and answers <code>{ "entry": {...}, "spam": true }</code>.</p>
259
+
260
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/forms/test-email</span></h3>
261
+ <p class="auth-note">Requires: an admin-level role (level 0 or 1).</p>
262
+ <p>Send a sample submission email through the site's SMTP settings to <code>to</code> (defaults to the SMTP from
263
+ address). Answers <code>{ "ok": true, "message": "Test email sent to ..." }</code> or <code>500</code> with the
264
+ transport error.</p>
265
+
266
+ </div>
267
+ </div>
268
+ </div>
@@ -69,53 +69,78 @@
69
69
  <div class="col-12">
70
70
  <div class="docs-body">
71
71
 
72
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/layouts</span>
73
- </h3>
74
- <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
75
- <p>Return all layout presets from <code>config/presets.json</code>.</p>
76
- <pre class="code-block"><code>// Response 200
77
- {
78
- "default": { "label": "Default", "sections": [...] },
79
- "full-width": { "label": "Full Width", "sections": [...] }
80
- }</code></pre>
81
-
82
- <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/layouts</span>
83
- </h3>
84
- <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
85
- <p>Replace the entire layout presets object.</p>
86
- <pre class="code-block"><code>// Response 200
72
+ <p>Layouts are the page frames a page picks with <code>layout:</code> in its frontmatter (navbar, footer, sidebar,
73
+ width, background). They are stored in <code>config/presets.json</code> and edited at System &gt; Layouts. The
74
+ endpoints need the <code>layouts</code> permission (read, or update for any change).</p>
75
+
76
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/layouts</span></h3>
77
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> read permission.</p>
78
+ <p>Every layout, keyed by name. Nine are built in: default, landing, blank, with-sidebar, minimal, article,
79
+ product, dashboard and wide.</p>
80
+ <pre class="code-block"><code>// Response 200
81
+ { "default": { "key": "default", "label": "Default", "description": "Standard page with navbar and footer.",
82
+ "builtin": true, "navbar": true, "footer": true, "sidebar": false, "width": "normal",
83
+ "bgColor": "", "bgImage": "", "class": "" }, ... }</code></pre>
84
+
85
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/layouts</span></h3>
86
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> update permission.</p>
87
+ <p>Add a layout. Its key is made from the label.</p>
88
+ <table class="table table-sm">
89
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
90
+ <tbody>
91
+ <tr><td><code>label</code></td><td>string</td><td>Required. Up to 60 characters</td></tr>
92
+ <tr><td><code>description</code></td><td>string</td><td>Up to 200 characters</td></tr>
93
+ <tr><td><code>navbar</code>, <code>footer</code></td><td>boolean</td><td>Show them (default true)</td></tr>
94
+ <tr><td><code>sidebar</code></td><td>boolean</td><td>Show a sidebar (default false)</td></tr>
95
+ <tr><td><code>width</code></td><td>string</td><td><code>narrow</code>, <code>normal</code> (default),
96
+ <code>wide</code> or <code>full</code></td></tr>
97
+ <tr><td><code>bgColor</code>, <code>bgImage</code>, <code>class</code></td><td>string</td><td>Background colour,
98
+ background picture and extra CSS classes</td></tr>
99
+ </tbody>
100
+ </table>
101
+ <pre class="code-block"><code>// Response 200
102
+ { "success": true, "key": "landing-wide", "preset": { ... } }
103
+ // Error 409
104
+ { "error": "A preset with this key already exists" }</code></pre>
105
+
106
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/layouts/:key</span></h3>
107
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> update permission.</p>
108
+ <p>Change a layout (same fields; <code>label</code> is required). A built-in layout can be changed but stays
109
+ built in.</p>
110
+
111
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/layouts/:key</span></h3>
112
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> update permission.</p>
113
+ <p>Delete a layout you added. Built-in layouts cannot be deleted (400). A page that names a missing layout is shown
114
+ with <code>default</code>.</p>
115
+
116
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/layouts/_usage</span></h3>
117
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> read permission.</p>
118
+ <p>Which pages use each layout, and which pages name a layout that does not exist.</p>
119
+
120
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/layouts</span></h3>
121
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> update permission.</p>
122
+ <p>Replace every layout at once. Each must be an object with a non-empty <code>label</code>; prefer the per-layout
123
+ routes above.</p>
124
+ <pre class="code-block"><code>// Response 200
87
125
  { "success": true }</code></pre>
88
126
 
89
- <h3><span class="method-badge method-get">GET</span><span
90
- class="endpoint-path">/api/layouts/options</span></h3>
91
- <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
92
- <p>Return layout display options stored in <code>config/site.json</code> under
93
- <code>layoutOptions</code>.</p>
94
- <pre class="code-block"><code>// Response 200
95
- { "spacerSize": 8 }</code></pre>
96
-
97
- <h3><span class="method-badge method-put">PUT</span><span
98
- class="endpoint-path">/api/layouts/options</span></h3>
99
- <p class="auth-note">Requires Bearer token + <code>layouts</code> permission.</p>
100
- <p>Merge layout option updates into the existing options. Existing keys not included are
101
- preserved.</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>spacerSize</code></td>
113
- <td>number</td>
114
- <td>Default spacer block size in pixels</td>
115
- </tr>
116
- </tbody>
117
- </table>
118
- <pre class="code-block"><code>// Response 200
127
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/layouts/options</span></h3>
128
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> read permission.</p>
129
+ <p>Layout options, kept in <code>config/site.json</code> under <code>layoutOptions</code>.</p>
130
+ <pre class="code-block"><code>// Response 200
131
+ { "spacerSize": 40 }</code></pre>
132
+
133
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/layouts/options</span></h3>
134
+ <p class="auth-note">Requires Bearer token + <code>layouts</code> update permission.</p>
135
+ <p>Merge option changes; keys you leave out are kept.</p>
136
+ <table class="table table-sm">
137
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
138
+ <tbody>
139
+ <tr><td><code>spacerSize</code></td><td>number</td><td>Default <code>[spacer]</code> size in pixels (0-500)</td></tr>
140
+ <tr><td><code>spacerClass</code></td><td>string</td><td>Extra CSS class on every spacer</td></tr>
141
+ </tbody>
142
+ </table>
143
+ <pre class="code-block"><code>// Response 200
119
144
  { "success": true }</code></pre>
120
145
 
121
146
  </div>