domma-cms 0.23.0 → 0.25.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/CLAUDE.md +14 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/templates/api-endpoint-editor.html +120 -0
- package/admin/js/templates/api-endpoints.html +13 -0
- package/admin/js/templates/api-tokens.html +13 -0
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/form-submissions.html +30 -30
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/views/api-endpoint-editor.js +1 -0
- package/admin/js/views/api-endpoints.js +7 -0
- package/admin/js/views/api-tokens.js +8 -0
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/project-detail.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/bin/lib/config-merge.js +44 -44
- package/bin/update.js +547 -547
- package/config/menus/admin-sidebar.json +13 -1
- package/package.json +1 -1
- package/server/middleware/auth.js +253 -253
- package/server/routes/api/api-endpoints.js +96 -0
- package/server/routes/api/api-tokens.js +83 -0
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/collections.js +114 -17
- package/server/routes/api/endpoints-public.js +88 -0
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/settings.js +141 -141
- package/server/routes/public.js +202 -202
- package/server/server.js +16 -1
- package/server/services/apiEndpoints.js +402 -0
- package/server/services/apiTokens.js +273 -0
- package/server/services/email.js +167 -167
- package/server/services/permissionRegistry.js +26 -0
- package/server/services/presetCollections.js +54 -0
- package/server/services/projects.js +18 -2
- package/server/services/roles.js +16 -0
- package/server/services/scaffolder.js +54 -1
- package/server/services/sidebar-migration.js +45 -0
- package/server/services/userProfiles.js +199 -199
- package/server/services/users.js +302 -302
- package/config/connections.json.bak +0 -9
package/CLAUDE.md
CHANGED
|
@@ -151,6 +151,8 @@ Add custom public-site JS to `public/js/site.js` or new files loaded from `publi
|
|
|
151
151
|
| `permissionRegistry.js` | Central permission map; route guards read this at request time |
|
|
152
152
|
| `rowAccess.js` | Row-level access control for collections |
|
|
153
153
|
| `collections.js` | Collection entry CRUD; delegates I/O to storage adapter |
|
|
154
|
+
| `apiTokens.js` | Project-scoped API tokens (`api-tokens` preset) for the external `/api/v1` surface; SHA-256 hash stored, plaintext shown once |
|
|
155
|
+
| `apiEndpoints.js` | Custom endpoint definitions (`api-endpoints` preset) — registry/matcher/executor for `/api/x/<project><path>` |
|
|
154
156
|
| `adapterRegistry.js` | `getAdapter(slug)` — resolves + caches adapter per collection; `invalidate(slug)` on schema change |
|
|
155
157
|
| `adapters/FileAdapter.js` | Default adapter — plain JSON files |
|
|
156
158
|
| `adapters/MongoAdapter.js` | Optional Pro adapter — native MongoDB driver; `cms_` prefix |
|
|
@@ -284,3 +286,15 @@ The single source of truth is `getProjectForPage(urlPath, frontmatterProject)` i
|
|
|
284
286
|
**Scaffolder integration.** A recipe with a `project: {...}` block creates the project record on apply (idempotent — re-apply doesn't clobber edits) using `tokens.namespace` as the slug, then stamps `meta.project: <namespace>` on every produced artefact. Seeded users also get `projects: ['<namespace>']` so they can log in and immediately see their project.
|
|
285
287
|
|
|
286
288
|
**Filtering on list endpoints.** Every artefact list path runs `list.filter(item => canSeeArtefact(user, item))`. Get/write endpoints return 403 when the artefact's project isn't in the user's scope. The Projects section in the admin sidebar always renders — `listProjectsForUser(user)` always includes core, so every site shows at least the Core project.
|
|
289
|
+
|
|
290
|
+
## External API & API tokens
|
|
291
|
+
|
|
292
|
+
Collections are externally consumable at `/api/v1/:slug[/:id]` — a stable alias of `/api/collections/:slug/public[...]` (same handlers). Per-verb access via `schema.api.<verb>.access`: `'public'`, a role name (JWT), or `'token'`. Token mode is **strict** — only a valid `dcms_<64 hex>` Bearer token is accepted (never a JWT), and a token never satisfies a role mode. `schema.api.read.fields` optionally allowlists which `data` fields public/external reads return (admin endpoints unaffected).
|
|
293
|
+
|
|
294
|
+
Tokens live in the `api-tokens` **preset collection** (always file-based and undeletable — both derived automatically from `PRESETS`). Each token is bound to one project (`data.project`, immutable) and only works on collections whose `resolveArtefactProject(schema)` matches; optional `scopes: [{collection, verbs[]}]` narrow it further. Service: `server/services/apiTokens.js` — SHA-256 hash stored, plaintext returned once by `createToken()`, in-memory validate cache invalidated via the `collection:entry*` hooks. Admin routes: `server/routes/api/api-tokens.js` (projects.js DI pattern). Admin UI: System → API Tokens (`admin/js/views/api-tokens.js`). Scaffolder recipes may declare `apiTokens: [{name, scopes?}]` — generated at apply time under the recipe's project, idempotent on re-apply, plaintext surfaced once in `created.apiTokens`.
|
|
295
|
+
|
|
296
|
+
Permission family `api-tokens.{read,create,update,delete}`: existing roles are **not back-filled** (same gotcha as Menus/Projects), but `roles.js seed()` self-heals the level-0 root role with any missing registry resources at boot, so super-admins always see new families. `ensureSidebarItem()` in `sidebar-migration.js` appends the System → API Tokens entry on existing installs (no-op when the item's URL already exists anywhere in the persisted menu). Note: `admin/js/views/collection-editor.js` was de-minified (identifiers still mangled) so the API-access tab could gain the Token mode + read-fields input.
|
|
297
|
+
|
|
298
|
+
## Custom API endpoints (API Builder)
|
|
299
|
+
|
|
300
|
+
Named endpoints over collection data at `/api/x/<project><path>` — e.g. `/api/x/world-cup/fixtures-day/:date`. Definitions are **data, never code**: entries in the `api-endpoints` preset collection binding a path (with `:param` segments) to a collection query — filter clauses whose values may embed `{{params.x}}` (required → 400 when missing) or `{{query.x}}` (optional → clause dropped), sort/limit, a read field allowlist, `mode: list|single`, and `auth: public|token|<role>`. One catch-all route (`server/routes/api/endpoints-public.js`, `fastify.get('/x/*')`) matches at runtime via the compiled registry in `server/services/apiEndpoints.js` (static segments beat `:param`; registry invalidated via the `collection:entry*` hooks + own mutations). Auth reuses `checkPublicAccess` from `routes/api/collections.js` (now exported) with a synthesized schema, so token project binding/scopes and the field allowlist are the same battle-tested path as `/api/v1`. Public-auth responses are cached with tags `collection:<data collection>` + `collection:api-endpoints` — both invalidated automatically by the service layer on entry/definition writes. Endpoints are the 10th project artefact type (`apis` in `getArtefactsForProject`; skipped by untag-all since the project IS the URL namespace; a project with endpoints refuses deletion). Admin: **Data → API Builder** (`admin/js/views/api-endpoints.js` list, `api-endpoint-editor.js` builder with try-it console — raw `fetch`, tests the SAVED definition only). Validation refuses system-managed collections (would leak `api-tokens` hashes), duplicate path shapes per project (`/a/:x` ≡ `/a/:y` → 409), and unknown roles/ops. Scaffolder block: `apiEndpoints: [{path, collection, filter, ...}]`, idempotent by path shape. Permission family `api-endpoints.*` — same back-fill gotcha as api-tokens.
|
package/admin/js/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const d="/api";function a(){return S.get("auth_token")}function m(){return S.get("auth_refresh_token")}function l(e){S.set("auth_token",e)}function h(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function u(){const e=m();if(!e)throw new Error("No refresh token");const o=await fetch(`${d}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw h(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return l(s),s}async function t(e,o={}){let s=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${d}${e}`,{...o,headers:r(s)});if(n.status===401&&m())try{s=await u(),n=await fetch(`${d}${e}`,{...o,headers:r(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function y(e,o){const s=a(),r=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${d}${e}`,{method:"POST",headers:r,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>y("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o)=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),s=await fetch(`${d}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const c=await s.json().catch(()=>({}));throw new Error(c.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${d}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Block already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),s=await fetch(`${d}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const c=await s.json().catch(()=>({}));throw new Error(c.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${d}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Component already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&l(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=m();e&&fetch(`${d}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),h(),R.navigate("/login")}export{t as apiRequest,u as refreshAccessToken,a as getToken,h as clearAuth};
|
|
1
|
+
const a="/api";function d(){return S.get("auth_token")}function c(){return S.get("auth_refresh_token")}function l(e){S.set("auth_token",e)}function h(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function u(){const e=c();if(!e)throw new Error("No refresh token");const o=await fetch(`${a}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw h(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return l(s),s}async function t(e,o={}){let s=d();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${a}${e}`,{...o,headers:r(s)});if(n.status===401&&c())try{s=await u(),n=await fetch(`${a}${e}`,{...o,headers:r(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function p(e,o){const s=d(),r=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${a}${e}`,{method:"POST",headers:r,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>p("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o)=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),s=await fetch(`${a}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const m=await s.json().catch(()=>({}));throw new Error(m.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${a}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Block already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),s=await fetch(`${a}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const m=await s.json().catch(()=>({}));throw new Error(m.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${a}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Component already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!d()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&l(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=c();e&&fetch(`${a}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),h(),R.navigate("/login")}export{t as apiRequest,u as refreshAccessToken,d as getToken,h as clearAuth};
|
package/admin/js/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{views as
|
|
2
|
-
<span id="topbar-user-name" class="topbar-user-name">${
|
|
3
|
-
<span class="topbar-role-badge topbar-role-badge--${
|
|
1
|
+
import{views as B}from"./views/index.js";import{api as l,getUser as c,isAuthenticated as m,logout as W}from"./api.js";import{installHttpInterceptor as x}from"./http-interceptor.js";$(()=>{x(),(async()=>{try{const t=m()?await l.settings.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();const k=["jb-company","jb-agent","jb-candidate"],A=["/job-board","/my-profile"];function h(t){return t&&k.includes(t.role)}function w(t){return A.some(e=>t===e||t.startsWith(e+"/"))}R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!m()){R.navigate("/login");return}if(h(c())&&!w(t.path)){R.navigate("/job-board");return}o()});async function v(){try{return(await l.get("/auth/permissions")).permissions||[]}catch{return[]}}async function b(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&n&&(clearInterval(n),n=null)});const N=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projectEditor",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projectEditor",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if(h(c())&&!w(t.path)){R.navigate("/job-board");return}if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await j();const o=await v();b(o);try{const i=await l.system.notifications.list().catch(()=>[]),s=(Array.isArray(i)?i:[]).filter(a=>a.unread&&["warning","critical"].includes(a.data?.severity)).slice(0,3);for(const a of s){const r=p(a.data?.title||""),d=p((a.data?.body||"").slice(0,120));E.toast(`${r} \u2014 ${d}`,{type:a.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}y()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const L=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function I(t){return/^\/(pages|menus|collections|forms|actions|views|blocks|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!I(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let s=0;const a=60,r=1500,d=()=>{s+=a;for(const T of L){const u=document.querySelector(T);if(!u)continue;if(Array.from(u.options||[]).find(_=>_.value===i)){u.value=i,u.dispatchEvent(new Event("change",{bubbles:!0}));return}}s<r&&setTimeout(d,a)};setTimeout(d,a)});const C="cms_card_states",f=S.get(C)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&f[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(f[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(C,f))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const D={...B},g=[...N];async function j(){if(m())try{const t=await l.plugins.adminConfig();t.routes?.length&&g.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);D[e]=i[o.exportName]}catch{}for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function y(){const t=c();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
|
|
2
|
+
<span id="topbar-user-name" class="topbar-user-name">${p(t.name)}</span>
|
|
3
|
+
<span class="topbar-role-badge topbar-role-badge--${p(t.role)}">${o}</span>
|
|
4
4
|
`),$("#topbar-actions").html(`
|
|
5
5
|
<a href="#/system/notifications" id="topbar-bell" class="topbar-action-link" data-tooltip="Notifications" data-tooltip-placement="bottom" hidden>
|
|
6
6
|
<span data-icon="bell"></span>
|
|
@@ -18,4 +18,4 @@ import{views as W}from"./views/index.js";import{api as r,getUser as l,isAuthenti
|
|
|
18
18
|
<span data-icon="log-out"></span>
|
|
19
19
|
<span>Sign out</span>
|
|
20
20
|
</a>
|
|
21
|
-
`),$("#topbar-logout-btn").on("click",i=>{i.preventDefault(),
|
|
21
|
+
`),$("#topbar-logout-btn").on("click",i=>{i.preventDefault(),W()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),P(),n||(n=setInterval(P,6e4))}function p(t){return String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}let n=null;async function P(){if(m())try{const{count:t}=await l.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text("")}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else{const i=(window.location.hash||"#/").slice(1)||"/";if(h(c())&&!w(i)&&(window.location.hash="#/job-board"),await j(),c()){const a=await v();await b(a),y()}}R.init({container:"#view-container",routes:g,views:D,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,s){const a=s.indexOf("?");if(a!==-1&&(s=s.slice(0,a)),i.endsWith("/*")){const r=i.slice(0,-2);return s.startsWith(r+"/")?{}:null}return t(i,s)};const e=(window.location.hash||"#/").slice(1)||"/";g.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function openCrudTutorial(t=null){const e=E.slideover({title:"CRUD shortcut",size:"md",position:"right"}),
|
|
1
|
+
export function openCrudTutorial(t=null){const e=E.slideover({title:"CRUD shortcut",size:"md",position:"right"}),c=document.createElement("div");c.className="crud-tutorial",c.style.cssText="padding:1rem;display:flex;flex-direction:column;gap:1.25rem;",c.appendChild(k()),c.appendChild(v(e)),c.appendChild(y(e,t)),e.setContent(c),e.open(),window.Domma?.icons?.scan&&Domma.icons.scan(c)}function v(t){const e=document.createElement("a");e.href="#/tutorials",e.className="cb-tutorial-link",e.addEventListener("click",()=>{try{t.close()}catch{}});const c=document.createElement("span");c.setAttribute("data-icon","book-open"),c.className="cb-icon",e.appendChild(c);const o=document.createElement("span"),a=document.createElement("strong");a.textContent="Open the full tutorial \u2192 ",o.appendChild(a),o.appendChild(document.createTextNode('"Building a CRUD App" in Tutorials')),e.appendChild(o);const n=document.createElement("span");return n.className="cb-arrow",n.textContent="\u203A",e.appendChild(n),e}function k(){const t=document.createElement("div");t.className="cb-intro";const e=document.createElement("p");return e.textContent="Drop a working Collection + Form + Actions onto this page in one click. Pick a starter template below \u2014 the slugs are editable, and we'll insert the matching shortcode at your cursor when it's done.",t.appendChild(e),t}function w(t){const e=document.createElement("div");e.className="cb-code";const c=document.createElement("pre");c.textContent=t,e.appendChild(c);const o=document.createElement("button");o.type="button",o.title="Copy",o.className="cb-copy";const a=document.createElement("span");return a.setAttribute("data-icon","copy"),o.appendChild(a),o.addEventListener("click",()=>{const n=document.createElement("textarea");n.value=t,n.style.cssText="position:fixed;opacity:0;",document.body.appendChild(n),n.select(),document.execCommand("copy"),n.remove(),E.toast("Copied!",{type:"success",duration:1200})}),e.appendChild(o),e}export function mountScaffolder(t,e=null,c=null){t.appendChild(y(e,c)),window.Domma?.icons?.scan&&Domma.icons.scan(t)}function y(t,e){const c=document.createElement("section");c.className="cb-scaffolder";const o=document.createElement("div");o.className="cb-scaffolder-head";const a=document.createElement("span");a.setAttribute("data-icon","sparkles"),a.className="cb-icon",o.appendChild(a);const n=document.createElement("h2");n.textContent="Or skip ahead \u2014 scaffold a working system in one click",o.appendChild(n),c.appendChild(o);const l=document.createElement("p");l.className="cb-scaffolder-blurb",l.textContent="Pick a starter template; we'll create the Collection, Form, and Actions for you. Rename the slugs if you want, then hit Apply. You can also paste the ready-made shortcode into this page.",c.appendChild(l);const d=document.createElement("div");return d.className="cb-recipe-list",d.textContent="Loading templates\u2026",c.appendChild(d),fetch("/api/scaffold/recipes",{headers:g()}).then(m=>m.json()).then(({recipes:m})=>{if(d.textContent="",!m?.length){d.textContent="No bundled templates available.";return}for(const h of m)d.appendChild(A(h,c,t,e))}).catch(()=>{d.textContent="Couldn't load templates \u2014 admin permissions required."}),c}function A(t,e,c,o){const a=document.createElement("button");a.type="button",a.className="cb-recipe-card";const n=document.createElement("span");n.setAttribute("data-icon",t.icon||"box"),n.className="cb-icon",a.appendChild(n);const l=document.createElement("div");l.className="cb-recipe-meta";const d=document.createElement("div");d.className="cb-recipe-name",d.textContent=t.name;const m=document.createElement("div");return m.className="cb-recipe-desc",m.textContent=t.description||"",l.appendChild(d),l.appendChild(m),a.appendChild(l),a.addEventListener("click",()=>D(t,e,c,o)),a}function D(t,e,c,o){const a=document.createElement("div");a.className="cb-apply-panel";const n=document.createElement("div");n.className="cb-apply-head";const l=document.createElement("span");l.setAttribute("data-icon",t.icon||"box"),l.className="cb-icon",n.appendChild(l);const d=document.createElement("h3");d.textContent=t.name,n.appendChild(d),a.appendChild(n);const m=document.createElement("p");m.className="cb-apply-desc",m.textContent=t.description||"",a.appendChild(m);const h={};for(const s of t.options||[]){const u=document.createElement("div");u.className="cb-apply-field";const C=document.createElement("label");C.textContent=s.label||s.name;const f=document.createElement("input");if(f.type="text",f.value=s.default||"",u.appendChild(C),u.appendChild(f),s.hint){const x=document.createElement("div");x.className="cb-apply-hint",x.textContent=s.hint,u.appendChild(x)}a.appendChild(u),h[s.name]=f}const p=document.createElement("div");p.className="cb-apply-status",a.appendChild(p);const b=document.createElement("div");b.className="cb-apply-buttons";const i=document.createElement("button");i.type="button",i.className="btn btn-outline",i.textContent="Cancel",i.addEventListener("click",()=>{const s=y(c,o);a.replaceWith(s),window.Domma?.icons?.scan&&Domma.icons.scan(s)});const r=document.createElement("button");r.type="button",r.className="btn btn-primary",r.textContent="Apply template",r.addEventListener("click",async()=>{r.disabled=!0,i.disabled=!0,p.textContent="Creating collection, form, and actions\u2026",p.classList.remove("cb-error");const s={};for(const[u,C]of Object.entries(h))s[u]=C.value.trim();try{const u=await fetch("/api/scaffold/apply",{method:"POST",headers:{"Content-Type":"application/json",...g()},body:JSON.stringify({recipe:t.slug,options:s})}),C=await u.json();if(!u.ok){p.classList.add("cb-error"),Array.isArray(C.conflicts)?p.textContent=`Conflicts: ${C.conflicts.join("; ")}`:p.textContent=C.error||"Apply failed.",r.disabled=!1,i.disabled=!1;return}const f=L(t,C,c,o,e);a.replaceWith(f),window.Domma?.icons?.scan&&Domma.icons.scan(f)}catch(u){p.classList.add("cb-error"),p.textContent=u.message||"Network error.",r.disabled=!1,i.disabled=!1}}),b.appendChild(i),b.appendChild(r),a.appendChild(b),e.replaceWith(a),window.Domma?.icons?.scan&&Domma.icons.scan(a)}function L(t,e,c,o,a){const n=document.createElement("div");n.className="cb-success-panel";const l=document.createElement("div");l.className="cb-apply-head";const d=document.createElement("span");d.setAttribute("data-icon","check-circle"),d.className="cb-icon",l.appendChild(d);const m=document.createElement("h3");m.textContent=`${t.name} \u2014 scaffolded`,l.appendChild(m),n.appendChild(l);const h=document.createElement("ul");if(h.className="cb-success-summary",e.created.collection&&h.appendChild(N(`Collection: ${e.created.collection}`)),e.created.form&&h.appendChild(N(`Form: ${e.created.form}`)),e.created.actions?.length&&h.appendChild(N(`Actions: ${e.created.actions.join(", ")}`)),e.created.apiEndpoints?.length&&h.appendChild(N(`API endpoints: ${e.created.apiEndpoints.join(", ")}`)),n.appendChild(h),e.created.apiTokens?.length){const i=document.createElement("p");i.className="cb-success-snippet-label",i.textContent="API tokens \u2014 copy these now, they will not be shown again:",n.appendChild(i);for(const r of e.created.apiTokens)n.appendChild(w(`${r.name}: ${r.token}`))}if(e.warnings?.length){const i=document.createElement("div");i.className="cb-success-warning",i.textContent=e.warnings.join(" \xB7 "),n.appendChild(i)}if(e.snippet){const i=document.createElement("p");i.className="cb-success-snippet-label",i.textContent="Ready-made shortcode:",n.appendChild(i),n.appendChild(w(e.snippet));const r=document.createElement("div");if(r.className="cb-success-actions",o&&c){const s=document.createElement("button");s.type="button",s.className="btn btn-primary",s.textContent="Insert into this page",s.addEventListener("click",()=>{T(o,e.snippet),E.toast("Inserted at cursor",{type:"success",duration:1500}),c.close()}),r.appendChild(s)}else{const s=document.createElement("a");s.className="btn btn-primary",s.textContent="Open a new page to paste this",s.href="#/pages/new",r.appendChild(s)}n.appendChild(r)}const p=document.createElement("button");p.type="button",p.className="btn btn-outline",p.textContent="Scaffold another",p.addEventListener("click",()=>{n.replaceWith(y(c,o)),window.Domma?.icons?.scan&&Domma.icons.scan(a.parentElement||document.body)});const b=n.querySelector(".cb-success-actions");return b?b.appendChild(p):n.appendChild(p),n}function N(t){const e=document.createElement("li");return e.textContent=t,e}function T(t,e){const c=t.selectionStart||0,o=t.selectionEnd||0,a=t.value||"";t.value=a.slice(0,c)+e+a.slice(o);const n=c+e.length;t.selectionStart=t.selectionEnd=n,t.focus(),t.dispatchEvent(new Event("input",{bubbles:!0}))}function g(){try{const t=typeof S<"u"&&S.get?S.get("auth_token"):null;return t?{Authorization:`Bearer ${t}`}:{}}catch{return{}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function getProjectFromHash(
|
|
1
|
+
export function getProjectFromHash(t=window.location.hash){try{const n=sessionStorage.getItem("__projectContext");if(n)return sessionStorage.removeItem("__projectContext"),n}catch{}if(!t)return null;const r=t.indexOf("?");if(r!==-1){const c=new URLSearchParams(t.slice(r+1)).get("project");if(c)return c}const o=(r===-1?t:t.slice(0,r)).match(/^#\/projects\/([^/]+)\/[^/?#]+/);if(o)try{return decodeURIComponent(o[1])}catch{return o[1]}return null}export const CORE_PROJECT_SLUG="core";export function filterByProject(t,r){return!r||!Array.isArray(t)?t||[]:t.filter(e=>e?(e.resolvedProject||e.meta&&e.meta.project||(typeof e.project=="string"&&e.project?e.project:null)||CORE_PROJECT_SLUG)===r:!1)}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2><span data-icon="code"></span> <span id="ep-title">API Endpoint</span></h2>
|
|
3
|
+
<div class="header-actions">
|
|
4
|
+
<a class="btn btn-ghost" href="#/api-endpoints">All endpoints</a>
|
|
5
|
+
<button class="btn btn-primary" id="ep-save"><span data-icon="save"></span> Save</button>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div style="display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:1rem;align-items:start;">
|
|
10
|
+
<div>
|
|
11
|
+
<div class="card mb-4">
|
|
12
|
+
<div class="card-header"><h2>Definition</h2></div>
|
|
13
|
+
<div class="card-body">
|
|
14
|
+
<div class="form-group">
|
|
15
|
+
<label class="form-label" for="ep-name">Name</label>
|
|
16
|
+
<input type="text" class="form-input" id="ep-name" placeholder="e.g. Fixtures by day">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label class="form-label" for="endpoint-project">Project</label>
|
|
20
|
+
<select class="form-input" id="endpoint-project"></select>
|
|
21
|
+
<p class="text-muted" style="font-size:.8rem;margin:.25rem 0 0;">The first URL segment. Fixed once saved.</p>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-group">
|
|
24
|
+
<label class="form-label" for="ep-path">Path</label>
|
|
25
|
+
<input type="text" class="form-input" id="ep-path" placeholder="/fixtures-day/:date">
|
|
26
|
+
<p class="text-muted" style="font-size:.8rem;margin:.25rem 0 0;">Lowercase segments or <code>:param</code> placeholders. Full URL: <code id="ep-url-preview">/api/x/…</code></p>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="form-group">
|
|
29
|
+
<label class="form-label" for="ep-collection">Collection</label>
|
|
30
|
+
<select class="form-input" id="ep-collection"></select>
|
|
31
|
+
</div>
|
|
32
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem;">
|
|
33
|
+
<div class="form-group">
|
|
34
|
+
<label class="form-label" for="ep-auth">Access</label>
|
|
35
|
+
<select class="form-input" id="ep-auth"></select>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-group">
|
|
38
|
+
<label class="form-label" for="ep-mode">Mode</label>
|
|
39
|
+
<select class="form-input" id="ep-mode">
|
|
40
|
+
<option value="list">List — array of entries</option>
|
|
41
|
+
<option value="single">Single — first match or 404</option>
|
|
42
|
+
</select>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="card mb-4">
|
|
49
|
+
<div class="card-header">
|
|
50
|
+
<h2>Query</h2>
|
|
51
|
+
<button class="btn btn-sm btn-ghost" id="ep-add-filter"><span data-icon="plus"></span> Add filter</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="card-body">
|
|
54
|
+
<div id="ep-filter-rows"></div>
|
|
55
|
+
<p class="text-muted" style="font-size:.8rem;margin:.5rem 0 0;">Values may use <code>{{params.name}}</code> (from a <code>:name</code> path segment, required) or <code>{{query.name}}</code> (optional — the filter is skipped when absent).</p>
|
|
56
|
+
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.75rem;margin-top:1rem;">
|
|
57
|
+
<div class="form-group">
|
|
58
|
+
<label class="form-label" for="ep-sort">Sort field</label>
|
|
59
|
+
<select class="form-input" id="ep-sort"></select>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="form-group">
|
|
62
|
+
<label class="form-label" for="ep-order">Order</label>
|
|
63
|
+
<select class="form-input" id="ep-order">
|
|
64
|
+
<option value="asc">Ascending</option>
|
|
65
|
+
<option value="desc">Descending</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="form-group">
|
|
69
|
+
<label class="form-label" for="ep-limit">Limit</label>
|
|
70
|
+
<input type="number" class="form-input" id="ep-limit" min="0" max="500" value="50">
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<p class="text-muted" style="font-size:.8rem;margin:0;">Limit 0 = no limit.</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="card mb-4">
|
|
78
|
+
<div class="card-header"><h2>Response fields</h2></div>
|
|
79
|
+
<div class="card-body">
|
|
80
|
+
<p class="text-muted" style="font-size:.85rem;margin:0 0 .5rem;">Tick fields to return only those — none ticked returns everything.</p>
|
|
81
|
+
<div id="ep-fields-list" style="display:flex;flex-wrap:wrap;gap:.5rem 1.25rem;"></div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="card">
|
|
86
|
+
<div class="card-body">
|
|
87
|
+
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
|
|
88
|
+
<input type="checkbox" id="ep-enabled" checked> <strong>Enabled</strong>
|
|
89
|
+
<span class="text-muted" style="font-size:.85rem;">— disabled endpoints return 404</span>
|
|
90
|
+
</label>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="card" style="position:sticky;top:1rem;">
|
|
96
|
+
<div class="card-header"><h2><span data-icon="zap"></span> Try it</h2></div>
|
|
97
|
+
<div class="card-body">
|
|
98
|
+
<div id="tryit-dirty-note" class="text-muted" style="display:none;font-size:.85rem;margin-bottom:.75rem;">
|
|
99
|
+
<span data-icon="info"></span> Save to test your changes.
|
|
100
|
+
</div>
|
|
101
|
+
<div id="tryit-params"></div>
|
|
102
|
+
<div class="form-group" id="tryit-token-group" style="display:none;">
|
|
103
|
+
<label class="form-label" for="tryit-token">API token</label>
|
|
104
|
+
<input type="text" class="form-input" id="tryit-token" placeholder="dcms_…">
|
|
105
|
+
</div>
|
|
106
|
+
<div class="form-group">
|
|
107
|
+
<label class="form-label" for="tryit-query">Extra query string (optional)</label>
|
|
108
|
+
<input type="text" class="form-input" id="tryit-query" placeholder="team=Mexico&limit=5">
|
|
109
|
+
</div>
|
|
110
|
+
<button class="btn btn-primary" id="tryit-send" style="width:100%;"><span data-icon="play"></span> Send</button>
|
|
111
|
+
<div id="tryit-result" style="display:none;margin-top:.75rem;">
|
|
112
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.4rem;">
|
|
113
|
+
<span id="tryit-status" class="badge badge-secondary">—</span>
|
|
114
|
+
<button class="btn btn-sm btn-ghost" id="tryit-copy-curl" data-tooltip="Copy as curl"><span data-icon="copy"></span> curl</button>
|
|
115
|
+
</div>
|
|
116
|
+
<pre id="tryit-body" style="max-height:420px;overflow:auto;font-size:.8rem;padding:.6rem;margin:0;white-space:pre-wrap;word-break:break-word;"></pre>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2><span data-icon="code"></span> API Builder</h2>
|
|
3
|
+
<div class="header-actions">
|
|
4
|
+
<a class="btn btn-primary" href="#/api-endpoints/new"><span data-icon="plus"></span> New endpoint</a>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
<p class="text-muted mb-4">Custom REST endpoints over collection data, served at <code>/api/x/<project><path></code>. Each endpoint binds a clean URL to a fixed collection query — definitions are data, never code.</p>
|
|
8
|
+
|
|
9
|
+
<div class="card">
|
|
10
|
+
<div class="card-body">
|
|
11
|
+
<div id="api-endpoints-table"></div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2><span data-icon="key"></span> API Tokens</h2>
|
|
3
|
+
<div class="header-actions">
|
|
4
|
+
<button class="btn btn-primary" id="btn-new-token"><span data-icon="plus"></span> New token</button>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
<p class="text-muted mb-4">Project-scoped tokens for the external collections API (<code>/api/v1/<collection></code>). A token only works on collections whose API access mode is set to <strong>Token</strong>, and only within the token's project. The token value is shown once, at creation.</p>
|
|
8
|
+
|
|
9
|
+
<div class="card">
|
|
10
|
+
<div class="card-body">
|
|
11
|
+
<div id="api-tokens-table"></div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|