domma-cms 0.2.1 → 0.3.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/admin/css/admin.css +1 -1200
- package/admin/js/api.js +1 -242
- package/admin/js/app.js +5 -279
- package/admin/js/config/sidebar-config.js +1 -115
- package/admin/js/lib/card.js +1 -63
- package/admin/js/lib/image-editor.js +1 -869
- package/admin/js/lib/markdown-toolbar.js +46 -421
- package/admin/js/templates/layouts.html +44 -7
- package/admin/js/templates/page-editor.html +9 -0
- package/admin/js/templates/settings.html +18 -1
- package/admin/js/templates/users.html +29 -4
- package/admin/js/views/collection-editor.js +3 -487
- package/admin/js/views/collection-entries.js +1 -484
- package/admin/js/views/collections.js +1 -153
- package/admin/js/views/dashboard.js +1 -56
- package/admin/js/views/documentation.js +1 -12
- package/admin/js/views/index.js +1 -39
- package/admin/js/views/layouts.js +9 -42
- package/admin/js/views/login.js +7 -251
- package/admin/js/views/media.js +1 -240
- package/admin/js/views/navigation.js +14 -212
- package/admin/js/views/page-editor.js +53 -661
- package/admin/js/views/pages.js +5 -72
- package/admin/js/views/plugins.js +13 -90
- package/admin/js/views/settings.js +1 -199
- package/admin/js/views/tutorials.js +1 -12
- package/admin/js/views/user-editor.js +1 -88
- package/admin/js/views/users.js +7 -76
- package/config/auth.json +1 -17
- package/config/navigation.json +15 -0
- package/config/site.json +5 -4
- package/package.json +1 -1
- package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
- package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
- package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
- package/plugins/domma-effects/public/celebrations/index.js +1 -535
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
- package/plugins/example-analytics/stats.json +16 -12
- package/plugins/form-builder/admin/templates/form-editor.html +158 -130
- package/plugins/form-builder/admin/views/form-editor.js +3 -1
- package/plugins/form-builder/data/forms/contact-details.json +71 -35
- package/plugins/form-builder/data/forms/feedback.json +130 -0
- package/plugins/form-builder/data/submissions/feedback.json +1 -0
- package/plugins/form-builder/public/form-logic-engine.js +1 -568
- package/public/css/site.css +1 -302
- package/public/js/btt.js +1 -90
- package/public/js/cookie-consent.js +1 -61
- package/public/js/site.js +1 -204
- package/scripts/setup.js +4 -4
- package/server/middleware/auth.js +44 -21
- package/server/routes/api/auth.js +38 -8
- package/server/routes/api/collections.js +18 -5
- package/server/routes/api/layouts.js +18 -4
- package/server/routes/api/media.js +2 -3
- package/server/routes/api/navigation.js +2 -3
- package/server/routes/api/pages.js +3 -3
- package/server/routes/api/settings.js +2 -3
- package/server/routes/api/users.js +4 -6
- package/server/routes/public.js +3 -3
- package/server/server.js +8 -0
- package/server/services/markdown.js +102 -3
- package/server/services/userTypes.js +167 -0
- package/plugins/form-builder/email.js +0 -103
package/public/js/site.js
CHANGED
|
@@ -1,204 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Public Site - Client-side Domma init
|
|
3
|
-
* Reads config injected by the server and initialises navbar, footer, and features.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
$(() => {
|
|
7
|
-
const nav = window.__CMS_NAV__ || {};
|
|
8
|
-
const site = window.__CMS_SITE__ || {};
|
|
9
|
-
|
|
10
|
-
// Initialise navbar
|
|
11
|
-
const $navbar = $('#site-navbar');
|
|
12
|
-
if ($navbar.length && nav.brand) {
|
|
13
|
-
// Build brand.html when an icon is configured — Domma navbar doesn't natively support data-icon spans
|
|
14
|
-
const brand = {...nav.brand};
|
|
15
|
-
if (brand.icon) {
|
|
16
|
-
let html = `<span data-icon="${brand.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`;
|
|
17
|
-
if (brand.text) html += `<span class="navbar-brand-text">${brand.text}</span>`;
|
|
18
|
-
brand.html = html;
|
|
19
|
-
}
|
|
20
|
-
Domma.elements.navbar('#site-navbar', {
|
|
21
|
-
brand,
|
|
22
|
-
items: nav.items || [],
|
|
23
|
-
variant: nav.variant || 'dark',
|
|
24
|
-
position: nav.position || 'sticky',
|
|
25
|
-
collapsible: true
|
|
26
|
-
});
|
|
27
|
-
Domma.icons.scan('#site-navbar');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Initialise footer
|
|
31
|
-
const $footer = $('#site-footer');
|
|
32
|
-
if ($footer.length) {
|
|
33
|
-
const social = site.social || {};
|
|
34
|
-
const SOCIAL_ICONS = {
|
|
35
|
-
twitter: {
|
|
36
|
-
label: 'X / Twitter',
|
|
37
|
-
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>'
|
|
38
|
-
},
|
|
39
|
-
facebook: {
|
|
40
|
-
label: 'Facebook',
|
|
41
|
-
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>'
|
|
42
|
-
},
|
|
43
|
-
instagram: {
|
|
44
|
-
label: 'Instagram',
|
|
45
|
-
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>'
|
|
46
|
-
},
|
|
47
|
-
linkedin: {
|
|
48
|
-
label: 'LinkedIn',
|
|
49
|
-
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>'
|
|
50
|
-
},
|
|
51
|
-
github: {
|
|
52
|
-
label: 'GitHub',
|
|
53
|
-
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>'
|
|
54
|
-
},
|
|
55
|
-
youtube: {
|
|
56
|
-
label: 'YouTube',
|
|
57
|
-
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>'
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
let html = '<div class="footer-inner container">';
|
|
62
|
-
|
|
63
|
-
if (site.footer) {
|
|
64
|
-
const footer = site.footer;
|
|
65
|
-
html += `<p>${footer.copyright || ''}</p>`;
|
|
66
|
-
if (footer.links?.length) {
|
|
67
|
-
html += `<nav class="footer-links">`;
|
|
68
|
-
footer.links.forEach(link => {
|
|
69
|
-
html += `<a href="${link.url}">${link.text}</a>`;
|
|
70
|
-
});
|
|
71
|
-
html += `</nav>`;
|
|
72
|
-
}
|
|
73
|
-
const activeSocial = Object.keys(SOCIAL_ICONS).filter(k => social[k]);
|
|
74
|
-
if (activeSocial.length) {
|
|
75
|
-
html += `<div class="footer-social">`;
|
|
76
|
-
activeSocial.forEach(k => {
|
|
77
|
-
const {label, svg} = SOCIAL_ICONS[k];
|
|
78
|
-
html += `<a href="${social[k]}" target="_blank" rel="noopener noreferrer" aria-label="${label}" class="footer-social-link">${svg}</a>`;
|
|
79
|
-
});
|
|
80
|
-
html += `</div>`;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const storedPref = S.get('reduced_motion');
|
|
85
|
-
const motionOn = storedPref !== null
|
|
86
|
-
? !!storedPref
|
|
87
|
-
: !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
88
|
-
html += `<label class="form-switch footer-motion-switch" title="Reduce motion">` +
|
|
89
|
-
`<input type="checkbox" class="form-switch-input" id="dm-motion-switch"${motionOn ? ' checked' : ''}>` +
|
|
90
|
-
`<span class="form-switch-label">Reduce motion</span>` +
|
|
91
|
-
`</label>`;
|
|
92
|
-
|
|
93
|
-
html += '</div>';
|
|
94
|
-
$footer.html(html, { safe: false });
|
|
95
|
-
|
|
96
|
-
document.getElementById('dm-motion-switch').addEventListener('change', function () {
|
|
97
|
-
S.set('reduced_motion', this.checked);
|
|
98
|
-
window.location.reload();
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Initialise sidebar (auto-generate from headings if enabled)
|
|
103
|
-
const $sidebar = $('#site-sidebar');
|
|
104
|
-
if ($sidebar.length) {
|
|
105
|
-
Domma.elements.sidebar('#site-sidebar', {
|
|
106
|
-
autoGenerate: true,
|
|
107
|
-
selector: 'h2, h3',
|
|
108
|
-
collapsible: false,
|
|
109
|
-
push: true,
|
|
110
|
-
contentSelector: '.site-content'
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Icons
|
|
115
|
-
Domma.icons.scan();
|
|
116
|
-
|
|
117
|
-
// -------------------------------------------------------
|
|
118
|
-
// Auto-initialise Domma components inside page content
|
|
119
|
-
// -------------------------------------------------------
|
|
120
|
-
const $pageBody = $('.page-body');
|
|
121
|
-
if ($pageBody.length) {
|
|
122
|
-
|
|
123
|
-
// Accordion — reads data-multi attribute set by [accordion multiple="true"] shortcode
|
|
124
|
-
$pageBody.find('.accordion').each(function () {
|
|
125
|
-
Domma.elements.accordion(this, {
|
|
126
|
-
allowMultiple: this.dataset.multi === 'true'
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Tabs
|
|
131
|
-
$pageBody.find('.tabs').each(function () {
|
|
132
|
-
Domma.elements.tabs(this);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Carousel — reads data attributes set by [carousel] shortcode
|
|
136
|
-
$pageBody.find('.carousel').each(function () {
|
|
137
|
-
Domma.elements.carousel(this, {
|
|
138
|
-
autoplay: this.dataset.autoplay === 'true',
|
|
139
|
-
interval: parseInt(this.dataset.interval, 10) || 5000,
|
|
140
|
-
loop: this.dataset.loop !== 'false',
|
|
141
|
-
animation: this.dataset.animation || 'slide'
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Countdown — initialises .dm-countdown elements via E.timer()
|
|
146
|
-
$pageBody.find('.dm-countdown').each(function () {
|
|
147
|
-
const opts = {autoStart: true};
|
|
148
|
-
if (this.dataset.to) opts.targetDate = new Date(this.dataset.to);
|
|
149
|
-
if (this.dataset.duration) opts.duration = parseInt(this.dataset.duration, 10);
|
|
150
|
-
if (this.dataset.format) opts.format = this.dataset.format;
|
|
151
|
-
Domma.elements.timer(this, opts);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Tooltips
|
|
155
|
-
$pageBody.find('[data-tooltip]').each(function () {
|
|
156
|
-
Domma.elements.tooltip(this, {
|
|
157
|
-
content: $(this).data('tooltip'),
|
|
158
|
-
position: $(this).data('tooltip-position') || 'top'
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Collapsible cards
|
|
163
|
-
$pageBody.find('.card[data-collapsible]').each(function () {
|
|
164
|
-
const header = this.querySelector('.card-header');
|
|
165
|
-
if (header) {
|
|
166
|
-
header.addEventListener('click', () => this.classList.toggle('is-collapsed'));
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Slideover triggers — wired from [slideover] shortcode
|
|
171
|
-
$pageBody.find('.dm-so-trigger').each(function () {
|
|
172
|
-
this.addEventListener('click', () => {
|
|
173
|
-
const targetId = this.dataset.soTarget;
|
|
174
|
-
const contentEl = document.getElementById(targetId);
|
|
175
|
-
if (!contentEl) return;
|
|
176
|
-
const so = E.slideover({
|
|
177
|
-
title: contentEl.dataset.soTitle || '',
|
|
178
|
-
size: contentEl.dataset.soSize || 'md',
|
|
179
|
-
position: contentEl.dataset.soPosition || 'right'
|
|
180
|
-
});
|
|
181
|
-
contentEl.style.display = '';
|
|
182
|
-
so.setContent(contentEl);
|
|
183
|
-
so.open();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// DConfig — merge window.__CMS_DCONFIG__ (editor section) with inline [dconfig] shortcodes
|
|
189
|
-
// Inline shortcodes win on selector conflict (assigned last via Object.assign)
|
|
190
|
-
if (typeof $.setup === 'function') {
|
|
191
|
-
const merged = Object.assign({}, window.__CMS_DCONFIG__ || {});
|
|
192
|
-
document.querySelectorAll('.dm-page-config[data-config]').forEach(el => {
|
|
193
|
-
try {
|
|
194
|
-
const decoded = atob(el.dataset.config);
|
|
195
|
-
const parsed = JSON.parse(decoded);
|
|
196
|
-
Object.assign(merged, parsed);
|
|
197
|
-
} catch { /* skip malformed blocks */
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
if (Object.keys(merged).length) {
|
|
201
|
-
$.setup(merged);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
});
|
|
1
|
+
$(()=>{const r=window.__CMS_NAV__||{},h=window.__CMS_SITE__||{};if($("#site-navbar").length&&r.brand){const t={...r.brand};if(t.icon){let a=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`;t.text&&(a+=`<span class="navbar-brand-text">${t.text}</span>`),t.html=a}Domma.elements.navbar("#site-navbar",{brand:t,items:r.items||[],variant:r.variant||"dark",position:r.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const m=$("#site-footer");if(m.length){const t=h.social||{},a={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 e='<div class="footer-inner container">';if(h.footer){const c=h.footer;e+=`<p>${c.copyright||""}</p>`,c.links?.length&&(e+='<nav class="footer-links">',c.links.forEach(o=>{e+=`<a href="${o.url}">${o.text}</a>`}),e+="</nav>");const l=Object.keys(a).filter(o=>t[o]);l.length&&(e+='<div class="footer-social">',l.forEach(o=>{const{label:s,svg:g}=a[o];e+=`<a href="${t[o]}" target="_blank" rel="noopener noreferrer" aria-label="${s}" class="footer-social-link">${g}</a>`}),e+="</div>")}const i=S.get("reduced_motion"),f=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches);e+=`<label class="form-switch footer-motion-switch" title="Reduce motion"><input type="checkbox" class="form-switch-input" id="dm-motion-switch"${f?" checked":""}><span class="form-switch-label">Reduce motion</span></label>`,e+="</div>",m.html(e,{safe:!1}),document.getElementById("dm-motion-switch").addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()})}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const n=$(".page-body");if(n.length&&(n.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),n.find(".tabs").each(function(){Domma.elements.tabs(this)}),n.find(".carousel").each(function(){Domma.elements.carousel(this,{autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"})}),n.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)}),n.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),n.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),n.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,a=document.getElementById(t);if(!a)return;const e=E.slideover({title:a.dataset.soTitle||"",size:a.dataset.soSize||"md",position:a.dataset.soPosition||"right"});a.style.display="",e.setContent(a),e.open()})})),typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(a=>{try{const e=atob(a.dataset.config),i=JSON.parse(e);Object.assign(t,i)}catch{}}),Object.keys(t).length){const a={};for(const[e,i]of Object.entries(t)){const f=i?.events?.click,{confirm:c,toast:l,alert:o,...s}=f||{};c||l||o?($(e).on("click",async function(v){if(v.preventDefault(),!(c&&!await E.confirm(c))){if(s.target){const d=$(s.target);s.toggleClass&&d.toggleClass(s.toggleClass),s.addClass&&d.addClass(s.addClass),s.removeClass&&d.removeClass(s.removeClass)}s.href&&(window.location.href=s.href),l&&E.toast(l,{type:s.toastType||"success"}),o&&E.alert(o)}}),Object.keys(s).length&&(a[e]={...i,events:{...i.events,click:s}})):a[e]=i}$.setup(a)}}});
|
package/scripts/setup.js
CHANGED
|
@@ -38,10 +38,10 @@ const THEMES = [
|
|
|
38
38
|
'royal-dark', 'royal-light',
|
|
39
39
|
'lemon-dark', 'lemon-light',
|
|
40
40
|
'silver-dark', 'silver-light',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
'grayve-dark', 'grayve-light',
|
|
42
|
+
'christmas-dark', 'christmas-light',
|
|
43
|
+
'unicorn-dark', 'unicorn-light',
|
|
44
|
+
'dreamy-dark', 'dreamy-light',
|
|
45
45
|
];
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication Middleware
|
|
3
3
|
* JWT-based authentication with role guards for Domma CMS.
|
|
4
|
+
* Role data is read from the userTypes cache (not config.auth.roles).
|
|
4
5
|
*/
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const { roles } = config.auth;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Role hierarchy ordered from most to least privileged.
|
|
11
|
-
* Used by canManageUser() to compare privilege levels.
|
|
12
|
-
*/
|
|
13
|
-
export const ROLE_HIERARCHY = Object.entries(roles)
|
|
14
|
-
.sort((a, b) => a[1].level - b[1].level)
|
|
15
|
-
.map(([key]) => key);
|
|
16
|
-
|
|
17
|
-
export const ROLES = Object.fromEntries(
|
|
18
|
-
Object.entries(roles).map(([key]) => [key.toUpperCase(), key])
|
|
19
|
-
);
|
|
6
|
+
import { getRoleLevel, getRoleHierarchy, getPermissionsFor } from '../services/userTypes.js';
|
|
20
7
|
|
|
21
8
|
/**
|
|
22
9
|
* Verify JWT Bearer token. Populates request.user on success.
|
|
@@ -67,7 +54,35 @@ export function requireRole(allowedRoles) {
|
|
|
67
54
|
}
|
|
68
55
|
|
|
69
56
|
/**
|
|
70
|
-
*
|
|
57
|
+
* Return a preHandler that checks the current user's role has access to a resource.
|
|
58
|
+
* Reads from the userTypes cache at request time — reflects live role changes.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} resource - Resource key (e.g. 'pages', 'users')
|
|
61
|
+
* @returns {Function}
|
|
62
|
+
*/
|
|
63
|
+
export function requirePermission(resource) {
|
|
64
|
+
return async (request, reply) => {
|
|
65
|
+
if (!request.user) {
|
|
66
|
+
return reply.code(401).send({
|
|
67
|
+
statusCode: 401,
|
|
68
|
+
error: 'Unauthorised',
|
|
69
|
+
message: 'Authentication required'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const allowed = getPermissionsFor(resource);
|
|
74
|
+
if (!allowed.includes(request.user.role)) {
|
|
75
|
+
return reply.code(403).send({
|
|
76
|
+
statusCode: 403,
|
|
77
|
+
error: 'Forbidden',
|
|
78
|
+
message: 'Insufficient permissions'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Shorthand preHandler — level-0 role (admin) only.
|
|
71
86
|
*
|
|
72
87
|
* @param {FastifyRequest} request
|
|
73
88
|
* @param {FastifyReply} reply
|
|
@@ -77,21 +92,29 @@ export async function requireAdmin(request, reply) {
|
|
|
77
92
|
if (!request.user) {
|
|
78
93
|
return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
|
|
79
94
|
}
|
|
80
|
-
if (request.user.role !==
|
|
95
|
+
if (getRoleLevel(request.user.role) !== 0) {
|
|
81
96
|
return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
|
|
82
97
|
}
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/**
|
|
86
101
|
* Determine whether an actor can manage a target user.
|
|
87
|
-
* Managers cannot create, edit, or delete
|
|
102
|
+
* Managers cannot create, edit, or delete users with a lower level number (higher privilege).
|
|
88
103
|
*
|
|
89
104
|
* @param {string} actorRole - Role of the user performing the action
|
|
90
105
|
* @param {string} targetRole - Role of the user being acted upon
|
|
91
106
|
* @returns {boolean}
|
|
92
107
|
*/
|
|
93
108
|
export function canManageUser(actorRole, targetRole) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
return getRoleLevel(actorRole) < getRoleLevel(targetRole);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Return role names ordered from most to least privileged.
|
|
114
|
+
* Computed from the userTypes cache.
|
|
115
|
+
*
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function getRoleHierarchyList() {
|
|
119
|
+
return getRoleHierarchy();
|
|
97
120
|
}
|
|
@@ -5,21 +5,40 @@
|
|
|
5
5
|
* POST /api/auth/login - { email, password } → { token, refreshToken, user }
|
|
6
6
|
* GET /api/auth/me - return current user from token
|
|
7
7
|
* POST /api/auth/refresh - { refreshToken } → { token }
|
|
8
|
+
* POST /api/auth/logout - { refreshToken } → { ok: true } — blacklists the refresh token
|
|
8
9
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import {config} from '../../config.js';
|
|
11
|
+
import {authenticate} from '../../middleware/auth.js';
|
|
11
12
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
countUsers,
|
|
14
|
+
createUser,
|
|
15
|
+
getUserByEmail,
|
|
16
|
+
getUserById,
|
|
17
|
+
touchLastLogin,
|
|
18
|
+
validatePassword
|
|
18
19
|
} from '../../services/users.js';
|
|
19
20
|
|
|
20
21
|
const { accessTokenExpiry, refreshTokenExpiry } = config.auth;
|
|
21
22
|
|
|
23
|
+
/** In-memory blacklist of invalidated refresh tokens (cleared on server restart). */
|
|
24
|
+
const blacklistedRefreshTokens = new Set();
|
|
25
|
+
|
|
26
|
+
// Purge expired tokens every 15 minutes to avoid unbounded growth.
|
|
27
|
+
setInterval(() => {
|
|
28
|
+
for (const token of blacklistedRefreshTokens) {
|
|
29
|
+
try {
|
|
30
|
+
fastifyRef.jwt.verify(token);
|
|
31
|
+
} catch {
|
|
32
|
+
blacklistedRefreshTokens.delete(token);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}, 15 * 60 * 1000);
|
|
36
|
+
|
|
37
|
+
/** Holds the fastify instance once routes are registered (needed by the cleanup interval). */
|
|
38
|
+
let fastifyRef;
|
|
39
|
+
|
|
22
40
|
export async function authRoutes(fastify) {
|
|
41
|
+
fastifyRef = fastify;
|
|
23
42
|
// GET /api/auth/setup-status
|
|
24
43
|
fastify.get('/auth/setup-status', async () => {
|
|
25
44
|
const count = await countUsers();
|
|
@@ -77,6 +96,13 @@ export async function authRoutes(fastify) {
|
|
|
77
96
|
return user;
|
|
78
97
|
});
|
|
79
98
|
|
|
99
|
+
// POST /api/auth/logout — blacklists the refresh token (fire-and-forget safe: no auth required)
|
|
100
|
+
fastify.post('/auth/logout', async (request) => {
|
|
101
|
+
const {refreshToken} = request.body || {};
|
|
102
|
+
if (refreshToken) blacklistedRefreshTokens.add(refreshToken);
|
|
103
|
+
return {ok: true};
|
|
104
|
+
});
|
|
105
|
+
|
|
80
106
|
// POST /api/auth/refresh
|
|
81
107
|
fastify.post('/auth/refresh', async (request, reply) => {
|
|
82
108
|
const { refreshToken } = request.body || {};
|
|
@@ -84,6 +110,10 @@ export async function authRoutes(fastify) {
|
|
|
84
110
|
return reply.status(400).send({ error: 'refreshToken is required' });
|
|
85
111
|
}
|
|
86
112
|
|
|
113
|
+
if (blacklistedRefreshTokens.has(refreshToken)) {
|
|
114
|
+
return reply.status(401).send({error: 'Refresh token has been revoked'});
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
let payload;
|
|
88
118
|
try {
|
|
89
119
|
payload = fastify.jwt.verify(refreshToken);
|
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
listEntries, getEntry, createEntry, updateEntry, deleteEntry, clearEntries,
|
|
29
29
|
exportEntries, importEntries
|
|
30
30
|
} from '../../services/collections.js';
|
|
31
|
-
import { authenticate,
|
|
32
|
-
import {
|
|
31
|
+
import { authenticate, requirePermission } from '../../middleware/auth.js';
|
|
32
|
+
import { getRoleLevel, invalidate as invalidateUserTypes } from '../../services/userTypes.js';
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Resolve the role level number for a named role.
|
|
@@ -38,7 +38,7 @@ import { config } from '../../config.js';
|
|
|
38
38
|
* @returns {number}
|
|
39
39
|
*/
|
|
40
40
|
function roleLevel(roleName) {
|
|
41
|
-
return
|
|
41
|
+
return getRoleLevel(roleName);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -76,7 +76,7 @@ async function checkPublicAccess(schema, operation, request, reply) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export async function collectionsRoutes(fastify) {
|
|
79
|
-
const guard = { preHandler: [authenticate,
|
|
79
|
+
const guard = { preHandler: [authenticate, requirePermission('collections')] };
|
|
80
80
|
|
|
81
81
|
// -------------------------------------------------------------------------
|
|
82
82
|
// Collection CRUD (schema management)
|
|
@@ -112,6 +112,9 @@ export async function collectionsRoutes(fastify) {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
fastify.delete('/collections/:slug', guard, async (request, reply) => {
|
|
115
|
+
if (request.params.slug === 'user-types') {
|
|
116
|
+
return reply.status(403).send({ error: 'Cannot delete a preset collection' });
|
|
117
|
+
}
|
|
115
118
|
try {
|
|
116
119
|
await deleteCollection(request.params.slug);
|
|
117
120
|
return { success: true };
|
|
@@ -150,6 +153,7 @@ export async function collectionsRoutes(fastify) {
|
|
|
150
153
|
createdBy: user?.id || null,
|
|
151
154
|
source: 'admin'
|
|
152
155
|
});
|
|
156
|
+
if (request.params.slug === 'user-types') await invalidateUserTypes();
|
|
153
157
|
return reply.status(201).send(entry);
|
|
154
158
|
} catch (err) {
|
|
155
159
|
return reply.status(400).send({ error: err.message });
|
|
@@ -158,7 +162,9 @@ export async function collectionsRoutes(fastify) {
|
|
|
158
162
|
|
|
159
163
|
fastify.put('/collections/:slug/entries/:id', guard, async (request, reply) => {
|
|
160
164
|
try {
|
|
161
|
-
|
|
165
|
+
const entry = await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
|
|
166
|
+
if (request.params.slug === 'user-types') await invalidateUserTypes();
|
|
167
|
+
return entry;
|
|
162
168
|
} catch (err) {
|
|
163
169
|
const status = err.message === 'Entry not found' ? 404 : 400;
|
|
164
170
|
return reply.status(status).send({ error: err.message });
|
|
@@ -166,8 +172,15 @@ export async function collectionsRoutes(fastify) {
|
|
|
166
172
|
});
|
|
167
173
|
|
|
168
174
|
fastify.delete('/collections/:slug/entries/:id', guard, async (request, reply) => {
|
|
175
|
+
if (request.params.slug === 'user-types') {
|
|
176
|
+
const entry = await getEntry('user-types', request.params.id);
|
|
177
|
+
if (entry?.data?.level === 0) {
|
|
178
|
+
return reply.status(403).send({ error: 'Cannot delete the root admin role' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
169
181
|
try {
|
|
170
182
|
await deleteEntry(request.params.slug, request.params.id);
|
|
183
|
+
if (request.params.slug === 'user-types') await invalidateUserTypes();
|
|
171
184
|
return { success: true };
|
|
172
185
|
} catch (err) {
|
|
173
186
|
return reply.status(404).send({ error: err.message });
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* GET /api/layouts - get all layout presets
|
|
4
4
|
* PUT /api/layouts - save layout presets
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { config } from '../../config.js';
|
|
6
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
7
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
9
8
|
|
|
10
9
|
export async function layoutsRoutes(fastify) {
|
|
11
|
-
const guard = { preHandler: [authenticate,
|
|
10
|
+
const guard = { preHandler: [authenticate, requirePermission('layouts')] };
|
|
12
11
|
|
|
13
12
|
fastify.get('/layouts', guard, async () => {
|
|
14
13
|
return getConfig('presets');
|
|
@@ -22,4 +21,19 @@ export async function layoutsRoutes(fastify) {
|
|
|
22
21
|
saveConfig('presets', data);
|
|
23
22
|
return { success: true };
|
|
24
23
|
});
|
|
24
|
+
|
|
25
|
+
fastify.get('/layouts/options', guard, async () => {
|
|
26
|
+
return getConfig('site')?.layoutOptions ?? {spacerSize: 8};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
fastify.put('/layouts/options', guard, async (request, reply) => {
|
|
30
|
+
const data = request.body;
|
|
31
|
+
if (!data || typeof data !== 'object') {
|
|
32
|
+
return reply.status(400).send({error: 'Invalid layout options'});
|
|
33
|
+
}
|
|
34
|
+
const site = getConfig('site');
|
|
35
|
+
site.layoutOptions = {...site.layoutOptions, ...data};
|
|
36
|
+
saveConfig('site', site);
|
|
37
|
+
return {success: true};
|
|
38
|
+
});
|
|
25
39
|
}
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import {deleteMedia, listMedia, renameMedia, saveMedia} from '../../services/content.js';
|
|
9
9
|
import {getImageInfo, isEditableImage, transformImage} from '../../services/images.js';
|
|
10
|
-
import {authenticate,
|
|
11
|
-
import {config} from '../../config.js';
|
|
10
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
12
11
|
|
|
13
12
|
// Safe filename: strip path traversal and restrict to alphanumeric + safe chars
|
|
14
13
|
function sanitiseFilename(name) {
|
|
@@ -16,7 +15,7 @@ function sanitiseFilename(name) {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export async function mediaRoutes(fastify) {
|
|
19
|
-
const guard = { preHandler: [authenticate,
|
|
18
|
+
const guard = { preHandler: [authenticate, requirePermission('media')] };
|
|
20
19
|
|
|
21
20
|
fastify.get('/media', guard, async () => {
|
|
22
21
|
return listMedia();
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
* PUT /api/navigation - save navigation config
|
|
5
5
|
*/
|
|
6
6
|
import { getConfig, saveConfig } from '../../config.js';
|
|
7
|
-
import { authenticate,
|
|
8
|
-
import { config } from '../../config.js';
|
|
7
|
+
import { authenticate, requirePermission } from '../../middleware/auth.js';
|
|
9
8
|
|
|
10
9
|
export async function navigationRoutes(fastify) {
|
|
11
|
-
const guard = { preHandler: [authenticate,
|
|
10
|
+
const guard = { preHandler: [authenticate, requirePermission('navigation')] };
|
|
12
11
|
|
|
13
12
|
fastify.get('/navigation', guard, async () => {
|
|
14
13
|
return getConfig('navigation');
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import {createPage, deletePage, getPage, listPages, renamePage, updatePage} from '../../services/content.js';
|
|
10
10
|
import {parseMarkdown} from '../../services/markdown.js';
|
|
11
|
-
import {authenticate,
|
|
12
|
-
import {
|
|
11
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
12
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
13
13
|
|
|
14
14
|
export async function pagesRoutes(fastify) {
|
|
15
|
-
const guard = { preHandler: [authenticate,
|
|
15
|
+
const guard = { preHandler: [authenticate, requirePermission('pages')] };
|
|
16
16
|
|
|
17
17
|
// Render markdown preview (shortcodes + sanitize, no frontmatter)
|
|
18
18
|
fastify.post('/pages/preview', guard, async (request, reply) => {
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* POST /api/settings/test-email - send a test email using stored SMTP config
|
|
6
6
|
*/
|
|
7
7
|
import { getConfig, saveConfig } from '../../config.js';
|
|
8
|
-
import { authenticate,
|
|
9
|
-
import { config } from '../../config.js';
|
|
8
|
+
import { authenticate, requirePermission } from '../../middleware/auth.js';
|
|
10
9
|
import nodemailer from 'nodemailer';
|
|
11
10
|
import fs from 'fs/promises';
|
|
12
11
|
import path from 'path';
|
|
@@ -17,7 +16,7 @@ const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
|
17
16
|
const CUSTOM_CSS_MAX = 100 * 1024; // 100 KB
|
|
18
17
|
|
|
19
18
|
export async function settingsRoutes(fastify) {
|
|
20
|
-
const guard = { preHandler: [authenticate,
|
|
19
|
+
const guard = { preHandler: [authenticate, requirePermission('settings')] };
|
|
21
20
|
|
|
22
21
|
fastify.get('/settings', guard, async () => {
|
|
23
22
|
return getConfig('site');
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* PUT /api/users/:id - update user (admin, manager — manager cannot edit admin)
|
|
7
7
|
* DELETE /api/users/:id - delete user (admin, manager — manager cannot delete admin, no self-delete)
|
|
8
8
|
*/
|
|
9
|
-
import { authenticate,
|
|
10
|
-
import {
|
|
9
|
+
import { authenticate, requirePermission, canManageUser } from '../../middleware/auth.js';
|
|
10
|
+
import { getPermissionsFor } from '../../services/userTypes.js';
|
|
11
11
|
import {
|
|
12
12
|
listUsers,
|
|
13
13
|
getUserById,
|
|
@@ -16,10 +16,8 @@ import {
|
|
|
16
16
|
deleteUser
|
|
17
17
|
} from '../../services/users.js';
|
|
18
18
|
|
|
19
|
-
const { permissions } = config.auth;
|
|
20
|
-
|
|
21
19
|
export async function usersRoutes(fastify) {
|
|
22
|
-
const guard = [authenticate,
|
|
20
|
+
const guard = [authenticate, requirePermission('users')];
|
|
23
21
|
|
|
24
22
|
// List all users
|
|
25
23
|
fastify.get('/users', { preHandler: guard }, async () => {
|
|
@@ -32,7 +30,7 @@ export async function usersRoutes(fastify) {
|
|
|
32
30
|
const actor = request.user;
|
|
33
31
|
|
|
34
32
|
const isSelf = actor.id === id;
|
|
35
|
-
const canManage =
|
|
33
|
+
const canManage = getPermissionsFor('users').includes(actor.role);
|
|
36
34
|
|
|
37
35
|
if (!isSelf && !canManage) {
|
|
38
36
|
return reply.code(403).send({ error: 'Forbidden' });
|
package/server/routes/public.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {getPage} from '../services/content.js';
|
|
8
8
|
import {renderPage} from '../services/renderer.js';
|
|
9
9
|
import {config} from '../config.js';
|
|
10
|
+
import { getRoleLevel } from '../services/userTypes.js';
|
|
10
11
|
|
|
11
12
|
export async function publicRoutes(fastify) {
|
|
12
13
|
// Admin panel: serve index.html for all /admin/* paths (SPA fallback)
|
|
@@ -56,9 +57,8 @@ export async function publicRoutes(fastify) {
|
|
|
56
57
|
} catch { /* no token — treat as unauthenticated */
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const requiredLevel = roles[page.visibility]?.level ?? 0;
|
|
60
|
+
const userLevel = getRoleLevel(userRole);
|
|
61
|
+
const requiredLevel = getRoleLevel(page.visibility) === Infinity ? 0 : getRoleLevel(page.visibility);
|
|
62
62
|
|
|
63
63
|
if (userLevel > requiredLevel) {
|
|
64
64
|
reply.status(403);
|