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.
- package/app-public/.config.json +2 -5
- package/bin/cli.js +4 -10
- 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,541 @@
|
|
|
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>User - 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/Toggle.js"></script>
|
|
126
|
+
<script type="module" src="/kempo-ui/components/Icon.js"></script>
|
|
127
|
+
<script type="module" src="/kempo-ui/components/Table.js"></script>
|
|
128
|
+
<script type="module" src="/kempo-ui/components/tableControls/FirstPage.js"></script>
|
|
129
|
+
<script type="module" src="/kempo-ui/components/tableControls/PrevPage.js"></script>
|
|
130
|
+
<script type="module" src="/kempo-ui/components/tableControls/NextPage.js"></script>
|
|
131
|
+
<script type="module" src="/kempo-ui/components/tableControls/LastPage.js"></script>
|
|
132
|
+
<script type="module" src="/kempo-ui/components/tableControls/PageSelect.js"></script>
|
|
133
|
+
<script type="module" src="/kempo-ui/components/tableControls/PageSize.js"></script>
|
|
134
|
+
<script type="module" src="/kempo-ui/components/tableControls/DeleteRecord.js"></script>
|
|
135
|
+
<script type="module" src="/kempo-ui/components/tableControls/DeleteSelected.js"></script>
|
|
136
|
+
<script type="module" src="/admin/components/GroupDeleteSelected.js"></script>
|
|
137
|
+
<script type="module" src="/admin/components/AddGroupControl.js"></script>
|
|
138
|
+
<script type="module" src="/admin/components/ClearExpiredSessionsControl.js"></script>
|
|
139
|
+
<script type="module" src="/admin/components/TableRowLink.js"></script>
|
|
140
|
+
<div class="d-f">
|
|
141
|
+
<div>
|
|
142
|
+
<a href="/admin/accounts/users" class="no-link mb">
|
|
143
|
+
<button class="mbh"><k-icon name="arrow" direction="left"></k-icon> Users</button>
|
|
144
|
+
</a>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex"></div>
|
|
147
|
+
<div>
|
|
148
|
+
<button id="deleteUserBtn" class="danger mb mrh" style="display:none;">Delete User</button>
|
|
149
|
+
<button id="saveBtn" class="primary mb" disabled style="display:none;"><k-icon name="save"></k-icon> Save</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<h1 id="pageTitle">User</h1>
|
|
153
|
+
|
|
154
|
+
<div id="loading">Loading user...</div>
|
|
155
|
+
<div id="notFound" style="display: none;">
|
|
156
|
+
<p>User not found.</p>
|
|
157
|
+
<a href="/admin/accounts/users">Back to Users</a>
|
|
158
|
+
</div>
|
|
159
|
+
<div id="content" style="display: none;">
|
|
160
|
+
|
|
161
|
+
<!-- User Information -->
|
|
162
|
+
<div class="card">
|
|
163
|
+
<h2>Information</h2>
|
|
164
|
+
<div class="d-f mb" style="align-items: center; gap: var(--spacer);">
|
|
165
|
+
<label for="userName" style="min-width: 120px;"><strong>Name</strong></label>
|
|
166
|
+
<input type="text" id="userName" style="max-width: 400px;">
|
|
167
|
+
</div>
|
|
168
|
+
<div class="d-f mb" style="align-items: center; gap: var(--spacer);">
|
|
169
|
+
<label for="userEmail" style="min-width: 120px;"><strong>Email</strong></label>
|
|
170
|
+
<input type="email" id="userEmail" style="max-width: 400px;">
|
|
171
|
+
</div>
|
|
172
|
+
<div class="d-f mb" style="align-items: center; gap: var(--spacer);">
|
|
173
|
+
<label style="min-width: 120px;"><strong>Verified</strong></label>
|
|
174
|
+
<k-toggle id="emailVerified"></k-toggle>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="d-f mb" style="align-items: center; gap: var(--spacer);">
|
|
177
|
+
<label style="min-width: 120px;"><strong>Created</strong></label>
|
|
178
|
+
<span id="userCreatedAt"></span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Groups -->
|
|
183
|
+
<div class="card">
|
|
184
|
+
<h2>Groups</h2>
|
|
185
|
+
<k-table id="groupsTable" request-delete placeholder="Not a member of any groups.">
|
|
186
|
+
<admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
|
|
187
|
+
<k-tc-delete-record slot="after"></k-tc-delete-record>
|
|
188
|
+
<div slot="top" id="groupsTableTop" class="d-f full" style="display:none; align-items: center;">
|
|
189
|
+
<div><admin-group-delete-selected></admin-group-delete-selected></div>
|
|
190
|
+
<div class="flex"></div>
|
|
191
|
+
<div><admin-add-group-control id="addGroupBtn"></admin-add-group-control></div>
|
|
192
|
+
</div>
|
|
193
|
+
</k-table>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- Sessions -->
|
|
197
|
+
<div class="card">
|
|
198
|
+
<h2>Sessions</h2>
|
|
199
|
+
<k-table id="sessionsTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No active sessions.">
|
|
200
|
+
<k-tc-delete-record slot="after"></k-tc-delete-record>
|
|
201
|
+
<div slot="top" class="d-f full" style="align-items: center;">
|
|
202
|
+
<div><k-tc-delete-selected></k-tc-delete-selected></div>
|
|
203
|
+
<div class="flex"></div>
|
|
204
|
+
<div><admin-clear-expired-sessions id="clearExpiredBtn" style="display:none;"></admin-clear-expired-sessions></div>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="d-f full" style="align-items: center;">
|
|
207
|
+
<div>
|
|
208
|
+
<k-tc-first-page></k-tc-first-page>
|
|
209
|
+
<k-tc-prev-page></k-tc-prev-page>
|
|
210
|
+
<k-tc-next-page></k-tc-next-page>
|
|
211
|
+
<k-tc-last-page></k-tc-last-page>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="flex"></div>
|
|
214
|
+
<div><k-tc-page-select></k-tc-page-select></div>
|
|
215
|
+
<div class="flex"></div>
|
|
216
|
+
<div><k-tc-page-size></k-tc-page-size></div>
|
|
217
|
+
</div>
|
|
218
|
+
</k-table>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<script type="module">
|
|
224
|
+
import {
|
|
225
|
+
getSession, getUser, updateUser, deleteUsers,
|
|
226
|
+
getUserSessions, deleteUserSession, deleteExpiredUserSessions,
|
|
227
|
+
getUserGroups, addUserToGroup, removeUserFromGroup,
|
|
228
|
+
listGroups, getAllCurrentUserPermissions
|
|
229
|
+
} from '/kempo/sdk.js';
|
|
230
|
+
import { html } from '/kempo-ui/lit-all.min.js';
|
|
231
|
+
import Toast from '/kempo-ui/components/Toast.js';
|
|
232
|
+
import Dialog from '/kempo-ui/components/Dialog.js';
|
|
233
|
+
|
|
234
|
+
const userid = window.location.pathname.split('/admin/accounts/users/')[1]?.replace(/\/$/, '');
|
|
235
|
+
|
|
236
|
+
if(!userid){
|
|
237
|
+
document.getElementById('loading').style.display = 'none';
|
|
238
|
+
document.getElementById('notFound').style.display = '';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/*
|
|
242
|
+
Permission checks
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
const [[, permResult], [, sessionData]] = await Promise.all([
|
|
246
|
+
getAllCurrentUserPermissions(),
|
|
247
|
+
getSession()
|
|
248
|
+
]);
|
|
249
|
+
const myPerms = new Set(permResult?.permissions ?? []);
|
|
250
|
+
const currentUserId = sessionData?.user?.id;
|
|
251
|
+
const canEdit = myPerms.has('system:user:update');
|
|
252
|
+
const canDelete = myPerms.has('system:user:delete');
|
|
253
|
+
const canEditGroups = myPerms.has('system:group:update');
|
|
254
|
+
const canReadGroups = myPerms.has('system:group:read');
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
Load user
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
const [userError, userData] = await getUser(userid);
|
|
261
|
+
|
|
262
|
+
document.getElementById('loading').style.display = 'none';
|
|
263
|
+
|
|
264
|
+
if(userError){
|
|
265
|
+
document.getElementById('notFound').style.display = '';
|
|
266
|
+
} else {
|
|
267
|
+
document.getElementById('content').style.display = '';
|
|
268
|
+
document.getElementById('pageTitle').textContent = `User: ${userData.name}`;
|
|
269
|
+
document.title = `${userData.name} - Admin`;
|
|
270
|
+
|
|
271
|
+
await customElements.whenDefined('k-toggle');
|
|
272
|
+
|
|
273
|
+
const $userName = document.getElementById('userName');
|
|
274
|
+
const $userEmail = document.getElementById('userEmail');
|
|
275
|
+
const $emailVerified = document.getElementById('emailVerified');
|
|
276
|
+
const $userCreatedAt = document.getElementById('userCreatedAt');
|
|
277
|
+
const $deleteBtn = document.getElementById('deleteUserBtn');
|
|
278
|
+
|
|
279
|
+
$userName.value = userData.name;
|
|
280
|
+
$userEmail.value = userData.email;
|
|
281
|
+
$emailVerified.value = userData.emailVerified;
|
|
282
|
+
$userCreatedAt.textContent = new Date(userData.createdAt).toLocaleString();
|
|
283
|
+
|
|
284
|
+
const isSelf = userData.id === currentUserId;
|
|
285
|
+
|
|
286
|
+
if(!canEdit){
|
|
287
|
+
$userName.readOnly = true;
|
|
288
|
+
$userEmail.readOnly = true;
|
|
289
|
+
$emailVerified.setAttribute('disabled', '');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if(!canDelete || isSelf){
|
|
293
|
+
$deleteBtn.style.display = 'none';
|
|
294
|
+
} else {
|
|
295
|
+
$deleteBtn.style.display = '';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/*
|
|
299
|
+
Save user fields
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
if(canEdit){
|
|
303
|
+
const $saveBtn = document.getElementById('saveBtn');
|
|
304
|
+
$saveBtn.style.display = '';
|
|
305
|
+
|
|
306
|
+
const origName = userData.name;
|
|
307
|
+
const origEmail = userData.email;
|
|
308
|
+
const origVerified = userData.emailVerified;
|
|
309
|
+
|
|
310
|
+
const handleBeforeUnload = e => { e.preventDefault(); e.returnValue = ''; };
|
|
311
|
+
|
|
312
|
+
const checkDirty = () => {
|
|
313
|
+
const dirty = $userName.value !== origName
|
|
314
|
+
|| $userEmail.value !== origEmail
|
|
315
|
+
|| $emailVerified.value !== origVerified;
|
|
316
|
+
$saveBtn.disabled = !dirty;
|
|
317
|
+
if(dirty){
|
|
318
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
319
|
+
} else {
|
|
320
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
$userName.addEventListener('input', checkDirty);
|
|
325
|
+
$userEmail.addEventListener('input', checkDirty);
|
|
326
|
+
$emailVerified.addEventListener('change', checkDirty);
|
|
327
|
+
|
|
328
|
+
$saveBtn.addEventListener('click', async () => {
|
|
329
|
+
const [error] = await updateUser({
|
|
330
|
+
id: userData.id,
|
|
331
|
+
name: $userName.value,
|
|
332
|
+
email: $userEmail.value,
|
|
333
|
+
emailVerified: $emailVerified.value
|
|
334
|
+
});
|
|
335
|
+
if(error){
|
|
336
|
+
Toast.error(error.msg || 'Failed to save');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
340
|
+
document.getElementById('pageTitle').textContent = `User: ${$userName.value}`;
|
|
341
|
+
document.title = `${$userName.value} - Admin`;
|
|
342
|
+
$saveBtn.disabled = true;
|
|
343
|
+
Toast.success('User saved');
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/*
|
|
348
|
+
Delete user
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
$deleteBtn.addEventListener('click', () => {
|
|
352
|
+
if(isSelf) return;
|
|
353
|
+
Dialog.confirm('Are you sure you want to delete this user? This action cannot be undone.', async (confirmed) => {
|
|
354
|
+
if(!confirmed) return;
|
|
355
|
+
const [error] = await deleteUsers([userData.id]);
|
|
356
|
+
|
|
357
|
+
if(error){
|
|
358
|
+
Toast.error(error.msg || 'Failed to delete user');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
Toast.success('User deleted');
|
|
363
|
+
window.location.href = '/admin/accounts/users';
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
/*
|
|
368
|
+
Groups
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
await customElements.whenDefined('k-table');
|
|
372
|
+
const $groupsTable = document.getElementById('groupsTable');
|
|
373
|
+
|
|
374
|
+
if(canEditGroups){
|
|
375
|
+
$groupsTable.setAttribute('enable-selection', '');
|
|
376
|
+
document.getElementById('groupsTableTop').style.display = '';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
$groupsTable.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
|
|
380
|
+
if(records[0].name === 'system:Users') { reject(); return; }
|
|
381
|
+
Dialog.confirm(`Remove from "${records[0].name}"?`, async (confirmed) => {
|
|
382
|
+
if(!confirmed){ reject(); return; }
|
|
383
|
+
const [error] = await removeUserFromGroup(userid, records[0].name);
|
|
384
|
+
if(error){
|
|
385
|
+
reject();
|
|
386
|
+
Toast.error(error.msg || 'Failed to remove from group');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
approve();
|
|
390
|
+
Toast.success(`Removed from ${records[0].name}`);
|
|
391
|
+
loadGroups();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
$groupsTable.addEventListener('groupRemoveSelected', ({ detail: { records, systemUsersSkipped } }) => {
|
|
396
|
+
const count = records.length;
|
|
397
|
+
const note = systemUsersSkipped ? '<p><small><em>Note: The user cannot be removed from system:Users, but will be removed from the other selected groups.</em></small></p>' : '';
|
|
398
|
+
Dialog.confirm(`<div class="p"><p>Remove from ${count} group${count !== 1 ? 's' : ''}?</p>${note}</div>`, async (confirmed) => {
|
|
399
|
+
if(!confirmed) return;
|
|
400
|
+
const errors = [];
|
|
401
|
+
for(const record of records){
|
|
402
|
+
const [error] = await removeUserFromGroup(userid, record.name);
|
|
403
|
+
if(error) errors.push(error.msg);
|
|
404
|
+
}
|
|
405
|
+
if(errors.length){
|
|
406
|
+
Toast.error(errors[0] || 'Failed to remove from group');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
Toast.success(`Removed from ${count} group${count !== 1 ? 's' : ''}`);
|
|
410
|
+
loadGroups();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const loadGroups = async () => {
|
|
415
|
+
const [error, data] = await getUserGroups(userid);
|
|
416
|
+
if(error){
|
|
417
|
+
Toast.error(error.msg || 'Failed to load groups');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
$groupsTable.setData({
|
|
421
|
+
records: data.groups,
|
|
422
|
+
fields: [{ name: 'name', label: 'Name' }]
|
|
423
|
+
});
|
|
424
|
+
$groupsTable.setupFetchRecords(data.total, async (start, count) => {
|
|
425
|
+
const [err, d] = await getUserGroups(userid, { limit: count, offset: start });
|
|
426
|
+
if(err) return [];
|
|
427
|
+
return d.groups;
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
document.getElementById('addGroupBtn').addEventListener('click', async () => {
|
|
432
|
+
const [error, data] = await listGroups({ limit: 1000 });
|
|
433
|
+
if(error){ Toast.error(error.msg || 'Failed to load groups'); return; }
|
|
434
|
+
|
|
435
|
+
const userGroupNames = new Set($groupsTable.records.map(g => g.name));
|
|
436
|
+
const available = data.groups.filter(g => !userGroupNames.has(g.name));
|
|
437
|
+
|
|
438
|
+
const $dialog = Dialog.create(html`
|
|
439
|
+
<div class="p">
|
|
440
|
+
<select id="dlg-group">
|
|
441
|
+
<option value="">Select a group...</option>
|
|
442
|
+
${available.map(g => html`<option value=${g.name}>${g.name}</option>`)}
|
|
443
|
+
</select>
|
|
444
|
+
</div>
|
|
445
|
+
`, {
|
|
446
|
+
title: 'Add to Group',
|
|
447
|
+
confirmText: 'Add',
|
|
448
|
+
confirmAction: async () => {
|
|
449
|
+
const groupName = $dialog.querySelector('#dlg-group').value;
|
|
450
|
+
if(!groupName) return;
|
|
451
|
+
const [err] = await addUserToGroup(userid, groupName);
|
|
452
|
+
if(err){ Toast.error(err.msg || 'Failed to add to group'); return; }
|
|
453
|
+
Toast.success(`Added to ${groupName}`);
|
|
454
|
+
loadGroups();
|
|
455
|
+
},
|
|
456
|
+
cancelText: 'Cancel'
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
loadGroups();
|
|
461
|
+
|
|
462
|
+
/*
|
|
463
|
+
Sessions
|
|
464
|
+
*/
|
|
465
|
+
|
|
466
|
+
const $sessionsTable = document.getElementById('sessionsTable');
|
|
467
|
+
|
|
468
|
+
if(canEdit){
|
|
469
|
+
$sessionsTable.setAttribute('request-delete', '');
|
|
470
|
+
$sessionsTable.setAttribute('enable-selection', '');
|
|
471
|
+
const $clearExpiredBtn = document.getElementById('clearExpiredBtn');
|
|
472
|
+
$clearExpiredBtn.style.display = '';
|
|
473
|
+
$clearExpiredBtn.addEventListener('click', async () => {
|
|
474
|
+
const [error, result] = await deleteExpiredUserSessions(userid);
|
|
475
|
+
if(error){
|
|
476
|
+
Toast.error(error.msg || 'Failed to clear expired sessions');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
Toast.success(`Cleared ${result.deleted} expired session${result.deleted !== 1 ? 's' : ''}`);
|
|
480
|
+
loadSessions();
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
$sessionsTable.addEventListener('requestDelete', async ({ detail: { records, approve, reject } }) => {
|
|
485
|
+
const errors = [];
|
|
486
|
+
for(const record of records){
|
|
487
|
+
const [error] = await deleteUserSession(userid, record.token);
|
|
488
|
+
if(error) errors.push(error.msg);
|
|
489
|
+
}
|
|
490
|
+
if(errors.length){
|
|
491
|
+
reject();
|
|
492
|
+
Toast.error(errors[0] || 'Failed to revoke session');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
approve();
|
|
496
|
+
Toast.success(records.length === 1 ? 'Session revoked' : `${records.length} sessions revoked`);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const loadSessions = async () => {
|
|
500
|
+
const [error, data] = await getUserSessions(userid);
|
|
501
|
+
if(error){
|
|
502
|
+
Toast.error(error.msg || 'Failed to load sessions');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const now = new Date();
|
|
506
|
+
$sessionsTable.setData({
|
|
507
|
+
records: data.sessions,
|
|
508
|
+
fields: [
|
|
509
|
+
{
|
|
510
|
+
name: 'createdAt',
|
|
511
|
+
label: 'Created',
|
|
512
|
+
formatter: v => new Date(v).toLocaleString()
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: 'expiresAt',
|
|
516
|
+
label: 'Expires',
|
|
517
|
+
formatter: v => {
|
|
518
|
+
const date = new Date(v);
|
|
519
|
+
return `${date.toLocaleString()}${date < now ? ' (expired)' : ''}`;
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
{ name: 'ipAddress', label: 'IP' },
|
|
523
|
+
{ name: 'userAgent', label: 'User Agent' }
|
|
524
|
+
]
|
|
525
|
+
});
|
|
526
|
+
$sessionsTable.setupFetchRecords(data.total, async (start, count) => {
|
|
527
|
+
const [err, d] = await getUserSessions(userid, { limit: count, offset: start });
|
|
528
|
+
if(err) return [];
|
|
529
|
+
return d.sessions;
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
loadSessions();
|
|
534
|
+
}
|
|
535
|
+
</script>
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
</k-main>
|
|
539
|
+
|
|
540
|
+
</body>
|
|
541
|
+
</html>
|