domma-cms 0.1.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/LICENSE +21 -0
- package/README.md +469 -0
- package/admin/css/admin.css +1123 -0
- package/admin/index.html +72 -0
- package/admin/js/api.js +210 -0
- package/admin/js/app.js +270 -0
- package/admin/js/config/sidebar-config.js +107 -0
- package/admin/js/lib/card.js +63 -0
- package/admin/js/lib/image-editor.js +869 -0
- package/admin/js/lib/markdown-toolbar.js +421 -0
- package/admin/js/templates/dashboard.html +50 -0
- package/admin/js/templates/documentation.html +237 -0
- package/admin/js/templates/layouts.html +11 -0
- package/admin/js/templates/login.html +58 -0
- package/admin/js/templates/media.html +16 -0
- package/admin/js/templates/navigation.html +50 -0
- package/admin/js/templates/page-editor.html +126 -0
- package/admin/js/templates/pages.html +18 -0
- package/admin/js/templates/plugins.html +12 -0
- package/admin/js/templates/settings.html +190 -0
- package/admin/js/templates/tutorials.html +233 -0
- package/admin/js/templates/user-editor.html +12 -0
- package/admin/js/templates/users.html +10 -0
- package/admin/js/views/dashboard.js +48 -0
- package/admin/js/views/documentation.js +12 -0
- package/admin/js/views/index.js +33 -0
- package/admin/js/views/layouts.js +49 -0
- package/admin/js/views/login.js +254 -0
- package/admin/js/views/media.js +240 -0
- package/admin/js/views/navigation.js +152 -0
- package/admin/js/views/page-editor.js +479 -0
- package/admin/js/views/pages.js +64 -0
- package/admin/js/views/plugins.js +100 -0
- package/admin/js/views/settings.js +64 -0
- package/admin/js/views/tutorials.js +12 -0
- package/admin/js/views/user-editor.js +88 -0
- package/admin/js/views/users.js +73 -0
- package/bin/cli.js +334 -0
- package/config/auth.json +20 -0
- package/config/content.json +10 -0
- package/config/navigation.json +63 -0
- package/config/plugins.json +47 -0
- package/config/presets.json +34 -0
- package/config/server.json +6 -0
- package/config/site.json +33 -0
- package/package.json +67 -0
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
- package/plugins/back-to-top/config.js +10 -0
- package/plugins/back-to-top/plugin.js +24 -0
- package/plugins/back-to-top/plugin.json +36 -0
- package/plugins/back-to-top/public/inject-body.html +105 -0
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
- package/plugins/cookie-consent/config.js +30 -0
- package/plugins/cookie-consent/plugin.js +24 -0
- package/plugins/cookie-consent/plugin.json +36 -0
- package/plugins/cookie-consent/public/inject-body.html +69 -0
- package/plugins/custom-css/admin/templates/custom-css.html +17 -0
- package/plugins/custom-css/admin/views/custom-css.js +35 -0
- package/plugins/custom-css/config.js +1 -0
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +63 -0
- package/plugins/custom-css/plugin.json +32 -0
- package/plugins/custom-css/public/inject-head.html +1 -0
- package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
- package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
- package/plugins/domma-effects/config.js +9 -0
- package/plugins/domma-effects/plugin.js +22 -0
- package/plugins/domma-effects/plugin.json +36 -0
- package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
- package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
- package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
- package/plugins/domma-effects/public/celebrations/index.js +535 -0
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
- package/plugins/domma-effects/public/inject-body.html +268 -0
- 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 +27 -0
- package/plugins/example-analytics/public/inject-body.html +13 -0
- package/plugins/example-analytics/public/inject-head.html +1 -0
- package/plugins/example-analytics/stats.json +1 -0
- package/plugins/form-builder/admin/templates/form-editor.html +158 -0
- package/plugins/form-builder/admin/templates/form-settings.html +29 -0
- package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
- package/plugins/form-builder/admin/templates/forms-list.html +17 -0
- package/plugins/form-builder/admin/views/form-editor.js +817 -0
- package/plugins/form-builder/admin/views/form-settings.js +38 -0
- package/plugins/form-builder/admin/views/form-submissions.js +295 -0
- package/plugins/form-builder/admin/views/forms-list.js +164 -0
- package/plugins/form-builder/config.js +9 -0
- package/plugins/form-builder/data/forms/contact-details.json +63 -0
- package/plugins/form-builder/data/forms/contact.json +52 -0
- package/plugins/form-builder/data/submissions/contact-details.json +1 -0
- package/plugins/form-builder/data/submissions/contact.json +14 -0
- package/plugins/form-builder/email.js +103 -0
- package/plugins/form-builder/plugin.js +454 -0
- package/plugins/form-builder/plugin.json +56 -0
- package/plugins/form-builder/public/inject-body.html +270 -0
- package/plugins/form-builder/public/inject-head.html +42 -0
- package/public/css/site.css +189 -0
- package/public/js/site.js +109 -0
- package/scripts/copy-domma.js +48 -0
- package/scripts/fresh.js +41 -0
- package/scripts/reset.js +124 -0
- package/scripts/seed.js +666 -0
- package/scripts/setup.js +263 -0
- package/server/config.js +56 -0
- package/server/middleware/auth.js +97 -0
- package/server/routes/api/auth.js +116 -0
- package/server/routes/api/layouts.js +25 -0
- package/server/routes/api/media.js +93 -0
- package/server/routes/api/navigation.js +37 -0
- package/server/routes/api/pages.js +118 -0
- package/server/routes/api/plugins.js +46 -0
- package/server/routes/api/settings.js +25 -0
- package/server/routes/api/users.js +110 -0
- package/server/routes/public.js +108 -0
- package/server/server.js +169 -0
- package/server/services/content.js +298 -0
- package/server/services/images.js +334 -0
- package/server/services/markdown.js +297 -0
- package/server/services/plugins.js +246 -0
- package/server/services/renderer.js +80 -0
- package/server/services/users.js +212 -0
- package/server/templates/page.html +78 -0
package/admin/index.html
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-GB">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Domma CMS - Admin</title>
|
|
7
|
+
|
|
8
|
+
<!-- DommaJS CSS -->
|
|
9
|
+
<link rel="stylesheet" href="/dist/domma/domma.css">
|
|
10
|
+
<link rel="stylesheet" href="/dist/domma/grid.css">
|
|
11
|
+
<link rel="stylesheet" href="/dist/domma/elements.css">
|
|
12
|
+
<link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
|
|
13
|
+
|
|
14
|
+
<!-- Cropper.js - image editor -->
|
|
15
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cropperjs@1/dist/cropper.min.css">
|
|
16
|
+
|
|
17
|
+
<!-- Admin CSS -->
|
|
18
|
+
<link rel="stylesheet" href="/admin/css/admin.css">
|
|
19
|
+
</head>
|
|
20
|
+
<body class="dm-cloaked dm-theme-charcoal-dark dashboard-layout">
|
|
21
|
+
|
|
22
|
+
<!-- Top Bar -->
|
|
23
|
+
<header id="admin-topbar">
|
|
24
|
+
<div class="topbar-brand">
|
|
25
|
+
<span data-icon="layout"></span>
|
|
26
|
+
<span class="topbar-brand-text">Domma CMS</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="topbar-user" id="topbar-user">
|
|
29
|
+
<!-- Populated after auth: name + role badge -->
|
|
30
|
+
</div>
|
|
31
|
+
<div class="topbar-actions" id="topbar-actions">
|
|
32
|
+
<!-- Populated after auth: Settings link + Sign out -->
|
|
33
|
+
</div>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<div class="dashboard-wrapper">
|
|
37
|
+
<!-- Sidebar -->
|
|
38
|
+
<aside id="admin-sidebar" class="dashboard-sidebar"></aside>
|
|
39
|
+
|
|
40
|
+
<!-- Main content area -->
|
|
41
|
+
<main class="dashboard-main">
|
|
42
|
+
<div id="view-container" class="view-container"></div>
|
|
43
|
+
</main>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- DOMPurify - must load before DommaJS -->
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
|
48
|
+
|
|
49
|
+
<!-- DommaJS -->
|
|
50
|
+
<script src="/dist/domma/domma.min.js"></script>
|
|
51
|
+
|
|
52
|
+
<!-- Initialise DommaJS before ES module loads -->
|
|
53
|
+
<script>
|
|
54
|
+
if (window.Domma && typeof window.Domma.init === 'function') {
|
|
55
|
+
window.Domma.init();
|
|
56
|
+
window.Domma.icons.scan();
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<!-- DommaJS Syntax Highlighter (adds Domma.syntax) -->
|
|
61
|
+
<script src="/dist/domma/domma-syntax.min.js"></script>
|
|
62
|
+
|
|
63
|
+
<!-- Cropper.js - image editor -->
|
|
64
|
+
<script src="https://cdn.jsdelivr.net/npm/cropperjs@1/dist/cropper.min.js"></script>
|
|
65
|
+
|
|
66
|
+
<!-- Marked - Markdown parser for page editor preview -->
|
|
67
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
68
|
+
|
|
69
|
+
<!-- Admin App (ES module entry) -->
|
|
70
|
+
<script type="module" src="/admin/js/app.js"></script>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
package/admin/js/api.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API Client
|
|
3
|
+
* Authenticated requests using Bearer tokens with automatic refresh on 401.
|
|
4
|
+
* All existing api.* namespaces are preserved — views need no changes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const BASE = '/api';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Token helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function getToken() { return S.get('auth_token'); }
|
|
14
|
+
function getRefreshToken() { return S.get('auth_refresh_token'); }
|
|
15
|
+
function setToken(t) { S.set('auth_token', t); }
|
|
16
|
+
function clearAuth() {
|
|
17
|
+
S.remove('auth_token');
|
|
18
|
+
S.remove('auth_refresh_token');
|
|
19
|
+
S.remove('auth_user');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Attempt to refresh the access token using the stored refresh token.
|
|
24
|
+
* Clears auth and redirects to login if refresh fails.
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise<string>} New access token
|
|
27
|
+
*/
|
|
28
|
+
async function refreshAccessToken() {
|
|
29
|
+
const refreshToken = getRefreshToken();
|
|
30
|
+
if (!refreshToken) throw new Error('No refresh token');
|
|
31
|
+
|
|
32
|
+
const res = await fetch(`${BASE}/auth/refresh`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ refreshToken })
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
clearAuth();
|
|
40
|
+
R.navigate('/login');
|
|
41
|
+
throw new Error('Token refresh failed');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { token } = await res.json();
|
|
45
|
+
setToken(token);
|
|
46
|
+
return token;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Core request function
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Make an authenticated API request.
|
|
55
|
+
* Automatically retries once after refreshing the access token on 401.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} endpoint
|
|
58
|
+
* @param {RequestInit} options
|
|
59
|
+
* @returns {Promise<any>}
|
|
60
|
+
*/
|
|
61
|
+
async function apiRequest(endpoint, options = {}) {
|
|
62
|
+
let token = getToken();
|
|
63
|
+
|
|
64
|
+
const buildHeaders = (t) => ({
|
|
65
|
+
...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
66
|
+
...options.headers,
|
|
67
|
+
...(t ? { Authorization: `Bearer ${t}` } : {})
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let res = await fetch(`${BASE}${endpoint}`, { ...options, headers: buildHeaders(token) });
|
|
71
|
+
|
|
72
|
+
// On 401, try refreshing once
|
|
73
|
+
if (res.status === 401 && getRefreshToken()) {
|
|
74
|
+
try {
|
|
75
|
+
token = await refreshAccessToken();
|
|
76
|
+
res = await fetch(`${BASE}${endpoint}`, { ...options, headers: buildHeaders(token) });
|
|
77
|
+
} catch {
|
|
78
|
+
return; // Already redirected to login
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const body = await res.json().catch(() => ({ error: 'Request failed' }));
|
|
84
|
+
throw new Error(body.error || body.message || `HTTP ${res.status}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 204 No Content
|
|
88
|
+
if (res.status === 204) return null;
|
|
89
|
+
|
|
90
|
+
return res.json();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Upload a file with Bearer token (multipart — no Content-Type override).
|
|
95
|
+
*
|
|
96
|
+
* @param {string} endpoint
|
|
97
|
+
* @param {FormData} formData
|
|
98
|
+
* @returns {Promise<any>}
|
|
99
|
+
*/
|
|
100
|
+
async function uploadRequest(endpoint, formData) {
|
|
101
|
+
const token = getToken();
|
|
102
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
103
|
+
|
|
104
|
+
const res = await fetch(`${BASE}${endpoint}`, { method: 'POST', headers, body: formData });
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const body = await res.json().catch(() => ({ error: 'Upload failed' }));
|
|
107
|
+
throw new Error(body.error || body.message || `HTTP ${res.status}`);
|
|
108
|
+
}
|
|
109
|
+
return res.json();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Domain API namespaces
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export const api = {
|
|
117
|
+
// Auth
|
|
118
|
+
auth: {
|
|
119
|
+
setupStatus: () => apiRequest('/auth/setup-status', { method: 'GET' }),
|
|
120
|
+
setup: (data) => apiRequest('/auth/setup', { method: 'POST', body: JSON.stringify(data) }),
|
|
121
|
+
login: (data) => apiRequest('/auth/login', { method: 'POST', body: JSON.stringify(data) }),
|
|
122
|
+
me: () => apiRequest('/auth/me', { method: 'GET' }),
|
|
123
|
+
refresh: (refreshToken) => apiRequest('/auth/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }) })
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Pages
|
|
127
|
+
pages: {
|
|
128
|
+
list: () => apiRequest('/pages', { method: 'GET' }),
|
|
129
|
+
get: (urlPath) => apiRequest(`/pages${urlPath}`, { method: 'GET' }),
|
|
130
|
+
create: (payload) => apiRequest('/pages', { method: 'POST', body: JSON.stringify(payload) }),
|
|
131
|
+
update: (urlPath, payload) => apiRequest(`/pages${urlPath}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
132
|
+
delete: (urlPath) => apiRequest(`/pages${urlPath}`, {method: 'DELETE'}),
|
|
133
|
+
preview: (markdown) => apiRequest('/pages/preview', {method: 'POST', body: JSON.stringify({markdown})})
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Settings
|
|
137
|
+
settings: {
|
|
138
|
+
get: () => apiRequest('/settings', { method: 'GET' }),
|
|
139
|
+
save: (data) => apiRequest('/settings', { method: 'PUT', body: JSON.stringify(data) })
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Navigation
|
|
143
|
+
navigation: {
|
|
144
|
+
get: () => apiRequest('/navigation', { method: 'GET' }),
|
|
145
|
+
save: (data) => apiRequest('/navigation', { method: 'PUT', body: JSON.stringify(data) })
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Layouts (presets)
|
|
149
|
+
layouts: {
|
|
150
|
+
get: () => apiRequest('/layouts', { method: 'GET' }),
|
|
151
|
+
save: (data) => apiRequest('/layouts', { method: 'PUT', body: JSON.stringify(data) })
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Media
|
|
155
|
+
media: {
|
|
156
|
+
list: () => apiRequest('/media', { method: 'GET' }),
|
|
157
|
+
upload: (formData) => uploadRequest('/media', formData),
|
|
158
|
+
delete: (name) => apiRequest(`/media/${encodeURIComponent(name)}`, {method: 'DELETE'}),
|
|
159
|
+
rename: (name, newName) => apiRequest(`/media/${encodeURIComponent(name)}`, {
|
|
160
|
+
method: 'PATCH',
|
|
161
|
+
body: JSON.stringify({newName})
|
|
162
|
+
}),
|
|
163
|
+
info: (name) => apiRequest(`/media/${encodeURIComponent(name)}/info`, {method: 'GET'}),
|
|
164
|
+
transform: (name, body) => apiRequest(`/media/${encodeURIComponent(name)}/transform`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
body: JSON.stringify(body)
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// Users
|
|
171
|
+
users: {
|
|
172
|
+
list: () => apiRequest('/users', { method: 'GET' }),
|
|
173
|
+
get: (id) => apiRequest(`/users/${id}`, { method: 'GET' }),
|
|
174
|
+
create: (data) => apiRequest('/users', { method: 'POST', body: JSON.stringify(data) }),
|
|
175
|
+
update: (id, data) => apiRequest(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
|
176
|
+
delete: (id) => apiRequest(`/users/${id}`, { method: 'DELETE' })
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Plugins
|
|
180
|
+
plugins: {
|
|
181
|
+
list: () => apiRequest('/plugins', { method: 'GET' }),
|
|
182
|
+
update: (name, data) => apiRequest(`/plugins/${name}`, { method: 'PUT', body: JSON.stringify(data) }),
|
|
183
|
+
adminConfig: () => apiRequest('/plugins/admin-config', { method: 'GET' })
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Auth state helpers (used by app.js)
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export function isAuthenticated() {
|
|
192
|
+
return !!getToken();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getUser() {
|
|
196
|
+
return S.get('auth_user');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function setAuthData({ token, refreshToken, user }) {
|
|
200
|
+
if (token) setToken(token);
|
|
201
|
+
if (refreshToken) S.set('auth_refresh_token', refreshToken);
|
|
202
|
+
if (user) S.set('auth_user', user);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function logout() {
|
|
206
|
+
clearAuth();
|
|
207
|
+
R.navigate('/login');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {apiRequest};
|
package/admin/js/app.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domma CMS Admin - SPA Entry Point
|
|
3
|
+
* Mirrors the domma-board candidate/app.js pattern.
|
|
4
|
+
* Adds JWT auth guard, role-aware sidebar, and dynamic plugin loading.
|
|
5
|
+
*/
|
|
6
|
+
import {getSidebarConfig} from './config/sidebar-config.js';
|
|
7
|
+
import {views as coreViews} from './views/index.js';
|
|
8
|
+
import {api, getUser, isAuthenticated, logout} from './api.js';
|
|
9
|
+
|
|
10
|
+
$(() => {
|
|
11
|
+
// Fetch admin theme preference from server config; fall back to charcoal-dark
|
|
12
|
+
(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const site = await api.settings.get();
|
|
15
|
+
Domma.theme.init({ theme: site.adminTheme || 'charcoal-dark', persist: true });
|
|
16
|
+
} catch {
|
|
17
|
+
Domma.theme.init({ theme: 'charcoal-dark', persist: true });
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
|
|
21
|
+
// -------------------------------------------------------------------------
|
|
22
|
+
// Auth guard — redirect to login unless already on /login
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
R.use(async (to, from, next) => {
|
|
25
|
+
if (to.path === '/login') return next();
|
|
26
|
+
|
|
27
|
+
if (!isAuthenticated()) {
|
|
28
|
+
R.navigate('/login');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// -------------------------------------------------------------------------
|
|
36
|
+
// Sidebar (role-aware — re-built after login lands on dashboard)
|
|
37
|
+
// -------------------------------------------------------------------------
|
|
38
|
+
let sidebar = null;
|
|
39
|
+
|
|
40
|
+
async function fetchSidebarCounts() {
|
|
41
|
+
if (!isAuthenticated()) return {};
|
|
42
|
+
try {
|
|
43
|
+
const [pages, media, users, plugins] = await Promise.all([
|
|
44
|
+
api.pages.list().catch(() => []),
|
|
45
|
+
api.media.list().catch(() => []),
|
|
46
|
+
api.users.list().catch(() => []),
|
|
47
|
+
api.plugins.list().catch(() => [])
|
|
48
|
+
]);
|
|
49
|
+
return {
|
|
50
|
+
pages: pages.length,
|
|
51
|
+
media: media.length,
|
|
52
|
+
users: users.length,
|
|
53
|
+
plugins: plugins.filter(p => p.enabled).length || plugins.length
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function buildSidebar(role) {
|
|
61
|
+
if (sidebar) {
|
|
62
|
+
$('#admin-sidebar').empty();
|
|
63
|
+
}
|
|
64
|
+
const counts = await fetchSidebarCounts();
|
|
65
|
+
sidebar = Domma.elements.sidebar('#admin-sidebar', {
|
|
66
|
+
header: { title: 'CMS Admin', icon: 'layout' },
|
|
67
|
+
items: getSidebarConfig(role, counts, pluginSidebarItems),
|
|
68
|
+
collapsible: true,
|
|
69
|
+
collapseAt: 992,
|
|
70
|
+
push: true,
|
|
71
|
+
contentSelector: '.dashboard-main',
|
|
72
|
+
top: '60px'
|
|
73
|
+
});
|
|
74
|
+
attachSidebarTooltips();
|
|
75
|
+
attachCollapsibleHeadings();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function attachSidebarTooltips() {
|
|
79
|
+
$('#admin-sidebar .sidebar-link').each(function () {
|
|
80
|
+
const label = $(this).find('.sidebar-text').text().trim();
|
|
81
|
+
if (label) $(this).attr('data-tooltip', label);
|
|
82
|
+
});
|
|
83
|
+
E.tooltip('#admin-sidebar [data-tooltip]', { placement: 'right' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function attachCollapsibleHeadings() {
|
|
87
|
+
$('#admin-sidebar .sidebar-heading').each(function () {
|
|
88
|
+
const $heading = $(this);
|
|
89
|
+
$heading.addClass('sidebar-heading--collapsible');
|
|
90
|
+
$heading.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>');
|
|
91
|
+
|
|
92
|
+
$heading.on('click', function () {
|
|
93
|
+
const collapsing = !$heading.hasClass('is-collapsed');
|
|
94
|
+
$heading.toggleClass('is-collapsed', collapsing);
|
|
95
|
+
|
|
96
|
+
let $el = $heading.next();
|
|
97
|
+
while ($el.length && !$el.hasClass('sidebar-heading') && !$el.hasClass('sidebar-divider')) {
|
|
98
|
+
$el.toggle(!collapsing);
|
|
99
|
+
$el = $el.next();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
Domma.icons.scan('#admin-sidebar');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build with no role initially (login page has no sidebar)
|
|
108
|
+
buildSidebar(null);
|
|
109
|
+
|
|
110
|
+
// Keep sidebar active item in sync with route
|
|
111
|
+
M.subscribe('router:afterChange', ({ to }) => {
|
|
112
|
+
if (sidebar && to.path !== '/login') {
|
|
113
|
+
sidebar.setActive('#' + to.path);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// Router — core routes + login
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
const coreRoutes = [
|
|
121
|
+
{ path: '/', view: 'dashboard', title: 'Dashboard - Domma CMS' },
|
|
122
|
+
{ path: '/pages', view: 'pages', title: 'Pages - Domma CMS' },
|
|
123
|
+
{ path: '/pages/new', view: 'pageEditor', title: 'New Page - Domma CMS' },
|
|
124
|
+
{ path: '/pages/edit/*', view: 'pageEditor', title: 'Edit Page - Domma CMS' },
|
|
125
|
+
{ path: '/media', view: 'media', title: 'Media - Domma CMS' },
|
|
126
|
+
{ path: '/navigation', view: 'navigation', title: 'Navigation - Domma CMS' },
|
|
127
|
+
{ path: '/layouts', view: 'layouts', title: 'Layouts - Domma CMS' },
|
|
128
|
+
{ path: '/settings', view: 'settings', title: 'Settings - Domma CMS' },
|
|
129
|
+
{ path: '/users', view: 'users', title: 'Users - Domma CMS' },
|
|
130
|
+
{ path: '/users/new', view: 'userEditor', title: 'New User - Domma CMS' },
|
|
131
|
+
{ path: '/users/edit/:id', view: 'userEditor', title: 'Edit User - Domma CMS' },
|
|
132
|
+
{path: '/plugins', view: 'plugins', title: 'Plugins - Domma CMS'},
|
|
133
|
+
{path: '/documentation', view: 'documentation', title: 'Usage - Domma CMS'},
|
|
134
|
+
{path: '/tutorials', view: 'tutorials', title: 'Tutorials - Domma CMS'},
|
|
135
|
+
{ path: '/login', view: 'login', title: 'Sign in - Domma CMS',
|
|
136
|
+
onEnter: () => {
|
|
137
|
+
$('#admin-sidebar').hide();
|
|
138
|
+
$('#admin-topbar').hide();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// After login, re-show chrome and rebuild sidebar with correct role
|
|
144
|
+
M.subscribe('router:afterChange', ({ to, from }) => {
|
|
145
|
+
if (to.path !== '/login') {
|
|
146
|
+
$('#admin-sidebar').show();
|
|
147
|
+
$('#admin-topbar').show();
|
|
148
|
+
if (from?.path === '/login') {
|
|
149
|
+
const user = getUser();
|
|
150
|
+
if (user) buildSidebar(user.role);
|
|
151
|
+
}
|
|
152
|
+
injectUserMenu();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Reveal primary/danger buttons after each view renders
|
|
157
|
+
M.subscribe('router:afterChange', () => {
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
if ($('.btn-primary, .btn-danger').length) {
|
|
160
|
+
Domma.effects.reveal('.btn-primary, .btn-danger', {animation: 'fade', stagger: 40, duration: 300});
|
|
161
|
+
}
|
|
162
|
+
}, 50);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Collapsible cards — event delegation handles all views including plugins
|
|
166
|
+
$('#view-container').on('click', '.card-collapsible .card-header', function (e) {
|
|
167
|
+
if ($(e.target).closest('button, a').length) return;
|
|
168
|
+
$(this).closest('.card').toggleClass('card-collapsed');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// Dynamic plugin routes (loaded before R.init)
|
|
173
|
+
// -------------------------------------------------------------------------
|
|
174
|
+
const allViews = { ...coreViews };
|
|
175
|
+
const allRoutes = [...coreRoutes];
|
|
176
|
+
let pluginSidebarItems = [];
|
|
177
|
+
|
|
178
|
+
async function loadPlugins() {
|
|
179
|
+
if (!isAuthenticated()) return;
|
|
180
|
+
try {
|
|
181
|
+
const pluginConfig = await api.plugins.adminConfig();
|
|
182
|
+
pluginSidebarItems = pluginConfig.sidebar || [];
|
|
183
|
+
if (pluginConfig.routes?.length) allRoutes.push(...pluginConfig.routes);
|
|
184
|
+
for (const [viewName, viewDef] of Object.entries(pluginConfig.views || {})) {
|
|
185
|
+
try {
|
|
186
|
+
const mod = await import(`/plugins/${viewDef.entry}`);
|
|
187
|
+
allViews[viewName] = mod[viewDef.exportName];
|
|
188
|
+
} catch { /* plugin view load failure is non-fatal */ }
|
|
189
|
+
}
|
|
190
|
+
} catch { /* plugins endpoint unreachable — continue without plugins */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
// Inject user info + sign out into topbar
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
function injectUserMenu() {
|
|
197
|
+
const user = getUser();
|
|
198
|
+
if (!user) return;
|
|
199
|
+
if ($('#topbar-user-name').length) return; // already injected
|
|
200
|
+
|
|
201
|
+
const roleLabelMap = {
|
|
202
|
+
admin: 'Admin', manager: 'Manager', editor: 'Editor', subscriber: 'Subscriber'
|
|
203
|
+
};
|
|
204
|
+
const roleLabel = roleLabelMap[user.role] || user.role;
|
|
205
|
+
|
|
206
|
+
$('#topbar-user').html(`
|
|
207
|
+
<span id="topbar-user-name" class="topbar-user-name">${escapeHtml(user.name)}</span>
|
|
208
|
+
<span class="topbar-role-badge topbar-role-badge--${escapeHtml(user.role)}">${roleLabel}</span>
|
|
209
|
+
`);
|
|
210
|
+
|
|
211
|
+
$('#topbar-actions').html(`
|
|
212
|
+
<a href="#/settings" class="topbar-action-link" data-tooltip="Settings" data-tooltip-placement="bottom">
|
|
213
|
+
<span data-icon="settings"></span>
|
|
214
|
+
<span>Settings</span>
|
|
215
|
+
</a>
|
|
216
|
+
<a href="#" id="topbar-logout-btn" class="topbar-action-link topbar-signout" data-tooltip="Sign out" data-tooltip-placement="bottom">
|
|
217
|
+
<span data-icon="log-out"></span>
|
|
218
|
+
<span>Sign out</span>
|
|
219
|
+
</a>
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
$('#topbar-logout-btn').on('click', (e) => { e.preventDefault(); logout(); });
|
|
223
|
+
Domma.icons.scan('#admin-topbar');
|
|
224
|
+
E.tooltip('#topbar-actions [data-tooltip]', { placement: 'bottom' });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function escapeHtml(str) {
|
|
228
|
+
return String(str)
|
|
229
|
+
.replace(/&/g, '&')
|
|
230
|
+
.replace(/</g, '<')
|
|
231
|
+
.replace(/>/g, '>');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
// Boot sequence
|
|
236
|
+
// -------------------------------------------------------------------------
|
|
237
|
+
(async () => {
|
|
238
|
+
await loadPlugins();
|
|
239
|
+
|
|
240
|
+
const user = getUser();
|
|
241
|
+
if (user) {
|
|
242
|
+
await buildSidebar(user.role);
|
|
243
|
+
injectUserMenu();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
R.init({
|
|
247
|
+
container: '#view-container',
|
|
248
|
+
routes: allRoutes,
|
|
249
|
+
views: allViews,
|
|
250
|
+
default: '/',
|
|
251
|
+
transitions: { enter: 'fadeIn', leave: 'fadeOut', duration: 150 }
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Patch Domma router to support /* wildcard routes (multi-segment catch-all)
|
|
255
|
+
const origExtract = R._extractParams.bind(R);
|
|
256
|
+
R._extractParams = function(routePath, actualPath) {
|
|
257
|
+
if (routePath.endsWith('/*')) {
|
|
258
|
+
const prefix = routePath.slice(0, -2);
|
|
259
|
+
return actualPath.startsWith(prefix + '/') ? {} : null;
|
|
260
|
+
}
|
|
261
|
+
return origExtract(routePath, actualPath);
|
|
262
|
+
};
|
|
263
|
+
// Only re-trigger if the current URL needs the wildcard patch (e.g. /pages/edit/about)
|
|
264
|
+
const currentPath = (window.location.hash || '#/').slice(1) || '/';
|
|
265
|
+
const needsWildcard = allRoutes
|
|
266
|
+
.filter(r => r.path.endsWith('/*'))
|
|
267
|
+
.some(r => currentPath.startsWith(r.path.slice(0, -2) + '/'));
|
|
268
|
+
if (needsWildcard) R._handleRouteChange();
|
|
269
|
+
})();
|
|
270
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Sidebar Configuration
|
|
3
|
+
* Role-aware — certain sections are only visible to privileged roles.
|
|
4
|
+
*
|
|
5
|
+
* @param {string|null} role - Current user role: 'admin' | 'manager' | 'editor' | null
|
|
6
|
+
* @param {object} counts - Optional badge counts: { pages, media, users }
|
|
7
|
+
* @param pluginItems
|
|
8
|
+
* @returns {object[]}
|
|
9
|
+
*/
|
|
10
|
+
export function getSidebarConfig(role, counts = {}, pluginItems = []) {
|
|
11
|
+
const isAdmin = role === 'admin';
|
|
12
|
+
const isManager = role === 'manager';
|
|
13
|
+
const canStructure = isAdmin || isManager;
|
|
14
|
+
|
|
15
|
+
const badge = (n) => (n != null && n > 0) ? String(n) : null;
|
|
16
|
+
|
|
17
|
+
const items = [];
|
|
18
|
+
|
|
19
|
+
items.push(
|
|
20
|
+
{heading: 'Overview'},
|
|
21
|
+
{id: 'dashboard', text: 'Dashboard', icon: 'home', url: '#/', section: '#/'},
|
|
22
|
+
{divider: true}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (canStructure) {
|
|
26
|
+
items.push(
|
|
27
|
+
{heading: 'Structure'},
|
|
28
|
+
{id: 'navigation', text: 'Navigation', icon: 'menu', url: '#/navigation', section: '#/navigation'},
|
|
29
|
+
{id: 'layouts', text: 'Layouts', icon: 'layout', url: '#/layouts', section: '#/layouts'},
|
|
30
|
+
{divider: true}
|
|
31
|
+
)
|
|
32
|
+
;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
items.push(
|
|
36
|
+
{heading: 'Content'},
|
|
37
|
+
{id: 'pages', text: 'Pages', icon: 'file-text', url: '#/pages', section: '#/pages', badge: badge(counts.pages)},
|
|
38
|
+
{id: 'media', text: 'Media', icon: 'image', url: '#/media', section: '#/media', badge: badge(counts.media)}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
items.push(
|
|
42
|
+
{divider: true},
|
|
43
|
+
{heading: 'Configuration'}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (canStructure) {
|
|
47
|
+
items.push(
|
|
48
|
+
{id: 'users', text: 'Users', icon: 'users', url: '#/users', section: '#/users', badge: badge(counts.users)},
|
|
49
|
+
{id: 'settings', text: 'Site Settings', icon: 'settings', url: '#/settings', section: '#/settings'}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
items.push(
|
|
54
|
+
{divider: true},
|
|
55
|
+
{heading: 'Plugins'}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (isAdmin) {
|
|
59
|
+
items.push(
|
|
60
|
+
{
|
|
61
|
+
id: 'plugins',
|
|
62
|
+
text: 'Plugins',
|
|
63
|
+
icon: 'package',
|
|
64
|
+
url: '#/plugins',
|
|
65
|
+
section: '#/plugins',
|
|
66
|
+
badge: badge(counts.plugins)
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
items.push(...pluginItems);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
items.push(
|
|
73
|
+
{divider: true},
|
|
74
|
+
{heading: 'View Site'}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
items.push(
|
|
78
|
+
{id: 'view-site', text: 'View Site', icon: 'external-link', url: '/', section: '/'}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
items.push(
|
|
82
|
+
{divider: true},
|
|
83
|
+
{heading: 'Documentation'}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
items.push(
|
|
87
|
+
{
|
|
88
|
+
id: 'documentation',
|
|
89
|
+
text: 'Usage',
|
|
90
|
+
icon: 'book',
|
|
91
|
+
url: '#/documentation',
|
|
92
|
+
section: '#/documentation',
|
|
93
|
+
badge: badge(counts.documents)
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'tutorials',
|
|
97
|
+
text: 'Tutorials',
|
|
98
|
+
icon: 'document',
|
|
99
|
+
url: '#/tutorials',
|
|
100
|
+
section: '#/documentation',
|
|
101
|
+
badge: badge(counts.tutorials)
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
// items.push(...pluginItems);
|
|
105
|
+
|
|
106
|
+
return items;
|
|
107
|
+
}
|