domma-cms 0.22.6 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CLAUDE.md +16 -5
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/crud-tutorial.js +1 -1
  5. package/admin/js/lib/project-context.js +1 -1
  6. package/admin/js/templates/api-tokens.html +13 -0
  7. package/admin/js/templates/effects.html +752 -752
  8. package/admin/js/templates/form-submissions.html +30 -30
  9. package/admin/js/templates/forms.html +17 -17
  10. package/admin/js/templates/my-profile.html +17 -17
  11. package/admin/js/templates/project-settings.html +1 -1
  12. package/admin/js/templates/role-editor.html +70 -70
  13. package/admin/js/templates/roles.html +10 -10
  14. package/admin/js/views/api-tokens.js +8 -0
  15. package/admin/js/views/collection-editor.js +4 -4
  16. package/admin/js/views/index.js +1 -1
  17. package/admin/js/views/project-settings.js +1 -1
  18. package/admin/js/views/projects.js +3 -3
  19. package/admin/js/views/roles.js +1 -1
  20. package/bin/lib/config-merge.js +44 -44
  21. package/bin/update.js +547 -547
  22. package/config/menus/admin-sidebar.json +7 -1
  23. package/package.json +3 -2
  24. package/server/middleware/auth.js +253 -253
  25. package/server/routes/api/api-tokens.js +83 -0
  26. package/server/routes/api/auth.js +309 -309
  27. package/server/routes/api/collections.js +113 -16
  28. package/server/routes/api/forms.js +765 -746
  29. package/server/routes/api/navigation.js +42 -42
  30. package/server/routes/api/projects.js +9 -2
  31. package/server/routes/api/settings.js +141 -141
  32. package/server/routes/public.js +202 -202
  33. package/server/server.js +10 -1
  34. package/server/services/apiTokens.js +259 -0
  35. package/server/services/email.js +167 -167
  36. package/server/services/forms.js +345 -255
  37. package/server/services/permissionRegistry.js +13 -0
  38. package/server/services/presetCollections.js +27 -1
  39. package/server/services/projects.js +115 -24
  40. package/server/services/roles.js +16 -0
  41. package/server/services/scaffolder.js +31 -1
  42. package/server/services/sidebar-migration.js +44 -0
  43. package/server/services/userProfiles.js +199 -199
  44. package/server/services/users.js +302 -302
  45. package/config/connections.json.bak +0 -9
package/CLAUDE.md CHANGED
@@ -151,6 +151,7 @@ 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 |
154
155
  | `adapterRegistry.js` | `getAdapter(slug)` — resolves + caches adapter per collection; `invalidate(slug)` on schema change |
155
156
  | `adapters/FileAdapter.js` | Default adapter — plain JSON files |
156
157
  | `adapters/MongoAdapter.js` | Optional Pro adapter — native MongoDB driver; `cms_` prefix |
@@ -262,23 +263,33 @@ The admin sidebar is now menu-data-driven — no more hardcoded `sidebar-config.
262
263
 
263
264
  Projects group related artefacts under a named slug — sidebar navigation, per-user access scope, and a tag the scaffolder stamps on recipe-produced artefacts. Cross-cutting: touches nine services (`pages`, `collections`, `forms`, `actions`, `menus`, `blocks`, `views`, `roles`, `users`).
264
265
 
265
- **Data model.** Project records live in the preset collection `content/collections/projects/` (same pattern as `user-types` and `roles`). Each record has `slug`, `name`, `description`, `icon`, `rootUrl`, `sortOrder`. The slug is immutable. Nine artefact types accept an optional `meta.project: '<slug>'` field — set it to file the artefact under a project; omit it to keep the artefact site-wide.
266
+ **Data model.** Project records live in the preset collection `content/collections/projects/` (same pattern as `user-types` and `roles`). Each record has `slug`, `name`, `description`, `icon`, `rootUrl`, `sortOrder` (plus system-only `protected`). The slug is immutable. Nine artefact types accept an optional `meta.project: '<slug>'` field — set it to file the artefact under a project; omit it and the artefact belongs to the **core** project by exclusion.
267
+
268
+ **The core project.** Seeded at boot (`seedCoreProject()`, create-if-absent) with `slug: 'core'`, `rootUrl: '/'`, `protected: true`. It owns everything not claimed by another project — by resolution fallback, never by stamping: `resolveArtefactProject(artefact)` returns `meta.project || 'core'`, and `getProjectForPage` falls back to `'core'` when no `rootUrl` prefix matches. Core is **not removable** (delete and untag-all are refused at service and route level; `CORE_PROJECT_SLUG` constant, same pattern as `BASE_ROLE_NAMES`), its `rootUrl`/`protected` are locked, and it is **never an access boundary** — core artefacts are visible to all users regardless of `projects: []` scope.
266
269
 
267
270
  **Page URL inheritance.** Page frontmatter has three states:
268
271
 
269
- - `project: null` → explicit opt-out; page stays untagged regardless of URL.
270
272
  - `project: '<slug>'` → explicit override.
271
- - Field missingresolver picks the project whose `rootUrl` is the longest prefix match of the page's URL; no match means untagged.
273
+ - `project: null`resolves to `core` (legacy opt-out, deprecated).
274
+ - Field missing → resolver picks the project whose `rootUrl` is the longest prefix match of the page's URL; no match means `core`.
272
275
 
273
276
  The single source of truth is `getProjectForPage(urlPath, frontmatterProject)` in `server/services/projects.js`; every page-load path (renderer, sidebar, API list endpoints) calls it.
274
277
 
275
278
  **User access scope.** Users gain `projects: ['<slug>', ...]` — the **access scope**, distinct from `user.meta.project` (the **administrative ownership** that decides where the user appears in the sidebar). Two fields, two jobs:
276
279
 
277
280
  - `user.meta.project` — sidebar placement under a Project section.
278
- - `user.projects: []` — what artefacts the user can see; **empty array = no restriction**; non-empty restricts to listed projects + untagged. Super-admins bypass entirely.
281
+ - `user.projects: []` — what artefacts the user can see; **empty array = no restriction**; non-empty restricts to listed projects + core artefacts. Super-admins bypass entirely.
279
282
 
280
283
  **Permissions.** New family `projects.{read, create, update, delete}` in `permissionRegistry`. Existing roles are not back-filled — admin grants `projects.read` via the role editor (same gotcha as Menus).
281
284
 
282
285
  **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.
283
286
 
284
- **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 only renders when `listProjectsForUser(user).length > 0`.
287
+ **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.
288
+
289
+ ## External API & API tokens
290
+
291
+ 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).
292
+
293
+ 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`.
294
+
295
+ 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.
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 y(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"})},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=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 W}from"./views/index.js";import{api as r,getUser as l,isAuthenticated as c,logout as B}from"./api.js";import{installHttpInterceptor as I}from"./http-interceptor.js";$(()=>{I(),(async()=>{try{const t=c()?await r.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 u(t){return t&&k.includes(t.role)}function h(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(!c()){R.navigate("/login");return}if(u(l())&&!h(t.path)){R.navigate("/job-board");return}o()});async function v(){try{return(await r.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 L=[{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:"/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",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(u(l())&&!h(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 r.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 p=m(a.data?.title||""),g=m((a.data?.body||"").slice(0,120));E.toast(`${p} \u2014 ${g}`,{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 N=["#field-project","#menu-project","#collection-project","#action-project","#view-project","#block-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function _(t){return/^\/(pages|menus|collections|forms|actions|views|blocks|users|roles)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{if(!t||!_(t.path))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(t.path))return;let i=0;const s=60,a=1500,p=()=>{i+=s;for(const g of N){const d=document.querySelector(g);if(!d)continue;if(Array.from(d.options||[]).find(T=>T.value===o)){d.value=o,d.dispatchEvent(new Event("change",{bubbles:!0}));return}}i<a&&setTimeout(p,s)};setTimeout(p,s)});const C="cms_card_states",w=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&&w[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&&(w[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(C,w))}),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={...W},f=[...L];async function j(){if(c())try{const t=await r.plugins.adminConfig();t.routes?.length&&f.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=l();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">${m(t.name)}</span>
3
- <span class="topbar-role-badge topbar-role-badge--${m(t.role)}">${o}</span>
1
+ import{views as W}from"./views/index.js";import{api as l,getUser as c,isAuthenticated as m,logout as x}from"./api.js";import{installHttpInterceptor as B}from"./http-interceptor.js";$(()=>{B(),(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 L=[{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:"/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",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 N=["#field-project","#menu-project","#collection-project","#action-project","#view-project","#block-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function T(t){return/^\/(pages|menus|collections|forms|actions|views|blocks|users|roles)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!T(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 _ of N){const u=document.querySelector(_);if(!u)continue;if(Array.from(u.options||[]).find(I=>I.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={...W},g=[...L];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(),B()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),P(),n||(n=setInterval(P,6e4))}function m(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let n=null;async function P(){if(c())try{const{count:t}=await r.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(!c()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else{const i=(window.location.hash||"#/").slice(1)||"/";if(u(l())&&!h(i)&&(window.location.hash="#/job-board"),await j(),l()){const a=await v();await b(a),y()}}R.init({container:"#view-container",routes:f,views:D,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,s){if(i.endsWith("/*")){const a=i.slice(0,-2);return s.startsWith(a+"/")?{}:null}return t(i,s)};const e=(window.location.hash||"#/").slice(1)||"/";f.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
21
+ `),$("#topbar-logout-btn").on("click",i=>{i.preventDefault(),x()}),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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}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"}),n=document.createElement("div");n.className="crud-tutorial",n.style.cssText="padding:1rem;display:flex;flex-direction:column;gap:1.25rem;",n.appendChild(g()),n.appendChild(v(e)),n.appendChild(y(e,t)),e.setContent(n),e.open(),window.Domma?.icons?.scan&&Domma.icons.scan(n)}function v(t){const e=document.createElement("a");e.href="#/tutorials",e.className="cb-tutorial-link",e.addEventListener("click",()=>{try{t.close()}catch{}});const n=document.createElement("span");n.setAttribute("data-icon","book-open"),n.className="cb-icon",e.appendChild(n);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 c=document.createElement("span");return c.className="cb-arrow",c.textContent="\u203A",e.appendChild(c),e}function g(){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 k(t){const e=document.createElement("div");e.className="cb-code";const n=document.createElement("pre");n.textContent=t,e.appendChild(n);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 c=document.createElement("textarea");c.value=t,c.style.cssText="position:fixed;opacity:0;",document.body.appendChild(c),c.select(),document.execCommand("copy"),c.remove(),E.toast("Copied!",{type:"success",duration:1200})}),e.appendChild(o),e}export function mountScaffolder(t,e=null,n=null){t.appendChild(y(e,n)),window.Domma?.icons?.scan&&Domma.icons.scan(t)}function y(t,e){const n=document.createElement("section");n.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 c=document.createElement("h2");c.textContent="Or skip ahead \u2014 scaffold a working system in one click",o.appendChild(c),n.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.",n.appendChild(l);const i=document.createElement("div");return i.className="cb-recipe-list",i.textContent="Loading templates\u2026",n.appendChild(i),fetch("/api/scaffold/recipes",{headers:w()}).then(r=>r.json()).then(({recipes:r})=>{if(i.textContent="",!r?.length){i.textContent="No bundled templates available.";return}for(const h of r)i.appendChild(A(h,n,t,e))}).catch(()=>{i.textContent="Couldn't load templates \u2014 admin permissions required."}),n}function A(t,e,n,o){const a=document.createElement("button");a.type="button",a.className="cb-recipe-card";const c=document.createElement("span");c.setAttribute("data-icon",t.icon||"box"),c.className="cb-icon",a.appendChild(c);const l=document.createElement("div");l.className="cb-recipe-meta";const i=document.createElement("div");i.className="cb-recipe-name",i.textContent=t.name;const r=document.createElement("div");return r.className="cb-recipe-desc",r.textContent=t.description||"",l.appendChild(i),l.appendChild(r),a.appendChild(l),a.addEventListener("click",()=>D(t,e,n,o)),a}function D(t,e,n,o){const a=document.createElement("div");a.className="cb-apply-panel";const c=document.createElement("div");c.className="cb-apply-head";const l=document.createElement("span");l.setAttribute("data-icon",t.icon||"box"),l.className="cb-icon",c.appendChild(l);const i=document.createElement("h3");i.textContent=t.name,c.appendChild(i),a.appendChild(c);const r=document.createElement("p");r.className="cb-apply-desc",r.textContent=t.description||"",a.appendChild(r);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 N=document.createElement("div");N.className="cb-apply-hint",N.textContent=s.hint,u.appendChild(N)}a.appendChild(u),h[s.name]=f}const d=document.createElement("div");d.className="cb-apply-status",a.appendChild(d);const b=document.createElement("div");b.className="cb-apply-buttons";const p=document.createElement("button");p.type="button",p.className="btn btn-outline",p.textContent="Cancel",p.addEventListener("click",()=>{const s=y(n,o);a.replaceWith(s),window.Domma?.icons?.scan&&Domma.icons.scan(s)});const m=document.createElement("button");m.type="button",m.className="btn btn-primary",m.textContent="Apply template",m.addEventListener("click",async()=>{m.disabled=!0,p.disabled=!0,d.textContent="Creating collection, form, and actions\u2026",d.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",...w()},body:JSON.stringify({recipe:t.slug,options:s})}),C=await u.json();if(!u.ok){d.classList.add("cb-error"),Array.isArray(C.conflicts)?d.textContent=`Conflicts: ${C.conflicts.join("; ")}`:d.textContent=C.error||"Apply failed.",m.disabled=!1,p.disabled=!1;return}const f=L(t,C,n,o,e);a.replaceWith(f),window.Domma?.icons?.scan&&Domma.icons.scan(f)}catch(u){d.classList.add("cb-error"),d.textContent=u.message||"Network error.",m.disabled=!1,p.disabled=!1}}),b.appendChild(p),b.appendChild(m),a.appendChild(b),e.replaceWith(a),window.Domma?.icons?.scan&&Domma.icons.scan(a)}function L(t,e,n,o,a){const c=document.createElement("div");c.className="cb-success-panel";const l=document.createElement("div");l.className="cb-apply-head";const i=document.createElement("span");i.setAttribute("data-icon","check-circle"),i.className="cb-icon",l.appendChild(i);const r=document.createElement("h3");r.textContent=`${t.name} \u2014 scaffolded`,l.appendChild(r),c.appendChild(l);const h=document.createElement("ul");if(h.className="cb-success-summary",e.created.collection&&h.appendChild(x(`Collection: ${e.created.collection}`)),e.created.form&&h.appendChild(x(`Form: ${e.created.form}`)),e.created.actions?.length&&h.appendChild(x(`Actions: ${e.created.actions.join(", ")}`)),c.appendChild(h),e.warnings?.length){const p=document.createElement("div");p.className="cb-success-warning",p.textContent=e.warnings.join(" \xB7 "),c.appendChild(p)}if(e.snippet){const p=document.createElement("p");p.className="cb-success-snippet-label",p.textContent="Ready-made shortcode:",c.appendChild(p),c.appendChild(k(e.snippet));const m=document.createElement("div");if(m.className="cb-success-actions",o&&n){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}),n.close()}),m.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",m.appendChild(s)}c.appendChild(m)}const d=document.createElement("button");d.type="button",d.className="btn btn-outline",d.textContent="Scaffold another",d.addEventListener("click",()=>{c.replaceWith(y(n,o)),window.Domma?.icons?.scan&&Domma.icons.scan(a.parentElement||document.body)});const b=c.querySelector(".cb-success-actions");return b?b.appendChild(d):c.appendChild(d),c}function x(t){const e=document.createElement("li");return e.textContent=t,e}function T(t,e){const n=t.selectionStart||0,o=t.selectionEnd||0,a=t.value||"";t.value=a.slice(0,n)+e+a.slice(o);const c=n+e.length;t.selectionStart=t.selectionEnd=c,t.focus(),t.dispatchEvent(new Event("input",{bubbles:!0}))}function w(){try{const t=typeof S<"u"&&S.get?S.get("auth_token"):null;return t?{Authorization:`Bearer ${t}`}:{}}catch{return{}}}
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 d=document.createElement("p");d.className="cb-scaffolder-blurb",d.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(d);const l=document.createElement("div");return l.className="cb-recipe-list",l.textContent="Loading templates\u2026",c.appendChild(l),fetch("/api/scaffold/recipes",{headers:g()}).then(m=>m.json()).then(({recipes:m})=>{if(l.textContent="",!m?.length){l.textContent="No bundled templates available.";return}for(const h of m)l.appendChild(A(h,c,t,e))}).catch(()=>{l.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 d=document.createElement("div");d.className="cb-recipe-meta";const l=document.createElement("div");l.className="cb-recipe-name",l.textContent=t.name;const m=document.createElement("div");return m.className="cb-recipe-desc",m.textContent=t.description||"",d.appendChild(l),d.appendChild(m),a.appendChild(d),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 d=document.createElement("span");d.setAttribute("data-icon",t.icon||"box"),d.className="cb-icon",n.appendChild(d);const l=document.createElement("h3");l.textContent=t.name,n.appendChild(l),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 N=document.createElement("div");N.className="cb-apply-hint",N.textContent=s.hint,u.appendChild(N)}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 d=document.createElement("div");d.className="cb-apply-head";const l=document.createElement("span");l.setAttribute("data-icon","check-circle"),l.className="cb-icon",d.appendChild(l);const m=document.createElement("h3");m.textContent=`${t.name} \u2014 scaffolded`,d.appendChild(m),n.appendChild(d);const h=document.createElement("ul");if(h.className="cb-success-summary",e.created.collection&&h.appendChild(x(`Collection: ${e.created.collection}`)),e.created.form&&h.appendChild(x(`Form: ${e.created.form}`)),e.created.actions?.length&&h.appendChild(x(`Actions: ${e.created.actions.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 x(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(r=window.location.hash){try{const o=sessionStorage.getItem("__projectContext");if(o)return sessionStorage.removeItem("__projectContext"),o}catch{}if(!r)return null;const e=r.indexOf("?");if(e!==-1){const c=new URLSearchParams(r.slice(e+1)).get("project");if(c)return c}const n=(e===-1?r:r.slice(0,e)).match(/^#\/projects\/([^/]+)\/[^/?#]+/);if(n)try{return decodeURIComponent(n[1])}catch{return n[1]}return null}export function filterByProject(r,e){return!e||!Array.isArray(r)?r||[]:r.filter(t=>t?!!(t.resolvedProject===e||t.meta&&t.meta.project===e||typeof t.project=="string"&&t.project===e):!1)}
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,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/&lt;collection&gt;</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>