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.
- package/app-public/.config.json +2 -5
- package/bin/cli.js +2 -8
- package/dist/admin/accounts/groups/[name]/index.html +518 -0
- package/dist/admin/accounts/groups/index.html +378 -0
- package/dist/admin/accounts/permissions/index.html +397 -0
- package/dist/admin/accounts/users/[id]/index.html +541 -0
- package/dist/admin/accounts/users/index.html +396 -0
- package/dist/admin/content/fragments/edit/index.html +132 -0
- package/dist/admin/content/fragments/index.html +375 -0
- package/dist/admin/content/globals/edit/index.html +132 -0
- package/dist/admin/content/globals/index.html +358 -0
- package/dist/admin/content/pages/edit/index.html +132 -0
- package/dist/admin/content/pages/index.html +509 -0
- package/dist/admin/content/templates/edit/index.html +132 -0
- package/dist/admin/content/templates/index.html +429 -0
- package/dist/admin/extensions/index.html +222 -0
- package/dist/admin/index.html +130 -0
- package/dist/admin/index.page.html +1 -0
- package/dist/admin/settings/[owner]/[name]/index.html +449 -0
- package/dist/admin/settings/index.html +410 -0
- package/middleware/{extension-scope-router.js → kempo.js} +40 -25
- package/package.json +3 -3
- package/middleware/kempo-auth.js +0 -26
|
@@ -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>
|