@vuu-ui/vuu-data-test 4.0.0-alpha.2 → 4.0.0-alpha.20
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/README.md +8 -0
- package/package.json +11 -9
- package/src/Table.js +5 -5
- package/src/core/filter/PermissionFilter.js +60 -0
- package/src/core/filter/PermissionFilteredTable.js +59 -0
- package/src/core/module/ModuleContainer.js +8 -0
- package/src/core/module/VuuModule.js +54 -6
- package/src/core/table/TableContainer.js +1 -1
- package/src/core/user/CurrentUser.js +13 -0
- package/src/index.js +7 -0
- package/src/local-datasource-provider/LocalDatasourceProvider.js +53 -43
- package/src/module-admin/ModuleAdminModule.js +124 -0
- package/src/module-admin/index.js +3 -0
- package/src/module-admin/module-admin-schemas.js +107 -0
- package/src/module-admin/snapshot-projection.js +29 -0
- package/src/notifications/NotificationModule.js +93 -0
- package/src/notifications/NotificationsSchema.js +52 -0
- package/src/notifications/index.js +6 -0
- package/src/notifications/simulated/DismissNotificationRpcHandler.js +36 -0
- package/src/notifications/simulated/SimulatedNotificationsModule.js +14 -0
- package/src/notifications/simulated/SimulatedNotificationsProvider.js +151 -0
- package/src/notifications/simulated/notificationsModule.js +3 -0
- package/src/schemas.js +8 -2
- package/src/user-admin/UserAdminModule.js +567 -0
- package/src/user-admin/index.js +3 -0
- package/src/user-admin/initialSnapshot.js +461 -0
- package/src/user-admin/snapshot-projection.js +151 -0
- package/types/core/filter/PermissionFilter.d.ts +41 -0
- package/types/core/filter/PermissionFilteredTable.d.ts +24 -0
- package/types/core/module/ModuleContainer.d.ts +6 -3
- package/types/core/module/VuuModule.d.ts +22 -0
- package/types/core/table/TableContainer.d.ts +5 -5
- package/types/core/user/CurrentUser.d.ts +14 -0
- package/types/index.d.ts +7 -0
- package/types/local-datasource-provider/LocalDatasourceProvider.d.ts +15 -1
- package/types/module-admin/ModuleAdminModule.d.ts +32 -0
- package/types/module-admin/index.d.ts +4 -0
- package/types/module-admin/module-admin-schemas.d.ts +4 -0
- package/types/module-admin/snapshot-projection.d.ts +13 -0
- package/types/notifications/NotificationModule.d.ts +39 -0
- package/types/notifications/NotificationsSchema.d.ts +20 -0
- package/types/notifications/index.d.ts +6 -0
- package/types/notifications/simulated/DismissNotificationRpcHandler.d.ts +8 -0
- package/types/notifications/simulated/SimulatedNotificationsModule.d.ts +2 -0
- package/types/notifications/simulated/SimulatedNotificationsProvider.d.ts +21 -0
- package/types/notifications/simulated/notificationsModule.d.ts +5 -0
- package/types/schemas.d.ts +5 -3
- package/types/user-admin/UserAdminModule.d.ts +54 -0
- package/types/user-admin/index.d.ts +3 -0
- package/types/user-admin/initialSnapshot.d.ts +2 -0
- package/types/user-admin/snapshot-projection.d.ts +9 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { USER_ADMIN_RPC_CONTRACT, USER_ADMIN_TABLE_SCHEMAS } from "@heswell/user-admin/contracts";
|
|
2
|
+
import { InMemoryUserAdminStore } from "@heswell/user-admin/in-memory";
|
|
3
|
+
import { VuuModule } from "../core/module/VuuModule.js";
|
|
4
|
+
import TableContainer from "../core/table/TableContainer.js";
|
|
5
|
+
import { Table, buildDataColumnMapFromSchema } from "../Table.js";
|
|
6
|
+
import { USER_ADMIN_INITIAL_SNAPSHOT } from "./initialSnapshot.js";
|
|
7
|
+
import { reconcileUserAdminTables, resolveDisplayName } from "./snapshot-projection.js";
|
|
8
|
+
const createTables = ()=>({
|
|
9
|
+
clients: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.clients),
|
|
10
|
+
group_roles: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.group_roles),
|
|
11
|
+
groups: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.groups),
|
|
12
|
+
roles: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.roles),
|
|
13
|
+
user_group_roles: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.user_group_roles),
|
|
14
|
+
user_groups: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.user_groups),
|
|
15
|
+
users: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.users)
|
|
16
|
+
});
|
|
17
|
+
const errorResult = (error)=>({
|
|
18
|
+
errorMessage: error instanceof Error ? error.message : "User admin request failed",
|
|
19
|
+
type: "ERROR_RESULT"
|
|
20
|
+
});
|
|
21
|
+
const isRecord = (value)=>"object" == typeof value && null !== value && !Array.isArray(value);
|
|
22
|
+
const readParams = (request, rpcName)=>{
|
|
23
|
+
if ("RPC_REQUEST" !== request.type || request.rpcName !== rpcName) throw new Error(`Expected ${rpcName} RPC request`);
|
|
24
|
+
if (!isRecord(request.params)) throw new Error(`${rpcName} requires named parameters`);
|
|
25
|
+
const allowed = new Set(USER_ADMIN_RPC_CONTRACT[rpcName]);
|
|
26
|
+
for (const name of Object.keys(request.params))if (!allowed.has(name)) throw new Error(`${rpcName} received an unsupported parameter: ${name}`);
|
|
27
|
+
return request.params;
|
|
28
|
+
};
|
|
29
|
+
const requiredString = (params, name)=>{
|
|
30
|
+
const value = params[name];
|
|
31
|
+
if ("string" != typeof value || 0 === value.length) throw new Error(`${name} must be a non-empty string`);
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
const optionalString = (params, name)=>{
|
|
35
|
+
const value = params[name];
|
|
36
|
+
if (void 0 !== value && "string" != typeof value) throw new Error(`${name} must be a string`);
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
const optionalBoolean = (params, name)=>{
|
|
40
|
+
const value = params[name];
|
|
41
|
+
if (void 0 !== value && "boolean" != typeof value) throw new Error(`${name} must be a boolean`);
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
const optionalStringArray = (params, name)=>{
|
|
45
|
+
const value = params[name];
|
|
46
|
+
if (void 0 !== value && (!Array.isArray(value) || value.some((item)=>"string" != typeof item))) throw new Error(`${name} must be an array of strings`);
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
const reference = (params, idName, name)=>{
|
|
50
|
+
const id = optionalString(params, idName);
|
|
51
|
+
const label = optionalString(params, name);
|
|
52
|
+
if (!id && !label) throw new Error(`${idName} or ${name} is required`);
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
label
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const parseAssignments = (value)=>{
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(value);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error("assignments must be valid JSON");
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(parsed) || parsed.some((assignment)=>!isRecord(assignment) || "string" != typeof assignment.accessRole || 0 === assignment.accessRole.length || "string" != typeof assignment.groupId || 0 === assignment.groupId.length || Object.keys(assignment).some((name)=>"accessRole" !== name && "groupId" !== name))) throw new Error("assignments must be a JSON array of accessRole and groupId strings");
|
|
66
|
+
return parsed;
|
|
67
|
+
};
|
|
68
|
+
const parseSessionRoleAssignments = (value)=>{
|
|
69
|
+
if ("string" != typeof value) throw new Error("role_assignments must be a serialized array");
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(value);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error("role_assignments must be a valid JSON array");
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(parsed) || parsed.some((roleId)=>"string" != typeof roleId || !roleId)) throw new Error("role_assignments must be an array of role ID strings");
|
|
77
|
+
return [
|
|
78
|
+
...new Set(parsed)
|
|
79
|
+
];
|
|
80
|
+
};
|
|
81
|
+
const parseSessionPermissions = (value)=>{
|
|
82
|
+
if ("string" != typeof value) throw new Error("permissions must be a serialized array");
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(value);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error("permissions must be valid JSON");
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(parsed)) throw new Error("permissions must contain application group assignments");
|
|
90
|
+
const accessRoles = new Set();
|
|
91
|
+
const permissions = parsed.map((application, index)=>{
|
|
92
|
+
if (!isRecord(application) || Object.keys(application).some((field)=>"clientIdentifier" !== field && "accessRole" !== field && "groupIds" !== field) || "string" != typeof application.clientIdentifier || 0 === application.clientIdentifier.length || "string" != typeof application.accessRole || 0 === application.accessRole.length || !Array.isArray(application.groupIds) || application.groupIds.some((groupId)=>"string" != typeof groupId || 0 === groupId.length)) throw new Error(`permissions[${index}] must contain clientIdentifier, accessRole, and groupIds`);
|
|
93
|
+
if (accessRoles.has(application.accessRole)) throw new Error(`permissions contains duplicate accessRole: ${application.accessRole}`);
|
|
94
|
+
const groupIds = application.groupIds;
|
|
95
|
+
if (new Set(groupIds).size !== groupIds.length) throw new Error(`permissions contains duplicate group IDs for: ${application.accessRole}`);
|
|
96
|
+
accessRoles.add(application.accessRole);
|
|
97
|
+
return {
|
|
98
|
+
clientIdentifier: application.clientIdentifier,
|
|
99
|
+
accessRole: application.accessRole,
|
|
100
|
+
groupIds
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return permissions.flatMap(({ groupIds, accessRole })=>groupIds.map((groupId)=>({
|
|
104
|
+
accessRole,
|
|
105
|
+
groupId
|
|
106
|
+
})));
|
|
107
|
+
};
|
|
108
|
+
class UserAdminModule extends VuuModule {
|
|
109
|
+
#tables;
|
|
110
|
+
store;
|
|
111
|
+
constructor(snapshot = USER_ADMIN_INITIAL_SNAPSHOT){
|
|
112
|
+
super("USER_ADMIN");
|
|
113
|
+
this.store = new InMemoryUserAdminStore(snapshot);
|
|
114
|
+
this.#tables = createTables();
|
|
115
|
+
reconcileUserAdminTables(snapshot, this.#tables);
|
|
116
|
+
}
|
|
117
|
+
get schemas() {
|
|
118
|
+
return USER_ADMIN_TABLE_SCHEMAS;
|
|
119
|
+
}
|
|
120
|
+
get tables() {
|
|
121
|
+
return this.#tables;
|
|
122
|
+
}
|
|
123
|
+
get menus() {
|
|
124
|
+
return {
|
|
125
|
+
clients: void 0,
|
|
126
|
+
group_roles: void 0,
|
|
127
|
+
groups: void 0,
|
|
128
|
+
roles: void 0,
|
|
129
|
+
user_group_roles: void 0,
|
|
130
|
+
user_groups: void 0,
|
|
131
|
+
users: void 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
get visualLinks() {}
|
|
135
|
+
get menuServices() {}
|
|
136
|
+
get services() {
|
|
137
|
+
const services = [
|
|
138
|
+
{
|
|
139
|
+
rpcName: "addUser",
|
|
140
|
+
service: this.addUser
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
rpcName: "updateUser",
|
|
144
|
+
service: this.updateUser
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
rpcName: "deleteUser",
|
|
148
|
+
service: this.deleteUser
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
rpcName: "addGroup",
|
|
152
|
+
service: this.addGroup
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
rpcName: "updateGroup",
|
|
156
|
+
service: this.updateGroup
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
rpcName: "deleteGroup",
|
|
160
|
+
service: this.deleteGroup
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
rpcName: "addClient",
|
|
164
|
+
service: this.addClient
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
rpcName: "updateClient",
|
|
168
|
+
service: this.updateClient
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
rpcName: "addRole",
|
|
172
|
+
service: this.addRole
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
rpcName: "addClientRole",
|
|
176
|
+
service: this.addClientRole
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
rpcName: "updateRole",
|
|
180
|
+
service: this.updateRole
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
rpcName: "assignGroupRole",
|
|
184
|
+
service: this.assignGroupRole
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
rpcName: "removeGroupRole",
|
|
188
|
+
service: this.removeGroupRole
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
rpcName: "assignUserToGroup",
|
|
192
|
+
service: this.assignUserToGroup
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
rpcName: "removeUserFromGroup",
|
|
196
|
+
service: this.removeUserFromGroup
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
rpcName: "getUserModuleAccessOptions",
|
|
200
|
+
service: this.getUserModuleAccessOptions
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
rpcName: "setUserModuleAccess",
|
|
204
|
+
service: this.setUserModuleAccess
|
|
205
|
+
}
|
|
206
|
+
];
|
|
207
|
+
return {
|
|
208
|
+
clients: services,
|
|
209
|
+
group_roles: services,
|
|
210
|
+
groups: services,
|
|
211
|
+
roles: services,
|
|
212
|
+
user_group_roles: services,
|
|
213
|
+
user_groups: services,
|
|
214
|
+
users: services
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async reconcile() {
|
|
218
|
+
reconcileUserAdminTables(await this.store.snapshot(), this.#tables);
|
|
219
|
+
}
|
|
220
|
+
createSessionTable(sourceTable, sessionTableName, editSessionMode, dataSource, sessionType) {
|
|
221
|
+
const sessionTable = super.createSessionTable(sourceTable, sessionTableName, editSessionMode, dataSource, sessionType);
|
|
222
|
+
const extraColumns = "users" === sourceTable.name ? [
|
|
223
|
+
{
|
|
224
|
+
name: "permissions",
|
|
225
|
+
serverDataType: "string"
|
|
226
|
+
}
|
|
227
|
+
] : "groups" === sourceTable.name ? [
|
|
228
|
+
{
|
|
229
|
+
name: "group_name",
|
|
230
|
+
serverDataType: "string"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "role_assignments",
|
|
234
|
+
serverDataType: "string"
|
|
235
|
+
}
|
|
236
|
+
] : [];
|
|
237
|
+
const columnsToAdd = extraColumns.filter(({ name })=>!sessionTable.schema.columns.some((column)=>column.name === name));
|
|
238
|
+
if (0 === columnsToAdd.length) return sessionTable;
|
|
239
|
+
const sessionSchema = {
|
|
240
|
+
...sessionTable.schema,
|
|
241
|
+
columns: sessionTable.schema.columns.concat(columnsToAdd)
|
|
242
|
+
};
|
|
243
|
+
return new Table(sessionSchema, sessionTable.data.map((row)=>row.concat(columnsToAdd.map(()=>""))), buildDataColumnMapFromSchema(sessionSchema));
|
|
244
|
+
}
|
|
245
|
+
async beforeSessionSave(sourceTable, sessionTable) {
|
|
246
|
+
if ("roles" === sourceTable.name) return void await this.saveClientRolesFromSession(sessionTable);
|
|
247
|
+
if ("groups" === sourceTable.name) return void await this.saveGroupsFromSession(sessionTable);
|
|
248
|
+
if ("users" !== sourceTable.name) return;
|
|
249
|
+
const permissionsIndex = sessionTable.map.permissions;
|
|
250
|
+
const actionIndex = sessionTable.map.vuuAction;
|
|
251
|
+
const userIdIndex = sessionTable.map.user_id;
|
|
252
|
+
const usernameIndex = sessionTable.map.username;
|
|
253
|
+
for (const row of sessionTable.data){
|
|
254
|
+
if ("editCell" !== row[actionIndex]) continue;
|
|
255
|
+
const userId = String(row[userIdIndex]);
|
|
256
|
+
const sourceRow = sourceTable.findByKey(userId);
|
|
257
|
+
if (!sourceRow || sourceRow[sourceTable.map.username] !== row[usernameIndex]) throw new Error("username is read-only");
|
|
258
|
+
if ("string" == typeof row[permissionsIndex] && "" !== row[permissionsIndex]) await this.store.setUserModuleAccess(userId, parseSessionPermissions(row[permissionsIndex]));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async afterSessionSave(sourceTable, sessionTable) {
|
|
262
|
+
const roleCreationSaved = "roles" === sourceTable.name && sessionTable.data.some((row)=>"addRow" === row[sessionTable.map.vuuAction] && "client role created" === row[sessionTable.map.vuuMsg]);
|
|
263
|
+
const groupChangesSaved = "groups" === sourceTable.name && sessionTable.data.some((row)=>"group created" === row[sessionTable.map.vuuMsg] || "group roles updated" === row[sessionTable.map.vuuMsg]);
|
|
264
|
+
if ("users" === sourceTable.name || roleCreationSaved || groupChangesSaved) await this.reconcile();
|
|
265
|
+
}
|
|
266
|
+
async saveGroupsFromSession(sessionTable) {
|
|
267
|
+
const actionIndex = sessionTable.map.vuuAction;
|
|
268
|
+
const messageIndex = sessionTable.map.vuuMsg;
|
|
269
|
+
const keyIndex = sessionTable.map[sessionTable.schema.key];
|
|
270
|
+
const groupIdIndex = sessionTable.map.group_id;
|
|
271
|
+
const groupNameIndex = sessionTable.map.group_name;
|
|
272
|
+
const roleAssignmentsIndex = sessionTable.map.role_assignments;
|
|
273
|
+
for (const row of sessionTable.data)if ("addRow" !== row[actionIndex] || row[messageIndex]) {
|
|
274
|
+
if ("editCell" === row[actionIndex] && "string" == typeof row[roleAssignmentsIndex] && "" !== row[roleAssignmentsIndex]) {
|
|
275
|
+
const groupId = requiredString({
|
|
276
|
+
group_id: row[groupIdIndex]
|
|
277
|
+
}, "group_id");
|
|
278
|
+
const roleIds = parseSessionRoleAssignments(row[roleAssignmentsIndex]);
|
|
279
|
+
await this.saveGroupRoleAssignments(groupId, roleIds, await this.store.snapshot());
|
|
280
|
+
sessionTable.update(String(row[keyIndex]), "vuuMsg", "group roles updated");
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
const name = requiredString({
|
|
284
|
+
group_name: row[groupNameIndex]
|
|
285
|
+
}, "group_name").trim();
|
|
286
|
+
if (!name) throw new Error("group_name must be a non-empty string");
|
|
287
|
+
const roleIds = parseSessionRoleAssignments(row[roleAssignmentsIndex]);
|
|
288
|
+
const before = await this.store.snapshot();
|
|
289
|
+
this.resolveGroupRoleAssignments(roleIds, before);
|
|
290
|
+
const existingGroupIds = new Set(before.groups.map(({ id })=>id));
|
|
291
|
+
await this.store.addGroup({
|
|
292
|
+
name
|
|
293
|
+
});
|
|
294
|
+
const snapshot = await this.store.snapshot();
|
|
295
|
+
const group = snapshot.groups.find(({ id, name: groupName })=>!existingGroupIds.has(id) && groupName === name);
|
|
296
|
+
if (!group) throw new Error("Created group could not be found");
|
|
297
|
+
await this.saveGroupRoleAssignments(group.id, roleIds, snapshot);
|
|
298
|
+
sessionTable.update(String(row[keyIndex]), "vuuMsg", "group created");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async saveGroupRoleAssignments(groupId, desiredRoleIds, snapshot) {
|
|
302
|
+
if (!snapshot.groups.some(({ id })=>id === groupId)) throw new Error(`User admin group not found: ${groupId}`);
|
|
303
|
+
const groupRoles = snapshot.groupRoles.filter(({ group })=>group.id === groupId);
|
|
304
|
+
const roleAssignmentsById = new Map(snapshot.clientRoles.map((assignment)=>[
|
|
305
|
+
assignment.role.id,
|
|
306
|
+
assignment
|
|
307
|
+
]));
|
|
308
|
+
const desiredRoleSet = new Set(desiredRoleIds);
|
|
309
|
+
const currentAssignments = groupRoles.map(({ role })=>{
|
|
310
|
+
const assignment = roleAssignmentsById.get(role.id);
|
|
311
|
+
if (!assignment) throw new Error(`Assigned user admin role no longer exists: ${role.id}`);
|
|
312
|
+
return assignment;
|
|
313
|
+
});
|
|
314
|
+
const currentRoleSet = new Set(groupRoles.map(({ role })=>role.id));
|
|
315
|
+
const desiredAssignments = this.resolveGroupRoleAssignments(desiredRoleIds, snapshot);
|
|
316
|
+
for (const assignment of currentAssignments)if (!desiredRoleSet.has(assignment.role.id)) {
|
|
317
|
+
const clientRef = assignment.role.clientRole && assignment.client?.clientId ? {
|
|
318
|
+
clientKey: assignment.client.clientId
|
|
319
|
+
} : void 0;
|
|
320
|
+
await this.store.removeRoleFromGroup({
|
|
321
|
+
groupId
|
|
322
|
+
}, {
|
|
323
|
+
roleId: assignment.role.id
|
|
324
|
+
}, clientRef);
|
|
325
|
+
}
|
|
326
|
+
for (const assignment of desiredAssignments)if (!currentRoleSet.has(assignment.role.id)) {
|
|
327
|
+
const clientRef = assignment.role.clientRole && assignment.client?.clientId ? {
|
|
328
|
+
clientKey: assignment.client.clientId
|
|
329
|
+
} : void 0;
|
|
330
|
+
await this.store.addRoleToGroup({
|
|
331
|
+
groupId
|
|
332
|
+
}, {
|
|
333
|
+
roleId: assignment.role.id
|
|
334
|
+
}, clientRef);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
resolveGroupRoleAssignments(roleIds, snapshot) {
|
|
338
|
+
const roleAssignmentsById = new Map(snapshot.clientRoles.map((assignment)=>[
|
|
339
|
+
assignment.role.id,
|
|
340
|
+
assignment
|
|
341
|
+
]));
|
|
342
|
+
return roleIds.map((roleId)=>{
|
|
343
|
+
const assignment = roleAssignmentsById.get(roleId);
|
|
344
|
+
if (!assignment) throw new Error(`User admin role not found: ${roleId}`);
|
|
345
|
+
if (assignment.role.clientRole && !assignment.client?.clientId) throw new Error(`User admin client role has no owning client: ${roleId}`);
|
|
346
|
+
return assignment;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async saveClientRolesFromSession(sessionTable) {
|
|
350
|
+
const actionIndex = sessionTable.map.vuuAction;
|
|
351
|
+
const messageIndex = sessionTable.map.vuuMsg;
|
|
352
|
+
const keyIndex = sessionTable.map[sessionTable.schema.key];
|
|
353
|
+
const snapshot = await this.store.snapshot();
|
|
354
|
+
const clientsById = new Map(snapshot.clients.map((client)=>[
|
|
355
|
+
client.id,
|
|
356
|
+
client
|
|
357
|
+
]));
|
|
358
|
+
const pendingRoles = [
|
|
359
|
+
...sessionTable.data
|
|
360
|
+
].flatMap((row)=>{
|
|
361
|
+
if ("addRow" !== row[actionIndex] || row[messageIndex]) return [];
|
|
362
|
+
const params = Object.fromEntries(sessionTable.schema.columns.map(({ name })=>[
|
|
363
|
+
name,
|
|
364
|
+
row[sessionTable.map[name]]
|
|
365
|
+
]));
|
|
366
|
+
const roleName = requiredString(params, "role_name").trim();
|
|
367
|
+
if (!roleName) throw new Error("role_name must be a non-empty string");
|
|
368
|
+
const clientId = requiredString(params, "client_id");
|
|
369
|
+
const clientIdentifier = requiredString(params, "client_identifier");
|
|
370
|
+
const client = clientsById.get(clientId);
|
|
371
|
+
if (!client || client.clientId !== clientIdentifier) throw new Error("client_id and client_identifier must identify the same client");
|
|
372
|
+
return [
|
|
373
|
+
{
|
|
374
|
+
client,
|
|
375
|
+
description: optionalString(params, "description") ?? "",
|
|
376
|
+
roleName,
|
|
377
|
+
row
|
|
378
|
+
}
|
|
379
|
+
];
|
|
380
|
+
});
|
|
381
|
+
for (const { client, description, roleName, row } of pendingRoles){
|
|
382
|
+
await this.store.addClientRole({
|
|
383
|
+
clientKey: client.clientId
|
|
384
|
+
}, {
|
|
385
|
+
description,
|
|
386
|
+
name: roleName
|
|
387
|
+
});
|
|
388
|
+
sessionTable.update(String(row[keyIndex]), "vuuMsg", "client role created");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
mutation = (rpcName, mutate)=>async (request)=>{
|
|
392
|
+
try {
|
|
393
|
+
await mutate(readParams(request, rpcName));
|
|
394
|
+
await this.reconcile();
|
|
395
|
+
return {
|
|
396
|
+
data: void 0,
|
|
397
|
+
type: "SUCCESS_RESULT"
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return errorResult(error);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
addUser = this.mutation("addUser", async (params)=>{
|
|
404
|
+
const groupIds = optionalStringArray(params, "group_ids");
|
|
405
|
+
const username = requiredString(params, "username");
|
|
406
|
+
await this.assertUsernameIsUnique(username);
|
|
407
|
+
await this.store.addUser({
|
|
408
|
+
email: optionalString(params, "email"),
|
|
409
|
+
emailVerified: optionalBoolean(params, "emailVerified"),
|
|
410
|
+
enabled: optionalBoolean(params, "enabled"),
|
|
411
|
+
firstName: optionalString(params, "firstName"),
|
|
412
|
+
lastName: optionalString(params, "lastName"),
|
|
413
|
+
temporary_password: optionalString(params, "temporary_password"),
|
|
414
|
+
username
|
|
415
|
+
});
|
|
416
|
+
if (groupIds) {
|
|
417
|
+
const user = await this.store.findUserByUsername(requiredString(params, "username"));
|
|
418
|
+
if (!user) throw new Error("Created user could not be found");
|
|
419
|
+
await this.store.syncUserGroups(user.id, groupIds);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
updateUser = this.mutation("updateUser", async (params)=>{
|
|
423
|
+
const groupIds = optionalStringArray(params, "group_ids");
|
|
424
|
+
const userId = requiredString(params, "userId");
|
|
425
|
+
const username = optionalString(params, "username");
|
|
426
|
+
if (username) await this.assertUsernameIsUnique(username, userId);
|
|
427
|
+
await this.store.updateUser({
|
|
428
|
+
email: optionalString(params, "email"),
|
|
429
|
+
emailVerified: optionalBoolean(params, "emailVerified"),
|
|
430
|
+
enabled: optionalBoolean(params, "enabled"),
|
|
431
|
+
firstName: optionalString(params, "firstName"),
|
|
432
|
+
lastName: optionalString(params, "lastName"),
|
|
433
|
+
temporary_password: optionalString(params, "temporary_password"),
|
|
434
|
+
userId,
|
|
435
|
+
username
|
|
436
|
+
});
|
|
437
|
+
if (groupIds) await this.store.syncUserGroups(userId, groupIds);
|
|
438
|
+
});
|
|
439
|
+
deleteUser = this.mutation("deleteUser", (params)=>this.store.deleteUser(requiredString(params, "userId")));
|
|
440
|
+
addGroup = this.mutation("addGroup", (params)=>this.store.addGroup({
|
|
441
|
+
name: requiredString(params, "name")
|
|
442
|
+
}));
|
|
443
|
+
updateGroup = this.mutation("updateGroup", (params)=>this.store.updateGroup(requiredString(params, "groupId"), {
|
|
444
|
+
name: requiredString(params, "name")
|
|
445
|
+
}));
|
|
446
|
+
deleteGroup = this.mutation("deleteGroup", (params)=>this.store.deleteGroup(requiredString(params, "groupId")));
|
|
447
|
+
addClient = this.mutation("addClient", (params)=>this.store.addClient({
|
|
448
|
+
clientId: requiredString(params, "clientId"),
|
|
449
|
+
description: optionalString(params, "description"),
|
|
450
|
+
enabled: optionalBoolean(params, "enabled"),
|
|
451
|
+
name: optionalString(params, "name")
|
|
452
|
+
}));
|
|
453
|
+
updateClient = this.mutation("updateClient", (params)=>this.store.updateClient(requiredString(params, "clientId"), {
|
|
454
|
+
description: optionalString(params, "description"),
|
|
455
|
+
enabled: optionalBoolean(params, "enabled"),
|
|
456
|
+
name: optionalString(params, "name")
|
|
457
|
+
}));
|
|
458
|
+
addRole = this.mutation("addRole", (params)=>this.store.addRole({
|
|
459
|
+
description: optionalString(params, "description"),
|
|
460
|
+
name: requiredString(params, "name")
|
|
461
|
+
}));
|
|
462
|
+
addClientRole = this.mutation("addClientRole", (params)=>this.store.addClientRole({
|
|
463
|
+
clientKey: requiredString(params, "clientId")
|
|
464
|
+
}, {
|
|
465
|
+
description: optionalString(params, "description"),
|
|
466
|
+
name: requiredString(params, "name")
|
|
467
|
+
}));
|
|
468
|
+
updateRole = this.mutation("updateRole", async (params)=>{
|
|
469
|
+
const clientId = optionalString(params, "clientId");
|
|
470
|
+
const changes = {
|
|
471
|
+
description: optionalString(params, "description"),
|
|
472
|
+
name: optionalString(params, "name")
|
|
473
|
+
};
|
|
474
|
+
if (clientId) await this.store.updateClientRole(clientId, requiredString(params, "roleName"), changes);
|
|
475
|
+
else await this.store.updateRealmRole(requiredString(params, "roleId"), changes);
|
|
476
|
+
});
|
|
477
|
+
assignGroupRole = this.mutation("assignGroupRole", (params)=>{
|
|
478
|
+
const group = reference(params, "groupId", "groupName");
|
|
479
|
+
const role = reference(params, "roleId", "roleName");
|
|
480
|
+
const clientId = optionalString(params, "clientId");
|
|
481
|
+
return this.store.addRoleToGroup({
|
|
482
|
+
groupId: group.id,
|
|
483
|
+
groupName: group.label
|
|
484
|
+
}, {
|
|
485
|
+
roleId: role.id,
|
|
486
|
+
roleName: role.label
|
|
487
|
+
}, clientId ? {
|
|
488
|
+
clientKey: clientId
|
|
489
|
+
} : void 0);
|
|
490
|
+
});
|
|
491
|
+
removeGroupRole = this.mutation("removeGroupRole", (params)=>{
|
|
492
|
+
const group = reference(params, "groupId", "groupName");
|
|
493
|
+
const role = reference(params, "roleId", "roleName");
|
|
494
|
+
const clientId = optionalString(params, "clientId");
|
|
495
|
+
return this.store.removeRoleFromGroup({
|
|
496
|
+
groupId: group.id,
|
|
497
|
+
groupName: group.label
|
|
498
|
+
}, {
|
|
499
|
+
roleId: role.id,
|
|
500
|
+
roleName: role.label
|
|
501
|
+
}, clientId ? {
|
|
502
|
+
clientKey: clientId
|
|
503
|
+
} : void 0);
|
|
504
|
+
});
|
|
505
|
+
assignUserToGroup = this.mutation("assignUserToGroup", (params)=>{
|
|
506
|
+
const user = reference(params, "userId", "username");
|
|
507
|
+
const group = reference(params, "groupId", "groupName");
|
|
508
|
+
return this.store.addUserToGroup({
|
|
509
|
+
userId: user.id,
|
|
510
|
+
username: user.label
|
|
511
|
+
}, {
|
|
512
|
+
groupId: group.id,
|
|
513
|
+
groupName: group.label
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
removeUserFromGroup = this.mutation("removeUserFromGroup", (params)=>{
|
|
517
|
+
const user = reference(params, "userId", "username");
|
|
518
|
+
const group = reference(params, "groupId", "groupName");
|
|
519
|
+
return this.store.removeUserFromGroup({
|
|
520
|
+
userId: user.id,
|
|
521
|
+
username: user.label
|
|
522
|
+
}, {
|
|
523
|
+
groupId: group.id,
|
|
524
|
+
groupName: group.label
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
getUserModuleAccessOptions = async (request)=>{
|
|
528
|
+
try {
|
|
529
|
+
const params = readParams(request, "getUserModuleAccessOptions");
|
|
530
|
+
const [options, snapshot] = await Promise.all([
|
|
531
|
+
this.store.getUserModuleAccessOptions(requiredString(params, "userId")),
|
|
532
|
+
this.store.snapshot()
|
|
533
|
+
]);
|
|
534
|
+
const groupDisplayNames = new Map(snapshot.groups.map(({ groupDisplayName, id, name })=>[
|
|
535
|
+
id,
|
|
536
|
+
resolveDisplayName(name, groupDisplayName)
|
|
537
|
+
]));
|
|
538
|
+
const roleDisplayNames = new Map(snapshot.clientRoles.map(({ role })=>[
|
|
539
|
+
role.id,
|
|
540
|
+
resolveDisplayName(role.name, role.roleDisplayName)
|
|
541
|
+
]));
|
|
542
|
+
return {
|
|
543
|
+
data: {
|
|
544
|
+
modules: options.modules.map(({ groups, ...module })=>({
|
|
545
|
+
...module,
|
|
546
|
+
groups: groups.map((group)=>({
|
|
547
|
+
...group,
|
|
548
|
+
groupDisplayName: groupDisplayNames.get(group.groupId) ?? resolveDisplayName(group.groupName),
|
|
549
|
+
roleDisplayName: roleDisplayNames.get(group.roleId) ?? resolveDisplayName(group.roleName)
|
|
550
|
+
}))
|
|
551
|
+
}))
|
|
552
|
+
},
|
|
553
|
+
type: "SUCCESS_RESULT"
|
|
554
|
+
};
|
|
555
|
+
} catch (error) {
|
|
556
|
+
return errorResult(error);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
setUserModuleAccess = this.mutation("setUserModuleAccess", (params)=>this.store.setUserModuleAccess(requiredString(params, "userId"), parseAssignments(requiredString(params, "assignments"))));
|
|
560
|
+
async assertUsernameIsUnique(username, userId) {
|
|
561
|
+
const normalizedUsername = username.toLocaleLowerCase();
|
|
562
|
+
const existingUser = (await this.store.snapshot()).users.find((user)=>user.id !== userId && user.username.toLocaleLowerCase() === normalizedUsername);
|
|
563
|
+
if (existingUser) throw new Error("username must be unique");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const userAdminModule = new UserAdminModule();
|
|
567
|
+
export { UserAdminModule, userAdminModule };
|