domma-cms 1.6.60 → 1.7.1
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/CHANGELOG.md +4349 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/mail-compose.js +1 -0
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/call-new.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/call-new.js +1 -1
- package/admin/js/views/contacts.js +15 -15
- package/admin/js/views/form-submissions.js +2 -2
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/compose-window.js +3 -1
- package/plugins/_lib/admin/mail/identity.js +18 -3
- package/plugins/_lib/admin/mail/reader-view.js +32 -3
- package/plugins/blog/CLAUDE.md +4 -0
- package/plugins/blog/admin/lib/email.js +191 -0
- package/plugins/blog/admin/views/comments.js +3 -1
- package/plugins/blog/plugin.json +2 -2
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/email.test.js +69 -0
- package/plugins/booking/CLAUDE.md +4 -0
- package/plugins/booking/admin/lib/email.js +191 -0
- package/plugins/booking/admin/views/panels.js +7 -3
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/email.test.js +69 -0
- package/plugins/free-tier.lock.json +27 -19
- package/plugins/shopping-cart/CLAUDE.md +4 -0
- package/plugins/shopping-cart/admin/lib/email.js +191 -0
- package/plugins/shopping-cart/admin/views/orders.js +11 -3
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/email.test.js +69 -0
- package/plugins/support/CLAUDE.md +4 -0
- package/plugins/support/admin/lib/email.js +191 -0
- package/plugins/support/admin/views/desk.js +4 -1
- package/plugins/support/admin/views/inbox.js +6 -2
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/email.test.js +69 -0
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
All API endpoints are served under the `/api` prefix. Unless noted otherwise:
|
|
4
|
+
|
|
5
|
+
- **Auth required** means the request must include `Authorization: Bearer <access_token>`
|
|
6
|
+
- Responses use `Content-Type: application/json`
|
|
7
|
+
- Error responses have the shape `{ "error": "message" }`
|
|
8
|
+
- Rate limiting applies to sensitive auth endpoints (noted per endpoint)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Public SEO endpoints
|
|
13
|
+
|
|
14
|
+
These are unauthenticated, served at the root rather than under `/api`, and intended for search engines.
|
|
15
|
+
|
|
16
|
+
### GET `/sitemap.xml`
|
|
17
|
+
|
|
18
|
+
Auto-generated XML sitemap of indexable pages.
|
|
19
|
+
|
|
20
|
+
**Auth required:** No
|
|
21
|
+
**Content-Type:** `application/xml`
|
|
22
|
+
**Caching:** Cached with tag `sitemap`; invalidated automatically on page create, update, rename, or delete.
|
|
23
|
+
|
|
24
|
+
Pages enter the sitemap based on the rules in `server/services/sitemap.js` → `shouldIncludeInSitemap()` - typically `status: published`, `visibility: public`, and `seo.noindex` not set. Entries include `<loc>` and `<lastmod>` (from the page's `updatedAt`). Absolute URLs are derived from the request's host and protocol automatically - set `TRUST_PROXY` when behind a reverse proxy so `X-Forwarded-*` headers are honoured - use `loopback` for a proxy on the same machine (or the proxy's addresses/CIDRs), not `true`, which trusts every hop and lets visitors spoof their IP address. `site.baseUrl` can be set in `config/site.json` to override this if your infrastructure doesn't forward Host correctly.
|
|
25
|
+
|
|
26
|
+
### GET `/robots.txt`
|
|
27
|
+
|
|
28
|
+
Auto-generated robots policy referencing the sitemap.
|
|
29
|
+
|
|
30
|
+
**Auth required:** No
|
|
31
|
+
**Content-Type:** `text/plain`
|
|
32
|
+
|
|
33
|
+
Default body:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
User-agent: *
|
|
37
|
+
Allow: /
|
|
38
|
+
Disallow: /admin/
|
|
39
|
+
Disallow: /api/
|
|
40
|
+
|
|
41
|
+
Sitemap: <request-origin>/sitemap.xml
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The sitemap URL uses the same origin auto-derivation as `/sitemap.xml`. Append additional directives via `site.seo.robotsExtra` in `config/site.json`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Authentication (`/api/auth/*`)
|
|
49
|
+
|
|
50
|
+
### GET `/api/auth/setup-status`
|
|
51
|
+
|
|
52
|
+
Check whether the initial admin account has been created.
|
|
53
|
+
|
|
54
|
+
**Auth required:** No
|
|
55
|
+
|
|
56
|
+
**Response**
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "needsSetup": true, "siteTitle": "My Site", "resetByEmail": true, "resetExpiresIn": "1 hour" }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`resetByEmail` is `false` when no mail server is set up - "Forgot your password?" cannot send anything then.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### POST `/api/auth/setup`
|
|
67
|
+
|
|
68
|
+
Create the very first administrator account. Returns `403` if any users already exist.
|
|
69
|
+
|
|
70
|
+
**Auth required:** No
|
|
71
|
+
|
|
72
|
+
**Request body**
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"name": "Admin User",
|
|
77
|
+
"email": "admin@example.com",
|
|
78
|
+
"password": "secret123"
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Response** `201`
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"token": "<access_token>",
|
|
87
|
+
"refreshToken": "<refresh_token>",
|
|
88
|
+
"user": { "id": "uuid", "name": "Admin User", "email": "admin@example.com", "role": "admin" }
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### POST `/api/auth/login`
|
|
95
|
+
|
|
96
|
+
Exchange credentials for tokens. Rate limited to **5 requests per minute**.
|
|
97
|
+
|
|
98
|
+
**Auth required:** No
|
|
99
|
+
|
|
100
|
+
**Request body**
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "email": "admin@example.com", "password": "secret123" }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Response** `200`
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"token": "<access_token>",
|
|
111
|
+
"refreshToken": "<refresh_token>",
|
|
112
|
+
"user": { "id": "uuid", "name": "Admin User", "email": "admin@example.com", "role": "admin" }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### POST `/api/auth/refresh`
|
|
119
|
+
|
|
120
|
+
Exchange a refresh token for a new access token.
|
|
121
|
+
|
|
122
|
+
**Auth required:** No
|
|
123
|
+
|
|
124
|
+
**Request body**
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{ "refreshToken": "<refresh_token>" }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Response** `200`
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "token": "<new_access_token>" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### POST `/api/auth/logout`
|
|
139
|
+
|
|
140
|
+
Blacklist a refresh token. Safe to call without authentication.
|
|
141
|
+
|
|
142
|
+
**Auth required:** No
|
|
143
|
+
|
|
144
|
+
**Request body**
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "refreshToken": "<refresh_token>" }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Response** `200`
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{ "ok": true }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### GET `/api/auth/me`
|
|
159
|
+
|
|
160
|
+
Return the authenticated user's profile.
|
|
161
|
+
|
|
162
|
+
**Auth required:** Yes
|
|
163
|
+
|
|
164
|
+
**Response** `200`
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"id": "uuid",
|
|
169
|
+
"name": "Admin User",
|
|
170
|
+
"email": "admin@example.com",
|
|
171
|
+
"role": "admin",
|
|
172
|
+
"isActive": true,
|
|
173
|
+
"lastLogin": "2026-01-01T00:00:00.000Z",
|
|
174
|
+
"profile": {}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### PUT `/api/auth/me`
|
|
181
|
+
|
|
182
|
+
Update the authenticated user's own name, email, password, or profile data.
|
|
183
|
+
|
|
184
|
+
**Auth required:** Yes
|
|
185
|
+
|
|
186
|
+
**Request body** (all fields optional)
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"name": "New Name",
|
|
191
|
+
"email": "new@example.com",
|
|
192
|
+
"password": "newpassword",
|
|
193
|
+
"profile": { "bio": "Hello" }
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Response** `200` - updated user object with `profile`
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### GET `/api/auth/permissions`
|
|
202
|
+
|
|
203
|
+
Return the permissions for every role the authenticated user holds, with the admin scope and home.
|
|
204
|
+
|
|
205
|
+
**Auth required:** Yes
|
|
206
|
+
|
|
207
|
+
**Response** `200`
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"permissions": ["pages.read", "pages.create", "..."],
|
|
212
|
+
"adminScope": null,
|
|
213
|
+
"adminHome": "/plugins/site-manager/dashboard"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`adminScope` is `null` for the whole admin, or `{home, allow}` for a confined role. `adminHome` is the
|
|
218
|
+
route `#/` opens for this user: a confined role's home, else the site's `adminHome` if the user may use
|
|
219
|
+
it and the sidebar knows it, else `/`.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### GET `/api/auth/permissions-registry`
|
|
224
|
+
|
|
225
|
+
Return the full permissions registry (all resources and their role assignments).
|
|
226
|
+
|
|
227
|
+
**Auth required:** Yes
|
|
228
|
+
|
|
229
|
+
**Response** `200`
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{ "groups": [...], "resources": { ... } }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### POST `/api/auth/forgot-password`
|
|
238
|
+
|
|
239
|
+
Trigger a password reset email. Answers at once and identically whether or not the address has an account - the lookup and the send happen after the reply, so neither the answer nor its timing says whether an account exists. Rate limited to **3 requests per hour**.
|
|
240
|
+
|
|
241
|
+
The link in the email points at `site.baseUrl` when it is set; otherwise at the address the request came from only if someone has signed in there, otherwise at the address signed in at most recently. With none of those (or no mail server) nothing is sent and the admins get a notification. Emits `auth:resetRequested` `{email, userId, ip, ua, sent, reason}`.
|
|
242
|
+
|
|
243
|
+
**Auth required:** No
|
|
244
|
+
|
|
245
|
+
**Request body**
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{ "email": "user@example.com" }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Response** `200`
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{ "ok": true, "expiresIn": "1 hour" }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### POST `/api/auth/reset-password`
|
|
260
|
+
|
|
261
|
+
Consume a reset token and set a new password. Rate limited to **5 requests per minute**.
|
|
262
|
+
|
|
263
|
+
**Auth required:** No
|
|
264
|
+
|
|
265
|
+
**Request body**
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{ "token": "<reset_token>", "password": "newpassword" }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Response** `200`
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{ "ok": true }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The link is used up before the password is set, the account is signed out everywhere, and the holder is emailed that
|
|
278
|
+
their password changed. A link for an account made inactive since it was sent no longer works. `400` with
|
|
279
|
+
`"expired": true` for a link that is unknown, used, withdrawn or out of time; with `problems` when the site's password
|
|
280
|
+
rules refuse the password.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### POST `/api/auth/reset-password/check`
|
|
285
|
+
|
|
286
|
+
Is a reset link still good - and, with a password, what the site's password rules make of it. The reset screen calls
|
|
287
|
+
it on opening (so a dead link says so before anything is typed) and as the new password is typed. Rate limited to
|
|
288
|
+
**60 requests per minute**.
|
|
289
|
+
|
|
290
|
+
**Auth required:** No (the token is the credential)
|
|
291
|
+
|
|
292
|
+
**Request body**
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{ "token": "<reset_token>", "password": "optional - to check against the rules" }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Response** `200`
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{ "email": "user@example.com", "expiresAt": "2026-09-27T09:00:00.000Z", "minutesLeft": 58, "problems": [] }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`400` `{ "error": "Invalid or expired reset link", "expired": true }` otherwise.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Pages (`/api/pages/*`)
|
|
309
|
+
|
|
310
|
+
All pages endpoints require the relevant `pages` permission.
|
|
311
|
+
|
|
312
|
+
### GET `/api/pages`
|
|
313
|
+
|
|
314
|
+
List all pages (summary fields only).
|
|
315
|
+
|
|
316
|
+
**Auth required:** Yes (`pages:read`)
|
|
317
|
+
|
|
318
|
+
**Response** `200` - array of page summaries
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
[
|
|
322
|
+
{
|
|
323
|
+
"urlPath": "/about",
|
|
324
|
+
"title": "About",
|
|
325
|
+
"slug": "about",
|
|
326
|
+
"status": "published",
|
|
327
|
+
"layout": "default",
|
|
328
|
+
"showInNav": true,
|
|
329
|
+
"sortOrder": 1,
|
|
330
|
+
"category": null,
|
|
331
|
+
"visibility": "public",
|
|
332
|
+
"tags": [],
|
|
333
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
334
|
+
"createdAt": "2026-01-01T00:00:00.000Z"
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### GET `/api/pages/tags`
|
|
342
|
+
|
|
343
|
+
Return a sorted, deduplicated list of all tags used across pages.
|
|
344
|
+
|
|
345
|
+
**Auth required:** Yes (`pages:read`)
|
|
346
|
+
|
|
347
|
+
**Response** `200`
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{ "tags": ["blog", "news", "tutorial"] }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### GET `/api/pages/*`
|
|
356
|
+
|
|
357
|
+
Get a single page by URL path. The wildcard maps to the URL path, e.g. `GET /api/pages/about` retrieves `/about`.
|
|
358
|
+
|
|
359
|
+
**Auth required:** Yes (`pages:read`)
|
|
360
|
+
|
|
361
|
+
**Response** `200` - full page object including `frontmatter`, `body` (raw Markdown), and `html`
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### POST `/api/pages`
|
|
366
|
+
|
|
367
|
+
Create a new page.
|
|
368
|
+
|
|
369
|
+
**Auth required:** Yes (`pages:create`)
|
|
370
|
+
|
|
371
|
+
**Request body**
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"urlPath": "/about",
|
|
376
|
+
"frontmatter": {
|
|
377
|
+
"title": "About Us",
|
|
378
|
+
"layout": "default",
|
|
379
|
+
"status": "draft"
|
|
380
|
+
},
|
|
381
|
+
"body": "## Hello\n\nWelcome to our site."
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Response** `201` - created page object
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### POST `/api/pages/preview`
|
|
390
|
+
|
|
391
|
+
Render Markdown (with shortcodes) to HTML without persisting anything.
|
|
392
|
+
|
|
393
|
+
**Auth required:** Yes (`pages:read`)
|
|
394
|
+
|
|
395
|
+
**Request body**
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
{ "markdown": "## Hello\n\n[card title=\"Example\"]Body[/card]" }
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Response** `200`
|
|
402
|
+
|
|
403
|
+
```json
|
|
404
|
+
{ "html": "<h2>Hello</h2>..." }
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### PUT `/api/pages/*`
|
|
410
|
+
|
|
411
|
+
Update a page. Optionally rename it to a new URL path.
|
|
412
|
+
|
|
413
|
+
**Auth required:** Yes (`pages:update`)
|
|
414
|
+
|
|
415
|
+
**Request body**
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{
|
|
419
|
+
"frontmatter": { "title": "Updated Title", "status": "published" },
|
|
420
|
+
"body": "Updated content.",
|
|
421
|
+
"newUrlPath": "/about-us"
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`newUrlPath` is optional. If supplied and different from the current path, the page file is renamed and any navigation items pointing to the old URL are updated automatically.
|
|
426
|
+
|
|
427
|
+
**Response** `200` - updated page object
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### DELETE `/api/pages/*`
|
|
432
|
+
|
|
433
|
+
Delete a page.
|
|
434
|
+
|
|
435
|
+
**Auth required:** Yes (`pages:delete`)
|
|
436
|
+
|
|
437
|
+
**Response** `200`
|
|
438
|
+
|
|
439
|
+
```json
|
|
440
|
+
{ "success": true }
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## List tags (`/api/list-tags/:kind`)
|
|
446
|
+
|
|
447
|
+
Admin-only tags that link pages or blocks together, so the admin lists can be narrowed to one tag. `:kind` is `pages` or `blocks`. A page stores its tags as `adminTags` in its frontmatter, and a block in `meta.adminTags`. The names are kept in `config/list-tags.json`. Nothing on the public site reads them. Every change applies only to the items the caller's projects allow.
|
|
448
|
+
|
|
449
|
+
| Method | Path | Body | Auth |
|
|
450
|
+
|--------|------|------|------|
|
|
451
|
+
| GET | `/api/list-tags/:kind` | - | `pages:read` - `{tags: [{name, count}]}` |
|
|
452
|
+
| POST | `/api/list-tags/:kind` | `{name}` | `pages:update` - creates a tag |
|
|
453
|
+
| PUT | `/api/list-tags/:kind/item` | `{id, tags}` | `pages:update` - `id` is the page's urlPath or the block's name; adds no page revision |
|
|
454
|
+
| POST | `/api/list-tags/:kind/rename` | `{from, to}` | `pages:update` - `{name, changed}`; renaming onto an existing tag merges the two |
|
|
455
|
+
| POST | `/api/list-tags/:kind/delete` | `{name}` | `pages:update` - `{changed}`; the items stay |
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Media (`/api/media/*`)
|
|
460
|
+
|
|
461
|
+
### GET `/api/media`
|
|
462
|
+
|
|
463
|
+
List all uploaded media files.
|
|
464
|
+
|
|
465
|
+
**Auth required:** Yes (`media:read`)
|
|
466
|
+
|
|
467
|
+
**Response** `200` - array of media objects with `name`, `url`, `size`, `mime`, `updatedAt`
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### POST `/api/media`
|
|
472
|
+
|
|
473
|
+
Upload one or more files. Uses `multipart/form-data`. Allowed types: images (JPEG, PNG, GIF, WebP, SVG, BMP, TIFF, ICO), documents (PDF, TXT, CSV, DOC, DOCX, XLS, XLSX), video (MP4, WebM, OGG), audio (MP3, OGG, WAV, WebM).
|
|
474
|
+
|
|
475
|
+
**Auth required:** Yes (`media:create`)
|
|
476
|
+
|
|
477
|
+
**Response** `201` - single media object, or array if multiple files uploaded
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
### PATCH `/api/media/:name`
|
|
482
|
+
|
|
483
|
+
Rename a media file.
|
|
484
|
+
|
|
485
|
+
**Auth required:** Yes (`media:update`)
|
|
486
|
+
|
|
487
|
+
**Request body**
|
|
488
|
+
|
|
489
|
+
```json
|
|
490
|
+
{ "newName": "new-filename.jpg" }
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**Response** `200` - updated media object
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
### DELETE `/api/media/:name`
|
|
498
|
+
|
|
499
|
+
Delete a media file.
|
|
500
|
+
|
|
501
|
+
**Auth required:** Yes (`media:delete`)
|
|
502
|
+
|
|
503
|
+
**Response** `200`
|
|
504
|
+
|
|
505
|
+
```json
|
|
506
|
+
{ "success": true }
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
### GET `/api/media/:name/info`
|
|
512
|
+
|
|
513
|
+
Get image metadata (dimensions, format, size) for editable image formats (JPEG, PNG, WebP, TIFF).
|
|
514
|
+
|
|
515
|
+
**Auth required:** Yes (`media:read`)
|
|
516
|
+
|
|
517
|
+
**Response** `200` - image metadata object
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
### POST `/api/media/:name/transform`
|
|
522
|
+
|
|
523
|
+
Apply image transformations (resize, crop, rotate, watermark, convert format) using Sharp.
|
|
524
|
+
|
|
525
|
+
**Auth required:** Yes (`media:update`)
|
|
526
|
+
|
|
527
|
+
**Request body**
|
|
528
|
+
|
|
529
|
+
```json
|
|
530
|
+
{
|
|
531
|
+
"operations": {
|
|
532
|
+
"resize": { "width": 800, "height": 600, "fit": "cover" },
|
|
533
|
+
"rotate": 90,
|
|
534
|
+
"format": "webp",
|
|
535
|
+
"quality": 85
|
|
536
|
+
},
|
|
537
|
+
"saveAs": "resized-image.webp"
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`saveAs` is optional; omit it to overwrite the original.
|
|
542
|
+
|
|
543
|
+
**Response** `200` - updated media object
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Navigation (`/api/navigation`)
|
|
548
|
+
|
|
549
|
+
### GET `/api/navigation`
|
|
550
|
+
|
|
551
|
+
Return the full navigation config object.
|
|
552
|
+
|
|
553
|
+
**Auth required:** Yes (`navigation:read`)
|
|
554
|
+
|
|
555
|
+
**Response** `200` - navigation config (see `config/navigation.json` in [Configuration](configuration.md))
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
### PUT `/api/navigation`
|
|
560
|
+
|
|
561
|
+
Replace the navigation config.
|
|
562
|
+
|
|
563
|
+
**Auth required:** Yes (`navigation:update`)
|
|
564
|
+
|
|
565
|
+
**Request body** - navigation config object (same shape as the file). The server normalises `children` keys to `items` before saving.
|
|
566
|
+
|
|
567
|
+
**Response** `200`
|
|
568
|
+
|
|
569
|
+
```json
|
|
570
|
+
{ "success": true }
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Sidebar (`/api/sidebar/*`)
|
|
576
|
+
|
|
577
|
+
### GET `/api/sidebar/registered-items`
|
|
578
|
+
|
|
579
|
+
Return the array of `{parent, folder, group?, item}` entries currently registered via `hooks.registerSidebarItem(...)` (and plugin manifests' `admin.sidebar`) by core and enabled plugins. `folder` is the Tool's default folder key (`tools` unless it names another); `group`, when present, is the sub-folder inside it (`Core` for the built-in Tools). Read-only snapshot of in-memory state - not persisted, regenerated on each server boot.
|
|
580
|
+
|
|
581
|
+
**Auth required:** Yes (`menus:read`)
|
|
582
|
+
|
|
583
|
+
**Response** `200`
|
|
584
|
+
|
|
585
|
+
```json
|
|
586
|
+
{
|
|
587
|
+
"items": [
|
|
588
|
+
{
|
|
589
|
+
"parent": null,
|
|
590
|
+
"folder": "system",
|
|
591
|
+
"item": {
|
|
592
|
+
"text": "My Plugin",
|
|
593
|
+
"url": "#/plugins/my-plugin",
|
|
594
|
+
"icon": "puzzle",
|
|
595
|
+
"permission": "my-plugin"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Settings (`/api/settings`)
|
|
605
|
+
|
|
606
|
+
### GET `/api/settings`
|
|
607
|
+
|
|
608
|
+
Return the site config. The SMTP password is stripped from the response.
|
|
609
|
+
|
|
610
|
+
**Auth required:** Yes (`settings:read`)
|
|
611
|
+
|
|
612
|
+
**Response** `200` - site config object (see `config/site.json` in [Configuration](configuration.md))
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### PUT `/api/settings`
|
|
617
|
+
|
|
618
|
+
Update site settings. Only the following keys are accepted: `title`, `tagline`, `description`, `logo`, `favicon`, `theme`, `smtp`, `footer`, `analytics`, `social`, `locale`, `layoutOptions`.
|
|
619
|
+
|
|
620
|
+
**Auth required:** Yes (`settings:update`)
|
|
621
|
+
|
|
622
|
+
**Request body** - partial or full site config object
|
|
623
|
+
|
|
624
|
+
**Response** `200`
|
|
625
|
+
|
|
626
|
+
```json
|
|
627
|
+
{ "success": true }
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
### POST `/api/settings/test-email`
|
|
633
|
+
|
|
634
|
+
Send a test email using the stored SMTP configuration.
|
|
635
|
+
|
|
636
|
+
**Auth required:** Yes (`settings:read`)
|
|
637
|
+
|
|
638
|
+
**Request body** (optional)
|
|
639
|
+
|
|
640
|
+
```json
|
|
641
|
+
{ "to": "test@example.com" }
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
If `to` is omitted, the `fromAddress` from SMTP config is used.
|
|
645
|
+
|
|
646
|
+
**Response** `200`
|
|
647
|
+
|
|
648
|
+
```json
|
|
649
|
+
{ "success": true, "message": "Test email sent to test@example.com" }
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### GET `/api/settings/custom-css`
|
|
655
|
+
|
|
656
|
+
Return the current custom CSS content.
|
|
657
|
+
|
|
658
|
+
**Auth required:** Yes (`settings:update`)
|
|
659
|
+
|
|
660
|
+
**Response** `200`
|
|
661
|
+
|
|
662
|
+
```json
|
|
663
|
+
{ "css": "/* custom styles */" }
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### PUT `/api/settings/custom-css`
|
|
669
|
+
|
|
670
|
+
Save custom CSS to `content/custom.css`. Maximum 100 KB.
|
|
671
|
+
|
|
672
|
+
**Auth required:** Yes (`settings:update`)
|
|
673
|
+
|
|
674
|
+
**Request body**
|
|
675
|
+
|
|
676
|
+
```json
|
|
677
|
+
{ "css": "body { background: red; }" }
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Response** `200`
|
|
681
|
+
|
|
682
|
+
```json
|
|
683
|
+
{ "success": true }
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## Users (`/api/users/*`)
|
|
689
|
+
|
|
690
|
+
Role hierarchy enforced: a user cannot create, edit, or delete users with an equal or higher role level.
|
|
691
|
+
|
|
692
|
+
### GET `/api/users`
|
|
693
|
+
|
|
694
|
+
List all users (includes profile data).
|
|
695
|
+
|
|
696
|
+
**Auth required:** Yes (`users:read`)
|
|
697
|
+
|
|
698
|
+
**Response** `200` - array of user objects with `profile` merged in
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
### GET `/api/users/:id`
|
|
703
|
+
|
|
704
|
+
Get a single user. Users with the `users` read permission (Super Admin and Admin by default) can view any user; all other users can only view themselves.
|
|
705
|
+
|
|
706
|
+
**Auth required:** Yes
|
|
707
|
+
|
|
708
|
+
**Response** `200` - user object with `profile`
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
### POST `/api/users`
|
|
713
|
+
|
|
714
|
+
Create a new user.
|
|
715
|
+
|
|
716
|
+
**Auth required:** Yes (`users:create`)
|
|
717
|
+
|
|
718
|
+
**Request body**
|
|
719
|
+
|
|
720
|
+
```json
|
|
721
|
+
{
|
|
722
|
+
"name": "Jane Smith",
|
|
723
|
+
"email": "jane@example.com",
|
|
724
|
+
"password": "password123",
|
|
725
|
+
"role": "user"
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Optional: `additionalRoles` (array), `projects` (array of project slugs - the user's access scope; empty =
|
|
730
|
+
unrestricted), `meta` (object, e.g. `{"project": "shop"}` for the owning project), `isActive` (boolean,
|
|
731
|
+
default `true`) and `profile` (object of profile field values). Send them here rather than in a follow-up
|
|
732
|
+
PUT, so a failure cannot leave the account unrestricted and switched on.
|
|
733
|
+
|
|
734
|
+
`role` defaults to `user`. You can only create users whose role is less senior than your own.
|
|
735
|
+
|
|
736
|
+
**Response** `201` - created user object (password stripped)
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
### PUT `/api/users/:id`
|
|
741
|
+
|
|
742
|
+
Update a user's details. Cannot escalate a user to a role higher than the actor's own role.
|
|
743
|
+
|
|
744
|
+
**Auth required:** Yes (`users:update`)
|
|
745
|
+
|
|
746
|
+
**Request body** (all fields optional)
|
|
747
|
+
|
|
748
|
+
```json
|
|
749
|
+
{
|
|
750
|
+
"name": "Jane Smith",
|
|
751
|
+
"email": "jane@example.com",
|
|
752
|
+
"role": "admin",
|
|
753
|
+
"isActive": true,
|
|
754
|
+
"profile": { "bio": "Site editor" }
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Response** `200` - updated user object with `profile`
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
### DELETE `/api/users/:id`
|
|
763
|
+
|
|
764
|
+
Delete a user. Cannot delete your own account.
|
|
765
|
+
|
|
766
|
+
**Auth required:** Yes (`users:delete`)
|
|
767
|
+
|
|
768
|
+
**Response** `200`
|
|
769
|
+
|
|
770
|
+
```json
|
|
771
|
+
{ "success": true }
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
### POST `/api/users/:id/password-reset`
|
|
777
|
+
|
|
778
|
+
An administrator's password reset: email the user a link to choose a new password, or get one to pass on yourself
|
|
779
|
+
(a site with no mail server, a mailbox that is not receiving). Their current password keeps working until the link is
|
|
780
|
+
used; a newer link replaces an older one. Not for your own account (use My Profile), and only for someone your role is
|
|
781
|
+
more senior to, on an active account.
|
|
782
|
+
|
|
783
|
+
**Auth required:** Yes (`users:update`)
|
|
784
|
+
|
|
785
|
+
**Request body**
|
|
786
|
+
|
|
787
|
+
```json
|
|
788
|
+
{ "via": "email" }
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Response** `200` - `via: "email"`: `{ "sent": true, "expiresAt": "…", "expiresIn": "1 hour" }`;
|
|
792
|
+
`via: "link"`: `{ "link": "https://example.com/admin/#/reset-password?token=…", "expiresAt": "…", "expiresIn": "1 hour" }`.
|
|
793
|
+
|
|
794
|
+
`409` with `code`: `NO_SMTP` (emailing with no mail server), `INACTIVE`, `NO_ORIGIN`. `403` for yourself or someone
|
|
795
|
+
not junior to you. The list (`GET /api/users`) shows an open link as `resetLinkExpires` - the time only, never the token.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
### DELETE `/api/users/:id/password-reset`
|
|
800
|
+
|
|
801
|
+
Withdraw an unused reset link: it stops working at once. Same permissions as above.
|
|
802
|
+
|
|
803
|
+
**Response** `200` `{ "ok": true }`
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## Collections (`/api/collections/*`)
|
|
808
|
+
|
|
809
|
+
**Number fields are stored as numbers.** Every entry write - admin, `/api/v1`, form submissions,
|
|
810
|
+
import, Actions - converts a `number` field sent as plain decimal text (`"42"`, `" 9.5 "`) to a
|
|
811
|
+
number, so it comes back as `42`. Text that is not a number (`""`, `"abc"`) is stored as sent, not
|
|
812
|
+
refused; a required number field sent as `""` is still refused as missing.
|
|
813
|
+
|
|
814
|
+
### Schema management
|
|
815
|
+
|
|
816
|
+
| Method | Path | Auth | Description |
|
|
817
|
+
|---|---|---|---|
|
|
818
|
+
| `GET` | `/api/collections` | `collections:read` | List all collections (schemas) |
|
|
819
|
+
| `POST` | `/api/collections` | `collections:create` | Create a collection |
|
|
820
|
+
| `GET` | `/api/collections/:slug` | `collections:read` | Get a collection schema |
|
|
821
|
+
| `PUT` | `/api/collections/:slug` | `collections:update` | Update a collection schema |
|
|
822
|
+
| `DELETE` | `/api/collections/:slug` | `collections:delete` | Delete a collection and all its data |
|
|
823
|
+
|
|
824
|
+
**Create collection request body**
|
|
825
|
+
|
|
826
|
+
```json
|
|
827
|
+
{
|
|
828
|
+
"title": "Blog Posts",
|
|
829
|
+
"slug": "blog-posts",
|
|
830
|
+
"description": "Optional description",
|
|
831
|
+
"fields": [
|
|
832
|
+
{ "name": "title", "type": "text", "required": true },
|
|
833
|
+
{ "name": "body", "type": "textarea" }
|
|
834
|
+
],
|
|
835
|
+
"api": {
|
|
836
|
+
"read": { "enabled": true, "access": "public" },
|
|
837
|
+
"create": { "enabled": false }
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Entry CRUD
|
|
843
|
+
|
|
844
|
+
| Method | Path | Auth | Description |
|
|
845
|
+
|---|---|---|---|
|
|
846
|
+
| `GET` | `/api/collections/:slug/entries` | `collections:read` | List entries (paginated, searchable) |
|
|
847
|
+
| `GET` | `/api/collections/:slug/entries/:id` | `collections:read` | Get a single entry |
|
|
848
|
+
| `POST` | `/api/collections/:slug/entries` | `collections:create` | Create an entry |
|
|
849
|
+
| `PUT` | `/api/collections/:slug/entries/:id` | `collections:update` | Update an entry |
|
|
850
|
+
| `DELETE` | `/api/collections/:slug/entries/:id` | `collections:delete` | Delete an entry |
|
|
851
|
+
| `DELETE` | `/api/collections/:slug/entries` | `collections:delete` | Clear all entries |
|
|
852
|
+
|
|
853
|
+
**List entries query parameters**
|
|
854
|
+
|
|
855
|
+
| Parameter | Default | Description |
|
|
856
|
+
|----------------------|--------------|----------------------------------------------------------------------|
|
|
857
|
+
| `page` | `1` | Page number |
|
|
858
|
+
| `limit` | `50` | Results per page (`0` falls back to the default - ask for a larger number instead) |
|
|
859
|
+
| `sort` | `"createdAt"`| Field to sort by |
|
|
860
|
+
| `order` | `"desc"` | Sort direction (`"asc"` or `"desc"`) |
|
|
861
|
+
| `search` | - | Full-text substring match across all field values |
|
|
862
|
+
| `filter[<field>]` | - | Structured filter - defaults to equality |
|
|
863
|
+
| `filter[<field>_op]` | - | Structured filter with explicit operator (see below) |
|
|
864
|
+
|
|
865
|
+
**Structured filter operators**
|
|
866
|
+
|
|
867
|
+
| Suffix | Meaning | Example |
|
|
868
|
+
|-------------|------------------------------------|--------------------------------------------------|
|
|
869
|
+
| (none) / `_eq` | equal | `?filter[status]=open` |
|
|
870
|
+
| `_ne` | not equal | `?filter[status_ne]=closed` |
|
|
871
|
+
| `_gt` | greater than | `?filter[salary_gt]=40000` |
|
|
872
|
+
| `_gte` | greater than or equal | `?filter[salary_gte]=50000` |
|
|
873
|
+
| `_lt` | less than | `?filter[salary_lt]=100000` |
|
|
874
|
+
| `_lte` | less than or equal | `?filter[postedAt_lte]=2026-06-01` |
|
|
875
|
+
| `_in` | value in comma-separated list | `?filter[type_in]=full-time,contract` |
|
|
876
|
+
| `_nin` | value not in list | `?filter[status_nin]=archived,withdrawn` |
|
|
877
|
+
| `_contains` | substring match (case-insensitive) | `?filter[title_contains]=engineer` |
|
|
878
|
+
| `_starts` | starts-with (case-insensitive) | `?filter[title_starts]=Senior` |
|
|
879
|
+
| `_ends` | ends-with (case-insensitive) | `?filter[email_ends]=@example.com` |
|
|
880
|
+
| `_exists` | field is present and non-null | `?filter[remote_exists]=true` |
|
|
881
|
+
|
|
882
|
+
Predicates AND together. Pseudo-fields: `createdBy` (matches `meta.createdBy`), `id` (top-level entry id).
|
|
883
|
+
Dot-paths into nested data are supported - `?filter[address.city]=London`.
|
|
884
|
+
|
|
885
|
+
For MongoDB-backed collections the filter is translated to a native query (`$gte`, `$in`, regex with `^…$/i`, etc.)
|
|
886
|
+
and pushed down to the database. File-mode collections apply the same operators in memory - semantics are identical.
|
|
887
|
+
|
|
888
|
+
**Create/update entry request body**
|
|
889
|
+
|
|
890
|
+
```json
|
|
891
|
+
{ "data": { "title": "My Post", "body": "Content here." } }
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Export / Import
|
|
895
|
+
|
|
896
|
+
| Method | Path | Auth | Description |
|
|
897
|
+
|---|---|---|---|
|
|
898
|
+
| `GET` | `/api/collections/:slug/export` | `collections:read` | Export entries (`?format=json` or `?format=csv`) |
|
|
899
|
+
| `POST` | `/api/collections/:slug/import` | `collections:create` | Import entries from JSON |
|
|
900
|
+
|
|
901
|
+
**Import request body**
|
|
902
|
+
|
|
903
|
+
```json
|
|
904
|
+
{ "entries": [{ "data": { "title": "Post 1" } }] }
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
### Public access endpoints
|
|
908
|
+
|
|
909
|
+
When a collection has its `api` settings configured, entries can be accessed without admin authentication:
|
|
910
|
+
|
|
911
|
+
| Method | Path | Access |
|
|
912
|
+
|----------|---------------------------------------|--------------------------------------------------|
|
|
913
|
+
| `GET` | `/api/collections/:slug/public` | Determined by `api.read.access` |
|
|
914
|
+
| `GET` | `/api/collections/:slug/public/:id` | Determined by `api.read.access` |
|
|
915
|
+
| `POST` | `/api/collections/:slug/public` | Determined by `api.create.access` |
|
|
916
|
+
| `PUT` | `/api/collections/:slug/public/:id` | Determined by `api.update.access` |
|
|
917
|
+
| `DELETE` | `/api/collections/:slug/public/:id` | Determined by `api.delete.access` |
|
|
918
|
+
| `POST` | `/api/collections/render-scope` | Authenticated; renders `[collection scope="mine"]` |
|
|
919
|
+
|
|
920
|
+
Access values: `"public"` (no auth), `"token"` (requires a project-scoped API token - see
|
|
921
|
+
[External API & API tokens](#external-api--api-tokens-apiv1)), or a role name (requires a JWT for that role or higher).
|
|
922
|
+
|
|
923
|
+
`api.read` additionally accepts a `fields` array - when non-empty, public/external read responses
|
|
924
|
+
include only the listed `data` fields (admin endpoints are unaffected).
|
|
925
|
+
|
|
926
|
+
The public `GET` list endpoint accepts all the same query parameters as the admin list endpoint
|
|
927
|
+
(`page`, `limit`, `sort`, `order`, `search`, `filter[…]`) plus:
|
|
928
|
+
|
|
929
|
+
| Parameter | Description |
|
|
930
|
+
|---|---|
|
|
931
|
+
| `scope=mine` | Restrict to the current user's own entries (`createdBy = <user.id>`). Requires a JWT. Bypasses the `api.read` gate - users can always view their own data. |
|
|
932
|
+
|
|
933
|
+
**`POST /api/collections/render-scope`** - renders a `[collection scope="mine"]` placeholder server-side with the
|
|
934
|
+
current user's id injected. Called by the public site JS when it encounters `.dm-collection-hydrate` placeholders.
|
|
935
|
+
An interactive block (`searchable`, `sortable`, `filterable` or `paginate`) comes back as a Collection Browser shell
|
|
936
|
+
with `scope: "mine"` in its config (so `transitions` works); anything else as the static fragment.
|
|
937
|
+
|
|
938
|
+
Request body:
|
|
939
|
+
```json
|
|
940
|
+
{ "attrs": "<base64-encoded JSON of the original shortcode attributes>" }
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Response:
|
|
944
|
+
```json
|
|
945
|
+
{ "html": "<div class=\"dm-collection-cards\">...</div>" }
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
The endpoint never trusts the client's choice of user - the JWT is verified server-side and the `createdBy` filter
|
|
949
|
+
is appended before rendering.
|
|
950
|
+
|
|
951
|
+
### Pro: Connections
|
|
952
|
+
|
|
953
|
+
| Method | Path | Auth | Description |
|
|
954
|
+
|---|---|---|---|
|
|
955
|
+
| `GET` | `/api/collections/pro-status` | `collections:read` | Check if Pro MongoDB mode is active |
|
|
956
|
+
| `GET` | `/api/collections/connections` | Admin only | List configured connections |
|
|
957
|
+
| `PUT` | `/api/collections/connections` | Admin only | Save connection config |
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## External API & API tokens (`/api/v1/*`)
|
|
962
|
+
|
|
963
|
+
The external API is a stable, versioned alias of the public collection endpoints - the documented
|
|
964
|
+
surface for third-party integrations:
|
|
965
|
+
|
|
966
|
+
| Method | Path | Equivalent |
|
|
967
|
+
|----------|--------------------|------------------------------------------|
|
|
968
|
+
| `GET` | `/api/v1/:slug` | `GET /api/collections/:slug/public` |
|
|
969
|
+
| `GET` | `/api/v1/:slug/:id` | `GET /api/collections/:slug/public/:id` |
|
|
970
|
+
| `POST` | `/api/v1/:slug` | `POST /api/collections/:slug/public` |
|
|
971
|
+
| `PUT` | `/api/v1/:slug/:id` | `PUT /api/collections/:slug/public/:id` |
|
|
972
|
+
| `DELETE` | `/api/v1/:slug/:id` | `DELETE /api/collections/:slug/public/:id` |
|
|
973
|
+
|
|
974
|
+
Both paths share handlers, access rules, and query parameters (`page`, `limit`, `sort`, `order`,
|
|
975
|
+
`search`, `filter[…]`, `resolveRefs`).
|
|
976
|
+
|
|
977
|
+
### API tokens
|
|
978
|
+
|
|
979
|
+
API tokens are machine credentials for external callers - created in the admin under
|
|
980
|
+
**System → API Tokens** (or by a scaffolder recipe). Format: `dcms_` + 64 hex characters.
|
|
981
|
+
Only a SHA-256 hash is stored; the plaintext is shown **once**, at creation.
|
|
982
|
+
|
|
983
|
+
A collection verb opts into token auth with `api.<verb>.access: "token"`. Callers then send:
|
|
984
|
+
|
|
985
|
+
```
|
|
986
|
+
Authorization: Bearer dcms_<64 hex>
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**Strictness:** a token is accepted *only* where the access mode is `"token"` - it is never a
|
|
990
|
+
substitute for a role-name mode, and a JWT is never accepted in token mode.
|
|
991
|
+
|
|
992
|
+
**Project binding:** every token belongs to one project and only works on collections whose
|
|
993
|
+
resolved project matches (the collection's `meta.project`, or `core` when untagged).
|
|
994
|
+
|
|
995
|
+
**Scopes (optional):** a token may carry `scopes: [{ "collection": "jobs", "verbs": ["read"] }]`.
|
|
996
|
+
Empty scopes = every collection in the token's project. A scoped token is refused (403) outside
|
|
997
|
+
its scope list. The verb must still be enabled with `access: "token"` on the collection itself.
|
|
998
|
+
A scope may instead be a Content API scope, `{ "content": "pages" | "menus" | "site" | "search" }`
|
|
999
|
+
(`@pages` in the token editor) - see [Content API](#content-api-apicontentv1).
|
|
1000
|
+
|
|
1001
|
+
Failure responses: `401` (missing/invalid/disabled/expired token), `403` (wrong project or scope,
|
|
1002
|
+
or the verb is disabled).
|
|
1003
|
+
|
|
1004
|
+
Entries created via token auth are attributed `meta.createdBy: "token:<token-id>"`, `meta.source: "api"`.
|
|
1005
|
+
|
|
1006
|
+
### Token management (`/api/api-tokens/*`)
|
|
1007
|
+
|
|
1008
|
+
| Method | Path | Auth | Description |
|
|
1009
|
+
|----------|-----------------------|---------------------|-------------|
|
|
1010
|
+
| `GET` | `/api/api-tokens` | `api-tokens:read` | List tokens visible to the caller (project scope applies). Never includes the hash. |
|
|
1011
|
+
| `POST` | `/api/api-tokens` | `api-tokens:create` | Create a token. The response's `plaintext` field is the **only** time the token value appears. |
|
|
1012
|
+
| `PUT` | `/api/api-tokens/:id` | `api-tokens:update` | Update `name`, `enabled`, `scopes`, `expiresAt`. The project binding and hash are immutable. |
|
|
1013
|
+
| `DELETE` | `/api/api-tokens/:id` | `api-tokens:delete` | Revoke - external callers lose access immediately. |
|
|
1014
|
+
|
|
1015
|
+
**Create request body**
|
|
1016
|
+
|
|
1017
|
+
```json
|
|
1018
|
+
{
|
|
1019
|
+
"name": "mobile-app",
|
|
1020
|
+
"project": "core",
|
|
1021
|
+
"scopes": [{ "collection": "jobs", "verbs": ["read", "create"] }],
|
|
1022
|
+
"expiresAt": "2027-01-01T00:00:00.000Z"
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
**Create response (201)**
|
|
1027
|
+
|
|
1028
|
+
```json
|
|
1029
|
+
{
|
|
1030
|
+
"token": { "id": "…", "name": "mobile-app", "project": "core", "tokenHint": "ab12", "enabled": true },
|
|
1031
|
+
"plaintext": "dcms_<64 hex - shown once, store it now>"
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
> Existing roles are **not** back-filled with the `api-tokens` permission family - grant it via the
|
|
1036
|
+
> role editor. The level-0 root role self-heals at boot.
|
|
1037
|
+
|
|
1038
|
+
---
|
|
1039
|
+
|
|
1040
|
+
## Content API (`/api/content/v1/*`)
|
|
1041
|
+
|
|
1042
|
+
Read-only, headless access to what the site publishes - pages, menus, the site settings public pages
|
|
1043
|
+
already show, and search - as JSON. Where `/api/v1` exposes collection **data**, the Content API exposes
|
|
1044
|
+
**content**. It has its own prefix because `/api/v1/:slug` matches any collection slug, and a
|
|
1045
|
+
collection called `pages` would otherwise be shadowed.
|
|
1046
|
+
|
|
1047
|
+
**Off by default.** Turn it on at **System → Content API** (stored in `config/content-api.json`); until
|
|
1048
|
+
then every address answers `404`.
|
|
1049
|
+
|
|
1050
|
+
| Method | Path | Returns |
|
|
1051
|
+
|--------|------------------------------------|---------|
|
|
1052
|
+
| `GET` | `/api/content/v1/pages` | `{pages, total, page, limit}` - page summaries, no bodies |
|
|
1053
|
+
| `GET` | `/api/content/v1/pages/<path>` | One page (`/pages/` is the home page; a directory path serves its `index`) |
|
|
1054
|
+
| `GET` | `/api/content/v1/menus/:location` | `{location, slug, name, items}` - the menu bound to a location |
|
|
1055
|
+
| `GET` | `/api/content/v1/site` | Title, brand, footer, social links, SEO defaults, theme |
|
|
1056
|
+
| `GET` | `/api/content/v1/search?q=` | `{results: [{urlPath, title, description, tags, snippet, score}]}` |
|
|
1057
|
+
|
|
1058
|
+
### Which pages
|
|
1059
|
+
|
|
1060
|
+
A page the API serves is exactly a page the site would serve the same caller: both call
|
|
1061
|
+
`resolvePublishedPage()` (`server/services/pagePublishing.js`), and a test pins them together.
|
|
1062
|
+
|
|
1063
|
+
- Only `status: published` - drafts, scheduled and status-less pages are `404`.
|
|
1064
|
+
- Visibility follows `checkVisibility()`: a role-gated page is `403` to a caller who could not open it.
|
|
1065
|
+
- A page in a switched-off project is `404`.
|
|
1066
|
+
- Front matter **`api: false`** keeps a page off the API (`404`, not listed, not searched); it still
|
|
1067
|
+
shows on the site.
|
|
1068
|
+
|
|
1069
|
+
### Page list - query parameters
|
|
1070
|
+
|
|
1071
|
+
| Parameter | Default | Description |
|
|
1072
|
+
|-----------|---------|-------------|
|
|
1073
|
+
| `page` | `1` | Page number |
|
|
1074
|
+
| `limit` | `50` | Page size, capped at the site's `maxLimit` (default 200). `0` or nonsense means the default |
|
|
1075
|
+
| `prefix` | - | Only pages whose path starts with this, e.g. `/services` |
|
|
1076
|
+
| `tag` / `category` | - | Only pages with this tag / in this category |
|
|
1077
|
+
| `project` | - | Only pages in this project |
|
|
1078
|
+
| `sort` / `order` | site order | `title`, `updatedAt` or `createdAt`; `order=desc` reverses |
|
|
1079
|
+
|
|
1080
|
+
Summaries carry `urlPath`, `title`, `description`, `tags`, `category`, `layout`, `updatedAt`. A site can
|
|
1081
|
+
turn the list off (`listable: false`) and keep single pages, so callers must know a path.
|
|
1082
|
+
|
|
1083
|
+
### A page
|
|
1084
|
+
|
|
1085
|
+
```
|
|
1086
|
+
GET /api/content/v1/pages/about?format=html|markdown|both
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
```json
|
|
1090
|
+
{
|
|
1091
|
+
"urlPath": "/about", "title": "About", "description": "…", "tags": ["company"], "category": "general",
|
|
1092
|
+
"layout": "default", "createdAt": "…", "updatedAt": "…",
|
|
1093
|
+
"seo": {"title": "…", "description": "…", "image": "…"},
|
|
1094
|
+
"html": "<section>…</section>",
|
|
1095
|
+
"requires": {"domma": true, "components": ["tabs", "effects"]},
|
|
1096
|
+
"perViewer": false
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
- `html` is the page **body** (no navbar or footer), shortcodes already rendered. `markdown` is the
|
|
1101
|
+
source, shortcodes included. A format the site does not offer answers `400`; HTML is always offered.
|
|
1102
|
+
- `requires.components` names the Domma behaviour the HTML relies on - the markup `site.js` and
|
|
1103
|
+
`effects.js` initialise (tabs, accordion, carousel, countdown, tooltip, reveal, effects, forms…) plus
|
|
1104
|
+
any custom `[component]`s. Without Domma's CSS and JS those parts are inert.
|
|
1105
|
+
- Front matter is **allowlisted**: only the keys above appear. `seo` keeps `title`, `description`,
|
|
1106
|
+
`image`, `canonical`, `noindex`.
|
|
1107
|
+
- `perViewer: true` means the body was rendered for this caller (a row-scoped `[view]`) and must not be
|
|
1108
|
+
shared between users.
|
|
1109
|
+
- **Drafts:** in role mode only, `?preview=1` from a caller holding `pages.read` returns unpublished pages
|
|
1110
|
+
(`Cache-Control: no-store`). Ignored in public and token mode.
|
|
1111
|
+
|
|
1112
|
+
### Menus, site settings, search
|
|
1113
|
+
|
|
1114
|
+
- **Menus** - `?page=/about` resolves page-bound menus as they would be on that page. Items are
|
|
1115
|
+
allowlisted (`type`, `text`, `url`, `icon`, `target`, `size`, `badge`, `items`) and filtered for the
|
|
1116
|
+
caller; structural items (`separator`, `spacer`) are kept. The `admin-sidebar` and `overlay` slots, and
|
|
1117
|
+
any slot starting `admin`, are never served.
|
|
1118
|
+
- **Site** - `title`, `brand` (`text`, `logo`, `url`), `footer` (hidden links removed), `social`, `seo`
|
|
1119
|
+
(`defaultTitle`, `titleSeparator`, `defaultDescription`), `theme`. Never `smtp`, `url`, `tagline` or
|
|
1120
|
+
admin settings.
|
|
1121
|
+
- **Search** - the site's own search (`q` at least the site's minimum length, else `400`; `limit` up to its
|
|
1122
|
+
max results). `404` when search is switched off in System → Search.
|
|
1123
|
+
|
|
1124
|
+
### Access
|
|
1125
|
+
|
|
1126
|
+
`access` in `config/content-api.json`, one mode for the whole API:
|
|
1127
|
+
|
|
1128
|
+
| `access` | Who may call | Rendered as |
|
|
1129
|
+
|----------|--------------|-------------|
|
|
1130
|
+
| `public` | Anyone, no credentials | Anonymous visitor |
|
|
1131
|
+
| `token` (default) | A project API token (`Authorization: Bearer dcms_…`). A JWT is refused | Anonymous visitor |
|
|
1132
|
+
| a role name | A Bearer **access** JWT holding that role or a more senior one. A role the site does not have admits level 0 only. The site's `dm_session` cookie is never accepted | That user |
|
|
1133
|
+
|
|
1134
|
+
**Tokens and projects.** A token reads only the pages of its own project - single pages, the list and
|
|
1135
|
+
search results; a page in another project is `404`. Menus and site settings are site-wide and readable by
|
|
1136
|
+
any valid token. Scopes narrow a token with content scopes - `{"content": "pages"}`, written `@pages` in
|
|
1137
|
+
the token editor (`@pages`, `@menus`, `@site`, `@search`; read-only, no verbs). A token with no scopes
|
|
1138
|
+
covers everything in its project; a token whose scopes name only collections gets **no** content.
|
|
1139
|
+
|
|
1140
|
+
### Responses and caching
|
|
1141
|
+
|
|
1142
|
+
| Case | Status | `Cache-Control` |
|
|
1143
|
+
|------|--------|-----------------|
|
|
1144
|
+
| Public mode | `200` | `public, max-age=60` + weak `ETag` (page mtime) |
|
|
1145
|
+
| Token mode | `200` | `private, no-cache` |
|
|
1146
|
+
| Role mode, or a draft | `200` | `no-store` |
|
|
1147
|
+
| API or resource off, page missing / unpublished / disabled project / `api: false`, slot not served | `404` | `no-store` |
|
|
1148
|
+
| No or invalid credential, refresh token, JWT in token mode | `401` | `no-store` |
|
|
1149
|
+
| Role too junior, scope does not allow it, visibility refuses | `403` | `no-store` |
|
|
1150
|
+
| Format not offered, search query too short | `400` | `no-store` |
|
|
1151
|
+
|
|
1152
|
+
### Calling from another domain
|
|
1153
|
+
|
|
1154
|
+
A browser on another origin can call the API only when that origin is allowed by `cors.origin` in
|
|
1155
|
+
`config/server.json` (see `docs/configuration.md`). Token and role modes send an `Authorization` header,
|
|
1156
|
+
so the browser preflights the request; `@fastify/cors` answers it for allowed origins. Server-to-server
|
|
1157
|
+
callers are unaffected.
|
|
1158
|
+
|
|
1159
|
+
```bash
|
|
1160
|
+
curl 'https://example.com/api/content/v1/pages?prefix=/blog&limit=10'
|
|
1161
|
+
curl -H 'Authorization: Bearer dcms_0123…cdef' 'https://example.com/api/content/v1/pages/about?format=both'
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### Admin endpoints (`/api/content-api/*`)
|
|
1165
|
+
|
|
1166
|
+
| Method | Path | Auth | Description |
|
|
1167
|
+
|--------|------|------|-------------|
|
|
1168
|
+
| `GET` | `/api/content-api/settings` | `settings:read` | The normalised settings |
|
|
1169
|
+
| `PUT` | `/api/content-api/settings` | `settings:update` | Save; a role the site does not have is `400` |
|
|
1170
|
+
| `GET` | `/api/content-api/_count` | `settings:read` | Sidebar badge: pages served to an anonymous caller |
|
|
1171
|
+
| `GET` | `/api/content-api/preview?path=&format=&as=anon` | `settings:read` | What an anonymous caller would get, even while the API is off; `offered: false` flags a format the site does not offer |
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
## Custom endpoints (`/api/x/*`)
|
|
1176
|
+
|
|
1177
|
+
Named, curated REST endpoints over collection data, built in the admin (**Data → API Builder**)
|
|
1178
|
+
or by scaffolder recipes. A definition binds a clean URL to a fixed collection query - definitions
|
|
1179
|
+
are data, never code.
|
|
1180
|
+
|
|
1181
|
+
**URL anatomy:** `/api/x/<project><path>` - e.g. `/api/x/world-cup/fixtures-day/2026-06-11` for a
|
|
1182
|
+
definition in the `world-cup` project with `path: /fixtures-day/:date`.
|
|
1183
|
+
|
|
1184
|
+
**Definition:**
|
|
1185
|
+
|
|
1186
|
+
```json
|
|
1187
|
+
{
|
|
1188
|
+
"name": "Fixtures by day",
|
|
1189
|
+
"project": "world-cup",
|
|
1190
|
+
"path": "/fixtures-day/:date",
|
|
1191
|
+
"collection": "wc-schedule",
|
|
1192
|
+
"auth": "public",
|
|
1193
|
+
"mode": "list",
|
|
1194
|
+
"filter": { "date": "{{params.date}}" },
|
|
1195
|
+
"sort": "seq", "order": "asc", "limit": 0,
|
|
1196
|
+
"fields": ["seq", "date", "title", "stage"],
|
|
1197
|
+
"enabled": true
|
|
1198
|
+
}
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
- **`path`** - lowercase/hyphen segments or `:param` placeholders. Two endpoints in one project
|
|
1202
|
+
cannot share a path *shape* (`/a/:x` and `/a/:y` collide). Static segments beat `:param`
|
|
1203
|
+
matches (`/m/latest` wins over `/m/:day`).
|
|
1204
|
+
- **`auth`** - `"public"`, `"token"` (a project API token whose project matches the endpoint's),
|
|
1205
|
+
or a role name (JWT). Same strictness rules as the collections API.
|
|
1206
|
+
- **`mode`** - `"list"` returns the standard `{entries, total, page, limit}` payload;
|
|
1207
|
+
`"single"` returns the first matching entry, or 404.
|
|
1208
|
+
- **`filter`** - `{field_op: value}` clauses using the standard 12 filter operators. Values may
|
|
1209
|
+
embed `{{params.<name>}}` (from a `:name` path segment - missing at request time → 400) or
|
|
1210
|
+
`{{query.<name>}}` (optional - the clause is dropped when the query param is absent, so query
|
|
1211
|
+
placeholders act as refinements). Substituted values only ever feed the filter engine.
|
|
1212
|
+
- **`fields`** - read allowlist; empty = all fields.
|
|
1213
|
+
- An endpoint grants access to exactly its own query - independent of the underlying collection's
|
|
1214
|
+
`api` settings. System-managed collections cannot be exposed.
|
|
1215
|
+
|
|
1216
|
+
Public-auth responses are cached and invalidated automatically when the collection's entries or
|
|
1217
|
+
the endpoint definition change.
|
|
1218
|
+
|
|
1219
|
+
**Example:**
|
|
1220
|
+
|
|
1221
|
+
```bash
|
|
1222
|
+
curl https://example.com/api/x/world-cup/fixtures-day/Thu%2011%20Jun
|
|
1223
|
+
curl -H 'Authorization: Bearer dcms_…' https://example.com/api/x/world-cup/squad/:team
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Endpoint management (`/api/api-endpoints/*`)
|
|
1227
|
+
|
|
1228
|
+
| Method | Path | Auth | Description |
|
|
1229
|
+
|----------|--------------------------|------------------------|-------------|
|
|
1230
|
+
| `GET` | `/api/api-endpoints` | `api-endpoints:read` | List definitions visible to the caller (project scope applies) |
|
|
1231
|
+
| `GET` | `/api/api-endpoints/:id` | `api-endpoints:read` | Single definition |
|
|
1232
|
+
| `POST` | `/api/api-endpoints` | `api-endpoints:create` | Create (400 validation; 409 duplicate path shape) |
|
|
1233
|
+
| `PUT` | `/api/api-endpoints/:id` | `api-endpoints:update` | Update - the `project` binding is immutable |
|
|
1234
|
+
| `DELETE` | `/api/api-endpoints/:id` | `api-endpoints:delete` | Delete - external callers get 404 immediately |
|
|
1235
|
+
|
|
1236
|
+
Known v1 limits: GET-only; no `resolveRefs`; endpoints survive project deletion (delete them
|
|
1237
|
+
explicitly); a project with endpoints refuses deletion until they are removed.
|
|
1238
|
+
|
|
1239
|
+
---
|
|
1240
|
+
|
|
1241
|
+
## Views (`/api/views/*`)
|
|
1242
|
+
|
|
1243
|
+
Saved Views - reusable queries over collection data, rendered in content with `[view slug="…" /]`.
|
|
1244
|
+
Works on every storage adapter (native aggregation on MongoDB-backed collections, built-in pipeline
|
|
1245
|
+
evaluator on file-backed ones). Admin endpoints require the `views` permission.
|
|
1246
|
+
|
|
1247
|
+
| Method | Path | Description |
|
|
1248
|
+
|---|---|---|
|
|
1249
|
+
| GET | `/api/views` | List all view configs |
|
|
1250
|
+
| POST | `/api/views` | Create a view config |
|
|
1251
|
+
| GET | `/api/views/:slug` | Get a view config |
|
|
1252
|
+
| PUT | `/api/views/:slug` | Update a view config |
|
|
1253
|
+
| DELETE | `/api/views/:slug` | Delete a view config |
|
|
1254
|
+
| GET | `/api/views/:slug/execute?page&limit` | Execute a view (admin) → `{results, total, page, limit}` |
|
|
1255
|
+
| GET | `/api/views/collection/:collectionSlug` | List views targeting a collection |
|
|
1256
|
+
| GET | `/api/views/:slug/public?page&limit` | Execute a view publicly (role-checked per the view's `access` config) |
|
|
1257
|
+
|
|
1258
|
+
View config shape: `{id, slug, title, description, connection, pipeline: {source, stages: [{type, config}]},
|
|
1259
|
+
display: {mode, columns, pageSize, block}, access: {roles, public, rowLevel}, meta}`. Configs are stored as
|
|
1260
|
+
JSON files in `content/views/`. Allowed stage types: `$match`, `$lookup`, `$sort`, `$project`, `$unwind`,
|
|
1261
|
+
`$limit`, `$skip`, `$count`, `$addFields`, `$group`.
|
|
1262
|
+
|
|
1263
|
+
## Forms (`/api/forms/*`)
|
|
1264
|
+
|
|
1265
|
+
Form definitions are JSON files in `content/forms/`; every submission is stored as an entry in the form's
|
|
1266
|
+
target collection (its collection action's `slug` when enabled, else a collection named after the form).
|
|
1267
|
+
Admin routes use the `collections` permission family - there is no separate forms permission. The list, get
|
|
1268
|
+
and submissions routes also apply the caller's project scope.
|
|
1269
|
+
|
|
1270
|
+
| Method | Path | Auth | Description |
|
|
1271
|
+
|---|---|---|---|
|
|
1272
|
+
| `GET` | `/api/forms` | `collections:read` | List forms with `submissionCount`, `submissionsThisWeek`, `lastSubmissionAt` |
|
|
1273
|
+
| `POST` | `/api/forms` | `collections:create` | Create (`title` required; `201`, `409` slug taken). Also creates a same-slug collection if none exists |
|
|
1274
|
+
| `GET` | `/api/forms/:slug` | `collections:read` | Full definition, including `actions` |
|
|
1275
|
+
| `GET` | `/api/forms/:slug/public` | None | Definition without `actions` (what the `[form]` embed renders) |
|
|
1276
|
+
| `PUT` | `/api/forms/:slug` | `collections:update` | Shallow-merge the body over the definition; `slug`/`createdAt` fixed |
|
|
1277
|
+
| `DELETE` | `/api/forms/:slug` | `collections:delete` | Delete the definition (submissions stay in the collection) |
|
|
1278
|
+
| `POST` | `/api/forms/submit/:slug` | None (JWT optional) | Submit - see below |
|
|
1279
|
+
| `GET` | `/api/forms/:slug/submissions` | `collections:read` | This form's submissions, newest first |
|
|
1280
|
+
| `GET` | `/api/forms/:slug/submissions/export` | `collections:read` | CSV download |
|
|
1281
|
+
| `GET` | `/api/forms/:slug/submissions/export/json` | `collections:read` | JSON download |
|
|
1282
|
+
| `DELETE` | `/api/forms/:slug/submissions` | `collections:delete` | Delete all of this form's submissions |
|
|
1283
|
+
| `DELETE` | `/api/forms/:slug/submissions/:id` | `collections:delete` | Delete one |
|
|
1284
|
+
| `POST` | `/api/forms/:slug/submissions/:id/spam` | `collections:update` | `{ "spam": true \| false }` - flag or unflag |
|
|
1285
|
+
| `POST` | `/api/forms/test-email` | Admin (level 0-1) | Send a test email to `to` via the site SMTP settings |
|
|
1286
|
+
|
|
1287
|
+
Create and update responses carry `warnings: [...]` when the form and its target collection disagree
|
|
1288
|
+
(missing collection, required collection fields the form does not collect, form fields the collection lacks).
|
|
1289
|
+
|
|
1290
|
+
### POST `/api/forms/submit/:slug`
|
|
1291
|
+
|
|
1292
|
+
JSON (field names as keys) or `multipart/form-data` for file fields. A valid access token, when sent,
|
|
1293
|
+
becomes the entry's `meta.createdBy` and the `{{user.*}}` of any Action; otherwise the submission is anonymous.
|
|
1294
|
+
|
|
1295
|
+
**Response** `200`
|
|
1296
|
+
|
|
1297
|
+
```json
|
|
1298
|
+
{ "ok": true, "entryId": "uuid", "ended": false, "message": "Thank you for your submission.", "redirect": null }
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
Order of checks:
|
|
1302
|
+
|
|
1303
|
+
1. Honeypot (`settings.honeypot`, default on): a filled `_hp`, or `_t` (render time in ms) less than 2 s
|
|
1304
|
+
ago, gets an ordinary success and nothing is stored.
|
|
1305
|
+
2. Triggers are resolved server-side from the same engine as the browser: `block-submit` → `400`;
|
|
1306
|
+
`hide-fields` / `disable-fields` / `end-form` masking excuse fields (their values are dropped);
|
|
1307
|
+
`require-fields` makes fields required.
|
|
1308
|
+
3. Visible fields are checked for required/validation rules → `400` `{ "error": "Required fields missing: ..." }`.
|
|
1309
|
+
4. Rate limit per form and IP (`settings.rateLimitPerMinute`, default 3 a minute, in memory) → `429`.
|
|
1310
|
+
5. The entry is stored with `meta.source: "form:<slug>"` (a missing collection is provisioned from the form).
|
|
1311
|
+
An `end-form` with `record: false` stores nothing.
|
|
1312
|
+
6. Email, webhook (`POST {form, data}`), `settings.actionSlug` and trigger events (`run-action`, `notify`,
|
|
1313
|
+
`redirect`) run after the store; their failures are logged and notified, never returned.
|
|
1314
|
+
|
|
1315
|
+
File parts are checked against the field's `file.maxSize` (default 5 MB) and `file.accept` (MIME list,
|
|
1316
|
+
`image/*` wildcards), saved to the media library and stored as `{url, name, size, mime}`. There is no CAPTCHA.
|
|
1317
|
+
`{{entryId}}` in `settings.successRedirect` (or a trigger redirect, which wins) is replaced with the new id.
|
|
1318
|
+
|
|
1319
|
+
---
|
|
1320
|
+
|
|
1321
|
+
## Actions (`/api/actions/*`)
|
|
1322
|
+
|
|
1323
|
+
Actions are configured step lists run against a collection entry. They need MongoDB (Pro): without a
|
|
1324
|
+
connection the admin routes answer `503` (`/api/actions/_count` answers a zero badge instead). Configs are
|
|
1325
|
+
stored in `cms__actions`.
|
|
1326
|
+
|
|
1327
|
+
| Method | Path | Auth | Description |
|
|
1328
|
+
|---|---|---|---|
|
|
1329
|
+
| `GET` | `/api/actions` | `actions:read` | List configs (project scope applies) |
|
|
1330
|
+
| `POST` | `/api/actions` | `actions:create` | Create (`201`; `409` slug exists) |
|
|
1331
|
+
| `GET` | `/api/actions/collection/:collectionSlug` | `actions:read` | Configs targeting one collection |
|
|
1332
|
+
| `GET` | `/api/actions/:slug` | `actions:read` | One config |
|
|
1333
|
+
| `PUT` | `/api/actions/:slug` | `actions:update` | Update |
|
|
1334
|
+
| `DELETE` | `/api/actions/:slug` | `actions:delete` | Delete |
|
|
1335
|
+
| `POST` | `/api/actions/:slug/execute` | `actions:read` | Run on `{ "entryId": "..." }` |
|
|
1336
|
+
| `POST` | `/api/actions/:slug/check-access` | `actions:read` | `{ "entryIds": [...] }` → `{ "allowed": [...] }` after row-level rules |
|
|
1337
|
+
| `POST` | `/api/actions/:slug/public` | JWT + role in `access.roles` (or more senior) | Run from the public site; optional `scope: "mine"` refuses rows the user did not create (`403`) |
|
|
1338
|
+
| `GET` | `/api/actions/transitions?collection=&entryId=` | JWT optional | Transitions this user may run on the entry (`{ "transitions": [...] }`; none when anonymous) |
|
|
1339
|
+
|
|
1340
|
+
**Config shape**
|
|
1341
|
+
|
|
1342
|
+
```json
|
|
1343
|
+
{
|
|
1344
|
+
"title": "Approve", "slug": "approve-application", "collection": "applications",
|
|
1345
|
+
"trigger": { "type": "manual", "label": "Approve", "icon": "check-circle", "confirmMessage": "Approve?" },
|
|
1346
|
+
"transition": { "field": "status", "from": ["pending"], "to": "approved" },
|
|
1347
|
+
"steps": [
|
|
1348
|
+
{ "type": "updateField", "config": { "field": "status", "value": "approved" } },
|
|
1349
|
+
{ "type": "email", "config": { "to": "{{entry.data.email}}", "subject": "Approved" } }
|
|
1350
|
+
],
|
|
1351
|
+
"access": { "roles": ["admin"], "rowLevel": null }
|
|
1352
|
+
}
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
**Execute response** `{ "success": true, "stepsCompleted": 2, "results": [...] }`. Errors: `400` bad input,
|
|
1356
|
+
`403` row-level denial or `scope: "mine"` on someone else's row, `404` missing action or entry, `409` the
|
|
1357
|
+
entry's `transition.field` is not in `from`.
|
|
1358
|
+
|
|
1359
|
+
---
|
|
1360
|
+
|
|
1361
|
+
## Scaffold (`/api/scaffold/*`)
|
|
1362
|
+
|
|
1363
|
+
Builds a working system from a bundled recipe (`server/services/recipes/*.json`) in one call. All routes need
|
|
1364
|
+
`collections:create` - and nothing else, although a recipe can also create roles, users, menus, API tokens and
|
|
1365
|
+
endpoints. Recipe format: `docs/scaffolding.md`.
|
|
1366
|
+
|
|
1367
|
+
| Method | Path | Auth | Description |
|
|
1368
|
+
|---|---|---|---|
|
|
1369
|
+
| `GET` | `/api/scaffold/recipes` | `collections:create` | `{ "recipes": [{slug, name, description, icon, options}] }` |
|
|
1370
|
+
| `GET` | `/api/scaffold/recipes/:slug` | `collections:create` | The full recipe, unresolved (`404` unknown) |
|
|
1371
|
+
| `POST` | `/api/scaffold/apply` | `collections:create` | Apply: `{ "recipe": "contact-list", "options": { "collectionSlug": "leads" } }` |
|
|
1372
|
+
|
|
1373
|
+
Option values sent in `options` are slugified. Apply creates, in order: the project (from the `namespace`
|
|
1374
|
+
option, if the recipe has a `project` block), then pre-flights - an existing collection, form, Action or menu
|
|
1375
|
+
answers `409` with `conflicts: [...]` - then roles, menus, collection, form, users, Actions (skipped with a
|
|
1376
|
+
warning without MongoDB), API tokens and API endpoints.
|
|
1377
|
+
|
|
1378
|
+
**Response** `200`
|
|
1379
|
+
|
|
1380
|
+
```json
|
|
1381
|
+
{
|
|
1382
|
+
"created": { "collection": "leads", "form": "contact-quick-add", "actions": ["contact-followup"],
|
|
1383
|
+
"roles": [], "users": [], "menus": [], "apiTokens": [], "apiEndpoints": [] },
|
|
1384
|
+
"skipped": [],
|
|
1385
|
+
"warnings": [],
|
|
1386
|
+
"snippet": "[form name=\"contact-quick-add\" /]\n\n..."
|
|
1387
|
+
}
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
Apply is not transactional: after pre-flight, anything that fails becomes a `warnings` entry and the rest
|
|
1391
|
+
continues. `created.apiTokens[].token` is the only time a generated token's plaintext is shown; re-applying
|
|
1392
|
+
never re-issues a token or overwrites an endpoint with the same path shape.
|
|
1393
|
+
|
|
1394
|
+
---
|
|
1395
|
+
|
|
1396
|
+
## Plugins (`/api/plugins/*`)
|
|
1397
|
+
|
|
1398
|
+
### GET `/api/plugins`
|
|
1399
|
+
|
|
1400
|
+
List all discovered plugins with their enabled state and settings.
|
|
1401
|
+
|
|
1402
|
+
**Auth required:** Yes (`plugins:read`)
|
|
1403
|
+
|
|
1404
|
+
**Response** `200` - array of plugin objects
|
|
1405
|
+
|
|
1406
|
+
```json
|
|
1407
|
+
[
|
|
1408
|
+
{
|
|
1409
|
+
"name": "analytics",
|
|
1410
|
+
"displayName": "Analytics",
|
|
1411
|
+
"version": "1.0.0",
|
|
1412
|
+
"description": "...",
|
|
1413
|
+
"author": "...",
|
|
1414
|
+
"date": "2026-03-01",
|
|
1415
|
+
"icon": "chart-bar",
|
|
1416
|
+
"enabled": true,
|
|
1417
|
+
"settings": {}
|
|
1418
|
+
}
|
|
1419
|
+
]
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
---
|
|
1423
|
+
|
|
1424
|
+
### PUT `/api/plugins/:name`
|
|
1425
|
+
|
|
1426
|
+
Enable/disable a plugin or update its settings.
|
|
1427
|
+
|
|
1428
|
+
**Auth required:** Yes (`plugins:update`)
|
|
1429
|
+
|
|
1430
|
+
**Request body**
|
|
1431
|
+
|
|
1432
|
+
```json
|
|
1433
|
+
{ "enabled": true, "settings": { "myOption": "value" } }
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
**Response** `200`
|
|
1437
|
+
|
|
1438
|
+
```json
|
|
1439
|
+
{ "success": true }
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
---
|
|
1443
|
+
|
|
1444
|
+
### GET `/api/plugins/admin-config`
|
|
1445
|
+
|
|
1446
|
+
Return the merged admin sidebar items, routes, and view registrations for all enabled plugins. Used by the admin SPA on startup.
|
|
1447
|
+
|
|
1448
|
+
**Auth required:** Yes
|
|
1449
|
+
|
|
1450
|
+
**Response** `200`
|
|
1451
|
+
|
|
1452
|
+
```json
|
|
1453
|
+
{
|
|
1454
|
+
"sidebar": [...],
|
|
1455
|
+
"routes": [...],
|
|
1456
|
+
"views": { ... }
|
|
1457
|
+
}
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
---
|
|
1461
|
+
|
|
1462
|
+
## Projects (`/api/projects/*`)
|
|
1463
|
+
|
|
1464
|
+
Projects group related artefacts (Pages, Collections, Forms, Actions, Menus, Blocks, Views, Roles, Users) under a named slug. See `docs/configuration.md` → Projects for the data model and `meta.project` tag.
|
|
1465
|
+
|
|
1466
|
+
All endpoints require the relevant `projects` permission. List/get results are filtered by the requesting user's `projects: []` access scope (super-admins bypass).
|
|
1467
|
+
|
|
1468
|
+
### GET `/api/projects`
|
|
1469
|
+
|
|
1470
|
+
List projects the user can see (scoped by `user.projects`).
|
|
1471
|
+
|
|
1472
|
+
**Auth required:** Yes (`projects:read`)
|
|
1473
|
+
|
|
1474
|
+
**Response** `200` - array of project records
|
|
1475
|
+
|
|
1476
|
+
```json
|
|
1477
|
+
[
|
|
1478
|
+
{
|
|
1479
|
+
"slug": "members",
|
|
1480
|
+
"name": "Members Area",
|
|
1481
|
+
"description": "Curated members-only zone.",
|
|
1482
|
+
"icon": "users",
|
|
1483
|
+
"rootUrl": "/members",
|
|
1484
|
+
"sortOrder": 10,
|
|
1485
|
+
"createdAt": "2026-05-25T10:00:00.000Z",
|
|
1486
|
+
"updatedAt": "2026-05-25T10:00:00.000Z"
|
|
1487
|
+
}
|
|
1488
|
+
]
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
---
|
|
1492
|
+
|
|
1493
|
+
### GET `/api/projects/:slug`
|
|
1494
|
+
|
|
1495
|
+
Return a single project record.
|
|
1496
|
+
|
|
1497
|
+
**Auth required:** Yes (`projects:read`)
|
|
1498
|
+
|
|
1499
|
+
**Response** `200` - full project record (404 if not found)
|
|
1500
|
+
|
|
1501
|
+
---
|
|
1502
|
+
|
|
1503
|
+
### POST `/api/projects`
|
|
1504
|
+
|
|
1505
|
+
Create a new project.
|
|
1506
|
+
|
|
1507
|
+
**Auth required:** Yes (`projects:create`)
|
|
1508
|
+
|
|
1509
|
+
**Request body**
|
|
1510
|
+
|
|
1511
|
+
```json
|
|
1512
|
+
{
|
|
1513
|
+
"slug": "members",
|
|
1514
|
+
"name": "Members Area",
|
|
1515
|
+
"description": "Members-only zone",
|
|
1516
|
+
"icon": "users",
|
|
1517
|
+
"rootUrl": "/members",
|
|
1518
|
+
"sortOrder": 10
|
|
1519
|
+
}
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
**Response** `201` - created project record. `409` on slug collision.
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
### PUT `/api/projects/:slug`
|
|
1527
|
+
|
|
1528
|
+
Update a project. The slug is immutable.
|
|
1529
|
+
|
|
1530
|
+
**Auth required:** Yes (`projects:update`)
|
|
1531
|
+
|
|
1532
|
+
**Request body** - any of `name`, `description`, `icon`, `rootUrl`, `sortOrder`
|
|
1533
|
+
|
|
1534
|
+
**Response** `200` - updated project record
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
### DELETE `/api/projects/:slug`
|
|
1539
|
+
|
|
1540
|
+
Delete a project. Refuses with `409` if any artefacts still carry `meta.project: <slug>`.
|
|
1541
|
+
|
|
1542
|
+
**Auth required:** Yes (`projects:delete`)
|
|
1543
|
+
|
|
1544
|
+
**Response** `200`
|
|
1545
|
+
|
|
1546
|
+
```json
|
|
1547
|
+
{ "success": true }
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
**Response** `409` - conflict body includes a per-type artefact count breakdown:
|
|
1551
|
+
|
|
1552
|
+
```json
|
|
1553
|
+
{
|
|
1554
|
+
"error": "Cannot delete project - tagged artefacts exist",
|
|
1555
|
+
"artefacts": {
|
|
1556
|
+
"pages": 3,
|
|
1557
|
+
"collections": 2,
|
|
1558
|
+
"forms": 1,
|
|
1559
|
+
"actions": 3,
|
|
1560
|
+
"menus": 1,
|
|
1561
|
+
"blocks": 0,
|
|
1562
|
+
"views": 0,
|
|
1563
|
+
"roles": 1,
|
|
1564
|
+
"users": 2
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
Clear the artefacts first (manually, or via `POST /api/projects/:slug/untag-all`).
|
|
1570
|
+
|
|
1571
|
+
---
|
|
1572
|
+
|
|
1573
|
+
### GET `/api/projects/:slug/artefacts`
|
|
1574
|
+
|
|
1575
|
+
Return all artefacts tagged with this project, grouped by type. Used by the sidebar and the project detail view.
|
|
1576
|
+
|
|
1577
|
+
**Auth required:** Yes (`projects:read`)
|
|
1578
|
+
|
|
1579
|
+
**Response** `200` - nine keys, each an array (empty arrays included):
|
|
1580
|
+
|
|
1581
|
+
```json
|
|
1582
|
+
{
|
|
1583
|
+
"pages": [ ... ],
|
|
1584
|
+
"collections": [ ... ],
|
|
1585
|
+
"forms": [ ... ],
|
|
1586
|
+
"actions": [ ... ],
|
|
1587
|
+
"menus": [ ... ],
|
|
1588
|
+
"blocks": [ ... ],
|
|
1589
|
+
"views": [ ... ],
|
|
1590
|
+
"roles": [ ... ],
|
|
1591
|
+
"users": [ ... ]
|
|
1592
|
+
}
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
---
|
|
1596
|
+
|
|
1597
|
+
### POST `/api/projects/:slug/untag-all`
|
|
1598
|
+
|
|
1599
|
+
Escape hatch - sets `meta.project: null` on every artefact currently tagged with the slug. Used before deleting a project when the admin doesn't want to retag artefacts individually.
|
|
1600
|
+
|
|
1601
|
+
**Auth required:** Yes (`projects:delete`)
|
|
1602
|
+
|
|
1603
|
+
**Response** `200`
|
|
1604
|
+
|
|
1605
|
+
```json
|
|
1606
|
+
{ "success": true, "untagged": { "pages": 3, "collections": 2, "forms": 1, "actions": 3, "menus": 1, "blocks": 0, "views": 0, "roles": 1, "users": 2 } }
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
---
|
|
1610
|
+
|
|
1611
|
+
## Layouts (`/api/layouts`)
|
|
1612
|
+
|
|
1613
|
+
### GET `/api/layouts`
|
|
1614
|
+
|
|
1615
|
+
Return all layout presets.
|
|
1616
|
+
|
|
1617
|
+
**Auth required:** Yes (`layouts:read`)
|
|
1618
|
+
|
|
1619
|
+
**Response** `200` - presets object (see `config/presets.json` in [Configuration](configuration.md))
|
|
1620
|
+
|
|
1621
|
+
---
|
|
1622
|
+
|
|
1623
|
+
### PUT `/api/layouts`
|
|
1624
|
+
|
|
1625
|
+
Replace layout presets. Each preset must be an object with at least a `label` field.
|
|
1626
|
+
|
|
1627
|
+
**Auth required:** Yes (`layouts:update`)
|
|
1628
|
+
|
|
1629
|
+
**Response** `200`
|
|
1630
|
+
|
|
1631
|
+
```json
|
|
1632
|
+
{ "success": true }
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
---
|
|
1636
|
+
|
|
1637
|
+
### GET `/api/layouts/options`
|
|
1638
|
+
|
|
1639
|
+
Return layout display options (e.g. spacer size).
|
|
1640
|
+
|
|
1641
|
+
**Auth required:** Yes (`layouts:read`)
|
|
1642
|
+
|
|
1643
|
+
**Response** `200`
|
|
1644
|
+
|
|
1645
|
+
```json
|
|
1646
|
+
{ "spacerSize": 40 }
|
|
1647
|
+
```
|
|
1648
|
+
|
|
1649
|
+
---
|
|
1650
|
+
|
|
1651
|
+
### PUT `/api/layouts/options`
|
|
1652
|
+
|
|
1653
|
+
Update layout display options.
|
|
1654
|
+
|
|
1655
|
+
**Auth required:** Yes (`layouts:update`)
|
|
1656
|
+
|
|
1657
|
+
**Request body**
|
|
1658
|
+
|
|
1659
|
+
```json
|
|
1660
|
+
{ "spacerSize": 32 }
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
**Response** `200`
|
|
1664
|
+
|
|
1665
|
+
```json
|
|
1666
|
+
{ "success": true }
|
|
1667
|
+
```
|