domma-cms 0.18.0 → 0.21.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 +37 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/crud-tutorial.js +1 -0
- package/admin/js/lib/markdown-toolbar.js +5 -5
- package/admin/js/lib/project-context.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -0
- package/admin/js/templates/action-editor.html +7 -0
- package/admin/js/templates/block-editor.html +7 -0
- package/admin/js/templates/collection-editor.html +9 -0
- package/admin/js/templates/form-editor.html +9 -0
- package/admin/js/templates/menu-editor.html +98 -0
- package/admin/js/templates/menu-locations.html +14 -0
- package/admin/js/templates/menus.html +14 -0
- package/admin/js/templates/page-editor.html +9 -2
- package/admin/js/templates/project-detail.html +50 -0
- package/admin/js/templates/project-editor.html +45 -0
- package/admin/js/templates/project-settings.html +60 -0
- package/admin/js/templates/projects.html +13 -0
- package/admin/js/templates/role-editor.html +11 -0
- package/admin/js/templates/tutorials.html +335 -2
- package/admin/js/templates/view-editor.html +7 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +5 -5
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/menu-editor.js +19 -0
- package/admin/js/views/menu-locations.js +1 -0
- package/admin/js/views/menus.js +5 -0
- package/admin/js/views/page-editor.js +24 -24
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/project-detail.js +4 -0
- package/admin/js/views/project-editor.js +1 -0
- package/admin/js/views/project-settings.js +1 -0
- package/admin/js/views/projects.js +7 -0
- package/admin/js/views/role-editor.js +1 -1
- package/admin/js/views/roles.js +3 -3
- package/admin/js/views/tutorials.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/config/menu-locations.json +5 -0
- package/config/menus/admin-sidebar.json +185 -0
- package/config/menus/footer.json +33 -0
- package/config/menus/main.json +35 -0
- package/config/menus/sproj-1779696558011-menu.json +17 -0
- package/config/menus/sproj-1779696960337-menu.json +18 -0
- package/config/menus/sproj-1779696985353-menu.json +18 -0
- package/config/site.json +6 -22
- package/package.json +4 -3
- package/plugins/analytics/daily.json +3 -0
- package/plugins/analytics/journeys.json +8 -0
- package/plugins/analytics/lifetime.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/collection-browser.js +4 -0
- package/public/js/forms.js +1 -1
- package/public/js/site.js +1 -1
- package/server/middleware/auth.js +88 -22
- package/server/routes/api/actions.js +58 -5
- package/server/routes/api/auth.js +2 -2
- package/server/routes/api/blocks.js +18 -3
- package/server/routes/api/collections.js +201 -8
- package/server/routes/api/forms.js +266 -21
- package/server/routes/api/menu-locations.js +46 -0
- package/server/routes/api/menus.js +115 -0
- package/server/routes/api/pages.js +1 -1
- package/server/routes/api/projects.js +107 -0
- package/server/routes/api/scaffold.js +86 -0
- package/server/routes/api/sidebar.js +23 -0
- package/server/routes/api/users.js +32 -7
- package/server/routes/api/views.js +10 -2
- package/server/routes/public.js +79 -6
- package/server/server.js +38 -0
- package/server/services/actions.js +137 -8
- package/server/services/adapters/FileAdapter.js +23 -8
- package/server/services/adapters/MongoAdapter.js +36 -18
- package/server/services/blocks.js +20 -8
- package/server/services/collections.js +85 -8
- package/server/services/content.js +23 -9
- package/server/services/filterEngine.js +281 -0
- package/server/services/hooks.js +48 -0
- package/server/services/markdown.js +686 -109
- package/server/services/menus-migration.js +107 -0
- package/server/services/menus.js +422 -0
- package/server/services/permissionRegistry.js +26 -0
- package/server/services/plugins.js +9 -2
- package/server/services/presetCollections.js +22 -0
- package/server/services/projects.js +429 -0
- package/server/services/recipes/contact-list.json +78 -0
- package/server/services/recipes/onboarding.json +426 -0
- package/server/services/references.js +174 -0
- package/server/services/renderer.js +237 -40
- package/server/services/roles.js +6 -1
- package/server/services/rowAccess.js +86 -13
- package/server/services/scaffolder.js +465 -0
- package/server/services/sidebar-migration.js +117 -0
- package/server/services/sitemap.js +112 -0
- package/server/services/userRoles.js +86 -0
- package/server/services/users.js +23 -2
- package/server/services/views.js +15 -4
- package/server/templates/page.html +7 -2
- /package/config/{navigation.json → navigation.json.bak} +0 -0
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
$(()=>{const w=window.__CMS_NAV__||{},d=window.__CMS_SITE__||{};if(d.autoTheme?.enabled){let e=function(s){const r=(s||"07:00").split(":");return+r[0]*60+(+r[1]||0)},o=function(){const s=new Date,r=s.getHours()*60+s.getMinutes();return r>=e(t.dayStart)&&r<e(t.nightStart)?t.dayTheme:t.nightTheme};var i=e,m=o;const t=d.autoTheme;Domma.theme.set(o()),setInterval(()=>Domma.theme.set(o()),6e4)}if($("#site-navbar").length&&w.brand){const t={...w.brand},e=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let o="";t.logo?o+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(o+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(o+=`<span class="navbar-brand-text${e}">${t.text}</span>`),t.tagline&&(o+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=o}else e&&t.text&&(t.html=`<span class="navbar-brand-text${e}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:w.items||[],variant:w.variant||"dark",position:w.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const b=$("#site-footer");if(b.length){const t=d.social||{},e={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let o='<div class="footer-inner container">';if(d.footer){const u=d.footer;o+=`<p>${u.copyright||""}</p>`,u.links?.length&&(o+='<nav class="footer-links">',u.links.forEach(n=>{o+=`<a href="${n.url}">${n.text}</a>`}),o+="</nav>");const f=Object.keys(e).filter(n=>t[n]);f.length&&(o+='<div class="footer-social">',f.forEach(n=>{const{label:a,svg:c}=e[n];o+=`<a href="${t[n]}" target="_blank" rel="noopener noreferrer" aria-label="${a}" class="footer-social-link">${c}</a>`}),o+="</div>")}o+="</div>",b.html(o);const s=S.get("reduced_motion"),r=s!==null?!!s:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),g=b.get(0).querySelector(".footer-inner");if(g){const u=document.createElement("input");u.type="checkbox",u.className="form-switch-input",u.id="dm-motion-switch",u.checked=r,u.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const f=document.createElement("span");f.className="form-switch-label",f.textContent="Reduce motion";const n=document.createElement("label");n.className="form-switch footer-motion-switch",n.title="Reduce motion",n.appendChild(u),n.appendChild(f),g.appendChild(n)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const h=$(".page-body");if(h.length){h.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),h.find(".tabs").each(function(){Domma.elements.tabs(this)}),h.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const e=parseInt(this.dataset.animationDuration,10);Number.isFinite(e)&&(t.animationDuration=e)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),h.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),h.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),h.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const e=t.dataset.revealMode||"stagger",o=t.dataset.revealAnimation||"slide-up",s=parseInt(t.dataset.revealDuration,10)||400,r=parseInt(t.dataset.revealStagger,10)||60,g=parseInt(t.dataset.revealDelay,10)||0,u=t.dataset.revealDirection==="rtl",f=Array.from(t.children),n={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};f.forEach((a,c)=>{a.style.opacity="0",a.style.transform=n[o]||"",a.style.transition=`opacity ${s}ms ease, transform ${s}ms ease`;const l=u?f.length-1-c:c;a.style.transitionDelay=e==="stagger"?`${g+l*r}ms`:`${g}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const a=new IntersectionObserver(c=>{c.forEach(l=>{l.isIntersecting&&(l.target.offsetWidth,l.target.style.opacity="1",l.target.style.transform="none",a.unobserve(l.target))})},{threshold:.1});f.forEach(c=>a.observe(c))}))}),h.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),h.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,e=document.getElementById(t);if(!e)return;const o=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",o.setContent(e),o.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(e=>{try{const o=atob(e.dataset.config),s=JSON.parse(o);Object.assign(t,s)}catch{}}),Object.keys(t).length>0){const e={};for(const[o,s]of Object.entries(t)){const r=s?.events?.click,{confirm:g,toast:u,alert:f,prompt:n,...a}=r||{};g||u||f||n?($(o).on("click",async function(l){if(l.preventDefault(),g&&!await E.confirm(g))return;let p=null;if(!(n&&(p=await E.prompt(n,{inputPlaceholder:a.promptPlaceholder||"",inputValue:a.promptDefault||""}),p===null))){if(a.target){const y=$(a.target);a.toggleClass&&y.toggleClass(a.toggleClass),a.addClass&&y.addClass(a.addClass),a.removeClass&&y.removeClass(a.removeClass),p!==null&&(a.setText&&y.text(p),a.setVal&&y.val(p),a.setAttr&&y.attr(a.setAttr,p))}a.href&&(window.location.href=a.href),u&&E.toast(u,{type:a.toastType||"success"}),f&&E.alert(f)}}),Object.keys(a).length&&(e[o]={...s,events:{...s.events,click:a}})):e[o]=s}$.setup(e)}}h.length&&wireCTAButtons(h.get(0))});function wireCTAButtons(w){w.querySelectorAll(".dm-cta-trigger").forEach(d=>{d.addEventListener("click",async()=>{const C=d.dataset.action,b=d.dataset.entry,v=d.dataset.confirm;let h=S.get("auth_token");if(!h){E.toast("Please log in to perform this action.",{type:"warning"});return}if(v&&!await E.confirm(v))return;const i=Array.from(d.childNodes).map(t=>t.cloneNode(!0));d.disabled=!0,d.textContent="Running\u2026";const m=t=>fetch(`/api/actions/${C}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({entryId:b})});try{let t=await m(h);if(t.status===401){const o=S.get("auth_refresh_token");if(o){const s=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(s.ok){const{token:r}=await s.json();S.set("auth_token",r),h=r,t=await m(h)}}}const e=await t.json().catch(()=>({}));if(!t.ok)throw new Error(e.error||e.message||`Error ${t.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(t){E.toast(t.message||"Action failed.",{type:"error"})}finally{d.disabled=!1,d.textContent="",i.forEach(t=>d.appendChild(t)),Domma.icons.scan(d)}})})}(function(){const d=document.querySelectorAll("[data-collection-table]");!d.length||typeof T>"u"||!T.create||d.forEach(C=>{let b;try{b=JSON.parse(atob(C.dataset.payload))}catch{return}const{columns:v,rows:h,search:i,sortable:m,exportable:t,pageSize:e,empty:o,ctaConfig:s}=b;if(!v?.length)return;if(s){const u=v.findIndex(f=>f.key==="_cta");u!==-1&&(v[u]={key:"_cta",title:"",render:(f,n)=>{const a=document.createElement("button");if(a.className=`btn btn-${s.style||"primary"} dm-cta-trigger`,a.dataset.action=s.action||"",a.dataset.entry=n._entryId||"",s.confirm&&(a.dataset.confirm=s.confirm),s.icon){const c=document.createElement("span");c.dataset.icon=s.icon,a.appendChild(c),a.appendChild(document.createTextNode(" "))}return a.appendChild(document.createTextNode(s.label||"Run")),a}})}const r="col-table-"+Math.random().toString(36).slice(2,7),g=document.createElement("div");g.id=r,C.replaceChildren(g),T.create("#"+r,{data:h,columns:v,search:i,sortable:m,exportable:t,pageSize:e,emptyMessage:o}),s&&wireCTAButtons(g)})})(),(function(){const d=document.querySelectorAll("[data-form-inline]");if(!d.length||typeof F>"u")return;function C(i,m){const t={};return(i||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const o=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,s={...e.formConfig||{}};s.span==="full"&&m&&(s.span=m);const r={type:o,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...s}};e.type==="chooser"&&(e.variant&&(r.variant=e.variant),e.multiple&&(r.multiple=!0),e.density&&(r.density=e.density),e.columns&&(r.columns=e.columns),e.accent&&(r.accent=e.accent),e.accentStyle&&(r.accentStyle=e.accentStyle),e.glow&&(r.glow=!0),e.glowColour&&(r.glowColour=e.glowColour),e.shadow&&(r.shadow=e.shadow),e.shadowColour&&(r.shadowColour=e.shadowColour)),t[e.name]=r}),t}function b(i){const m={};return(i||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(m[t.name]=typeof e=="object"?e.value:e)}}),m}function v(i,m){(m||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=i.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function h(i,m,t){let e=i.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",i.appendChild(e)),e.textContent=m,e.style.cssText=t?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}d.forEach(i=>{let m;try{m=JSON.parse(atob(i.dataset.formInline))}catch{return}const t=m.fields||[],e=m.settings||{},o=e.columns||1,s=e.layout||"stacked",r=t.some(n=>n.type==="page-break"),g=async n=>{try{const a=i.querySelector('[name="website"]'),c=i.querySelector('[name="_t"]'),l=Object.assign({},n);a!==null&&(l._hp=a.value),c!==null&&(l._t=c.value);const p=await H.post(`/api/forms/submit/${m.slug}`,l);if(p?.redirect){window.location.href=p.redirect;return}for(;i.firstChild;)i.removeChild(i.firstChild);h(i,p?.message||e.successMessage||"Thank you for your submission.",!1)}catch(a){throw h(i,a.message||"Submission failed. Please try again.",!0),a}};function u(n){const a=n.querySelector("form");if(!a)return;const c=document.createElement("div");c.className="fb-form-honeypot",c.setAttribute("aria-hidden","true");const l=document.createElement("input");l.name="website",l.type="text",l.tabIndex=-1,l.autocomplete="url",l.placeholder="https://",c.appendChild(l);const p=document.createElement("input");p.name="_t",p.type="hidden",p.value=Date.now(),c.appendChild(p),a.appendChild(c)}function f(){if(!window.FormLogicEngine||!t.some(a=>a.logic))return;const n=new window.FormLogicEngine.FormLogicRuntime(m,i);if(n.init(),i._formLogicRuntime=n,i.parentNode&&typeof MutationObserver<"u"){const a=new MutationObserver(function(c){for(const l of c)for(const p of l.removedNodes)if(p===i||p.nodeType===1&&p.contains&&p.contains(i)){n.destroy(),a.disconnect();return}});a.observe(i.parentNode,{childList:!0,subtree:!1})}}if(r&&F.wizard){const n=[];let a=[],c=m.title||"Step 1",l="";t.forEach(y=>{y.type==="page-break"?(n.push({title:c,description:l,fields:C(a,o)}),a=[],c=y.label||`Step ${n.length+1}`,l=y.description||""):y.type!=="spacer"&&a.push(y)}),n.push({title:c,description:l,fields:C(a,o)});const p=F.wizard(i,{schema:{steps:n},onSubmit:g});Promise.resolve(p).then(function(){v(i,t),e.honeypot!==!1&&u(i),f()})}else if(F.render){const n=F.render(i,C(t,o),b(t),{submitText:e.submitText||"Submit",layout:s,columns:o,onSubmit:g});Promise.resolve(n).then(function(){if(s==="grid"&&e.submitSpan==="full"){const a=i.querySelector(".form-buttons");a&&a.classList.add("col-span-full")}v(i,t),e.honeypot!==!1&&u(i),f()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
1
|
+
$(()=>{const x=window.__CMS_NAV__||{},p=window.__CMS_SITE__||{};if(p.autoTheme?.enabled){let n=function(h){const g=(h||"07:00").split(":");return+g[0]*60+(+g[1]||0)},r=function(){const h=new Date,g=h.getHours()*60+h.getMinutes();return g>=n(e.dayStart)&&g<n(e.nightStart)?e.dayTheme:e.nightTheme};var s=n,d=r;const e=p.autoTheme;Domma.theme.set(r()),setInterval(()=>Domma.theme.set(r()),6e4)}if($("#site-navbar").length&&x.brand){const e={...x.brand},n=e.size&&e.size!=="md"?` navbar-brand-${e.size}`:"";if(e.logo||e.icon||e.tagline){let r="";e.logo?r+=`<img src="${e.logo}" class="navbar-brand-logo" alt="${e.text||""}">`:e.icon&&(r+=`<span data-icon="${e.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),e.text&&(r+=`<span class="navbar-brand-text${n}">${e.text}</span>`),e.tagline&&(r+=`<small class="navbar-brand-tagline">${e.tagline}</small>`),e.html=r}else n&&e.text&&(e.html=`<span class="navbar-brand-text${n}">${e.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:e,items:x.items||[],variant:x.variant||"dark",position:x.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const b=$("#site-footer");if(b.length){let g=function(c){return String(c??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},m=function(c){return g(c).replace(/'/g,"'")},i=function(c){const y=m(c.url||"#"),v=g(c.text||""),_=Array.isArray(c.items)&&c.items.length?`<ul>${c.items.map(i).join("")}</ul>`:"";return`<li><a href="${y}">${v}</a>${_}</li>`};var a=g,t=m,w=i;const e=p.social||{},n={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let r='<div class="footer-inner container">';const h=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(h.copyright&&(r+=`<p>${g(h.copyright)}</p>`),h.primary.length&&(r+=`<nav class="footer-nav footer-nav--primary"><ul>${h.primary.map(i).join("")}</ul></nav>`),h.legal.length&&(r+=`<nav class="footer-nav footer-nav--legal"><ul>${h.legal.map(i).join("")}</ul></nav>`),p.footer){const c=Object.keys(n).filter(y=>e[y]);c.length&&(r+='<div class="footer-social">',c.forEach(y=>{const{label:v,svg:_}=n[y];r+=`<a href="${e[y]}" target="_blank" rel="noopener noreferrer" aria-label="${v}" class="footer-social-link">${_}</a>`}),r+="</div>")}r+="</div>",b.html(r);const u=S.get("reduced_motion"),f=u!==null?!!u:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),o=b.get(0).querySelector(".footer-inner");if(o){const c=document.createElement("input");c.type="checkbox",c.className="form-switch-input",c.id="dm-motion-switch",c.checked=f,c.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const y=document.createElement("span");y.className="form-switch-label",y.textContent="Reduce motion";const v=document.createElement("label");v.className="form-switch footer-motion-switch",v.title="Reduce motion",v.appendChild(c),v.appendChild(y),o.appendChild(v)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const l=$(".page-body");if(l.length){l.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),l.find(".tabs").each(function(){Domma.elements.tabs(this)}),l.find(".carousel").each(function(){const e={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const n=parseInt(this.dataset.animationDuration,10);Number.isFinite(n)&&(e.animationDuration=n)}this.dataset.animationEasing&&(e.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,e)}),l.find(".dm-countdown").each(function(){const e={autoStart:!0};this.dataset.to&&(e.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(e.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(e.format=this.dataset.format),Domma.elements.timer(this,e)}),l.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),l.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(e=>{const n=e.dataset.revealMode||"stagger",r=e.dataset.revealAnimation||"slide-up",h=parseInt(e.dataset.revealDuration,10)||400,g=parseInt(e.dataset.revealStagger,10)||60,m=parseInt(e.dataset.revealDelay,10)||0,i=e.dataset.revealDirection==="rtl",u=Array.from(e.children),f={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};u.forEach((o,c)=>{o.style.opacity="0",o.style.transform=f[r]||"",o.style.transition=`opacity ${h}ms ease, transform ${h}ms ease`;const y=i?u.length-1-c:c;o.style.transitionDelay=n==="stagger"?`${m+y*g}ms`:`${m}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const o=new IntersectionObserver(c=>{c.forEach(y=>{y.isIntersecting&&(y.target.offsetWidth,y.target.style.opacity="1",y.target.style.transform="none",o.unobserve(y.target))})},{threshold:.1});u.forEach(c=>o.observe(c))}))}),l.find(".card[data-collapsible]").each(function(){const e=this.querySelector(".card-header");e&&e.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),l.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const e=this.dataset.soTarget,n=document.getElementById(e);if(!n)return;const r=E.slideover({title:n.dataset.soTitle||"",size:n.dataset.soSize||"md",position:n.dataset.soPosition||"right"});n.style.display="",r.setContent(n),r.open()})})}if(typeof $.setup=="function"){const e=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(n=>{try{const r=atob(n.dataset.config),h=JSON.parse(r);Object.assign(e,h)}catch{}}),Object.keys(e).length>0){const n={};for(const[r,h]of Object.entries(e)){const g=h?.events?.click,{confirm:m,toast:i,alert:u,prompt:f,...o}=g||{};m||i||u||f?($(r).on("click",async function(y){if(y.preventDefault(),m&&!await E.confirm(m))return;let v=null;if(!(f&&(v=await E.prompt(f,{inputPlaceholder:o.promptPlaceholder||"",inputValue:o.promptDefault||""}),v===null))){if(o.target){const _=$(o.target);o.toggleClass&&_.toggleClass(o.toggleClass),o.addClass&&_.addClass(o.addClass),o.removeClass&&_.removeClass(o.removeClass),v!==null&&(o.setText&&_.text(v),o.setVal&&_.val(v),o.setAttr&&_.attr(o.setAttr,v))}o.href&&(window.location.href=o.href),i&&E.toast(i,{type:o.toastType||"success"}),u&&E.alert(u)}}),Object.keys(o).length&&(n[r]={...h,events:{...h.events,click:o}})):n[r]=h}$.setup(n)}}l.length&&wireCTAButtons(l.get(0))});function wireCTAButtons(x){x.querySelectorAll(".dm-cta-trigger").forEach(p=>{p.__ctaWired||(p.__ctaWired=!0,p.addEventListener("click",async()=>{const k=p.dataset.action,b=p.dataset.entry,C=p.dataset.confirm;let l=S.get("auth_token");if(!l){E.toast("Please log in to perform this action.",{type:"warning"});return}if(C&&!await E.confirm(C))return;const s=Array.from(p.childNodes).map(a=>a.cloneNode(!0));p.disabled=!0,p.textContent="Running\u2026";const d=a=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:b})});try{let a=await d(l);if(a.status===401){const w=S.get("auth_refresh_token");if(w){const e=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:w})});if(e.ok){const{token:n}=await e.json();S.set("auth_token",n),l=n,a=await d(l)}}}const t=await a.json().catch(()=>({}));if(!a.ok)throw new Error(t.error||t.message||`Error ${a.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{p.disabled=!1,p.textContent="",s.forEach(a=>p.appendChild(a)),Domma.icons.scan(p)}}))})}(function(){const p=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!p.length)return;const k=S.get("auth_token");function b(l,s){const d=document.createElement("div");d.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(s){a.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",a.appendChild(t),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=l;return d.appendChild(a),d}function C(l,s){const d=document.createElement("template");d.innerHTML=s||"",l.replaceWith(d.content)}p.forEach(async l=>{const s=l.dataset.collectionAttrs;if(!s)return;if(!k){l.replaceChildren(b("",!0));return}const d=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:s})});try{let a=k,t=await d(a);if(t.status===401){const e=S.get("auth_refresh_token");if(e){const n=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(n.ok){const{token:r}=await n.json();S.set("auth_token",r),a=r,t=await d(a)}}}if(!t.ok){const{error:e}=await t.json().catch(()=>({}));l.replaceChildren(b(e||"Unable to load entries.",!1));return}const{html:w}=await t.json();C(l,w)}catch{l.replaceChildren(b("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),wireCTAButtons(document.body)},100)})(),(function(){const p=document.querySelectorAll("[data-collection-table]");!p.length||typeof T>"u"||!T.create||p.forEach(k=>{let b;try{b=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:C,rows:l,search:s,sortable:d,exportable:a,pageSize:t,empty:w,ctaConfig:e}=b;if(!C?.length)return;if(e){const h=C.findIndex(g=>g.key==="_cta");h!==-1&&(C[h]={key:"_cta",title:"",render:(g,m)=>{const i=document.createElement("button");if(i.className=`btn btn-${e.style||"primary"} dm-cta-trigger`,i.dataset.action=e.action||"",i.dataset.entry=m._entryId||"",e.confirm&&(i.dataset.confirm=e.confirm),e.icon){const u=document.createElement("span");u.dataset.icon=e.icon,i.appendChild(u),i.appendChild(document.createTextNode(" "))}return i.appendChild(document.createTextNode(e.label||"Run")),i}})}const n="col-table-"+Math.random().toString(36).slice(2,7),r=document.createElement("div");r.id=n,k.replaceChildren(r),T.create("#"+n,{data:l,columns:C,search:s,sortable:d,exportable:a,pageSize:t,emptyMessage:w}),e&&wireCTAButtons(r)})})(),(function(){const p=document.querySelectorAll("[data-form-inline]");if(!p.length||typeof F>"u")return;function k(s,d){const a={};return(s||[]).forEach(t=>{if(t.type==="page-break"||t.type==="spacer"||!t.name)return;const w=t.type==="checkbox"?"boolean":t.type==="date"?"string":t.type,e={...t.formConfig||{}};e.span==="full"&&d&&(e.span=d);const n={type:w,label:t.label,required:t.required,options:t.options,formConfig:{...t.placeholder&&{placeholder:t.placeholder},...t.helper&&{hint:t.helper},...e}};t.type==="chooser"&&(t.variant&&(n.variant=t.variant),t.multiple&&(n.multiple=!0),t.density&&(n.density=t.density),t.columns&&(n.columns=t.columns),t.accent&&(n.accent=t.accent),t.accentStyle&&(n.accentStyle=t.accentStyle),t.glow&&(n.glow=!0),t.glowColour&&(n.glowColour=t.glowColour),t.shadow&&(n.shadow=t.shadow),t.shadowColour&&(n.shadowColour=t.shadowColour)),a[t.name]=n}),a}function b(s){const d={};return(s||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const t=(a.options||[])[0];t&&(d[a.name]=typeof t=="object"?t.value:t)}}),d}function C(s,d){(d||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const t=s.querySelector(`[name="${a.name}"]`);t&&t.type!=="date"&&(t.type="date")})}function l(s,d,a){let t=s.querySelector(".cms-form-message");t||(t=document.createElement("p"),t.className="cms-form-message",s.appendChild(t)),t.textContent=d,t.style.cssText=a?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}p.forEach(s=>{let d;try{d=JSON.parse(atob(s.dataset.formInline))}catch{return}const a=d.fields||[],t=d.settings||{},w=t.columns||1,e=t.layout||"stacked",n=a.some(m=>m.type==="page-break"),r=async m=>{try{const i=s.querySelector('[name="website"]'),u=s.querySelector('[name="_t"]'),f=Object.assign({},m);i!==null&&(f._hp=i.value),u!==null&&(f._t=u.value);const o=await H.post(`/api/forms/submit/${d.slug}`,f);if(o&&o.error){l(s,o.error,!0);const c=new Error(o.error);throw c.formSubmitFailed=!0,c}if(o?.redirect){window.location.href=o.redirect;return}for(;s.firstChild;)s.removeChild(s.firstChild);l(s,o?.message||t.successMessage||"Thank you for your submission.",!1)}catch(i){throw i.formSubmitFailed||l(s,i.message||"Submission failed. Please try again.",!0),i}};function h(m){const i=m.querySelector("form");if(!i)return;const u=document.createElement("div");u.className="fb-form-honeypot",u.setAttribute("aria-hidden","true");const f=document.createElement("input");f.name="website",f.type="text",f.tabIndex=-1,f.autocomplete="url",f.placeholder="https://",u.appendChild(f);const o=document.createElement("input");o.name="_t",o.type="hidden",o.value=Date.now(),u.appendChild(o),i.appendChild(u)}function g(){if(!window.FormLogicEngine||!a.some(i=>i.logic))return;const m=new window.FormLogicEngine.FormLogicRuntime(d,s);if(m.init(),s._formLogicRuntime=m,s.parentNode&&typeof MutationObserver<"u"){const i=new MutationObserver(function(u){for(const f of u)for(const o of f.removedNodes)if(o===s||o.nodeType===1&&o.contains&&o.contains(s)){m.destroy(),i.disconnect();return}});i.observe(s.parentNode,{childList:!0,subtree:!1})}}if(n&&F.wizard){const m=[];let i=[],u=d.title||"Step 1",f="";a.forEach(c=>{c.type==="page-break"?(m.push({title:u,description:f,fields:k(i,w)}),i=[],u=c.label||`Step ${m.length+1}`,f=c.description||""):c.type!=="spacer"&&i.push(c)}),m.push({title:u,description:f,fields:k(i,w)});const o=F.wizard(s,{schema:{steps:m},onSubmit:r});Promise.resolve(o).then(function(){C(s,a),t.honeypot!==!1&&h(s),g()})}else if(F.render){const m=F.render(s,k(a,w),b(a),{submitText:t.submitText||"Submit",layout:e,columns:w,onSubmit:r});Promise.resolve(m).then(function(){if(e==="grid"&&t.submitSpan==="full"){const i=s.querySelector(".form-buttons");i&&i.classList.add("col-span-full")}C(s,a),t.honeypot!==!1&&h(s),g()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Role data is read from the roles cache (not config.auth.roles).
|
|
5
5
|
*/
|
|
6
6
|
import {getPermissionsFor, getPermissionsForRole, getRoleHierarchy, getRoleLevel} from '../services/roles.js';
|
|
7
|
+
import {getEffectiveLevel, getEffectiveRoles} from '../services/userRoles.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Verify JWT Bearer token. Populates request.user on success.
|
|
@@ -78,8 +79,12 @@ export function requirePermission(resource, action) {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// Check ANY of the user's effective roles (primary + additional) against
|
|
83
|
+
// the resource's permission list — union semantics so a user with both
|
|
84
|
+
// candidate and recruiter roles can do either role's actions.
|
|
81
85
|
const allowed = getPermissionsFor(resource, action);
|
|
82
|
-
|
|
86
|
+
const roles = getEffectiveRoles(request.user);
|
|
87
|
+
if (!roles.some(r => allowed.includes(r))) {
|
|
83
88
|
return reply.code(403).send({
|
|
84
89
|
statusCode: 403,
|
|
85
90
|
error: 'Forbidden',
|
|
@@ -111,7 +116,9 @@ export async function requireAdmin(request, reply) {
|
|
|
111
116
|
if (!request.user) {
|
|
112
117
|
return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
|
|
113
118
|
}
|
|
114
|
-
|
|
119
|
+
// Use effective level — a user with "additionalRoles: ['admin']" can do admin work
|
|
120
|
+
// even if their primary role is something lower-privilege.
|
|
121
|
+
if (getEffectiveLevel(request.user) > 1) {
|
|
115
122
|
return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
|
|
116
123
|
}
|
|
117
124
|
}
|
|
@@ -120,53 +127,112 @@ export async function requireAdmin(request, reply) {
|
|
|
120
127
|
* Determine whether an actor can manage a target user.
|
|
121
128
|
* Managers cannot create, edit, or delete users with a lower level number (higher privilege).
|
|
122
129
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
130
|
+
* Accepts either user objects (preferred — uses effective level across all roles)
|
|
131
|
+
* or bare role-name strings (legacy compat). Strings are looked up via
|
|
132
|
+
* `getRoleLevel`; objects via `getEffectiveLevel` so multi-role actors and
|
|
133
|
+
* targets are compared by their HIGHEST-privilege role.
|
|
134
|
+
*
|
|
135
|
+
* @param {string|object} actor - Role name OR user object
|
|
136
|
+
* @param {string|object} target - Role name OR user object
|
|
125
137
|
* @returns {boolean}
|
|
126
138
|
*/
|
|
127
|
-
export function canManageUser(
|
|
128
|
-
|
|
139
|
+
export function canManageUser(actor, target) {
|
|
140
|
+
const actorLevel = typeof actor === 'string' ? getRoleLevel(actor) : getEffectiveLevel(actor);
|
|
141
|
+
const targetLevel = typeof target === 'string' ? getRoleLevel(target) : getEffectiveLevel(target);
|
|
142
|
+
return actorLevel < targetLevel;
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
/**
|
|
132
146
|
* Check whether a user role satisfies a visibility requirement.
|
|
133
147
|
* Used by both requireVisibility() and the public page renderer.
|
|
134
148
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
149
|
+
* Visibility may be either:
|
|
150
|
+
* - A single string ('public', 'private', or a role name)
|
|
151
|
+
* - An array of role names — granted if ANY entry passes the per-role check
|
|
152
|
+
*
|
|
153
|
+
* Per-role semantics are unchanged: each role check passes if the visitor's
|
|
154
|
+
* role level is at or above the required role (lower or equal level number).
|
|
155
|
+
* 'private' resolves to super-admin only (Infinity → level 0).
|
|
156
|
+
*
|
|
157
|
+
* The "any of" semantics for arrays means siblings at different tiers of the
|
|
158
|
+
* hierarchy are all granted access — e.g. `visibility: [candidate, employer]`
|
|
159
|
+
* lets both roles in, plus anyone more privileged than either (typically
|
|
160
|
+
* admins inherit access automatically via the level comparison).
|
|
161
|
+
*
|
|
162
|
+
* @param {string|null} userRole - The visitor's role, or null if unauthenticated
|
|
163
|
+
* @param {string|string[]} visibility - Required visibility — single value or array
|
|
137
164
|
* @returns {boolean} true if access is granted
|
|
138
165
|
*/
|
|
139
|
-
export function checkVisibility(
|
|
140
|
-
if (!visibility
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
export function checkVisibility(userRoleOrObj, visibility) {
|
|
167
|
+
if (!visibility) return true;
|
|
168
|
+
|
|
169
|
+
// Accept either a bare role name (legacy) or a user object with multi-role
|
|
170
|
+
// support. When given an object we walk every effective role and grant
|
|
171
|
+
// access if ANY satisfies — same union semantics as permissions.
|
|
172
|
+
const roles = typeof userRoleOrObj === 'string'
|
|
173
|
+
? (userRoleOrObj ? [userRoleOrObj] : [])
|
|
174
|
+
: getEffectiveRoles(userRoleOrObj);
|
|
175
|
+
|
|
176
|
+
if (Array.isArray(visibility)) {
|
|
177
|
+
if (visibility.length === 0) return true;
|
|
178
|
+
if (visibility.includes('public')) return true;
|
|
179
|
+
if (!roles.length) return false;
|
|
180
|
+
return roles.some(r => visibility.some(v => checkSingleVisibility(r, v)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (visibility === 'public') return true;
|
|
184
|
+
if (!roles.length) return false;
|
|
185
|
+
return roles.some(r => checkSingleVisibility(r, visibility));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Internal helper — single-role visibility check.
|
|
190
|
+
* Returns true if the user role is at or above the required role's level.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} userRole - Must not be null
|
|
193
|
+
* @param {string} visibility - Single visibility token (role name or 'private')
|
|
194
|
+
* @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
function checkSingleVisibility(userRole, visibility) {
|
|
197
|
+
const userLevel = getRoleLevel(userRole);
|
|
198
|
+
const requiredLevel = getRoleLevel(visibility);
|
|
199
|
+
const threshold = requiredLevel === Infinity ? 0 : requiredLevel;
|
|
145
200
|
return userLevel <= threshold;
|
|
146
201
|
}
|
|
147
202
|
|
|
148
203
|
/**
|
|
149
204
|
* Fastify preHandler factory — gates a route by visibility level.
|
|
150
|
-
* Works identically to the content-page visibility system
|
|
151
|
-
*
|
|
205
|
+
* Works identically to the content-page visibility system; accepts the same
|
|
206
|
+
* single-string or array-of-roles syntax as checkVisibility().
|
|
152
207
|
*
|
|
153
|
-
*
|
|
208
|
+
* Returns a no-op for 'public' (or any array containing 'public') so it is
|
|
209
|
+
* safe to apply unconditionally.
|
|
210
|
+
*
|
|
211
|
+
* @param {string|string[]} visibility - 'public' | 'private' | role name | array of role names
|
|
154
212
|
* @returns {Function} Fastify preHandler
|
|
155
213
|
*/
|
|
156
214
|
export function requireVisibility(visibility) {
|
|
157
|
-
|
|
215
|
+
const isPublic = !visibility
|
|
216
|
+
|| visibility === 'public'
|
|
217
|
+
|| (Array.isArray(visibility) && (visibility.length === 0 || visibility.includes('public')));
|
|
218
|
+
|
|
219
|
+
if (isPublic) {
|
|
158
220
|
return (_request, _reply, done) => { if (done) done(); };
|
|
159
221
|
}
|
|
160
222
|
|
|
161
223
|
return async (request, reply) => {
|
|
162
|
-
|
|
224
|
+
// Build a user-shaped object so checkVisibility can see multi-role
|
|
225
|
+
// (primary + additional). Unauthenticated → null → public-only access.
|
|
226
|
+
let userObj = null;
|
|
163
227
|
try {
|
|
164
228
|
const decoded = await request.jwtVerify();
|
|
165
|
-
if (decoded.type === 'access')
|
|
229
|
+
if (decoded.type === 'access') {
|
|
230
|
+
userObj = { role: decoded.role, additionalRoles: decoded.additionalRoles || [] };
|
|
231
|
+
}
|
|
166
232
|
} catch { /* unauthenticated */ }
|
|
167
233
|
|
|
168
|
-
if (!checkVisibility(
|
|
169
|
-
const code =
|
|
234
|
+
if (!checkVisibility(userObj, visibility)) {
|
|
235
|
+
const code = userObj ? 403 : 401;
|
|
170
236
|
return reply.code(code).send({
|
|
171
237
|
statusCode: code,
|
|
172
238
|
error: code === 403 ? 'Forbidden' : 'Unauthorised',
|
|
@@ -20,11 +20,13 @@ import {
|
|
|
20
20
|
getAction,
|
|
21
21
|
listActions,
|
|
22
22
|
listActionsForCollection,
|
|
23
|
+
listTransitionsForEntry,
|
|
23
24
|
updateAction
|
|
24
25
|
} from '../../services/actions.js';
|
|
25
26
|
import {getEntry} from '../../services/collections.js';
|
|
26
27
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
27
28
|
import {getRoleLevel} from '../../services/roles.js';
|
|
29
|
+
import {getEffectiveLevel} from '../../services/userRoles.js';
|
|
28
30
|
import {checkEntryAccess} from '../../services/rowAccess.js';
|
|
29
31
|
|
|
30
32
|
export async function actionsRoutes(fastify) {
|
|
@@ -39,7 +41,10 @@ export async function actionsRoutes(fastify) {
|
|
|
39
41
|
|
|
40
42
|
fastify.get('/actions', canRead, async (request, reply) => {
|
|
41
43
|
try {
|
|
42
|
-
|
|
44
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
45
|
+
const all = await listActions();
|
|
46
|
+
// listActions returns full records with meta inline — filter directly
|
|
47
|
+
return all.filter(a => canSeeArtefact(request.user, a));
|
|
43
48
|
} catch (err) {
|
|
44
49
|
return reply.status(503).send({ error: err.message });
|
|
45
50
|
}
|
|
@@ -68,6 +73,10 @@ export async function actionsRoutes(fastify) {
|
|
|
68
73
|
try {
|
|
69
74
|
const action = await getAction(request.params.slug);
|
|
70
75
|
if (!action) return reply.status(404).send({ error: 'Action not found' });
|
|
76
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
77
|
+
if (!canSeeArtefact(request.user, action)) {
|
|
78
|
+
return reply.status(403).send({ error: 'Access denied for this project' });
|
|
79
|
+
}
|
|
71
80
|
return action;
|
|
72
81
|
} catch (err) {
|
|
73
82
|
return reply.status(503).send({ error: err.message });
|
|
@@ -136,8 +145,8 @@ export async function actionsRoutes(fastify) {
|
|
|
136
145
|
const rowLevel = action.access?.rowLevel;
|
|
137
146
|
const user = request.user;
|
|
138
147
|
|
|
139
|
-
// Admin or no row-level config → all IDs allowed
|
|
140
|
-
if (!rowLevel ||
|
|
148
|
+
// Admin (across primary OR additional roles) or no row-level config → all IDs allowed
|
|
149
|
+
if (!rowLevel || getEffectiveLevel(user) === 0) {
|
|
141
150
|
return {allowed: entryIds};
|
|
142
151
|
}
|
|
143
152
|
|
|
@@ -146,7 +155,7 @@ export async function actionsRoutes(fastify) {
|
|
|
146
155
|
entryIds.map(async (id) => {
|
|
147
156
|
try {
|
|
148
157
|
const entry = await getEntry(action.collection, id);
|
|
149
|
-
return entry && checkEntryAccess(entry, user, rowLevel) ? id : null;
|
|
158
|
+
return (entry && (await checkEntryAccess(entry, user, rowLevel))) ? id : null;
|
|
150
159
|
} catch {
|
|
151
160
|
return null;
|
|
152
161
|
}
|
|
@@ -179,7 +188,8 @@ export async function actionsRoutes(fastify) {
|
|
|
179
188
|
|
|
180
189
|
const user = request.user;
|
|
181
190
|
const allowedRoles = action.access?.roles || ['admin', 'super-admin'];
|
|
182
|
-
|
|
191
|
+
// Use effective level — a candidate-with-also-admin can run admin-tier actions.
|
|
192
|
+
const userLevel = getEffectiveLevel(user);
|
|
183
193
|
const minAllowed = Math.min(...allowedRoles.map(r => getRoleLevel(r)));
|
|
184
194
|
|
|
185
195
|
if (userLevel > minAllowed) {
|
|
@@ -197,4 +207,47 @@ export async function actionsRoutes(fastify) {
|
|
|
197
207
|
return reply.status(status).send({ error: err.message });
|
|
198
208
|
}
|
|
199
209
|
});
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* GET /api/actions/transitions?collection=<slug>&entryId=<id>
|
|
213
|
+
*
|
|
214
|
+
* Returns the actions valid as *next-state transitions* for the given
|
|
215
|
+
* entry, filtered by the requesting user's role. Used by the per-row
|
|
216
|
+
* transitions UI in the Collection Browser and the `[transitions]`
|
|
217
|
+
* shortcode to render only the buttons that will actually succeed.
|
|
218
|
+
*
|
|
219
|
+
* Auth: optional. Anonymous calls return actions accessible to anonymous
|
|
220
|
+
* users (rare — most transitions require a role). With a JWT we narrow
|
|
221
|
+
* to what the signed-in user can do.
|
|
222
|
+
*
|
|
223
|
+
* Response: { transitions: [{slug, title, trigger, transition}, ...] }
|
|
224
|
+
* Returns 503 if MongoDB is unavailable (actions are a Pro feature).
|
|
225
|
+
*/
|
|
226
|
+
fastify.get('/actions/transitions', async (request, reply) => {
|
|
227
|
+
const { collection: collectionSlug, entryId } = request.query;
|
|
228
|
+
if (!collectionSlug || !entryId) {
|
|
229
|
+
return reply.status(400).send({ error: 'collection and entryId are required' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let user = null;
|
|
233
|
+
try {
|
|
234
|
+
const decoded = await request.jwtVerify();
|
|
235
|
+
if (decoded.type === 'access') user = {id: decoded.id, role: decoded.role};
|
|
236
|
+
} catch { /* anonymous */ }
|
|
237
|
+
|
|
238
|
+
let entry;
|
|
239
|
+
try {
|
|
240
|
+
entry = await getEntry(collectionSlug, entryId);
|
|
241
|
+
} catch {
|
|
242
|
+
return reply.status(404).send({ error: 'Entry not found' });
|
|
243
|
+
}
|
|
244
|
+
if (!entry) return reply.status(404).send({ error: 'Entry not found' });
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const transitions = await listTransitionsForEntry(collectionSlug, entry, user);
|
|
248
|
+
return { transitions };
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return reply.status(503).send({ error: err.message, transitions: [] });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
200
253
|
}
|
|
@@ -108,7 +108,7 @@ export async function authRoutes(fastify) {
|
|
|
108
108
|
|
|
109
109
|
await touchLastLogin(user.id);
|
|
110
110
|
|
|
111
|
-
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, level: getRoleLevel(user.role) };
|
|
111
|
+
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, additionalRoles: user.additionalRoles || [], level: getRoleLevel(user.role) };
|
|
112
112
|
hooks.emit('user:loggedIn', {userId: user.id, email: user.email, role: user.role});
|
|
113
113
|
const { token, refreshToken } = signTokens(fastify, safeUser);
|
|
114
114
|
return { token, refreshToken, user: safeUser };
|
|
@@ -271,7 +271,7 @@ export async function authRoutes(fastify) {
|
|
|
271
271
|
return reply.status(401).send({ error: 'User not found or inactive' });
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, level: getRoleLevel(user.role) };
|
|
274
|
+
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, additionalRoles: user.additionalRoles || [], level: getRoleLevel(user.role) };
|
|
275
275
|
const token = fastify.jwt.sign({ ...safeUser, type: 'access' }, { expiresIn: accessTokenExpiry });
|
|
276
276
|
return { token };
|
|
277
277
|
});
|
|
@@ -20,20 +20,35 @@ export async function blocksRoutes(fastify) {
|
|
|
20
20
|
const canDelete = {preHandler: [authenticate, requirePermission('pages', 'delete')]};
|
|
21
21
|
|
|
22
22
|
// List all blocks
|
|
23
|
-
fastify.get('/blocks', canRead, async () => {
|
|
24
|
-
|
|
23
|
+
fastify.get('/blocks', canRead, async (request) => {
|
|
24
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
25
|
+
const all = await listBlocks();
|
|
26
|
+
const filtered = [];
|
|
27
|
+
for (const b of all) {
|
|
28
|
+
// listBlocks returns metadata only; getBlock returns full record with meta
|
|
29
|
+
let full = null;
|
|
30
|
+
try { full = await getBlock(b.name); } catch { /* skip */ }
|
|
31
|
+
if (canSeeArtefact(request.user, full)) filtered.push(b);
|
|
32
|
+
}
|
|
33
|
+
return filtered;
|
|
25
34
|
});
|
|
26
35
|
|
|
27
36
|
// Get single block
|
|
28
37
|
fastify.get('/blocks/:name', canRead, async (request, reply) => {
|
|
29
38
|
const {name} = request.params;
|
|
39
|
+
let block;
|
|
30
40
|
try {
|
|
31
|
-
|
|
41
|
+
block = await getBlock(name);
|
|
32
42
|
} catch (err) {
|
|
33
43
|
if (err.code === 'INVALID_NAME') return reply.status(400).send({error: err.message});
|
|
34
44
|
if (err.code === 'ENOENT') return reply.status(404).send({error: 'Block not found'});
|
|
35
45
|
throw err;
|
|
36
46
|
}
|
|
47
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
48
|
+
if (!canSeeArtefact(request.user, block)) {
|
|
49
|
+
return reply.status(403).send({error: 'Access denied for this project'});
|
|
50
|
+
}
|
|
51
|
+
return block;
|
|
37
52
|
});
|
|
38
53
|
|
|
39
54
|
// Export single block as downloadable .dmblock.json bundle
|