kempo 4.2.9 → 4.2.11

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.
@@ -4,14 +4,11 @@
4
4
  "/kempo-ui/icons/**": "../node_modules/kempo-ui/icons/**",
5
5
  "/kempo-ui/**": "../node_modules/kempo-ui/dist/**",
6
6
  "/monaco-editor/**": "../node_modules/monaco-editor/min/**",
7
- "/kempo/**": "../dist/kempo/**",
8
- "/admin": "../dist/admin/",
9
- "/admin/**": "../dist/admin/**"
7
+ "/kempo/**": "../node_modules/kempo/dist/kempo/**"
10
8
  },
11
9
  "middleware": {
12
10
  "custom": [
13
- "../middleware/kempo-auth.js",
14
- "../middleware/extension-scope-router.js"
11
+ "../node_modules/kempo/middleware/kempo.js"
15
12
  ]
16
13
  },
17
14
  "templating": {
package/bin/cli.js CHANGED
@@ -117,13 +117,13 @@ if(existsSync(join(projectDir, 'public'))){
117
117
  const { confirm } = await inquirer.prompt([{
118
118
  type: 'confirm',
119
119
  name: 'confirm',
120
- message: 'This will delete public/, server/, templates/, extensions/, .env, drizzle.config.js, and docker-compose.yml. Are you sure?',
120
+ message: 'This will delete public/, server/, templates/, .env, drizzle.config.js, and docker-compose.yml. Are you sure?',
121
121
  default: false
122
122
  }]);
123
123
 
124
124
  if(!confirm) process.exit(0);
125
125
 
126
- for(const item of ['public', 'server', 'templates', 'extensions', '.env', 'docker-compose.yml', 'drizzle.config.js']){
126
+ for(const item of ['public', 'server', 'templates', '.env', 'docker-compose.yml', 'drizzle.config.js']){
127
127
  const p = join(projectDir, item);
128
128
  if(existsSync(p)) rmSync(p, { recursive: true, force: true });
129
129
  }
@@ -276,8 +276,8 @@ if(!existsSync(pkgPath)){
276
276
  type: 'module',
277
277
  private: true,
278
278
  scripts: {
279
- dev: 'kempo-server --root public',
280
- start: 'kempo-server --root public'
279
+ dev: 'kempo-server --root public --port 9876',
280
+ start: 'kempo-server --root public --port 9876'
281
281
  }
282
282
  }, null, 2) + '\n');
283
283
  console.log('Created package.json');
@@ -312,12 +312,6 @@ if(!existsSync(templatesDir) && existsSync(templatesSource)){
312
312
  console.log('Scaffolded templates/');
313
313
  }
314
314
 
315
- const extensionsDir = join(projectDir, 'extensions');
316
- if(!existsSync(extensionsDir)){
317
- mkdirSync(extensionsDir);
318
- console.log('Created extensions/');
319
- }
320
-
321
315
  const drizzleConfigPath = join(projectDir, 'drizzle.config.js');
322
316
  const scaffoldDrizzle = join(moduleRoot, 'app-drizzle.config.js');
323
317
  if(!existsSync(drizzleConfigPath) && existsSync(scaffoldDrizzle)){
@@ -0,0 +1,518 @@
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>Group - 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/Icon.js"></script>
126
+ <script type="module" src="/kempo-ui/components/Table.js"></script>
127
+ <script type="module" src="/kempo-ui/components/tableControls/FirstPage.js"></script>
128
+ <script type="module" src="/kempo-ui/components/tableControls/PrevPage.js"></script>
129
+ <script type="module" src="/kempo-ui/components/tableControls/NextPage.js"></script>
130
+ <script type="module" src="/kempo-ui/components/tableControls/LastPage.js"></script>
131
+ <script type="module" src="/kempo-ui/components/tableControls/PageSelect.js"></script>
132
+ <script type="module" src="/kempo-ui/components/tableControls/PageSize.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/tableControls/AddMember.js"></script>
136
+ <script type="module" src="/admin/components/tableControls/AddPermission.js"></script>
137
+ <script type="module" src="/admin/components/TableRowLink.js"></script>
138
+
139
+ <div class="mb d-f" style="align-items: flex-start; justify-content: space-between;">
140
+ <div>
141
+ <a href="/admin/accounts/groups" class="no-link">
142
+ <button class="mbh"><k-icon name="arrow" direction="left"></k-icon> Groups</button>
143
+ </a>
144
+ <h1 class="mb0" id="pageTitle">Group</h1>
145
+ </div>
146
+ <button id="saveBtn" class="primary" disabled style="display:none;"><k-icon name="save"></k-icon> Save</button>
147
+ </div>
148
+
149
+ <div id="loading">Loading group...</div>
150
+ <div id="notFound" style="display: none;">
151
+ <p>Group not found.</p>
152
+ <a href="/admin/accounts/groups">Back to Groups</a>
153
+ </div>
154
+ <div id="content" style="display: none;">
155
+
156
+ <!-- Group Information -->
157
+ <div class="card">
158
+ <h2>Information</h2>
159
+ <div class="d-f mb">
160
+ <label style="min-width: 120px;"><strong>Owner</strong></label>
161
+ <span id="groupOwner"></span>
162
+ </div>
163
+ <div class="d-f mb">
164
+ <label for="groupNameInput" style="min-width: 120px;"><strong>Name</strong></label>
165
+ <input type="text" id="groupNameInput" style="max-width: 400px;">
166
+ </div>
167
+ <div class="d-f mb">
168
+ <label for="groupDescription" style="min-width: 120px;"><strong>Description</strong></label>
169
+ <input type="text" id="groupDescription" style="max-width: 400px;">
170
+ </div>
171
+ <div class="d-f mb">
172
+ <label style="min-width: 120px;"><strong>Created</strong></label>
173
+ <span id="groupCreatedAt"></span>
174
+ </div>
175
+ <div class="d-f mb">
176
+ <label style="min-width: 120px;"><strong>Updated</strong></label>
177
+ <span id="groupUpdatedAt"></span>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Members -->
182
+ <div class="card">
183
+ <h2>Members</h2>
184
+ <p id="usersMembersMsg" style="display:none">All users are automatically members of this group.</p>
185
+ <k-table id="membersTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="This group has no members.">
186
+ <admin-table-row-link slot="before" url-pattern="/admin/accounts/users/{id}" field-name="id"></admin-table-row-link>
187
+ <k-tc-delete-selected id="tcDeleteSelected" slot="top" style="display:none;"></k-tc-delete-selected>
188
+ <admin-add-member id="tcAddMember" slot="top" style="display:none;"></admin-add-member>
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
+ </div>
203
+
204
+ <!-- Permissions -->
205
+ <div class="card">
206
+ <h2>Permissions</h2>
207
+ <p id="adminPermsMsg" style="display:none">Members of this group automatically have all permissions.</p>
208
+ <k-table id="permissionsTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No permissions assigned to this group.">
209
+ <k-tc-delete-selected id="tcDeleteSelectedPerm" slot="top" style="display:none;"></k-tc-delete-selected>
210
+ <admin-add-permission id="tcAddPermission" slot="top" style="display:none;"></admin-add-permission>
211
+ <div class="d-f full" style="align-items: center;">
212
+ <div>
213
+ <k-tc-first-page></k-tc-first-page>
214
+ <k-tc-prev-page></k-tc-prev-page>
215
+ <k-tc-next-page></k-tc-next-page>
216
+ <k-tc-last-page></k-tc-last-page>
217
+ </div>
218
+ <div class="flex"></div>
219
+ <div><k-tc-page-select></k-tc-page-select></div>
220
+ <div class="flex"></div>
221
+ <div><k-tc-page-size></k-tc-page-size></div>
222
+ </div>
223
+ </k-table>
224
+ </div>
225
+
226
+ </div>
227
+
228
+ <script type="module">
229
+ import {
230
+ getGroup, updateGroup, deleteGroups, getGroupMembers,
231
+ removeMemberFromGroup, getGroupPermissions, removePermissionFromGroup,
232
+ getAllCurrentUserPermissions
233
+ } from '/kempo/sdk.js';
234
+ import Toast from '/kempo-ui/components/Toast.js';
235
+ import Dialog from '/kempo-ui/components/Dialog.js';
236
+
237
+ const groupName = decodeURIComponent(
238
+ window.location.pathname.split('/admin/accounts/groups/')[1]?.replace(/\/$/, '') || ''
239
+ );
240
+
241
+ if(!groupName){
242
+ document.getElementById('loading').style.display = 'none';
243
+ document.getElementById('notFound').style.display = '';
244
+ }
245
+
246
+ /*
247
+ Permission checks
248
+ */
249
+
250
+ const [, permResult] = await getAllCurrentUserPermissions();
251
+ const myPerms = new Set(permResult?.permissions ?? []);
252
+ const canEdit = myPerms.has('system:group:update');
253
+ const canDelete = myPerms.has('system:group:delete');
254
+ const canReadPerms = myPerms.has('system:permissions:read');
255
+
256
+ /*
257
+ Load group
258
+ */
259
+
260
+ const [groupError, groupData] = await getGroup(groupName);
261
+
262
+ document.getElementById('loading').style.display = 'none';
263
+
264
+ if(groupError){
265
+ document.getElementById('notFound').style.display = '';
266
+ } else {
267
+ document.getElementById('content').style.display = '';
268
+ const displayName = groupName.includes(':') ? groupName.split(':').slice(1).join(':') : groupName;
269
+ document.getElementById('pageTitle').textContent = `Group: ${displayName}`;
270
+ document.title = `${displayName} - Admin`;
271
+
272
+ const owner = groupName.includes(':') ? groupName.split(':')[0] : '';
273
+ const isAdmin = owner === 'admin' || !owner;
274
+
275
+ await customElements.whenDefined('k-table');
276
+
277
+ /*
278
+ Group Information
279
+ */
280
+
281
+ const isCustom = owner === 'custom';
282
+ const $saveBtn = document.getElementById('saveBtn');
283
+ const $groupOwner = document.getElementById('groupOwner');
284
+ const $groupNameInput = document.getElementById('groupNameInput');
285
+ const $groupDescription = document.getElementById('groupDescription');
286
+
287
+ $groupOwner.textContent = owner || '(none)';
288
+ $groupNameInput.value = displayName;
289
+ $groupDescription.value = groupData.description || '';
290
+ document.getElementById('groupCreatedAt').textContent = new Date(groupData.createdAt).toLocaleString();
291
+ document.getElementById('groupUpdatedAt').textContent = groupData.updatedAt ? new Date(groupData.updatedAt).toLocaleString() : 'Never';
292
+ if(!canEdit || !isCustom){
293
+ $groupNameInput.readOnly = true;
294
+ $groupDescription.readOnly = true;
295
+ } else {
296
+ $saveBtn.style.display = '';
297
+
298
+ const origName = displayName;
299
+ const origDescription = groupData.description || '';
300
+
301
+ const handleBeforeUnload = e => { e.preventDefault(); e.returnValue = ''; };
302
+
303
+ const checkDirty = () => {
304
+ const dirty = $groupNameInput.value !== origName || $groupDescription.value !== origDescription;
305
+ $saveBtn.disabled = !dirty;
306
+ if(dirty){
307
+ window.addEventListener('beforeunload', handleBeforeUnload);
308
+ } else {
309
+ window.removeEventListener('beforeunload', handleBeforeUnload);
310
+ }
311
+ };
312
+
313
+ $groupNameInput.addEventListener('input', checkDirty);
314
+ $groupDescription.addEventListener('input', checkDirty);
315
+
316
+ $saveBtn.addEventListener('click', async () => {
317
+ const newDisplayName = $groupNameInput.value.trim();
318
+ if(!newDisplayName){
319
+ Toast.error('Name is required');
320
+ return;
321
+ }
322
+ const newFullName = `${owner}:${newDisplayName}`;
323
+ const patch = { name: groupName, description: $groupDescription.value };
324
+ if(newFullName !== groupName) patch.newName = newFullName;
325
+ const [error] = await updateGroup(patch);
326
+ if(error){
327
+ Toast.error(error.msg || 'Failed to save group');
328
+ return;
329
+ }
330
+ window.removeEventListener('beforeunload', handleBeforeUnload);
331
+ Toast.success('Group saved');
332
+ if(newFullName !== groupName){
333
+ window.location.href = `/admin/accounts/groups/${encodeURIComponent(newFullName)}`;
334
+ } else {
335
+ $saveBtn.disabled = true;
336
+ document.getElementById('groupUpdatedAt').textContent = new Date().toLocaleString();
337
+ }
338
+ });
339
+ }
340
+
341
+ /*
342
+ Members
343
+ */
344
+
345
+ const $membersTable = document.getElementById('membersTable');
346
+
347
+ if(groupName === 'system:Users'){
348
+ document.getElementById('usersMembersMsg').style.display = '';
349
+ $membersTable.style.display = 'none';
350
+ } else {
351
+ const $tcDeleteSelected = document.getElementById('tcDeleteSelected');
352
+ const $tcAddMember = document.getElementById('tcAddMember');
353
+ $tcAddMember.groupName = groupName;
354
+
355
+ if(canEdit){
356
+ $membersTable.setAttribute('request-delete', '');
357
+ $membersTable.setAttribute('enable-selection', '');
358
+ $tcDeleteSelected.style.display = '';
359
+ $tcAddMember.style.display = '';
360
+
361
+ const del = document.createElement('k-tc-delete-record');
362
+ del.setAttribute('slot', 'after');
363
+ $membersTable.appendChild(del);
364
+ }
365
+
366
+ $membersTable.addEventListener('memberAdded', e => loadMembers());
367
+
368
+ $membersTable.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
369
+ const count = records.length;
370
+ const msg = count === 1
371
+ ? `<div class="p"><p>Remove "${records[0].name}" from this group?</p></div>`
372
+ : `<div class="p"><p>Remove ${count} members from this group?</p></div>`;
373
+ Dialog.confirm(msg, async (confirmed) => {
374
+ if(!confirmed){ reject(); return; }
375
+ const errors = [];
376
+ for(const record of records){
377
+ const [error] = await removeMemberFromGroup(groupName, record.id);
378
+ if(error) errors.push(error.msg);
379
+ }
380
+ if(errors.length){
381
+ reject();
382
+ Toast.error(errors[0] || 'Failed to remove member');
383
+ return;
384
+ }
385
+ approve();
386
+ Toast.success(count === 1 ? 'Member removed' : `${count} members removed`);
387
+ loadMembers();
388
+ });
389
+ });
390
+
391
+ const loadMembers = async () => {
392
+ const [error, data] = await getGroupMembers(groupName);
393
+ if(error){
394
+ Toast.error(error.msg || 'Failed to load members');
395
+ return;
396
+ }
397
+ $membersTable.setData({
398
+ records: data.members,
399
+ fields: [
400
+ { name: 'name', label: 'Name' },
401
+ { name: 'email', label: 'Email' },
402
+ {
403
+ name: 'createdAt',
404
+ label: 'Joined',
405
+ formatter: v => new Date(v).toLocaleString()
406
+ }
407
+ ]
408
+ });
409
+ $membersTable.setupFetchRecords(data.total, async (start, count) => {
410
+ const [err, d] = await getGroupMembers(groupName, { limit: count, offset: start });
411
+ if(err) return [];
412
+ return d.members;
413
+ });
414
+ };
415
+
416
+ loadMembers();
417
+ }
418
+
419
+ /*
420
+ Permissions
421
+ */
422
+
423
+ const $permissionsTable = document.getElementById('permissionsTable');
424
+
425
+ if(groupName === 'system:Administrators'){
426
+ document.getElementById('adminPermsMsg').style.display = '';
427
+ $permissionsTable.style.display = 'none';
428
+ } else if(canReadPerms){
429
+ if(canEdit){
430
+ $permissionsTable.setAttribute('request-delete', '');
431
+ $permissionsTable.setAttribute('enable-selection', '');
432
+
433
+ const $tcDeleteSelectedPerm = document.getElementById('tcDeleteSelectedPerm');
434
+ $tcDeleteSelectedPerm.style.display = '';
435
+
436
+ const $tcAddPermission = document.getElementById('tcAddPermission');
437
+ $tcAddPermission.groupName = groupName;
438
+ $tcAddPermission.style.display = '';
439
+
440
+ const delPerm = document.createElement('k-tc-delete-record');
441
+ delPerm.setAttribute('slot', 'after');
442
+ $permissionsTable.appendChild(delPerm);
443
+
444
+ $permissionsTable.addEventListener('permissionAdded', e => loadPermissions());
445
+
446
+ $permissionsTable.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
447
+ const count = records.length;
448
+ const msg = count === 1
449
+ ? `<div class="p"><p>Remove permission "${records[0].name}" from this group?</p></div>`
450
+ : `<div class="p"><p>Remove ${count} permissions from this group?</p></div>`;
451
+ Dialog.confirm(msg, async (confirmed) => {
452
+ if(!confirmed){ reject(); return; }
453
+ const errors = [];
454
+ for(const record of records){
455
+ const [error] = await removePermissionFromGroup(groupName, record.name);
456
+ if(error) errors.push(error.msg);
457
+ }
458
+ if(errors.length){
459
+ reject();
460
+ Toast.error(errors[0] || 'Failed to remove permission');
461
+ return;
462
+ }
463
+ approve();
464
+ Toast.success(count === 1 ? 'Permission removed' : `${count} permissions removed`);
465
+ loadPermissions();
466
+ });
467
+ });
468
+ }
469
+
470
+ const loadPermissions = async () => {
471
+ const [error, data] = await getGroupPermissions(groupName);
472
+ if(error){
473
+ Toast.error(error.msg || 'Failed to load permissions');
474
+ return;
475
+ }
476
+ $permissionsTable.setData({
477
+ records: data.permissions.map(name => ({ name })),
478
+ fields: [
479
+ {
480
+ name: 'owner',
481
+ label: 'Owner',
482
+ size: 120,
483
+ calculator: r => r.name.includes(':') ? r.name.split(':')[0] : ''
484
+ },
485
+ {
486
+ name: 'resource',
487
+ label: 'Resource',
488
+ size: 180,
489
+ calculator: r => {
490
+ const parts = r.name.split(':');
491
+ return parts.length >= 3 ? parts[1] : parts.length === 2 ? parts[1] : '';
492
+ }
493
+ },
494
+ {
495
+ name: 'action',
496
+ label: 'Action',
497
+ size: 140,
498
+ calculator: r => {
499
+ const parts = r.name.split(':');
500
+ return parts.length >= 3 ? parts.slice(2).join(':') : '';
501
+ }
502
+ }
503
+ ]
504
+ });
505
+ };
506
+
507
+ loadPermissions();
508
+ } else {
509
+ $permissionsTable.closest('.card').style.display = 'none';
510
+ }
511
+ }
512
+ </script>
513
+
514
+
515
+ </k-main>
516
+
517
+ </body>
518
+ </html>