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.
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/templates/docs/api-actions.html +86 -64
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +68 -38
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +55 -14
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/plugin-guides.js +5 -0
- package/bin/cli.js +6 -6
- package/package.json +1 -1
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/plugin.json +1 -1
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/plugin.json +1 -1
- package/plugins/free-tier.lock.json +16 -11
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.json +1 -1
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/plugin.json +1 -1
- package/server/routes/api/documentation.js +42 -0
- package/server/server.js +12 -0
- package/server/services/docs.js +13 -2
- package/server/services/pluginGuides.js +255 -0
- 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[<field>]</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&filter[salary_gte]=40000&sort=postedAt&order=desc&limit=20'</code></pre>
|
|
111
|
+
|
|
112
|
+
<h2>Access rules</h2>
|
|
113
|
+
<p>Each verb is set on the collection under <strong>API & Export</strong> in the collection editor, stored as
|
|
114
|
+
<code>schema.api.<verb></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:<token id></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 > 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 > 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_<64 lower-case hex></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={{entryId}}",
|
|
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>{{user.*}}</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:<slug>"</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>{{entryId}}</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:<slug>"</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><slug>-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
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 > 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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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>
|