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
|
@@ -69,133 +69,169 @@
|
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<th>Field</th>
|
|
121
|
-
<th>Type</th>
|
|
122
|
-
<th>Description</th>
|
|
123
|
-
</tr>
|
|
124
|
-
</thead>
|
|
125
|
-
<tbody>
|
|
126
|
-
<tr>
|
|
127
|
-
<td><code>email</code></td>
|
|
128
|
-
<td>string</td>
|
|
129
|
-
<td>User email</td>
|
|
130
|
-
</tr>
|
|
131
|
-
<tr>
|
|
132
|
-
<td><code>password</code></td>
|
|
133
|
-
<td>string</td>
|
|
134
|
-
<td>User password</td>
|
|
135
|
-
</tr>
|
|
136
|
-
</tbody>
|
|
137
|
-
</table>
|
|
138
|
-
<pre class="code-block"><code>// Response 200
|
|
139
|
-
{ "token": "eyJ...", "refreshToken": "eyJ...", "user": { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin" } }
|
|
140
|
-
|
|
72
|
+
<p>The admin signs in with a short-lived <strong>access token</strong> (sent as
|
|
73
|
+
<code>Authorization: Bearer <token></code>) and a longer <strong>refresh token</strong> that gets a new
|
|
74
|
+
access token. The lifetimes are set in <code>config/auth.json</code> (default 15 minutes and 7 days). Each sign-in
|
|
75
|
+
is a <strong>session</strong> kept on disk, so signing out holds across a restart, and a new password ends the
|
|
76
|
+
account's other sessions. External programs should use an API token instead - see
|
|
77
|
+
<a href="#/docs/api/external">External API & tokens</a>.</p>
|
|
78
|
+
|
|
79
|
+
<h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/auth/setup-status</span></h3>
|
|
80
|
+
<p class="auth-note">No authentication required.</p>
|
|
81
|
+
<p>Whether the site still needs its first account, plus what the sign-in screen needs to know.</p>
|
|
82
|
+
<pre class="code-block"><code>// Response 200
|
|
83
|
+
{ "needsSetup": false, "siteTitle": "My Site", "resetByEmail": true, "resetExpiresIn": "1 hour" }</code></pre>
|
|
84
|
+
|
|
85
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/setup</span></h3>
|
|
86
|
+
<p class="auth-note">No authentication required. Only succeeds while the site has no users.</p>
|
|
87
|
+
<p>Create the first account, as the level-0 role (<code>super-admin</code>), and sign it in. Refused with 403 once
|
|
88
|
+
any user exists.</p>
|
|
89
|
+
<table class="table table-sm">
|
|
90
|
+
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
91
|
+
<tbody>
|
|
92
|
+
<tr><td><code>name</code></td><td>string</td><td>Display name</td></tr>
|
|
93
|
+
<tr><td><code>email</code></td><td>string</td><td>Email address</td></tr>
|
|
94
|
+
<tr><td><code>password</code></td><td>string</td><td>At least 8 characters, and whatever further rules a plugin
|
|
95
|
+
such as Security adds</td></tr>
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
<pre class="code-block"><code>// Response 201
|
|
99
|
+
{ "token": "eyJ...", "refreshToken": "eyJ...", "user": { "id": "...", "name": "...", "email": "...", "role": "super-admin" } }
|
|
100
|
+
// Error 400 - a missing field, a bad email address or a password the rules refuse
|
|
101
|
+
{ "error": "Password must be at least 8 characters" }</code></pre>
|
|
102
|
+
|
|
103
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/login</span></h3>
|
|
104
|
+
<p class="auth-note">No authentication required. Limited to 5 attempts a minute per address.</p>
|
|
105
|
+
<p>Sign in with email and password. Usually answers with tokens. When a plugin adds a second step (two-factor from
|
|
106
|
+
Security, for example) it answers with a <code>challenge</code> and a five-minute <code>ticket</code> instead,
|
|
107
|
+
which is completed at <code>/api/auth/login/verify</code>.</p>
|
|
108
|
+
<table class="table table-sm">
|
|
109
|
+
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
110
|
+
<tbody>
|
|
111
|
+
<tr><td><code>email</code></td><td>string</td><td>User email</td></tr>
|
|
112
|
+
<tr><td><code>password</code></td><td>string</td><td>User password</td></tr>
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
<pre class="code-block"><code>// Response 200 - signed in
|
|
116
|
+
{ "token": "eyJ...", "refreshToken": "eyJ...",
|
|
117
|
+
"user": { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "additionalRoles": [], "level": 1 } }
|
|
118
|
+
// Response 200 - a second step is needed
|
|
119
|
+
{ "challenge": { "kind": "code", "title": "Two-factor code", "label": "Code", "inputMode": "numeric", ... }, "ticket": "eyJ..." }
|
|
141
120
|
// Error 401
|
|
142
121
|
{ "error": "Invalid credentials" }</code></pre>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
</tbody>
|
|
170
|
-
</table>
|
|
171
|
-
<pre class="code-block"><code>// Response 200
|
|
172
|
-
{ "ok": true }</code></pre>
|
|
173
|
-
|
|
174
|
-
<h3><span class="method-badge method-post">POST</span><span
|
|
175
|
-
class="endpoint-path">/api/auth/refresh</span></h3>
|
|
176
|
-
<p class="auth-note">No authentication required. Provide a valid refresh token.</p>
|
|
177
|
-
<p>Exchange a refresh token for a new access token.</p>
|
|
178
|
-
<table class="table table-sm">
|
|
179
|
-
<thead>
|
|
180
|
-
<tr>
|
|
181
|
-
<th>Field</th>
|
|
182
|
-
<th>Type</th>
|
|
183
|
-
<th>Description</th>
|
|
184
|
-
</tr>
|
|
185
|
-
</thead>
|
|
186
|
-
<tbody>
|
|
187
|
-
<tr>
|
|
188
|
-
<td><code>refreshToken</code></td>
|
|
189
|
-
<td>string</td>
|
|
190
|
-
<td>A valid, non-revoked refresh token</td>
|
|
191
|
-
</tr>
|
|
192
|
-
</tbody>
|
|
193
|
-
</table>
|
|
194
|
-
<pre class="code-block"><code>// Response 200
|
|
122
|
+
<p>A successful sign-in also sets a session cookie, used only so the public site can recognise a signed-in visitor
|
|
123
|
+
(role-gated pages, draft previews). No API route accepts it in place of the Bearer token.</p>
|
|
124
|
+
|
|
125
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/login/verify</span></h3>
|
|
126
|
+
<p class="auth-note">No authentication required. Provide the ticket from <code>/api/auth/login</code>.</p>
|
|
127
|
+
<p>Answer a sign-in challenge. Five wrong answers end the ticket; sign in again.</p>
|
|
128
|
+
<table class="table table-sm">
|
|
129
|
+
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
130
|
+
<tbody>
|
|
131
|
+
<tr><td><code>ticket</code></td><td>string</td><td>The ticket from the login response</td></tr>
|
|
132
|
+
<tr><td><code>response</code></td><td>string</td><td>The code (or recovery code)</td></tr>
|
|
133
|
+
<tr><td><code>mode</code></td><td>string</td><td><code>code</code> (default) or <code>recovery</code></td></tr>
|
|
134
|
+
<tr><td><code>trust</code></td><td>boolean</td><td>Remember this browser, where the challenge offers it</td></tr>
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
<pre class="code-block"><code>// Response 200 - the same as a successful /api/auth/login
|
|
138
|
+
// Error 401
|
|
139
|
+
{ "error": "...", "triesLeft": 4 }</code></pre>
|
|
140
|
+
|
|
141
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/refresh</span></h3>
|
|
142
|
+
<p class="auth-note">No authentication required. Provide a valid refresh token.</p>
|
|
143
|
+
<p>Exchange a refresh token for a new access token. Refused once the session behind it has ended (signed out, a
|
|
144
|
+
password change, or the account made inactive).</p>
|
|
145
|
+
<pre class="code-block"><code>// Request body
|
|
146
|
+
{ "refreshToken": "eyJ..." }
|
|
147
|
+
// Response 200
|
|
195
148
|
{ "token": "eyJ..." }
|
|
196
|
-
|
|
197
149
|
// Error 401
|
|
198
|
-
{ "error": "
|
|
150
|
+
{ "error": "This session has ended. Sign in again." }</code></pre>
|
|
151
|
+
|
|
152
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/logout</span></h3>
|
|
153
|
+
<p class="auth-note">No authentication required. Safe to call without a token.</p>
|
|
154
|
+
<p>Ends the session the refresh token belongs to (sessions are stored on disk, so this survives a restart) and
|
|
155
|
+
clears the session cookie.</p>
|
|
156
|
+
<pre class="code-block"><code>// Request body (optional)
|
|
157
|
+
{ "refreshToken": "eyJ..." }
|
|
158
|
+
// Response 200
|
|
159
|
+
{ "ok": true }</code></pre>
|
|
160
|
+
|
|
161
|
+
<h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/auth/me</span></h3>
|
|
162
|
+
<p class="auth-note">Requires Bearer token.</p>
|
|
163
|
+
<p>Your own account, with your profile fields (the <code>user-profiles</code> collection).</p>
|
|
164
|
+
<pre class="code-block"><code>// Response 200
|
|
165
|
+
{ "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "additionalRoles": [],
|
|
166
|
+
"isActive": true, "profile": { "phone": "..." } }</code></pre>
|
|
167
|
+
|
|
168
|
+
<h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/auth/me</span></h3>
|
|
169
|
+
<p class="auth-note">Requires Bearer token.</p>
|
|
170
|
+
<p>Update your own name, email, password or profile (My Profile). A new password is checked against the site's
|
|
171
|
+
password rules and ends your other sessions. Your role cannot be changed here.</p>
|
|
172
|
+
<pre class="code-block"><code>// Request body - any of
|
|
173
|
+
{ "name": "Alice B", "email": "alice@example.com", "password": "...", "profile": { "phone": "..." } }
|
|
174
|
+
// Response 200 - the updated account, as GET /api/auth/me
|
|
175
|
+
// Error 409 - the email is already used by another account</code></pre>
|
|
176
|
+
|
|
177
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/me/avatar</span></h3>
|
|
178
|
+
<p class="auth-note">Requires Bearer token.</p>
|
|
179
|
+
<p>Upload your picture (multipart, one file; JPEG, PNG, WebP or GIF, up to 8 MB). It is stored as a 256 px WebP.
|
|
180
|
+
<code>DELETE /api/auth/me/avatar</code> removes it. Pictures are served at
|
|
181
|
+
<code>GET /api/auth/avatar/:name</code>.</p>
|
|
182
|
+
|
|
183
|
+
<h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/auth/permissions</span></h3>
|
|
184
|
+
<p class="auth-note">Requires Bearer token.</p>
|
|
185
|
+
<p>What you may do: the permissions of every role you hold combined (a bare name such as <code>pages</code> means every action on it; <code>resource.action</code> one action), whether your roles confine you to a few admin
|
|
186
|
+
screens (<code>adminScope</code>, <code>null</code> for the whole admin), and the screen the admin opens on.</p>
|
|
187
|
+
<pre class="code-block"><code>// Response 200
|
|
188
|
+
{ "permissions": ["pages", "media", "contacts.read", ...], "adminScope": null, "adminHome": "#/" }</code></pre>
|
|
189
|
+
<p><code>GET /api/auth/permissions-registry</code> lists every permission the role editor can grant, including
|
|
190
|
+
those added by plugins.</p>
|
|
191
|
+
|
|
192
|
+
<h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/auth/sessions</span></h3>
|
|
193
|
+
<p class="auth-note">Requires Bearer token.</p>
|
|
194
|
+
<p>Your sessions - where and when you are signed in - with the one you are using marked <code>current</code>.
|
|
195
|
+
<code>DELETE /api/auth/sessions/:sid</code> ends one; <code>POST /api/auth/sessions/revoke-others</code> signs you
|
|
196
|
+
out everywhere else.</p>
|
|
197
|
+
<pre class="code-block"><code>// Response 200
|
|
198
|
+
[ { "sid": "...", "createdAt": "...", "lastUsedAt": "...", "expiresAt": "...", "ip": "...", "ua": "...", "current": true } ]
|
|
199
|
+
// POST /api/auth/sessions/revoke-others - Response 200
|
|
200
|
+
{ "ok": true, "ended": 2 }</code></pre>
|
|
201
|
+
|
|
202
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/forgot-password</span></h3>
|
|
203
|
+
<p class="auth-note">No authentication required. Limited to 3 requests an hour.</p>
|
|
204
|
+
<p>Ask for a password reset email. It answers at once and the same way whether or not the address has an account;
|
|
205
|
+
the email is sent afterwards, and at most 3 an hour go to any one account. The link is built on the Site URL
|
|
206
|
+
(Site Settings, General).</p>
|
|
207
|
+
<pre class="code-block"><code>// Request body
|
|
208
|
+
{ "email": "alice@example.com" }
|
|
209
|
+
// Response 200
|
|
210
|
+
{ "ok": true, "expiresIn": "1 hour" }</code></pre>
|
|
211
|
+
|
|
212
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/reset-password/check</span></h3>
|
|
213
|
+
<p class="auth-note">No authentication required. Provide the token from the reset link.</p>
|
|
214
|
+
<p>Is the link still good, and (with a <code>password</code>) what the password rules make of a new password as it
|
|
215
|
+
is typed.</p>
|
|
216
|
+
<pre class="code-block"><code>// Request body
|
|
217
|
+
{ "token": "...", "password": "optional" }
|
|
218
|
+
// Response 200
|
|
219
|
+
{ "email": "alice@example.com", "expiresAt": "...", "minutesLeft": 42, "problems": [] }
|
|
220
|
+
// Error 400
|
|
221
|
+
{ "error": "Invalid or expired reset link", "expired": true }</code></pre>
|
|
222
|
+
|
|
223
|
+
<h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/auth/reset-password</span></h3>
|
|
224
|
+
<p class="auth-note">No authentication required. Provide the token from the reset link.</p>
|
|
225
|
+
<p>Set a new password. The link is used up, every session of the account ends, and the account holder is emailed
|
|
226
|
+
that the password changed.</p>
|
|
227
|
+
<pre class="code-block"><code>// Request body
|
|
228
|
+
{ "token": "...", "password": "new password" }
|
|
229
|
+
// Response 200
|
|
230
|
+
{ "ok": true }
|
|
231
|
+
// Error 400
|
|
232
|
+
{ "error": "Invalid or expired reset link", "expired": true }</code></pre>
|
|
233
|
+
<p>Administrators send, copy or withdraw reset links for other users with
|
|
234
|
+
<a href="#/docs/api/users">the Users API</a>.</p>
|
|
199
235
|
|
|
200
236
|
</div>
|
|
201
237
|
</div>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="code"></span> API · API Builder</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>API Builder publishes named, read-only endpoints over collection data at
|
|
74
|
+
<code>/api/x/<project><path></code> - for example <code>/api/x/world-cup/fixtures-day/:date</code>.
|
|
75
|
+
A definition is data, never code: it binds a path to one fixed collection query. An endpoint grants access to
|
|
76
|
+
exactly that query, whatever the collection's own <a href="#/docs/api/external">external API</a> settings say.</p>
|
|
77
|
+
|
|
78
|
+
<h2>How it works</h2>
|
|
79
|
+
<ul>
|
|
80
|
+
<li>Definitions are entries in the file-based <code>api-endpoints</code> preset collection, edited under
|
|
81
|
+
<strong>Data > API Builder</strong> (permission family <code>api-endpoints.*</code>) or created by a
|
|
82
|
+
scaffolder recipe (<code>apiEndpoints: [...]</code>).</li>
|
|
83
|
+
<li>The first URL segment after <code>/api/x/</code> is the endpoint's project; the rest is matched against
|
|
84
|
+
its <code>path</code>. Static segments beat <code>:param</code> segments, so <code>/m/latest</code> wins
|
|
85
|
+
over <code>/m/:day</code>.</li>
|
|
86
|
+
<li>An endpoint may only expose a collection in its own project or in <code>core</code>, and never a
|
|
87
|
+
system-managed or preset collection (roles, users, API tokens and so on).</li>
|
|
88
|
+
<li>Only <code>GET</code> is served. An unknown project, unknown path or switched-off endpoint answers
|
|
89
|
+
<code>404</code> identically; so does every endpoint of a switched-off project.</li>
|
|
90
|
+
<li>Endpoints are project artefacts: a project that still has endpoints cannot be deleted.</li>
|
|
91
|
+
</ul>
|
|
92
|
+
|
|
93
|
+
<h2>The API Builder screen</h2>
|
|
94
|
+
<p><strong>Data > API Builder</strong> lists the endpoints you can see (your project scope applies), with the
|
|
95
|
+
public ones counted on the sidebar badge. The editor has three tabs and a try-it console:</p>
|
|
96
|
+
<ul>
|
|
97
|
+
<li><strong>Definition</strong> - name, project (fixed once saved), path, collection, auth and mode.</li>
|
|
98
|
+
<li><strong>Query</strong> - filter rows (field, operator, value; a value can use a <code>:param</code> or a
|
|
99
|
+
query placeholder), sort, order and limit.</li>
|
|
100
|
+
<li><strong>Response fields</strong> - tick the fields the endpoint returns. None ticked returns every
|
|
101
|
+
field.</li>
|
|
102
|
+
<li><strong>Try it</strong> - calls the <em>saved</em> definition with the path parameters and query you enter,
|
|
103
|
+
keeps a history, and copies a request as curl. Save first to test a change. For a token endpoint, paste a
|
|
104
|
+
token.</li>
|
|
105
|
+
</ul>
|
|
106
|
+
|
|
107
|
+
<h2>Definition</h2>
|
|
108
|
+
<table class="table table-sm">
|
|
109
|
+
<thead>
|
|
110
|
+
<tr><th>Field</th><th>Default</th><th>Rules</th></tr>
|
|
111
|
+
</thead>
|
|
112
|
+
<tbody>
|
|
113
|
+
<tr><td><code>name</code></td><td>-</td><td>Required</td></tr>
|
|
114
|
+
<tr><td><code>project</code></td><td>-</td><td>Required, must exist; cannot be changed after creation</td></tr>
|
|
115
|
+
<tr><td><code>path</code></td><td>-</td><td>Starts with <code>/</code>; segments are lower-case letters, digits and hyphens, or <code>:name</code>. Two endpoints in one project cannot share a path shape (<code>/a/:x</code> and <code>/a/:y</code> collide - <code>409</code>).</td></tr>
|
|
116
|
+
<tr><td><code>collection</code></td><td>-</td><td>Required; own project or <code>core</code>; not system-managed</td></tr>
|
|
117
|
+
<tr><td><code>auth</code></td><td><code>public</code></td><td><code>public</code>, <code>token</code> or an existing role name</td></tr>
|
|
118
|
+
<tr><td><code>mode</code></td><td><code>list</code></td><td><code>list</code> returns <code>{ entries, total, page, limit }</code>; <code>single</code> returns the first match or <code>404</code></td></tr>
|
|
119
|
+
<tr><td><code>filter</code></td><td><code>{}</code></td><td><code>{ "field_op": value }</code> with the standard operators (<code>_eq</code>, <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>). Values are strings, numbers or booleans.</td></tr>
|
|
120
|
+
<tr><td><code>sort</code> / <code>order</code></td><td><code>createdAt</code> / <code>desc</code></td><td>Order is <code>asc</code> or <code>desc</code></td></tr>
|
|
121
|
+
<tr><td><code>limit</code></td><td><code>50</code></td><td>Integer 0-500; <code>0</code> means no limit</td></tr>
|
|
122
|
+
<tr><td><code>fields</code></td><td><code>[]</code></td><td>Response allowlist of <code>data</code> fields; empty returns all</td></tr>
|
|
123
|
+
<tr><td><code>enabled</code></td><td><code>true</code></td><td>Off answers <code>404</code></td></tr>
|
|
124
|
+
</tbody>
|
|
125
|
+
</table>
|
|
126
|
+
<p>Filter values may carry placeholders:</p>
|
|
127
|
+
<ul>
|
|
128
|
+
<li><code>{{params.name}}</code> - from a <code>:name</code> path segment. It must exist in the path (checked on
|
|
129
|
+
save); empty at request time answers <code>400</code>.</li>
|
|
130
|
+
<li><code>{{query.name}}</code> - from <code>?name=</code>. When absent, the whole clause is dropped, so it works
|
|
131
|
+
as an optional refinement.</li>
|
|
132
|
+
</ul>
|
|
133
|
+
<pre class="code-block"><code class="language-json">{
|
|
134
|
+
"name": "Fixtures by day",
|
|
135
|
+
"project": "world-cup",
|
|
136
|
+
"path": "/fixtures-day/:date",
|
|
137
|
+
"collection": "wc-schedule",
|
|
138
|
+
"auth": "public",
|
|
139
|
+
"mode": "list",
|
|
140
|
+
"filter": { "date": "{{params.date}}", "stage": "{{query.stage}}" },
|
|
141
|
+
"sort": "seq", "order": "asc", "limit": 0,
|
|
142
|
+
"fields": ["seq", "date", "title", "stage"],
|
|
143
|
+
"enabled": true
|
|
144
|
+
}</code></pre>
|
|
145
|
+
|
|
146
|
+
<h2>Calling an endpoint</h2>
|
|
147
|
+
<p>List-mode endpoints also accept, within the definition's bounds:</p>
|
|
148
|
+
<table class="table table-sm">
|
|
149
|
+
<thead>
|
|
150
|
+
<tr><th>Query</th><th>Effect</th></tr>
|
|
151
|
+
</thead>
|
|
152
|
+
<tbody>
|
|
153
|
+
<tr><td><code>page</code></td><td>Page number</td></tr>
|
|
154
|
+
<tr><td><code>limit</code></td><td>Smaller pages; never above the definition's <code>limit</code> (or 500 when that is 0)</td></tr>
|
|
155
|
+
<tr><td><code>sort</code> / <code>order</code></td><td>Sort by any field the endpoint returns</td></tr>
|
|
156
|
+
<tr><td><code><field>_<op>=value</code></td><td>Extra filters on fields the endpoint returns (no <code>filter[...]</code> wrapper). A field the definition already filters on cannot be overridden.</td></tr>
|
|
157
|
+
</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
<pre class="code-block"><code class="language-bash"># Public, list mode
|
|
160
|
+
curl 'https://example.com/api/x/world-cup/fixtures-day/2026-06-11?stage=group&limit=10'
|
|
161
|
+
# Token auth
|
|
162
|
+
curl -H 'Authorization: Bearer dcms_0123...cdef' \
|
|
163
|
+
https://example.com/api/x/world-cup/squad/eng
|
|
164
|
+
# Role auth - a user's access token
|
|
165
|
+
curl -H 'Authorization: Bearer eyJ...' https://example.com/api/x/members/directory</code></pre>
|
|
166
|
+
<p>Auth follows the same rules as the <a href="#/docs/api/external">external API</a>:</p>
|
|
167
|
+
<ul>
|
|
168
|
+
<li><strong>public</strong> - anyone. These responses are cached and refreshed automatically when the
|
|
169
|
+
collection's entries or the definition change.</li>
|
|
170
|
+
<li><strong>token</strong> - a project API token whose project is the endpoint's project. Scopes are checked
|
|
171
|
+
against the endpoint's collection with the <code>read</code> verb. A JWT is refused.</li>
|
|
172
|
+
<li><strong>role name</strong> - a signed-in user with that role or a more senior one.</li>
|
|
173
|
+
</ul>
|
|
174
|
+
<p>Errors: <code>401</code>/<code>403</code> from auth, <code>400</code> for a missing path parameter,
|
|
175
|
+
<code>404</code> for no such endpoint or no match in single mode.</p>
|
|
176
|
+
|
|
177
|
+
<h2>Managing endpoints over HTTP</h2>
|
|
178
|
+
<p>All need a JWT; project scope applies to the endpoint and to the collection it queries (<code>403</code>
|
|
179
|
+
otherwise).</p>
|
|
180
|
+
<table class="table table-sm">
|
|
181
|
+
<thead>
|
|
182
|
+
<tr><th>Method</th><th>Path</th><th>Permission</th><th>Description</th></tr>
|
|
183
|
+
</thead>
|
|
184
|
+
<tbody>
|
|
185
|
+
<tr><td><code>GET</code></td><td><code>/api/api-endpoints</code></td><td><code>api-endpoints.read</code></td><td>List definitions</td></tr>
|
|
186
|
+
<tr><td><code>GET</code></td><td><code>/api/api-endpoints/:id</code></td><td><code>api-endpoints.read</code></td><td>One definition</td></tr>
|
|
187
|
+
<tr><td><code>POST</code></td><td><code>/api/api-endpoints</code></td><td><code>api-endpoints.create</code></td><td>Create; <code>201</code>, <code>400</code> invalid, <code>409</code> duplicate path shape</td></tr>
|
|
188
|
+
<tr><td><code>PUT</code></td><td><code>/api/api-endpoints/:id</code></td><td><code>api-endpoints.update</code></td><td>Update the fields sent; <code>project</code> is ignored</td></tr>
|
|
189
|
+
<tr><td><code>DELETE</code></td><td><code>/api/api-endpoints/:id</code></td><td><code>api-endpoints.delete</code></td><td>Delete; the URL answers <code>404</code> at once</td></tr>
|
|
190
|
+
</tbody>
|
|
191
|
+
</table>
|
|
192
|
+
<p>The base <code>super-admin</code> and <code>admin</code> roles hold <code>api-endpoints.*</code>; custom roles
|
|
193
|
+
need it granted in the role editor.</p>
|
|
194
|
+
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|