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.
- package/app-public/.config.json +2 -5
- package/bin/cli.js +35 -14
- 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
package/app-public/.config.json
CHANGED
|
@@ -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/**": "../node_modules/kempo/dist/kempo/**"
|
|
8
|
-
"/admin": "../node_modules/kempo/dist/admin/",
|
|
9
|
-
"/admin/**": "../node_modules/kempo/dist/admin/**"
|
|
7
|
+
"/kempo/**": "../node_modules/kempo/dist/kempo/**"
|
|
10
8
|
},
|
|
11
9
|
"middleware": {
|
|
12
10
|
"custom": [
|
|
13
|
-
"../middleware/kempo
|
|
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/,
|
|
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', '
|
|
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
|
}
|
|
@@ -180,17 +180,44 @@ if(existingContainers.length > 0 && !useDefaults){
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
let dbName = 'kempo';
|
|
184
|
+
let dbPassword = generatePassword();
|
|
185
|
+
|
|
183
186
|
if(containerMode === 'new' && !useDefaults){
|
|
184
187
|
const { name } = await inquirer.prompt([{
|
|
185
188
|
type: 'input',
|
|
186
189
|
name: 'name',
|
|
187
|
-
message: '
|
|
188
|
-
default:
|
|
190
|
+
message: 'Database name (used for container and database):',
|
|
191
|
+
default: projectName
|
|
189
192
|
}]);
|
|
193
|
+
dbName = name;
|
|
190
194
|
containerName = name;
|
|
195
|
+
|
|
196
|
+
const { passwordChoice } = await inquirer.prompt([{
|
|
197
|
+
type: 'list',
|
|
198
|
+
name: 'passwordChoice',
|
|
199
|
+
message: 'Database password:',
|
|
200
|
+
choices: [
|
|
201
|
+
{ name: 'Generate a random password', value: 'generate' },
|
|
202
|
+
{ name: 'Enter a password manually', value: 'manual' }
|
|
203
|
+
]
|
|
204
|
+
}]);
|
|
205
|
+
|
|
206
|
+
if(passwordChoice === 'manual'){
|
|
207
|
+
const { manualPassword } = await inquirer.prompt([{
|
|
208
|
+
type: 'password',
|
|
209
|
+
name: 'manualPassword',
|
|
210
|
+
message: 'Enter database password:',
|
|
211
|
+
mask: '*'
|
|
212
|
+
}]);
|
|
213
|
+
dbPassword = manualPassword;
|
|
214
|
+
}
|
|
215
|
+
} else if(containerMode === 'new' && useDefaults){
|
|
216
|
+
dbName = projectName;
|
|
217
|
+
containerName = projectName;
|
|
191
218
|
}
|
|
192
219
|
|
|
193
|
-
const databaseUrl = customDatabaseUrl || `postgresql://kempo
|
|
220
|
+
const databaseUrl = customDatabaseUrl || `postgresql://kempo:${dbPassword}@localhost:5433/${dbName}`;
|
|
194
221
|
|
|
195
222
|
const generatedAdminPassword = generatePassword();
|
|
196
223
|
let adminName, adminEmail, adminPassword;
|
|
@@ -312,12 +339,6 @@ if(!existsSync(templatesDir) && existsSync(templatesSource)){
|
|
|
312
339
|
console.log('Scaffolded templates/');
|
|
313
340
|
}
|
|
314
341
|
|
|
315
|
-
const extensionsDir = join(projectDir, 'extensions');
|
|
316
|
-
if(!existsSync(extensionsDir)){
|
|
317
|
-
mkdirSync(extensionsDir);
|
|
318
|
-
console.log('Created extensions/');
|
|
319
|
-
}
|
|
320
|
-
|
|
321
342
|
const drizzleConfigPath = join(projectDir, 'drizzle.config.js');
|
|
322
343
|
const scaffoldDrizzle = join(moduleRoot, 'app-drizzle.config.js');
|
|
323
344
|
if(!existsSync(drizzleConfigPath) && existsSync(scaffoldDrizzle)){
|
|
@@ -340,8 +361,8 @@ if(containerMode === 'new'){
|
|
|
340
361
|
restart: unless-stopped
|
|
341
362
|
environment:
|
|
342
363
|
POSTGRES_USER: kempo
|
|
343
|
-
POSTGRES_PASSWORD:
|
|
344
|
-
POSTGRES_DB:
|
|
364
|
+
POSTGRES_PASSWORD: ${dbPassword}
|
|
365
|
+
POSTGRES_DB: ${dbName}
|
|
345
366
|
ports:
|
|
346
367
|
- "5433:5432"
|
|
347
368
|
volumes:
|
|
@@ -352,7 +373,7 @@ volumes:
|
|
|
352
373
|
);
|
|
353
374
|
console.log('Created docker-compose.yml');
|
|
354
375
|
}
|
|
355
|
-
|
|
376
|
+
|
|
356
377
|
|
|
357
378
|
writeFileSync(join(projectDir, '.env'),
|
|
358
379
|
`# Database
|
|
@@ -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>
|