kempo 4.2.10 → 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.
@@ -0,0 +1,397 @@
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>Permissions - 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="/kempo-ui/components/Tags.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">Permissions</h1>
140
+ <k-permission has="system:permissions:manage">
141
+ <button id="createPermBtn" class="primary">Create Permissions</button>
142
+ </k-permission>
143
+ </div>
144
+
145
+ <h2>Custom Permissions</h2>
146
+ <k-table id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom permissions have been created yet.">
147
+ <div slot="top" class="d-f full" style="align-items: center;">
148
+ <div>
149
+ <k-permission has="system:permissions:manage">
150
+ <k-tc-delete-selected></k-tc-delete-selected>
151
+ </k-permission>
152
+ </div>
153
+ </div>
154
+ <div class="d-f full" style="align-items: center;">
155
+ <div>
156
+ <k-tc-first-page></k-tc-first-page>
157
+ <k-tc-prev-page></k-tc-prev-page>
158
+ <k-tc-next-page></k-tc-next-page>
159
+ <k-tc-last-page></k-tc-last-page>
160
+ </div>
161
+ <div class="flex"></div>
162
+ <div><k-tc-page-select></k-tc-page-select></div>
163
+ <div class="flex"></div>
164
+ <div><k-tc-page-size></k-tc-page-size></div>
165
+ </div>
166
+ </k-table>
167
+
168
+ <h2>Extension Permissions</h2>
169
+ <k-table id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension permissions installed.">
170
+ <div class="d-f full" style="align-items: center;">
171
+ <div>
172
+ <k-tc-first-page></k-tc-first-page>
173
+ <k-tc-prev-page></k-tc-prev-page>
174
+ <k-tc-next-page></k-tc-next-page>
175
+ <k-tc-last-page></k-tc-last-page>
176
+ </div>
177
+ <div class="flex"></div>
178
+ <div><k-tc-page-select></k-tc-page-select></div>
179
+ <div class="flex"></div>
180
+ <div><k-tc-page-size></k-tc-page-size></div>
181
+ </div>
182
+ </k-table>
183
+
184
+ <h2>System Permissions</h2>
185
+ <k-table id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system permissions found.">
186
+ <div class="d-f full" style="align-items: center;">
187
+ <div>
188
+ <k-tc-first-page></k-tc-first-page>
189
+ <k-tc-prev-page></k-tc-prev-page>
190
+ <k-tc-next-page></k-tc-next-page>
191
+ <k-tc-last-page></k-tc-last-page>
192
+ </div>
193
+ <div class="flex"></div>
194
+ <div><k-tc-page-select></k-tc-page-select></div>
195
+ <div class="flex"></div>
196
+ <div><k-tc-page-size></k-tc-page-size></div>
197
+ </div>
198
+ </k-table>
199
+
200
+ <script type="module">
201
+ import { listPermissions, createPermissions, updatePermission, deletePermissions, getAllCurrentUserPermissions } from '/kempo/sdk.js';
202
+ import { html } from '/kempo-ui/lit-all.min.js';
203
+ import Toast from '/kempo-ui/components/Toast.js';
204
+ import Dialog from '/kempo-ui/components/Dialog.js';
205
+
206
+ await window.customElements.whenDefined('k-table');
207
+
208
+ const [, permData] = await getAllCurrentUserPermissions();
209
+ const myPerms = new Set(permData?.permissions ?? []);
210
+ const canManage = myPerms.has('system:permissions:manage');
211
+
212
+ const fields = [
213
+ {
214
+ name: 'resource',
215
+ label: 'Resource',
216
+ size: 180,
217
+ editable: false,
218
+ calculator: r => {
219
+ const parts = r.name.split(':');
220
+ return parts.length >= 3 ? parts[1] : parts.length === 2 ? parts[1] : '';
221
+ }
222
+ },
223
+ {
224
+ name: 'action',
225
+ label: 'Action',
226
+ size: 140,
227
+ editable: false,
228
+ calculator: r => {
229
+ const parts = r.name.split(':');
230
+ return parts.length >= 3 ? parts.slice(2).join(':') : '';
231
+ }
232
+ },
233
+ { name: 'description', label: 'Description', size: 360, editable: canManage },
234
+ {
235
+ name: 'createdAt',
236
+ label: 'Created',
237
+ formatter: v => new Date(v).toLocaleString(),
238
+ size: 208,
239
+ editable: false
240
+ }
241
+ ];
242
+
243
+ const systemFields = fields.map(f => f.name === 'description' ? { ...f, editable: false } : f);
244
+
245
+ const setupTable = ($table, owner, loadFn) => {
246
+ if(canManage && owner !== 'system'){
247
+ $table.setAttribute('request-edit', '');
248
+ const edit = document.createElement('k-tc-edit');
249
+ edit.setAttribute('slot', 'after');
250
+ $table.appendChild(edit);
251
+ }
252
+
253
+ if(canManage && owner === 'custom'){
254
+ $table.setAttribute('request-delete', '');
255
+ $table.setAttribute('enable-selection', '');
256
+ const del = document.createElement('k-tc-delete-record');
257
+ del.setAttribute('slot', 'after');
258
+ $table.appendChild(del);
259
+ }
260
+
261
+ $table.addEventListener('requestSave', async ({ detail: { record, newData, approve, reject } }) => {
262
+ const [error] = await updatePermission({ name: record.name, ...newData });
263
+ if(error){
264
+ reject();
265
+ Toast.error(error.msg || 'Failed to update permission');
266
+ return;
267
+ }
268
+ approve();
269
+ Toast.success('Permission updated');
270
+ });
271
+
272
+ if(owner === 'custom'){
273
+ $table.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
274
+ const displayName = r => r.name.includes(':') ? r.name.split(':').slice(1).join(':') : r.name;
275
+ const count = records.length;
276
+ const msg = count === 1
277
+ ? `<div class="p"><p>Delete "${displayName(records[0])}"? This action cannot be undone.</p></div>`
278
+ : `<div class="p"><p>Delete ${count} permissions? This action cannot be undone.</p></div>`;
279
+ Dialog.confirm(msg, async (confirmed) => {
280
+ if(!confirmed){ reject(); return; }
281
+ const [error] = await deletePermissions(records.map(r => r.name));
282
+ if(error){
283
+ reject();
284
+ Toast.error(error.msg || 'Failed to delete');
285
+ return;
286
+ }
287
+ approve();
288
+ Toast.success(count === 1 ? 'Permission deleted' : `${count} permissions deleted`);
289
+ loadFn();
290
+ });
291
+ });
292
+ }
293
+ };
294
+
295
+ const loadTable = async ($table, owner) => {
296
+ const [error, data] = await listPermissions({ owner });
297
+ if(error){
298
+ Toast.error(error.msg || 'Failed to load permissions');
299
+ return;
300
+ }
301
+ $table.setData({ records: data.permissions, fields: owner === 'system' ? systemFields : fields });
302
+ $table.setupFetchRecords(data.total, async (start, count) => {
303
+ const [err, d] = await listPermissions({ limit: count, offset: start, owner });
304
+ if(err) return [];
305
+ return d.permissions;
306
+ });
307
+ };
308
+
309
+ const $customTable = document.getElementById('customTable');
310
+ const $extensionTable = document.getElementById('extensionTable');
311
+ const $systemTable = document.getElementById('systemTable');
312
+
313
+ setupTable($customTable, 'custom', () => loadTable($customTable, 'custom'));
314
+ setupTable($extensionTable, 'extension', () => loadTable($extensionTable, 'extension'));
315
+ setupTable($systemTable, 'system', () => loadTable($systemTable, 'system'));
316
+
317
+ await Promise.all([
318
+ loadTable($customTable, 'custom'),
319
+ loadTable($extensionTable, 'extension'),
320
+ loadTable($systemTable, 'system')
321
+ ]);
322
+
323
+ if(canManage){
324
+ document.getElementById('createPermBtn').addEventListener('click', async () => {
325
+ await customElements.whenDefined('k-tags');
326
+ const $dialog = Dialog.create(html`
327
+ <div class="p">
328
+ <div class="mb">
329
+ <label class="d-b mb-sm">Resource Name</label>
330
+ <input type="text" id="dlg-resource" class="full" placeholder="e.g. posts, comments, media" required>
331
+ </div>
332
+ <div class="mb">
333
+ <label class="d-b mb-sm">Actions</label>
334
+ <k-tags id="dlg-actions" value="create,read,update,delete" @addtag=${e => {
335
+ if(e.detail.tag.includes(':')){
336
+ e.target.removeTag(e.detail.tag);
337
+ Toast.error('Action cannot contain ":"');
338
+ }
339
+ }}></k-tags>
340
+ </div>
341
+ <div>
342
+ <label class="d-b mb-sm">Description (applied to all)</label>
343
+ <input type="text" id="dlg-description" class="full">
344
+ </div>
345
+ </div>
346
+ `, {
347
+ title: 'Create Permissions',
348
+ confirmText: 'Create',
349
+ confirmAction: async () => {
350
+ const resource = $dialog.querySelector('#dlg-resource').value.trim().toLowerCase();
351
+ if(!resource){
352
+ Toast.error('Resource name is required');
353
+ return;
354
+ }
355
+ if(resource.includes(':')){
356
+ Toast.error('Resource name cannot contain ":"');
357
+ return;
358
+ }
359
+
360
+ const actions = $dialog.querySelector('#dlg-actions').value.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
361
+ if(!actions.length){
362
+ Toast.error('At least one action is required');
363
+ return;
364
+ }
365
+
366
+ const description = $dialog.querySelector('#dlg-description').value.trim();
367
+ const permissions = actions.map(action => ({
368
+ resource,
369
+ action,
370
+ description: description || `${action} ${resource}`
371
+ }));
372
+
373
+ const [error, result] = await createPermissions(permissions);
374
+ if(error){
375
+ Toast.error(error.msg || 'Failed to create permissions');
376
+ return;
377
+ }
378
+ const created = result.created?.length ?? permissions.length;
379
+ const errs = result.errors?.length ?? 0;
380
+ if(errs){
381
+ Toast.warning(`${created} created, ${errs} failed (may already exist)`);
382
+ } else {
383
+ Toast.success(`${created} permission${created !== 1 ? 's' : ''} created`);
384
+ }
385
+ loadTable($customTable, 'custom');
386
+ },
387
+ cancelText: 'Cancel'
388
+ });
389
+ });
390
+ }
391
+ </script>
392
+
393
+
394
+ </k-main>
395
+
396
+ </body>
397
+ </html>