kempo 4.2.10 → 4.2.12

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.
@@ -0,0 +1,410 @@
1
+ <!--
2
+ owner: system
3
+ locked: true
4
+ name:
5
+ author:
6
+ created:
7
+ updated:
8
+ -->
9
+ <!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <link rel="icon" type="image/png" sizes="48x48" href="/media/icon48.png">
15
+ <link rel="manifest" href="/manifest.json">
16
+ <title>Settings - Admin</title>
17
+ <link rel="stylesheet" href="/kempo-css/kempo.min.css">
18
+ <script src="/admin/init.js"></script>
19
+
20
+ </head>
21
+ <body>
22
+ <!--
23
+ locked: true
24
+ -->
25
+
26
+ <k-aside
27
+ main="push"
28
+ state="expanded"
29
+ style="--aside_padding: 0;"
30
+ persistent-id="admin-nav"
31
+ >
32
+ <k-aside-toggle>
33
+ <h3 class="m0">Kempo Admin</h3>
34
+ </k-aside-toggle>
35
+ <k-aside-item
36
+ icon="preview"
37
+ href="/"
38
+ no-expand
39
+ >Site</k-aside-item>
40
+ <k-aside-item
41
+ icon="cards"
42
+ href="/admin"
43
+ no-expand
44
+ >Dashboard</k-aside-item>
45
+ <k-aside-menu
46
+ icon="manage-accounts"
47
+ label="Accounts"
48
+ >
49
+ <k-aside-item
50
+ icon="person"
51
+ href="/admin/accounts/users"
52
+ no-expand
53
+ >Users</k-aside-item>
54
+ <k-aside-item
55
+ icon="group"
56
+ href="/admin/accounts/groups"
57
+ no-expand
58
+ >Groups</k-aside-item>
59
+ <k-aside-item
60
+ icon="lock"
61
+ href="/admin/accounts/permissions"
62
+ no-expand
63
+ >Permissions</k-aside-item>
64
+ </k-aside-menu>
65
+ <k-aside-menu
66
+ icon="cards_stack"
67
+ label="Content"
68
+ >
69
+ <k-aside-item
70
+ icon="browse"
71
+ href="/admin/content/templates"
72
+ >Templates</k-aside-item>
73
+ <k-aside-item
74
+ icon="docs"
75
+ href="/admin/content/pages"
76
+ >Pages</k-aside-item>
77
+ <k-aside-item
78
+ icon="content"
79
+ href="/admin/content/fragments"
80
+ >Fragments</k-aside-item>
81
+ <k-aside-item
82
+ icon="globe"
83
+ href="/admin/content/globals"
84
+ >Global Content</k-aside-item>
85
+ </k-aside-menu>
86
+ <k-aside-item
87
+ icon="settings"
88
+ href="/admin/settings"
89
+ no-expand
90
+ >Settings</k-aside-item>
91
+ <k-aside-item
92
+ icon="extension"
93
+ href="/admin/extensions"
94
+ no-expand
95
+ >Extensions</k-aside-item>
96
+ <hr>
97
+ <k-aside-spacer></k-aside-spacer>
98
+ <k-theme-switcher
99
+ labels
100
+ style="--padding:var(--spacer_h);margin:var(--spacer_h) auto"
101
+ ></k-theme-switcher>
102
+ </k-aside>
103
+ <script type="module">
104
+ import '/kempo-ui/components/ThemeSwitcher.js';
105
+ const items = document.querySelectorAll('k-aside-item');
106
+ const currentPath = window.location.pathname;
107
+
108
+ items.forEach(item => {
109
+ const href = item.getAttribute('href');
110
+ if (!href) return;
111
+ const isExact = href === '/admin' || href === '/';
112
+ const match = isExact
113
+ ? (currentPath === href || currentPath === href + '/')
114
+ : (currentPath === href || currentPath.startsWith(href + '/'));
115
+ if (match) {
116
+ item.setAttribute('active', '');
117
+ const menu = item.closest('k-aside-menu');
118
+ if (menu) menu.setAttribute('open', '');
119
+ }
120
+ });
121
+ </script>
122
+
123
+ <k-main>
124
+
125
+ <script type="module" src="/kempo-ui/components/Table.js"></script>
126
+ <script type="module" src="/kempo-ui/components/tableControls/FirstPage.js"></script>
127
+ <script type="module" src="/kempo-ui/components/tableControls/PrevPage.js"></script>
128
+ <script type="module" src="/kempo-ui/components/tableControls/NextPage.js"></script>
129
+ <script type="module" src="/kempo-ui/components/tableControls/LastPage.js"></script>
130
+ <script type="module" src="/kempo-ui/components/tableControls/PageSelect.js"></script>
131
+ <script type="module" src="/kempo-ui/components/tableControls/PageSize.js"></script>
132
+ <script type="module" src="/kempo-ui/components/tableControls/Edit.js"></script>
133
+ <script type="module" src="/kempo-ui/components/tableControls/DeleteRecord.js"></script>
134
+ <script type="module" src="/kempo-ui/components/tableControls/DeleteSelected.js"></script>
135
+ <script type="module" src="/admin/components/TableRowLink.js"></script>
136
+ <script type="module" src="/kempo/components/Permission.js"></script>
137
+
138
+ <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
139
+ <h1 class="flex mb0">Settings</h1>
140
+ <k-permission has="system:custom-settings:manage">
141
+ <button id="createSettingBtn" class="primary">Create Setting</button>
142
+ </k-permission>
143
+ </div>
144
+
145
+ <h2>Custom Settings</h2>
146
+ <k-table id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom settings have been created yet.">
147
+ <admin-table-row-link slot="before" url-pattern="{_link}" field-name="_link"></admin-table-row-link>
148
+ <div slot="top" class="d-f full" style="align-items: center;">
149
+ <div>
150
+ <k-permission has="system:custom-settings:manage">
151
+ <k-tc-delete-selected></k-tc-delete-selected>
152
+ </k-permission>
153
+ </div>
154
+ </div>
155
+ <div class="d-f full" style="align-items: center;">
156
+ <div>
157
+ <k-tc-first-page></k-tc-first-page>
158
+ <k-tc-prev-page></k-tc-prev-page>
159
+ <k-tc-next-page></k-tc-next-page>
160
+ <k-tc-last-page></k-tc-last-page>
161
+ </div>
162
+ <div class="flex"></div>
163
+ <div><k-tc-page-select></k-tc-page-select></div>
164
+ <div class="flex"></div>
165
+ <div><k-tc-page-size></k-tc-page-size></div>
166
+ </div>
167
+ </k-table>
168
+
169
+ <h2>Extension Settings</h2>
170
+ <k-table id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension settings installed.">
171
+ <admin-table-row-link slot="before" url-pattern="{_link}" field-name="_link"></admin-table-row-link>
172
+ <div class="d-f full" style="align-items: center;">
173
+ <div>
174
+ <k-tc-first-page></k-tc-first-page>
175
+ <k-tc-prev-page></k-tc-prev-page>
176
+ <k-tc-next-page></k-tc-next-page>
177
+ <k-tc-last-page></k-tc-last-page>
178
+ </div>
179
+ <div class="flex"></div>
180
+ <div><k-tc-page-select></k-tc-page-select></div>
181
+ <div class="flex"></div>
182
+ <div><k-tc-page-size></k-tc-page-size></div>
183
+ </div>
184
+ </k-table>
185
+
186
+ <h2>System Settings</h2>
187
+ <k-table id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system settings found.">
188
+ <admin-table-row-link slot="before" url-pattern="{_link}" field-name="_link"></admin-table-row-link>
189
+ <div class="d-f full" style="align-items: center;">
190
+ <div>
191
+ <k-tc-first-page></k-tc-first-page>
192
+ <k-tc-prev-page></k-tc-prev-page>
193
+ <k-tc-next-page></k-tc-next-page>
194
+ <k-tc-last-page></k-tc-last-page>
195
+ </div>
196
+ <div class="flex"></div>
197
+ <div><k-tc-page-select></k-tc-page-select></div>
198
+ <div class="flex"></div>
199
+ <div><k-tc-page-size></k-tc-page-size></div>
200
+ </div>
201
+ </k-table>
202
+
203
+ <script type="module">
204
+ import { listSettings, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';
205
+ import { html } from '/kempo-ui/lit-all.min.js';
206
+ import Toast from '/kempo-ui/components/Toast.js';
207
+ import Dialog from '/kempo-ui/components/Dialog.js';
208
+
209
+ await window.customElements.whenDefined('k-table');
210
+
211
+ const [, permData] = await getAllCurrentUserPermissions();
212
+ const myPerms = new Set(permData?.permissions ?? []);
213
+ const canManageSystem = myPerms.has('system:settings:update');
214
+ const canManageCustom = myPerms.has('system:custom-settings:manage');
215
+
216
+ const buildFields = (editable, showOwner = false) => [
217
+ ...(showOwner ? [{ name: 'owner', label: 'Owner', size: 120, editable: false, calculator: r => r.name.split(':')[0] }] : []),
218
+ { name: 'name', label: 'Name', size: 220, editable: false, calculator: r => r.name.split(':').slice(1).join(':') },
219
+ { name: 'value', label: 'Value', size: 280, editable, formatter: v => {
220
+ if(v === null || v === undefined) return '';
221
+ const s = String(v);
222
+ return s.length > 120 ? s.slice(0, 117) + '...' : s;
223
+ }},
224
+ { name: 'type', label: 'Type', size: 90, editable },
225
+ { name: 'isPublic', label: 'Public', size: 80, editable, formatter: v => String(v) },
226
+ { name: 'description', label: 'Description', size: 320, editable },
227
+ { name: 'updatedAt', label: 'Updated', size: 190, editable: false, formatter: v => new Date(v).toLocaleString() }
228
+ ];
229
+
230
+ const coerceValue = (value, type) => {
231
+ if(type === 'number') return Number(value);
232
+ if(type === 'boolean') return String(value).toLowerCase() === 'true';
233
+ if(type === 'json') return JSON.parse(String(value));
234
+ return String(value);
235
+ };
236
+
237
+ const setupTable = ($table, owner, canEdit, loadFn) => {
238
+ if(canEdit){
239
+ $table.setAttribute('request-edit', '');
240
+ const edit = document.createElement('k-tc-edit');
241
+ edit.setAttribute('slot', 'after');
242
+ $table.appendChild(edit);
243
+ }
244
+
245
+ if(owner === 'custom' && canEdit){
246
+ $table.setAttribute('request-delete', '');
247
+ $table.setAttribute('enable-selection', '');
248
+ const del = document.createElement('k-tc-delete-record');
249
+ del.setAttribute('slot', 'after');
250
+ $table.appendChild(del);
251
+ }
252
+
253
+ $table.addEventListener('requestSave', async ({ detail: { record, newData, approve, reject } }) => {
254
+ const type = newData.type ?? record.type;
255
+ let value = newData.value !== undefined ? newData.value : record.value;
256
+
257
+ try {
258
+ value = coerceValue(value, type);
259
+ } catch(e){
260
+ reject();
261
+ Toast.error(`Invalid ${type} value`);
262
+ return;
263
+ }
264
+
265
+ const isPublic = newData.isPublic !== undefined
266
+ ? String(newData.isPublic).toLowerCase() === 'true'
267
+ : record.isPublic;
268
+ const [recordOwner, ...nameParts] = record.name.split(':');
269
+ const name = nameParts.join(':');
270
+ const [error] = await setSetting(recordOwner, name, {
271
+ value,
272
+ type,
273
+ isPublic,
274
+ description: newData.description ?? record.description
275
+ });
276
+
277
+ if(error){ reject(); Toast.error(error.msg || 'Failed to update setting'); return; }
278
+ approve();
279
+ Toast.success('Setting updated');
280
+ });
281
+
282
+ if(owner === 'custom' && canEdit){
283
+ $table.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
284
+ const displayName = r => r.name.split(':').slice(1).join(':');
285
+ const count = records.length;
286
+ const msg = count === 1
287
+ ? `<div class="p"><p>Delete "${displayName(records[0])}"? This action cannot be undone.</p></div>`
288
+ : `<div class="p"><p>Delete ${count} settings? This action cannot be undone.</p></div>`;
289
+ Dialog.confirm(msg, async (confirmed) => {
290
+ if(!confirmed){ reject(); return; }
291
+ let failed = 0;
292
+ for(const record of records){
293
+ const [recOwner, ...nameParts] = record.name.split(':');
294
+ const [error] = await deleteSetting(recOwner, nameParts.join(':'));
295
+ if(error) failed++;
296
+ }
297
+ if(failed){
298
+ reject();
299
+ Toast.error(`Failed to delete ${failed} setting${failed !== 1 ? 's' : ''}`);
300
+ return;
301
+ }
302
+ approve();
303
+ Toast.success(count === 1 ? 'Setting deleted' : `${count} settings deleted`);
304
+ loadFn();
305
+ });
306
+ });
307
+ }
308
+ };
309
+
310
+ const addLinks = settings => {
311
+ settings.forEach(s => {
312
+ const [o, ...parts] = s.name.split(':');
313
+ s._link = `/admin/settings/${encodeURIComponent(o)}/${encodeURIComponent(parts.join(':'))}/`;
314
+ });
315
+ };
316
+
317
+ const loadTable = async ($table, owner, canEdit) => {
318
+ const [error, data] = await listSettings({ owner });
319
+ if(error){ Toast.error(error.msg || 'Failed to load settings'); return; }
320
+ addLinks(data.settings);
321
+ $table.setData({ records: data.settings, fields: buildFields(canEdit, owner === 'extension') });
322
+ $table.setupFetchRecords(data.total, async (start, count) => {
323
+ const [err, d] = await listSettings({ owner, limit: count, offset: start });
324
+ if(err) return [];
325
+ addLinks(d.settings);
326
+ return d.settings;
327
+ });
328
+ };
329
+
330
+ const $customTable = document.getElementById('customTable');
331
+ const $extensionTable = document.getElementById('extensionTable');
332
+ const $systemTable = document.getElementById('systemTable');
333
+
334
+ setupTable($customTable, 'custom', canManageCustom, () => loadTable($customTable, 'custom', canManageCustom));
335
+ setupTable($extensionTable, 'extension', false, () => loadTable($extensionTable, 'extension', false));
336
+ setupTable($systemTable, 'system', canManageSystem, () => loadTable($systemTable, 'system', canManageSystem));
337
+
338
+ await Promise.all([
339
+ loadTable($customTable, 'custom', canManageCustom),
340
+ loadTable($extensionTable, 'extension', false),
341
+ loadTable($systemTable, 'system', canManageSystem)
342
+ ]);
343
+
344
+ if(canManageCustom){
345
+ document.getElementById('createSettingBtn').addEventListener('click', () => {
346
+ const $dialog = Dialog.create(html`
347
+ <div class="p">
348
+ <div class="mb">
349
+ <label class="d-b mb-sm">Name</label>
350
+ <input type="text" id="dlg-name" class="full" placeholder="e.g. my-setting" required>
351
+ </div>
352
+ <div class="mb">
353
+ <label class="d-b mb-sm">Value</label>
354
+ <input type="text" id="dlg-value" class="full">
355
+ </div>
356
+ <div class="mb">
357
+ <label class="d-b mb-sm">Type</label>
358
+ <select id="dlg-type" class="full">
359
+ <option value="string">String</option>
360
+ <option value="number">Number</option>
361
+ <option value="boolean">Boolean</option>
362
+ <option value="json">JSON</option>
363
+ </select>
364
+ </div>
365
+ <div class="mb d-f" style="align-items: center; gap: var(--spacer-sm);">
366
+ <input type="checkbox" id="dlg-isPublic">
367
+ <label for="dlg-isPublic">Public (accessible without authentication)</label>
368
+ </div>
369
+ <div>
370
+ <label class="d-b mb-sm">Description</label>
371
+ <input type="text" id="dlg-description" class="full">
372
+ </div>
373
+ </div>
374
+ `, {
375
+ title: 'Create Setting',
376
+ confirmText: 'Create',
377
+ confirmAction: async () => {
378
+ const name = $dialog.querySelector('#dlg-name').value.trim();
379
+ if(!name){ Toast.error('Name is required'); return; }
380
+ if(name.includes(':')){ Toast.error('Name cannot contain ":"'); return; }
381
+
382
+ const rawValue = $dialog.querySelector('#dlg-value').value;
383
+ const type = $dialog.querySelector('#dlg-type').value;
384
+ const isPublic = $dialog.querySelector('#dlg-isPublic').checked;
385
+ const description = $dialog.querySelector('#dlg-description').value.trim();
386
+
387
+ let value;
388
+ try {
389
+ value = coerceValue(rawValue, type);
390
+ } catch(e){
391
+ Toast.error(`Invalid ${type} value`);
392
+ return;
393
+ }
394
+
395
+ const [error] = await setSetting('custom', name, { value, type, isPublic, description });
396
+ if(error){ Toast.error(error.msg || 'Failed to create setting'); return; }
397
+ Toast.success('Setting created');
398
+ loadTable($customTable, 'custom', canManageCustom);
399
+ },
400
+ cancelText: 'Cancel'
401
+ });
402
+ });
403
+ }
404
+ </script>
405
+
406
+
407
+ </k-main>
408
+
409
+ </body>
410
+ </html>
@@ -1,7 +1,9 @@
1
1
  import { dirname, join, extname } from 'path';
2
2
  import { readFile, stat, readdir } from 'fs/promises';
3
- import { pathToFileURL } from 'url';
3
+ import { pathToFileURL, fileURLToPath } from 'url';
4
4
  import { renderExternalPage } from 'kempo-server/templating';
5
+ import getSession from '../server/utils/auth/getSession.js';
6
+ import currentUserHasPermission from '../server/utils/permissions/currentUserHasPermission.js';
5
7
  import { getEnabledExtensions } from '../server/utils/extensions/scopeCache.js';
6
8
 
7
9
  const MIME_TYPES = {
@@ -22,7 +24,9 @@ const MIME_TYPES = {
22
24
  const ROUTE_FILES = ['GET.js','POST.js','PUT.js','DELETE.js','PATCH.js','HEAD.js','OPTIONS.js','index.js','CATCH.js'];
23
25
 
24
26
  const NODE_MODULES = join(process.cwd(), 'node_modules');
25
- const ADMIN_ROOT = join(process.cwd(), 'dist', 'admin');
27
+
28
+ // Resolves to dist/admin relative to this file — works both in this repo and as a consumer dependency
29
+ const ADMIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'admin');
26
30
 
27
31
  const buildResolveDir = (base, url) => {
28
32
  const parts = url.replace(/\/+$/, '').split('/').filter(Boolean);
@@ -65,13 +69,13 @@ const executeRouteFile = async (filePath, request, response, params = {}) => {
65
69
  return true;
66
70
  };
67
71
 
68
- const serveDir = async (dirPath, method, request, response, resolveDir, projectPublic, params = {}) => {
72
+ const serveDir = async (dirPath, method, request, response, resolveDir, rootDir, params = {}) => {
69
73
  const candidates = [`${method}.js`, 'index.page.html', 'index.js', 'index.html', 'CATCH.js'];
70
74
  for(const candidate of candidates){
71
75
  const candidatePath = join(dirPath, candidate);
72
76
  try { await stat(candidatePath); } catch { continue; }
73
77
  if(candidate.endsWith('.page.html')){
74
- const html = await renderExternalPage(candidatePath, projectPublic, resolveDir);
78
+ const html = await renderExternalPage(candidatePath, rootDir, resolveDir);
75
79
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
76
80
  response.end(html);
77
81
  return true;
@@ -91,20 +95,37 @@ export default config => {
91
95
  const PROJECT_PUBLIC = config.rootPath || join(process.cwd(), 'public');
92
96
 
93
97
  return async (request, response, next) => {
98
+ const { path } = request;
94
99
  const url = request.url.split('?')[0];
95
100
 
96
101
  /*
97
- Core admin routing: /admin/** -> dist/admin/
98
- Renders .page.html files with ADMIN_ROOT so the admin template and fragments are resolved correctly.
99
- Must run before kempo-server's custom route handler, which would incorrectly use app-public as rootDir.
102
+ Auth: protect /account and /admin routes
103
+ */
104
+ if(path.startsWith('/account') || path.startsWith('/admin')){
105
+ const [error, session] = await getSession({ token: request.cookies.session_token });
106
+ if(error || !session?.user) return response.redirect('/login');
107
+
108
+ if(path.startsWith('/admin')){
109
+ const [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:admin:access');
110
+ if(permError || !hasPermission) return response.redirect('/account');
111
+
112
+ if(path.startsWith('/admin/pages/edit')){
113
+ const [editErr, canEdit] = await currentUserHasPermission(request.cookies.session_token, 'system:pages:update');
114
+ if(editErr || !canEdit) return response.redirect('/admin/pages');
115
+ }
116
+ }
117
+ }
118
+
119
+ /*
120
+ Admin routing: /admin/** -> ADMIN_ROOT (dist/admin)
121
+ Uses renderExternalPage so template walk-up starts in ADMIN_ROOT, not the consumer's public/.
100
122
  */
101
123
  if(url === '/admin' || url.startsWith('/admin/')){
102
- const subPath = url.slice('/admin'.length) || '/';
103
- const segments = subPath.replace(/^\//, '').split('/').filter(Boolean);
104
124
  const method = request.method?.toUpperCase() || 'GET';
105
125
 
106
- // Don't intercept extension admin pages here — they're handled below
126
+ // Extension admin pages handled below
107
127
  if(!url.startsWith('/admin/extension/')){
128
+ const segments = url.slice('/admin'.length).replace(/^\//, '').split('/').filter(Boolean);
108
129
  const resolveDir = buildResolveDir(ADMIN_ROOT, url);
109
130
 
110
131
  if(segments.length === 0){
@@ -139,21 +160,18 @@ export default config => {
139
160
  }
140
161
  }
141
162
  }
142
- // Not found in dist/admin — fall through to next()
143
163
  return next();
144
164
  }
145
165
  }
146
166
 
147
167
  /*
148
- Admin extension routing: /admin/extension/{name}/** -> extension's admin/ directory
149
- Protected by kempo-auth; no enabled check needed here.
168
+ Extension admin routing: /admin/extension/{name}/** -> extension's admin/ directory
150
169
  */
151
170
  const adminMatch = url.match(/^\/admin\/extension\/((?:@[^/]+\/)?[^/]+)(\/.*)?$/);
152
171
  if(adminMatch){
153
172
  const extName = adminMatch[1];
154
173
  const subPath = adminMatch[2] || '/';
155
- const extRoot = join(NODE_MODULES, extName);
156
- const adminDir = join(extRoot, 'admin');
174
+ const adminDir = join(NODE_MODULES, extName, 'admin');
157
175
  const pageCandidates = subPath.endsWith('/')
158
176
  ? [join(adminDir, subPath, 'index.page.html')]
159
177
  : [
@@ -169,26 +187,25 @@ export default config => {
169
187
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
170
188
  response.end(html);
171
189
  return;
172
- } catch { /* try next candidate */ }
190
+ } catch { /* try next */ }
173
191
  }
174
192
 
175
193
  const filePath = join(adminDir, subPath === '/' ? 'index.html' : subPath);
176
194
  try {
177
- const stats = await stat(filePath);
178
- if(stats.isFile()){
195
+ const fileStat = await stat(filePath);
196
+ if(fileStat.isFile()){
179
197
  const mime = MIME_TYPES[extname(filePath)] || 'application/octet-stream';
180
198
  response.writeHead(200, { 'Content-Type': mime });
181
199
  response.end(await readFile(filePath));
182
200
  return;
183
201
  }
184
- } catch { /* fall through to next() */ }
202
+ } catch { /* fall through */ }
185
203
 
186
204
  return next();
187
205
  }
188
206
 
189
207
  /*
190
- Public scope routing: /{scope}/** -> enabled extension's public/ directory
191
- Supports: .page.html templates, JS route handlers (GET.js etc.), dynamic [param] dirs, static files.
208
+ Extension public scope routing: /{scope}/** -> extension's public/ directory
192
209
  */
193
210
  const extensions = await getEnabledExtensions();
194
211
 
@@ -197,9 +214,7 @@ export default config => {
197
214
  try {
198
215
  pkgPath = join(NODE_MODULES, ext.name, 'package.json');
199
216
  pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
200
- } catch {
201
- continue;
202
- }
217
+ } catch { continue; }
203
218
 
204
219
  const scope = pkg.kempo?.['public-scope'];
205
220
  if(!scope) continue;
@@ -252,4 +267,4 @@ export default config => {
252
267
 
253
268
  next();
254
269
  };
255
- };
270
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "kempo",
3
- "version": "4.2.10",
3
+ "version": "4.2.12",
4
4
  "type": "module",
5
5
  "description": "A fullstack CMS framework for creating websites with user authentication",
6
6
  "scripts": {
7
- "dev": "kempo-server --root app-public --port 9876 --config ./dev.config.json",
7
+ "dev": "node dev.mjs",
8
8
  "build": "node scripts/build.js",
9
9
  "db:generate": "drizzle-kit generate",
10
10
  "db:migrate": "drizzle-kit migrate",
@@ -29,7 +29,7 @@
29
29
  "server/db/index.js",
30
30
  "server/db/schema.js",
31
31
  "server/sdk.js",
32
- "middleware",
32
+ "middleware/kempo.js",
33
33
  "app-templates",
34
34
  "scripts/init-db.js"
35
35
  ],
@@ -1,26 +0,0 @@
1
- import getSession from '../server/utils/auth/getSession.js';
2
- import currentUserHasPermission from '../server/utils/permissions/currentUserHasPermission.js';
3
-
4
- export default (config) => async (request, response, next) => {
5
- const { path } = request;
6
- if(path.startsWith('/account') || path.startsWith('/admin')){
7
- const sessionToken = request.cookies.session_token;
8
- const [error, session] = await getSession({ token: sessionToken });
9
- if(error || !session || !session.user){
10
- return response.redirect('/login');
11
- }
12
- if(request.path.startsWith('/admin')){
13
- const [permError, hasPermission] = await currentUserHasPermission(sessionToken, 'system:admin:access');
14
- if(permError || !hasPermission){
15
- return response.redirect('/account');
16
- }
17
- if(request.path.startsWith('/admin/pages/edit')){
18
- const [editPermError, hasEditPermission] = await currentUserHasPermission(sessionToken, 'system:pages:update');
19
- if(editPermError || !hasEditPermission){
20
- return response.redirect('/admin/pages');
21
- }
22
- }
23
- }
24
- }
25
- return next();
26
- };