domma-cms 0.6.14 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/js/templates/pages.html +5 -1
- package/admin/js/views/pages.js +2 -2
- package/config/plugins.json +7 -1
- package/package.json +1 -1
- package/plugins/analytics/stats.json +3 -3
- package/plugins/example-analytics/admin/templates/analytics.html +10 -0
- package/plugins/example-analytics/admin/views/analytics.js +51 -0
- package/plugins/example-analytics/config.js +6 -0
- package/plugins/example-analytics/plugin.js +58 -0
- package/plugins/example-analytics/plugin.json +45 -0
- package/plugins/example-analytics/public/inject-body.html +14 -0
- package/plugins/example-analytics/public/inject-head.html +1 -0
- package/plugins/example-analytics/stats.json +24 -0
- package/plugins/form-builder/data/forms/contacts.json +66 -0
- package/plugins/form-builder/data/forms/enquiries.json +103 -0
- package/plugins/form-builder/data/forms/feedback.json +131 -0
- package/plugins/form-builder/data/forms/notes.json +79 -0
- package/plugins/form-builder/data/forms/to-do.json +100 -0
- package/plugins/form-builder/data/submissions/contacts.json +1 -0
- package/plugins/form-builder/data/submissions/enquiries.json +1 -0
- package/plugins/form-builder/data/submissions/feedback.json +1 -0
- package/plugins/form-builder/data/submissions/notes.json +1 -0
- package/plugins/form-builder/data/submissions/to-do.json +1 -0
- package/public/css/site.css +1 -1
- package/server/server.js +1 -0
- package/server/services/userTypes.js +227 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
2
|
<h1><span data-icon="file-text"></span> Pages</h1>
|
|
3
|
-
<
|
|
3
|
+
<div style="display:flex;gap:.5rem;align-items:center;margin-left:auto;">
|
|
4
|
+
<input id="pages-search" type="text" class="form-input form-input-sm" placeholder="Search pages…"
|
|
5
|
+
style="width:220px;">
|
|
6
|
+
<a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
|
|
7
|
+
</div>
|
|
4
8
|
</div>
|
|
5
9
|
|
|
6
10
|
<div class="toolbar mb-3" style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
|
package/admin/js/views/pages.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as p}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(e){const c=E.loader(e.get(0),{type:"dots"});let i=await p.pages.list().catch(()=>[]);c.destroy();const l=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,a)=>`<a href="#/pages/edit${a.urlPath}">${t}</a>`},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(a=>`<span class="badge badge-info badge-pill badge-sm">${a}</span>`).join(" "):"\u2014"},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,a)=>`
|
|
2
2
|
<a href="#/pages/edit${a.urlPath}" class="btn btn-sm btn-primary">Edit</a>
|
|
3
3
|
<a href="${a.urlPath}" target="_blank" class="btn btn-sm btn-ghost" data-tooltip="View"><span data-icon="external-link"></span></a>
|
|
4
4
|
<button class="btn btn-sm btn-danger btn-delete" data-path="${a.urlPath}">Delete</button>
|
|
5
|
-
`}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(
|
|
5
|
+
`}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(i);const u=s=>{const t=e.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const a=s.map(n=>{const d=n.urlPath.split("/").filter(Boolean),r=d.length>1?"/"+d.slice(0,-1).join("/"):null,g=r&&s.some(f=>f.urlPath===r);return{id:n.urlPath,parent_id:g?r:null,name:n.title||n.urlPath,icon:n.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:a,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:n=>{R.navigate(`/pages/edit${n}`)}}),Domma.icons.scan(t)};e.find("#view-table-btn").on("click",function(){e.find("#pages-table").show(),e.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),e.find("#view-tree-btn").on("click",function(){e.find("#pages-table").hide(),e.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),u(i)}),e.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const o=()=>{const s=e.find("#status-filter").val(),t=e.find("#pages-search").val().toLowerCase().trim(),a=i.filter(n=>!(s&&n.status!==s||t&&!`${n.title} ${n.urlPath} ${(n.tags||[]).join(" ")}`.toLowerCase().includes(t)));l(a)};e.find("#status-filter").off("change").on("change",o),e.find("#pages-search").get(0).addEventListener("input",o),e.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path");if(await E.confirm(`Delete page at <strong>${s}</strong>? This cannot be undone.`))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),i=i.filter(a=>a.urlPath!==s),l(i)}catch{E.toast("Failed to delete page.",{type:"error"})}})}};
|
package/config/plugins.json
CHANGED
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"site-search": {
|
|
11
11
|
"enabled": true,
|
|
12
|
-
"settings": {
|
|
12
|
+
"settings": {
|
|
13
|
+
"placeholder": "Search",
|
|
14
|
+
"keyboardShortcut": false,
|
|
15
|
+
"maxResults": 10,
|
|
16
|
+
"minQueryLength": 2,
|
|
17
|
+
"debounceMs": 300
|
|
18
|
+
}
|
|
13
19
|
},
|
|
14
20
|
"domma-effects": {
|
|
15
21
|
"enabled": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"/":
|
|
3
|
-
"/about":
|
|
2
|
+
"/": 156,
|
|
3
|
+
"/about": 72,
|
|
4
4
|
"/blog": 36,
|
|
5
5
|
"/contact": 30,
|
|
6
6
|
"/resources/typography": 4,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"/resources/dependencies": 2,
|
|
17
17
|
"/resources/components": 6,
|
|
18
18
|
"/gdpr": 3,
|
|
19
|
-
"/scratch":
|
|
19
|
+
"/scratch": 70,
|
|
20
20
|
"/getting-started": 3,
|
|
21
21
|
"/resources/pro": 1,
|
|
22
22
|
"/todo": 23,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="chart-bar"></span> Analytics</h1>
|
|
3
|
+
<button id="reset-btn" class="btn btn-ghost btn-sm">Reset stats</button>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="card">
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<div id="analytics-table"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Plugin — Admin View
|
|
3
|
+
* Shows a sortable table of page hit counts.
|
|
4
|
+
* Loaded dynamically from /plugins/ static path.
|
|
5
|
+
*/
|
|
6
|
+
export const analyticsView = {
|
|
7
|
+
templateUrl: '/plugins/example-analytics/admin/templates/analytics.html',
|
|
8
|
+
|
|
9
|
+
async onMount($container) {
|
|
10
|
+
await loadStats($container);
|
|
11
|
+
|
|
12
|
+
$container.find('#reset-btn').on('click', async () => {
|
|
13
|
+
const confirmed = await E.confirm('Reset all analytics data? This cannot be undone.');
|
|
14
|
+
if (!confirmed) return;
|
|
15
|
+
try {
|
|
16
|
+
await fetch('/api/plugins/example-analytics/stats', {
|
|
17
|
+
method: 'DELETE',
|
|
18
|
+
headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
|
|
19
|
+
});
|
|
20
|
+
E.toast('Analytics reset.', {type: 'success'});
|
|
21
|
+
await loadStats($container);
|
|
22
|
+
} catch {
|
|
23
|
+
E.toast('Reset failed.', {type: 'error'});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
Domma.icons.scan();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function loadStats($container) {
|
|
32
|
+
let stats = [];
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch('/api/plugins/example-analytics/stats', {
|
|
35
|
+
headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
38
|
+
stats = await res.json();
|
|
39
|
+
} catch {
|
|
40
|
+
E.toast('Could not load analytics data.', {type: 'error'});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
T.create('#analytics-table', {
|
|
44
|
+
data: stats,
|
|
45
|
+
columns: [
|
|
46
|
+
{key: 'url', title: 'Page URL', render: (val) => `<code>${val}</code>`},
|
|
47
|
+
{key: 'hits', title: 'Page views'}
|
|
48
|
+
],
|
|
49
|
+
emptyMessage: 'No page views recorded yet.'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Analytics Plugin — Server
|
|
3
|
+
* Tracks page hits in a JSON file alongside the plugin.
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* POST /api/plugins/example-analytics/hit - public: record a hit { url }
|
|
6
|
+
* GET /api/plugins/example-analytics/stats - admin: return all hit counts
|
|
7
|
+
* DELETE /api/plugins/example-analytics/stats - admin: reset all stats
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import {fileURLToPath} from 'url';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const STATS_FILE = path.join(__dirname, 'stats.json');
|
|
15
|
+
|
|
16
|
+
async function readStats() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await fs.readFile(STATS_FILE, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function writeStats(stats) {
|
|
25
|
+
await fs.writeFile(STATS_FILE, JSON.stringify(stats, null, 2) + '\n', 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function analyticsPlugin(fastify, options) {
|
|
29
|
+
const {authenticate, requireAdmin} = options.auth;
|
|
30
|
+
|
|
31
|
+
// Record a page hit — called by the client-side injection script (public)
|
|
32
|
+
fastify.post('/hit', async (request, reply) => {
|
|
33
|
+
const {url} = request.body || {};
|
|
34
|
+
if (!url || typeof url !== 'string') {
|
|
35
|
+
return reply.status(400).send({error: 'url is required'});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalised = url.split('?')[0].replace(/\/$/, '') || '/';
|
|
39
|
+
const stats = await readStats();
|
|
40
|
+
stats[normalised] = (stats[normalised] || 0) + 1;
|
|
41
|
+
await writeStats(stats);
|
|
42
|
+
return {ok: true};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Return all stats — admin only
|
|
46
|
+
fastify.get('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
|
|
47
|
+
const stats = await readStats();
|
|
48
|
+
return Object.entries(stats)
|
|
49
|
+
.map(([url, hits]) => ({url, hits}))
|
|
50
|
+
.sort((a, b) => b.hits - a.hits);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Reset stats — admin only
|
|
54
|
+
fastify.delete('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
|
|
55
|
+
await writeStats({});
|
|
56
|
+
return {ok: true};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-analytics",
|
|
3
|
+
"displayName": "Analytics",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
|
|
6
|
+
"author": "Darryl Waterhouse",
|
|
7
|
+
"date": "2026-03-01",
|
|
8
|
+
"icon": "chart-bar",
|
|
9
|
+
"admin": {
|
|
10
|
+
"sidebar": [
|
|
11
|
+
{
|
|
12
|
+
"id": "analytics",
|
|
13
|
+
"text": "Analytics",
|
|
14
|
+
"icon": "chart-bar",
|
|
15
|
+
"url": "#/plugins/analytics",
|
|
16
|
+
"section": "#/plugins/analytics"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"routes": [
|
|
20
|
+
{
|
|
21
|
+
"path": "/plugins/analytics",
|
|
22
|
+
"view": "plugin-analytics",
|
|
23
|
+
"title": "Analytics - Domma CMS"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"views": {
|
|
27
|
+
"plugin-analytics": {
|
|
28
|
+
"entry": "example-analytics/admin/views/analytics.js",
|
|
29
|
+
"exportName": "analyticsView"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"inject": {
|
|
34
|
+
"head": "public/inject-head.html",
|
|
35
|
+
"bodyEnd": "public/inject-body.html"
|
|
36
|
+
},
|
|
37
|
+
"scaffold": {
|
|
38
|
+
"reset": [
|
|
39
|
+
{
|
|
40
|
+
"path": "stats.json",
|
|
41
|
+
"content": "{}"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!-- example-analytics: page view tracker -->
|
|
2
|
+
<script>
|
|
3
|
+
(function () {
|
|
4
|
+
var url = window.location.pathname;
|
|
5
|
+
if (typeof fetch === 'function') {
|
|
6
|
+
fetch('/api/plugins/example-analytics/hit', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {'Content-Type': 'application/json'},
|
|
9
|
+
body: JSON.stringify({url: url})
|
|
10
|
+
}).catch(function () { /* silent fail */
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!-- example-analytics: head injection (empty — tracking is done via body script) -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"/": 138,
|
|
3
|
+
"/about": 71,
|
|
4
|
+
"/blog": 30,
|
|
5
|
+
"/contact": 30,
|
|
6
|
+
"/resources/typography": 4,
|
|
7
|
+
"/resources": 13,
|
|
8
|
+
"/resources/shortcodes": 14,
|
|
9
|
+
"/resources/cards": 15,
|
|
10
|
+
"/resources/interactive": 13,
|
|
11
|
+
"/resources/grid": 6,
|
|
12
|
+
"/forms": 14,
|
|
13
|
+
"/resources/effects": 6,
|
|
14
|
+
"/blog/hello-world": 20,
|
|
15
|
+
"/feedback": 38,
|
|
16
|
+
"/resources/dependencies": 2,
|
|
17
|
+
"/resources/components": 6,
|
|
18
|
+
"/gdpr": 3,
|
|
19
|
+
"/scratch": 51,
|
|
20
|
+
"/getting-started": 3,
|
|
21
|
+
"/resources/pro": 1,
|
|
22
|
+
"/todo": 23,
|
|
23
|
+
"/thank-you": 1
|
|
24
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "contacts",
|
|
3
|
+
"title": "Contacts",
|
|
4
|
+
"description": "Contact Information",
|
|
5
|
+
"fields": [
|
|
6
|
+
{
|
|
7
|
+
"name": "full_name",
|
|
8
|
+
"type": "string",
|
|
9
|
+
"label": "Full Name",
|
|
10
|
+
"required": false,
|
|
11
|
+
"placeholder": "Full Name",
|
|
12
|
+
"helper": "Full Name",
|
|
13
|
+
"minLength": 8,
|
|
14
|
+
"maxLength": 255
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "spacer"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "phone_number",
|
|
21
|
+
"type": "tel",
|
|
22
|
+
"label": "Phone Number",
|
|
23
|
+
"required": false,
|
|
24
|
+
"placeholder": "Phone Number",
|
|
25
|
+
"helper": "Primary Phone Number"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "spacer"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "email_address",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"label": "Email Address",
|
|
34
|
+
"required": false,
|
|
35
|
+
"placeholder": "Email Address",
|
|
36
|
+
"helper": "Email Address",
|
|
37
|
+
"minLength": 8,
|
|
38
|
+
"maxLength": 255
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"settings": {
|
|
42
|
+
"submitText": "Submit",
|
|
43
|
+
"successMessage": "Thank you for your submission.",
|
|
44
|
+
"layout": "stacked",
|
|
45
|
+
"honeypot": true,
|
|
46
|
+
"rateLimitPerMinute": 3
|
|
47
|
+
},
|
|
48
|
+
"actions": {
|
|
49
|
+
"email": {
|
|
50
|
+
"enabled": false,
|
|
51
|
+
"recipients": "",
|
|
52
|
+
"subjectPrefix": "[Contacts]"
|
|
53
|
+
},
|
|
54
|
+
"webhook": {
|
|
55
|
+
"enabled": false,
|
|
56
|
+
"url": "",
|
|
57
|
+
"method": "POST"
|
|
58
|
+
},
|
|
59
|
+
"collection": {
|
|
60
|
+
"enabled": true,
|
|
61
|
+
"slug": "contacts"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"createdAt": "2026-03-17T12:35:44.569Z",
|
|
65
|
+
"updatedAt": "2026-03-17T12:35:44.569Z"
|
|
66
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "enquiries",
|
|
3
|
+
"title": "Enquiries",
|
|
4
|
+
"description": "Get in touch with us",
|
|
5
|
+
"fields": [
|
|
6
|
+
{
|
|
7
|
+
"name": "full_name",
|
|
8
|
+
"type": "string",
|
|
9
|
+
"label": "Full Name",
|
|
10
|
+
"required": true,
|
|
11
|
+
"placeholder": "Your full name",
|
|
12
|
+
"helper": "",
|
|
13
|
+
"validation": {
|
|
14
|
+
"min": 2,
|
|
15
|
+
"max": 100
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "email",
|
|
20
|
+
"type": "email",
|
|
21
|
+
"label": "Email Address",
|
|
22
|
+
"required": true,
|
|
23
|
+
"placeholder": "your@email.com",
|
|
24
|
+
"helper": ""
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "phone",
|
|
28
|
+
"type": "tel",
|
|
29
|
+
"label": "Phone Number",
|
|
30
|
+
"required": false,
|
|
31
|
+
"placeholder": "+44 7700 000000",
|
|
32
|
+
"helper": "Optional"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "subject",
|
|
36
|
+
"type": "select",
|
|
37
|
+
"label": "Subject",
|
|
38
|
+
"required": true,
|
|
39
|
+
"placeholder": "Please select a subject",
|
|
40
|
+
"helper": "",
|
|
41
|
+
"options": [
|
|
42
|
+
{
|
|
43
|
+
"value": "general",
|
|
44
|
+
"label": "General Enquiry"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"value": "support",
|
|
48
|
+
"label": "Support"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"value": "sales",
|
|
52
|
+
"label": "Sales"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"value": "partnership",
|
|
56
|
+
"label": "Partnership"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"value": "other",
|
|
60
|
+
"label": "Other"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "message",
|
|
66
|
+
"type": "textarea",
|
|
67
|
+
"label": "Message",
|
|
68
|
+
"required": true,
|
|
69
|
+
"placeholder": "How can we help you?",
|
|
70
|
+
"helper": "",
|
|
71
|
+
"rows": 4,
|
|
72
|
+
"validation": {
|
|
73
|
+
"min": 10,
|
|
74
|
+
"max": 2000
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"settings": {
|
|
79
|
+
"submitText": "Send Message",
|
|
80
|
+
"successMessage": "Thanks for reaching out! We'll get back to you shortly.",
|
|
81
|
+
"layout": "stacked",
|
|
82
|
+
"honeypot": true,
|
|
83
|
+
"rateLimitPerMinute": 3
|
|
84
|
+
},
|
|
85
|
+
"actions": {
|
|
86
|
+
"email": {
|
|
87
|
+
"enabled": false,
|
|
88
|
+
"recipients": "",
|
|
89
|
+
"subjectPrefix": "[enquiries]"
|
|
90
|
+
},
|
|
91
|
+
"webhook": {
|
|
92
|
+
"enabled": false,
|
|
93
|
+
"url": "",
|
|
94
|
+
"method": "POST"
|
|
95
|
+
},
|
|
96
|
+
"collection": {
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"slug": "enquiries"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"createdAt": "2026-03-17T12:35:44.569Z",
|
|
102
|
+
"updatedAt": "2026-03-17T12:35:44.569Z"
|
|
103
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "feedback",
|
|
3
|
+
"title": "Feedback",
|
|
4
|
+
"description": "Share your feedback with us",
|
|
5
|
+
"fields": [
|
|
6
|
+
{
|
|
7
|
+
"name": "name",
|
|
8
|
+
"type": "string",
|
|
9
|
+
"label": "Your Name",
|
|
10
|
+
"required": true,
|
|
11
|
+
"placeholder": "Please enter your full name",
|
|
12
|
+
"helper": "Please enter your full name",
|
|
13
|
+
"validation": {
|
|
14
|
+
"min": 2,
|
|
15
|
+
"max": 100
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "email",
|
|
20
|
+
"type": "email",
|
|
21
|
+
"label": "Email Address",
|
|
22
|
+
"required": true,
|
|
23
|
+
"placeholder": "your@email.com",
|
|
24
|
+
"helper": "Please enter your email address"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "rating",
|
|
28
|
+
"type": "select",
|
|
29
|
+
"label": "Overall Rating",
|
|
30
|
+
"required": true,
|
|
31
|
+
"helper": "Tell us how we are doing!",
|
|
32
|
+
"options": [
|
|
33
|
+
{
|
|
34
|
+
"value": "none",
|
|
35
|
+
"label": "Please Choose"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"value": "excellent",
|
|
39
|
+
"label": "Excellent"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"value": "good",
|
|
43
|
+
"label": "Good"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"value": "average",
|
|
47
|
+
"label": "Average"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"value": "poor",
|
|
51
|
+
"label": "Poor"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "category",
|
|
57
|
+
"type": "select",
|
|
58
|
+
"label": "Category",
|
|
59
|
+
"required": true,
|
|
60
|
+
"placeholder": "Please select a category",
|
|
61
|
+
"helper": "",
|
|
62
|
+
"options": [
|
|
63
|
+
{
|
|
64
|
+
"value": "general",
|
|
65
|
+
"label": "General"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"value": "bug-report",
|
|
69
|
+
"label": "Bug Report"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"value": "feature-request",
|
|
73
|
+
"label": "Feature Request"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"value": "praise",
|
|
77
|
+
"label": "Praise"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "subject",
|
|
83
|
+
"type": "string",
|
|
84
|
+
"label": "Subject",
|
|
85
|
+
"required": true,
|
|
86
|
+
"placeholder": "Brief summary of your feedback",
|
|
87
|
+
"helper": "",
|
|
88
|
+
"validation": {
|
|
89
|
+
"max": 200
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "message",
|
|
94
|
+
"type": "textarea",
|
|
95
|
+
"label": "Your Feedback",
|
|
96
|
+
"required": true,
|
|
97
|
+
"placeholder": "Please share your thoughts in detail…",
|
|
98
|
+
"helper": "Please share your thoughts in detail…",
|
|
99
|
+
"rows": 4,
|
|
100
|
+
"validation": {
|
|
101
|
+
"min": 10,
|
|
102
|
+
"max": 2000
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"settings": {
|
|
107
|
+
"submitText": "Submit Feedback",
|
|
108
|
+
"successMessage": "Thank you for your feedback! We appreciate you taking the time.",
|
|
109
|
+
"layout": "stacked",
|
|
110
|
+
"honeypot": true,
|
|
111
|
+
"rateLimitPerMinute": 3
|
|
112
|
+
},
|
|
113
|
+
"actions": {
|
|
114
|
+
"email": {
|
|
115
|
+
"enabled": true,
|
|
116
|
+
"recipients": "",
|
|
117
|
+
"subjectPrefix": "[feedback]"
|
|
118
|
+
},
|
|
119
|
+
"webhook": {
|
|
120
|
+
"enabled": false,
|
|
121
|
+
"url": "",
|
|
122
|
+
"method": "POST"
|
|
123
|
+
},
|
|
124
|
+
"collection": {
|
|
125
|
+
"enabled": true,
|
|
126
|
+
"slug": "feedback"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"createdAt": "2026-03-17T12:35:44.569Z",
|
|
130
|
+
"updatedAt": "2026-03-17T12:35:44.569Z"
|
|
131
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "notes",
|
|
3
|
+
"title": "Notes",
|
|
4
|
+
"description": "Free-form notes with categories and tags.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{
|
|
7
|
+
"name": "title",
|
|
8
|
+
"type": "string",
|
|
9
|
+
"label": "Title",
|
|
10
|
+
"required": true,
|
|
11
|
+
"placeholder": "Note title"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "content",
|
|
15
|
+
"type": "textarea",
|
|
16
|
+
"label": "Content",
|
|
17
|
+
"required": true,
|
|
18
|
+
"placeholder": "Write your note here…",
|
|
19
|
+
"rows": 5
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "category",
|
|
23
|
+
"type": "select",
|
|
24
|
+
"label": "Category",
|
|
25
|
+
"required": false,
|
|
26
|
+
"options": [
|
|
27
|
+
{
|
|
28
|
+
"value": "general",
|
|
29
|
+
"label": "General"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"value": "idea",
|
|
33
|
+
"label": "Idea"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"value": "reminder",
|
|
37
|
+
"label": "Reminder"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"value": "reference",
|
|
41
|
+
"label": "Reference"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "tags",
|
|
47
|
+
"type": "string",
|
|
48
|
+
"label": "Tags",
|
|
49
|
+
"required": false,
|
|
50
|
+
"placeholder": "Comma-separated tags",
|
|
51
|
+
"helper": "Separate tags with commas"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"settings": {
|
|
55
|
+
"submitText": "Save Note",
|
|
56
|
+
"successMessage": "Note saved successfully.",
|
|
57
|
+
"layout": "stacked",
|
|
58
|
+
"honeypot": true,
|
|
59
|
+
"rateLimitPerMinute": 3
|
|
60
|
+
},
|
|
61
|
+
"actions": {
|
|
62
|
+
"email": {
|
|
63
|
+
"enabled": false,
|
|
64
|
+
"recipients": "",
|
|
65
|
+
"subjectPrefix": "[notes]"
|
|
66
|
+
},
|
|
67
|
+
"webhook": {
|
|
68
|
+
"enabled": false,
|
|
69
|
+
"url": "",
|
|
70
|
+
"method": "POST"
|
|
71
|
+
},
|
|
72
|
+
"collection": {
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"slug": "notes"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"createdAt": "2026-03-17T12:35:44.569Z",
|
|
78
|
+
"updatedAt": "2026-03-17T12:35:44.569Z"
|
|
79
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "to-do",
|
|
3
|
+
"title": "To-Do",
|
|
4
|
+
"description": "Task tracking with status, priority, and due dates.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{
|
|
7
|
+
"name": "title",
|
|
8
|
+
"type": "string",
|
|
9
|
+
"label": "Title",
|
|
10
|
+
"required": true,
|
|
11
|
+
"placeholder": "Task title"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "description",
|
|
15
|
+
"type": "textarea",
|
|
16
|
+
"label": "Description",
|
|
17
|
+
"required": false,
|
|
18
|
+
"placeholder": "Task details…",
|
|
19
|
+
"rows": 3
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "status",
|
|
23
|
+
"type": "select",
|
|
24
|
+
"label": "Status",
|
|
25
|
+
"required": true,
|
|
26
|
+
"options": [
|
|
27
|
+
{
|
|
28
|
+
"value": "pending",
|
|
29
|
+
"label": "Pending"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"value": "in-progress",
|
|
33
|
+
"label": "In Progress"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"value": "done",
|
|
37
|
+
"label": "Done"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "priority",
|
|
43
|
+
"type": "select",
|
|
44
|
+
"label": "Priority",
|
|
45
|
+
"required": false,
|
|
46
|
+
"options": [
|
|
47
|
+
{
|
|
48
|
+
"value": "low",
|
|
49
|
+
"label": "Low"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"value": "medium",
|
|
53
|
+
"label": "Medium"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"value": "high",
|
|
57
|
+
"label": "High"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "due_date",
|
|
63
|
+
"type": "date",
|
|
64
|
+
"label": "Due Date",
|
|
65
|
+
"required": false
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "assigned_to",
|
|
69
|
+
"type": "string",
|
|
70
|
+
"label": "Assigned To",
|
|
71
|
+
"required": false,
|
|
72
|
+
"placeholder": "Name or email"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"settings": {
|
|
76
|
+
"submitText": "Add Task",
|
|
77
|
+
"successMessage": "Task added successfully.",
|
|
78
|
+
"layout": "stacked",
|
|
79
|
+
"honeypot": true,
|
|
80
|
+
"rateLimitPerMinute": 3
|
|
81
|
+
},
|
|
82
|
+
"actions": {
|
|
83
|
+
"email": {
|
|
84
|
+
"enabled": false,
|
|
85
|
+
"recipients": "",
|
|
86
|
+
"subjectPrefix": "[to-do]"
|
|
87
|
+
},
|
|
88
|
+
"webhook": {
|
|
89
|
+
"enabled": false,
|
|
90
|
+
"url": "",
|
|
91
|
+
"method": "POST"
|
|
92
|
+
},
|
|
93
|
+
"collection": {
|
|
94
|
+
"enabled": true,
|
|
95
|
+
"slug": "to-do"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"createdAt": "2026-03-17T12:35:44.569Z",
|
|
99
|
+
"updatedAt": "2026-03-17T12:35:44.569Z"
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
package/public/css/site.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:clamp(1.5rem,4vw,2rem);font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,1.25rem)}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
|
|
1
|
+
body,button,input,select,textarea{font-family:Roboto,sans-serif}.navbar-actions{order:4;margin-left:auto;display:flex;align-items:center;gap:.5rem}.navbar-dark .navbar-actions{color:var(--dm-text-inverse, rgba(255, 255, 255, .85))}.navbar-dark .site-search-shortcut-hint{color:#fff9;border-color:#ffffff40}.navbar-light .navbar-actions{color:var(--dm-text, #111)}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:clamp(1.5rem,4vw,2rem);font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,1.25rem)}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
|
package/server/server.js
CHANGED
|
@@ -75,6 +75,7 @@ await app.register(helmet, {
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
crossOriginEmbedderPolicy: false, // allow embedding images/resources
|
|
78
|
+
hsts: false, // disable HSTS — server runs HTTP only; HSTS would force browser to https
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
await app.register(jwt, { secret: process.env.JWT_SECRET });
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Types Service
|
|
3
|
+
* Preset Collection — seeds roles on first startup, caches them in memory.
|
|
4
|
+
* Auth middleware calls getRoleMap() / getPermissionsFor() at request time.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {v4 as uuidv4} from 'uuid';
|
|
9
|
+
import {config} from '../config.js';
|
|
10
|
+
|
|
11
|
+
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
12
|
+
const SLUG = 'user-types';
|
|
13
|
+
const DIR = path.join(COLLECTIONS_DIR, SLUG);
|
|
14
|
+
const SCHEMA_PATH = path.join(DIR, 'schema.json');
|
|
15
|
+
const DATA_PATH = path.join(DIR, 'data.json');
|
|
16
|
+
|
|
17
|
+
export const RESOURCES = ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'plugins', 'collections', 'views', 'actions', 'blocks'];
|
|
18
|
+
export const ACTIONS = ['read', 'create', 'update', 'delete'];
|
|
19
|
+
|
|
20
|
+
const PRESET_SCHEMA = {
|
|
21
|
+
slug: SLUG,
|
|
22
|
+
title: 'User Types',
|
|
23
|
+
description: 'CMS role definitions — managed by the system.',
|
|
24
|
+
preset: true,
|
|
25
|
+
fields: [
|
|
26
|
+
{name: 'name', label: 'Name (slug)', type: 'text', required: true},
|
|
27
|
+
{name: 'label', label: 'Label', type: 'text', required: true},
|
|
28
|
+
{name: 'level', label: 'Level', type: 'number', required: true},
|
|
29
|
+
{
|
|
30
|
+
name: 'permissions',
|
|
31
|
+
label: 'Permissions',
|
|
32
|
+
type: 'multi-select',
|
|
33
|
+
options: RESOURCES.flatMap(r => [r, ...ACTIONS.map(a => `${r}.${a}`)])
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'badgeClass', label: 'Badge Class', type: 'select',
|
|
37
|
+
options: ['badge-danger', 'badge-warning', 'badge-info', 'badge-secondary', 'badge-success', 'badge-primary']
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
api: {
|
|
41
|
+
create: {enabled: false, access: 'admin'},
|
|
42
|
+
read: {enabled: false, access: 'admin'},
|
|
43
|
+
update: {enabled: false, access: 'admin'},
|
|
44
|
+
delete: {enabled: false, access: 'admin'}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const SEED_ENTRIES = [
|
|
49
|
+
{name: 'admin', label: 'Admin', level: 0, permissions: RESOURCES, badgeClass: 'badge-danger'},
|
|
50
|
+
{
|
|
51
|
+
name: 'manager',
|
|
52
|
+
label: 'Manager',
|
|
53
|
+
level: 1,
|
|
54
|
+
permissions: ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'collections', 'views', 'actions'],
|
|
55
|
+
badgeClass: 'badge-warning'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'editor',
|
|
59
|
+
label: 'Editor',
|
|
60
|
+
level: 2,
|
|
61
|
+
permissions: ['pages.read', 'pages.create', 'pages.update', 'media'],
|
|
62
|
+
badgeClass: 'badge-info'
|
|
63
|
+
},
|
|
64
|
+
{name: 'subscriber', label: 'Subscriber', level: 3, permissions: [], badgeClass: 'badge-secondary'}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// In-memory cache
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** @type {Map<string,{label:string,level:number,badgeClass:string}>} */
|
|
72
|
+
let roleMap = new Map();
|
|
73
|
+
|
|
74
|
+
/** @type {Map<string,string[]>} resource (and resource.action) → role names */
|
|
75
|
+
let permissionsMap = new Map();
|
|
76
|
+
|
|
77
|
+
/** @type {Map<string,string[]>} role name → raw permissions array */
|
|
78
|
+
let rawPermissionsMap = new Map();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build in-memory maps from an array of data entries.
|
|
82
|
+
* Supports both bare resource names ('pages') and dotted action strings ('pages.read').
|
|
83
|
+
* Bare names expand to all four actions for backward compatibility.
|
|
84
|
+
*
|
|
85
|
+
* @param {object[]} entries
|
|
86
|
+
*/
|
|
87
|
+
function buildCache(entries) {
|
|
88
|
+
roleMap = new Map();
|
|
89
|
+
permissionsMap = new Map();
|
|
90
|
+
rawPermissionsMap = new Map();
|
|
91
|
+
|
|
92
|
+
const addTo = (key, role) => {
|
|
93
|
+
if (!permissionsMap.has(key)) permissionsMap.set(key, []);
|
|
94
|
+
if (!permissionsMap.get(key).includes(role)) permissionsMap.get(key).push(role);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const d = entry.data;
|
|
99
|
+
roleMap.set(d.name, {label: d.label, level: d.level, badgeClass: d.badgeClass || ''});
|
|
100
|
+
rawPermissionsMap.set(d.name, d.permissions || []);
|
|
101
|
+
for (const perm of (d.permissions || [])) {
|
|
102
|
+
if (perm.includes('.')) {
|
|
103
|
+
const [res] = perm.split('.');
|
|
104
|
+
addTo(perm, d.name); // 'pages.read' → [role]
|
|
105
|
+
addTo(res, d.name); // 'pages' → [role] (any-action union)
|
|
106
|
+
} else {
|
|
107
|
+
addTo(perm, d.name); // 'pages' (bare) → [role]
|
|
108
|
+
for (const action of ACTIONS) {
|
|
109
|
+
addTo(`${perm}.${action}`, d.name); // expand to all actions
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Public API
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Seed the preset collection on first startup (no-op if data.json already exists).
|
|
122
|
+
*
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
export async function seed() {
|
|
126
|
+
await fs.mkdir(DIR, {recursive: true});
|
|
127
|
+
|
|
128
|
+
// Always write schema (overwrite to keep in sync with code)
|
|
129
|
+
await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
|
|
130
|
+
|
|
131
|
+
// Only write data if it doesn't exist yet
|
|
132
|
+
try {
|
|
133
|
+
await fs.access(DATA_PATH);
|
|
134
|
+
} catch {
|
|
135
|
+
const entries = SEED_ENTRIES.map(data => ({
|
|
136
|
+
id: uuidv4(),
|
|
137
|
+
data,
|
|
138
|
+
createdAt: new Date().toISOString(),
|
|
139
|
+
updatedAt: new Date().toISOString()
|
|
140
|
+
}));
|
|
141
|
+
await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Load the collection from disk into the in-memory cache.
|
|
147
|
+
*
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
export async function load() {
|
|
151
|
+
try {
|
|
152
|
+
const raw = await fs.readFile(DATA_PATH, 'utf8');
|
|
153
|
+
const entries = JSON.parse(raw);
|
|
154
|
+
buildCache(entries);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn('[userTypes] Failed to load user-types collection:', err.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reload from disk — call after any CRUD on the user-types collection.
|
|
162
|
+
*
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
*/
|
|
165
|
+
export async function invalidate() {
|
|
166
|
+
await load();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Return the full role map.
|
|
171
|
+
*
|
|
172
|
+
* @returns {Map<string,{label:string,level:number,badgeClass:string}>}
|
|
173
|
+
*/
|
|
174
|
+
export function getRoleMap() {
|
|
175
|
+
return roleMap;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Return the level for a named role, or Infinity if not found.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} roleName
|
|
182
|
+
* @returns {number}
|
|
183
|
+
*/
|
|
184
|
+
export function getRoleLevel(roleName) {
|
|
185
|
+
return roleMap.get(roleName)?.level ?? Infinity;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Return the role names allowed to access a resource (and optional action).
|
|
190
|
+
* - getPermissionsFor('pages') → roles with ANY action on pages (backward compat)
|
|
191
|
+
* - getPermissionsFor('pages', 'delete')→ roles with delete on pages
|
|
192
|
+
* - getPermissionsFor('pages.delete') → same as above (dot-notation shorthand)
|
|
193
|
+
*
|
|
194
|
+
* @param {string} resource - Resource key, or 'resource.action' dot notation
|
|
195
|
+
* @param {string} [action] - Optional action (read | create | update | delete)
|
|
196
|
+
* @returns {string[]}
|
|
197
|
+
*/
|
|
198
|
+
export function getPermissionsFor(resource, action) {
|
|
199
|
+
if (action) {
|
|
200
|
+
return permissionsMap.get(`${resource}.${action}`) ?? [];
|
|
201
|
+
}
|
|
202
|
+
if (resource.includes('.')) {
|
|
203
|
+
return permissionsMap.get(resource) ?? [];
|
|
204
|
+
}
|
|
205
|
+
return permissionsMap.get(resource) ?? [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Return the raw permissions array for a role — used by the /api/auth/permissions endpoint.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} roleName
|
|
212
|
+
* @returns {string[]}
|
|
213
|
+
*/
|
|
214
|
+
export function getPermissionsForRole(roleName) {
|
|
215
|
+
return rawPermissionsMap.get(roleName) ?? [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Return role names ordered from most to least privileged.
|
|
220
|
+
*
|
|
221
|
+
* @returns {string[]}
|
|
222
|
+
*/
|
|
223
|
+
export function getRoleHierarchy() {
|
|
224
|
+
return [...roleMap.entries()]
|
|
225
|
+
.sort((a, b) => a[1].level - b[1].level)
|
|
226
|
+
.map(([key]) => key);
|
|
227
|
+
}
|