ldap-rest 0.0.1
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/LICENSE +661 -0
- package/README.md +48 -0
- package/bin/cleanup-external-users.mjs +219 -0
- package/bin/index.mjs +54 -0
- package/bin/sync-james.mjs +190 -0
- package/dist/abstract/ldapFlat.js +1 -0
- package/dist/abstract/plugin.js +1 -0
- package/dist/abstract/twakePlugin.js +1 -0
- package/dist/bin/index.js +1 -0
- package/dist/config/args.js +1 -0
- package/dist/lib/auth/base.js +1 -0
- package/dist/lib/authz/base.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -0
- package/dist/lib/ldapActions.js +1 -0
- package/dist/lib/parseConfig.js +1 -0
- package/dist/lib/utils.js +1 -0
- package/dist/logger/winston.js +1 -0
- package/dist/plugins/auth/authzLinid1.js +1 -0
- package/dist/plugins/auth/authzPerBranch.js +1 -0
- package/dist/plugins/auth/crowdsec.js +1 -0
- package/dist/plugins/auth/llng.js +1 -0
- package/dist/plugins/auth/openidconnect.js +1 -0
- package/dist/plugins/auth/rateLimit.js +1 -0
- package/dist/plugins/auth/token.js +1 -0
- package/dist/plugins/configApi.js +1 -0
- package/dist/plugins/demo/helloworld.js +1 -0
- package/dist/plugins/ldap/bulkImport.js +1 -0
- package/dist/plugins/ldap/departmentSync.js +1 -0
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -0
- package/dist/plugins/ldap/flatGeneric.js +1 -0
- package/dist/plugins/ldap/groups.js +1 -0
- package/dist/plugins/ldap/onChange.js +1 -0
- package/dist/plugins/ldap/organization.js +1 -0
- package/dist/plugins/ldap/trash.js +1 -0
- package/dist/plugins/priority.json.js +1 -0
- package/dist/plugins/static.js +1 -0
- package/dist/plugins/twake/calendarResources.js +1 -0
- package/dist/plugins/twake/james.js +1 -0
- package/dist/plugins/weblogs.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +94 -0
- package/dist/src/abstract/plugin.d.ts +43 -0
- package/dist/src/abstract/twakePlugin.d.ts +90 -0
- package/dist/src/bin/index.d.ts +43 -0
- package/dist/src/browser/ldap-group-editor/LdapGroupEditor.d.ts +31 -0
- package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +23 -0
- package/dist/src/browser/ldap-group-editor/components/CreateGroupModal.d.ts +19 -0
- package/dist/src/browser/ldap-group-editor/components/GroupPropertyEditor.d.ts +30 -0
- package/dist/src/browser/ldap-group-editor/components/GroupTree.d.ts +17 -0
- package/dist/src/browser/ldap-group-editor/components/MoveGroupModal.d.ts +20 -0
- package/dist/src/browser/ldap-group-editor/index.d.ts +6 -0
- package/dist/src/browser/ldap-group-editor/types.d.ts +58 -0
- package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +53 -0
- package/dist/src/browser/ldap-resource-editor/cache/CacheManager.d.ts +65 -0
- package/dist/src/browser/ldap-resource-editor/index.d.ts +6 -0
- package/dist/src/browser/ldap-resource-editor/types.d.ts +78 -0
- package/dist/src/browser/ldap-tree-viewer/LdapTreeViewer.d.ts +26 -0
- package/dist/src/browser/ldap-tree-viewer/api/LdapApiClient.d.ts +30 -0
- package/dist/src/browser/ldap-tree-viewer/components/TreeNode.d.ts +17 -0
- package/dist/src/browser/ldap-tree-viewer/components/TreeRoot.d.ts +14 -0
- package/dist/src/browser/ldap-tree-viewer/index.d.ts +7 -0
- package/dist/src/browser/ldap-tree-viewer/store/Store.d.ts +18 -0
- package/dist/src/browser/ldap-tree-viewer/store/actions.d.ts +28 -0
- package/dist/src/browser/ldap-tree-viewer/store/reducers.d.ts +7 -0
- package/dist/src/browser/ldap-tree-viewer/types.d.ts +34 -0
- package/dist/src/browser/ldap-unit-editor/LdapUnitEditor.d.ts +27 -0
- package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +24 -0
- package/dist/src/browser/ldap-unit-editor/components/MoveUnitModal.d.ts +28 -0
- package/dist/src/browser/ldap-unit-editor/components/UnitPropertyEditor.d.ts +28 -0
- package/dist/src/browser/ldap-unit-editor/components/UnitTree.d.ts +18 -0
- package/dist/src/browser/ldap-unit-editor/index.d.ts +6 -0
- package/dist/src/browser/ldap-unit-editor/types.d.ts +55 -0
- package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +32 -0
- package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +55 -0
- package/dist/src/browser/ldap-user-editor/cache/CacheManager.d.ts +65 -0
- package/dist/src/browser/ldap-user-editor/components/MoveUserModal.d.ts +28 -0
- package/dist/src/browser/ldap-user-editor/components/PointerField.d.ts +22 -0
- package/dist/src/browser/ldap-user-editor/components/UserEditor.d.ts +46 -0
- package/dist/src/browser/ldap-user-editor/components/UserList.d.ts +24 -0
- package/dist/src/browser/ldap-user-editor/components/UserTree.d.ts +24 -0
- package/dist/src/browser/ldap-user-editor/index.d.ts +7 -0
- package/dist/src/browser/ldap-user-editor/types.d.ts +67 -0
- package/dist/src/browser/shared/components/Modal.d.ts +38 -0
- package/dist/src/browser/shared/components/StatusMessage.d.ts +48 -0
- package/dist/src/browser/shared/index.d.ts +10 -0
- package/dist/src/browser/shared/types/index.d.ts +65 -0
- package/dist/src/browser/shared/utils/dom.d.ts +33 -0
- package/dist/src/browser/shared/utils/form.d.ts +28 -0
- package/dist/src/browser/shared/utils/schema.d.ts +29 -0
- package/dist/src/config/args.d.ts +128 -0
- package/dist/src/config/schema.d.ts +18 -0
- package/dist/src/hooks.d.ts +76 -0
- package/dist/src/lib/auth/base.d.ts +9 -0
- package/dist/src/lib/authz/base.d.ts +63 -0
- package/dist/src/lib/expressFormatedResponses.d.ts +26 -0
- package/dist/src/lib/ldapActions.d.ts +91 -0
- package/dist/src/lib/parseConfig.d.ts +23 -0
- package/dist/src/lib/utils.d.ts +73 -0
- package/dist/src/logger/winston.d.ts +3 -0
- package/dist/src/plugins/auth/authzLinid1.d.ts +46 -0
- package/dist/src/plugins/auth/authzPerBranch.d.ts +59 -0
- package/dist/src/plugins/auth/crowdsec.d.ts +29 -0
- package/dist/src/plugins/auth/llng.d.ts +8 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +11 -0
- package/dist/src/plugins/auth/rateLimit.d.ts +24 -0
- package/dist/src/plugins/auth/token.d.ts +17 -0
- package/dist/src/plugins/configApi.d.ts +24 -0
- package/dist/src/plugins/demo/helloworld.d.ts +12 -0
- package/dist/src/plugins/ldap/bulkImport.d.ts +48 -0
- package/dist/src/plugins/ldap/departmentSync.d.ts +35 -0
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +21 -0
- package/dist/src/plugins/ldap/flatGeneric.d.ts +31 -0
- package/dist/src/plugins/ldap/groups.d.ts +95 -0
- package/dist/src/plugins/ldap/onChange.d.ts +32 -0
- package/dist/src/plugins/ldap/organization.d.ts +65 -0
- package/dist/src/plugins/ldap/trash.d.ts +52 -0
- package/dist/src/plugins/static.d.ts +7 -0
- package/dist/src/plugins/twake/calendarResources.d.ts +31 -0
- package/dist/src/plugins/twake/james.d.ts +83 -0
- package/dist/src/plugins/weblogs.d.ts +8 -0
- package/docs/DEVELOPER_GUIDE.md +298 -0
- package/docs/OPENAPI.md +440 -0
- package/docs/PLUGIN_DEPENDENCIES.md +612 -0
- package/docs/README.md +466 -0
- package/docs/api/REFERENCE.md +983 -0
- package/docs/api/REST_API.md +1237 -0
- package/docs/authentication.md +469 -0
- package/docs/authzLinid1.md +521 -0
- package/docs/authzPerBranch.md +596 -0
- package/docs/browser/LIBRARIES.md +1196 -0
- package/docs/examples/EXAMPLES.md +1659 -0
- package/docs/examples/twake-mailbox-type-nomenclature.ldif +37 -0
- package/docs/examples/twake-mailbox-type-schema-amendment.ldif +30 -0
- package/docs/jamesTeamMailboxes.md +343 -0
- package/docs/ldapBulkImport.md +722 -0
- package/docs/ldapExternalUsersInGroups.md +325 -0
- package/docs/ldapFlatGeneric.md +444 -0
- package/docs/ldapGroups.md +589 -0
- package/docs/ldapOrganizations.md +659 -0
- package/docs/ldapTrash.md +260 -0
- package/docs/linagora.png +0 -0
- package/docs/onChange.md +318 -0
- package/docs/plugins/DEVELOPMENT.md +1219 -0
- package/docs/presenterm-fr.md +721 -0
- package/docs/presenterm.md +721 -0
- package/docs/schemas/SCHEMAS.md +1212 -0
- package/docs/static.md +410 -0
- package/docs/twakeCalendarResources.md +148 -0
- package/docs/twakeJames.md +661 -0
- package/docs/weblogs.md +399 -0
- package/examples/web/ldap-group-editor.html +113 -0
- package/examples/web/ldap-tree-viewer.html +368 -0
- package/examples/web/ldap-unit-editor.html +108 -0
- package/examples/web/ldap-user-editor.html +412 -0
- package/package.json +458 -0
- package/static/browser/common-demo.css +450 -0
- package/static/browser/ldap-group-editor/LdapGroupEditor.js +207 -0
- package/static/browser/ldap-group-editor/LdapGroupEditor.js.map +1 -0
- package/static/browser/ldap-group-editor/api/GroupApiClient.js +112 -0
- package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -0
- package/static/browser/ldap-group-editor/components/CreateGroupModal.js +212 -0
- package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -0
- package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +497 -0
- package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -0
- package/static/browser/ldap-group-editor/components/GroupTree.js +153 -0
- package/static/browser/ldap-group-editor/components/GroupTree.js.map +1 -0
- package/static/browser/ldap-group-editor/components/MoveGroupModal.js +235 -0
- package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -0
- package/static/browser/ldap-group-editor/index.js +2 -0
- package/static/browser/ldap-group-editor/index.js.map +1 -0
- package/static/browser/ldap-group-editor/types.js +2 -0
- package/static/browser/ldap-group-editor/types.js.map +1 -0
- package/static/browser/ldap-group-editor.esm.js +1399 -0
- package/static/browser/ldap-group-editor.esm.js.map +1 -0
- package/static/browser/ldap-group-editor.js +1407 -0
- package/static/browser/ldap-group-editor.js.map +1 -0
- package/static/browser/ldap-group-editor.min.js +2 -0
- package/static/browser/ldap-group-editor.min.js.map +1 -0
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +226 -0
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -0
- package/static/browser/ldap-resource-editor/cache/CacheManager.js +123 -0
- package/static/browser/ldap-resource-editor/cache/CacheManager.js.map +1 -0
- package/static/browser/ldap-resource-editor/index.js +2 -0
- package/static/browser/ldap-resource-editor/index.js.map +1 -0
- package/static/browser/ldap-resource-editor/types.js +2 -0
- package/static/browser/ldap-resource-editor/types.js.map +1 -0
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js +187 -0
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -0
- package/static/browser/ldap-tree-viewer/api/LdapApiClient.js +50 -0
- package/static/browser/ldap-tree-viewer/api/LdapApiClient.js.map +1 -0
- package/static/browser/ldap-tree-viewer/components/TreeNode.js +140 -0
- package/static/browser/ldap-tree-viewer/components/TreeNode.js.map +1 -0
- package/static/browser/ldap-tree-viewer/components/TreeRoot.js +98 -0
- package/static/browser/ldap-tree-viewer/components/TreeRoot.js.map +1 -0
- package/static/browser/ldap-tree-viewer/index.js +2 -0
- package/static/browser/ldap-tree-viewer/index.js.map +1 -0
- package/static/browser/ldap-tree-viewer/store/Store.js +29 -0
- package/static/browser/ldap-tree-viewer/store/Store.js.map +1 -0
- package/static/browser/ldap-tree-viewer/store/actions.js +53 -0
- package/static/browser/ldap-tree-viewer/store/actions.js.map +1 -0
- package/static/browser/ldap-tree-viewer/store/reducers.js +79 -0
- package/static/browser/ldap-tree-viewer/store/reducers.js.map +1 -0
- package/static/browser/ldap-tree-viewer/types.js +2 -0
- package/static/browser/ldap-tree-viewer/types.js.map +1 -0
- package/static/browser/ldap-tree-viewer.css +2 -0
- package/static/browser/ldap-tree-viewer.css.map +1 -0
- package/static/browser/ldap-tree-viewer.esm.js +606 -0
- package/static/browser/ldap-tree-viewer.esm.js.map +1 -0
- package/static/browser/ldap-tree-viewer.js +614 -0
- package/static/browser/ldap-tree-viewer.js.map +1 -0
- package/static/browser/ldap-tree-viewer.min.js +2 -0
- package/static/browser/ldap-tree-viewer.min.js.map +1 -0
- package/static/browser/ldap-unit-editor/LdapUnitEditor.js +119 -0
- package/static/browser/ldap-unit-editor/LdapUnitEditor.js.map +1 -0
- package/static/browser/ldap-unit-editor/api/UnitApiClient.js +95 -0
- package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -0
- package/static/browser/ldap-unit-editor/components/MoveUnitModal.js +295 -0
- package/static/browser/ldap-unit-editor/components/MoveUnitModal.js.map +1 -0
- package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +321 -0
- package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -0
- package/static/browser/ldap-unit-editor/components/UnitTree.js +197 -0
- package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -0
- package/static/browser/ldap-unit-editor/index.js +2 -0
- package/static/browser/ldap-unit-editor/index.js.map +1 -0
- package/static/browser/ldap-unit-editor/types.js +2 -0
- package/static/browser/ldap-unit-editor/types.js.map +1 -0
- package/static/browser/ldap-unit-editor.esm.js +1013 -0
- package/static/browser/ldap-unit-editor.esm.js.map +1 -0
- package/static/browser/ldap-unit-editor.js +1021 -0
- package/static/browser/ldap-unit-editor.js.map +1 -0
- package/static/browser/ldap-unit-editor.min.js +2 -0
- package/static/browser/ldap-unit-editor.min.js.map +1 -0
- package/static/browser/ldap-user-editor/LdapUserEditor.js +184 -0
- package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -0
- package/static/browser/ldap-user-editor/api/UserApiClient.js +209 -0
- package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -0
- package/static/browser/ldap-user-editor/cache/CacheManager.js +123 -0
- package/static/browser/ldap-user-editor/cache/CacheManager.js.map +1 -0
- package/static/browser/ldap-user-editor/components/MoveUserModal.js +302 -0
- package/static/browser/ldap-user-editor/components/MoveUserModal.js.map +1 -0
- package/static/browser/ldap-user-editor/components/PointerField.js +152 -0
- package/static/browser/ldap-user-editor/components/PointerField.js.map +1 -0
- package/static/browser/ldap-user-editor/components/UserEditor.js +608 -0
- package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -0
- package/static/browser/ldap-user-editor/components/UserList.js +181 -0
- package/static/browser/ldap-user-editor/components/UserList.js.map +1 -0
- package/static/browser/ldap-user-editor/components/UserTree.js +164 -0
- package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -0
- package/static/browser/ldap-user-editor/index.js +2 -0
- package/static/browser/ldap-user-editor/index.js.map +1 -0
- package/static/browser/ldap-user-editor/types.js +2 -0
- package/static/browser/ldap-user-editor/types.js.map +1 -0
- package/static/browser/ldap-user-editor.css +2 -0
- package/static/browser/ldap-user-editor.css.map +1 -0
- package/static/browser/ldap-user-editor.esm.js +1897 -0
- package/static/browser/ldap-user-editor.esm.js.map +1 -0
- package/static/browser/ldap-user-editor.js +1905 -0
- package/static/browser/ldap-user-editor.js.map +1 -0
- package/static/browser/ldap-user-editor.min.js +2 -0
- package/static/browser/ldap-user-editor.min.js.map +1 -0
- package/static/browser/shared/components/Modal.js +121 -0
- package/static/browser/shared/components/Modal.js.map +1 -0
- package/static/browser/shared/components/StatusMessage.js +119 -0
- package/static/browser/shared/components/StatusMessage.js.map +1 -0
- package/static/browser/shared/index.js +7 -0
- package/static/browser/shared/index.js.map +1 -0
- package/static/browser/shared/types/index.js +37 -0
- package/static/browser/shared/types/index.js.map +1 -0
- package/static/browser/shared/utils/dom.js +78 -0
- package/static/browser/shared/utils/dom.js.map +1 -0
- package/static/browser/shared/utils/form.js +234 -0
- package/static/browser/shared/utils/form.js.map +1 -0
- package/static/browser/shared/utils/schema.js +121 -0
- package/static/browser/shared/utils/schema.js.map +1 -0
- package/static/browser/shared.esm.js +694 -0
- package/static/browser/shared.esm.js.map +1 -0
- package/static/browser/shared.js +723 -0
- package/static/browser/shared.js.map +1 -0
- package/static/browser/shared.min.js +2 -0
- package/static/browser/shared.min.js.map +1 -0
- package/static/schemas/ad/computers.json +92 -0
- package/static/schemas/ad/groups.json +55 -0
- package/static/schemas/ad/organizations.json +66 -0
- package/static/schemas/ad/users.json +116 -0
- package/static/schemas/standard/devices.json +72 -0
- package/static/schemas/standard/groups.json +54 -0
- package/static/schemas/standard/organizations.json +49 -0
- package/static/schemas/standard/users.json +86 -0
- package/static/schemas/twake/groups.json +75 -0
- package/static/schemas/twake/nomenclature/twakeAccountStatus.json +31 -0
- package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +31 -0
- package/static/schemas/twake/nomenclature/twakeListType.json +31 -0
- package/static/schemas/twake/nomenclature/twakeMailboxType.json +31 -0
- package/static/schemas/twake/nomenclature/twakeTitle.json +31 -0
- package/static/schemas/twake/organizations.json +52 -0
- package/static/schemas/twake/positions.json +32 -0
- package/static/schemas/twake/users.json +112 -0
|
@@ -0,0 +1,1897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory cache manager with LRU eviction
|
|
3
|
+
* Prevents excessive API calls when navigating the organization tree
|
|
4
|
+
*/
|
|
5
|
+
class CacheManager {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.cache = new Map();
|
|
8
|
+
this.ttl = options.ttl ?? 5 * 60 * 1000; // 5 minutes default
|
|
9
|
+
this.maxEntries = options.maxEntries ?? 200; // 200 entries max
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get an item from cache
|
|
13
|
+
* Returns null if not found or expired
|
|
14
|
+
*/
|
|
15
|
+
get(key) {
|
|
16
|
+
const entry = this.cache.get(key);
|
|
17
|
+
if (!entry)
|
|
18
|
+
return null;
|
|
19
|
+
// Check if expired
|
|
20
|
+
if (Date.now() - entry.timestamp >= this.ttl) {
|
|
21
|
+
this.cache.delete(key);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
// Update access count for LRU
|
|
25
|
+
entry.accessCount++;
|
|
26
|
+
entry.timestamp = Date.now();
|
|
27
|
+
return entry.data;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set an item in cache
|
|
31
|
+
* Evicts least recently used items if cache is full
|
|
32
|
+
*/
|
|
33
|
+
set(key, data) {
|
|
34
|
+
// If cache is full, evict LRU entry
|
|
35
|
+
if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
|
|
36
|
+
this.evictLRU();
|
|
37
|
+
}
|
|
38
|
+
this.cache.set(key, {
|
|
39
|
+
data,
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
accessCount: 0,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if key exists and is not expired
|
|
46
|
+
*/
|
|
47
|
+
has(key) {
|
|
48
|
+
return this.get(key) !== null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Invalidate a specific key
|
|
52
|
+
*/
|
|
53
|
+
invalidate(key) {
|
|
54
|
+
this.cache.delete(key);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Invalidate all keys matching a pattern
|
|
58
|
+
* Pattern can contain wildcards (*)
|
|
59
|
+
*/
|
|
60
|
+
invalidatePattern(pattern) {
|
|
61
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
62
|
+
for (const key of this.cache.keys()) {
|
|
63
|
+
if (regex.test(key)) {
|
|
64
|
+
this.cache.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Clear all cache
|
|
70
|
+
*/
|
|
71
|
+
clear() {
|
|
72
|
+
this.cache.clear();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get cache statistics
|
|
76
|
+
*/
|
|
77
|
+
getStats() {
|
|
78
|
+
return {
|
|
79
|
+
size: this.cache.size,
|
|
80
|
+
maxSize: this.maxEntries,
|
|
81
|
+
ttl: this.ttl,
|
|
82
|
+
keys: Array.from(this.cache.keys()),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Evict least recently used entry
|
|
87
|
+
*/
|
|
88
|
+
evictLRU() {
|
|
89
|
+
let lruKey = null;
|
|
90
|
+
let lruTime = Infinity;
|
|
91
|
+
let lruCount = Infinity;
|
|
92
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
93
|
+
// Prioritize by access count, then by timestamp
|
|
94
|
+
if (entry.accessCount < lruCount ||
|
|
95
|
+
(entry.accessCount === lruCount && entry.timestamp < lruTime)) {
|
|
96
|
+
lruKey = key;
|
|
97
|
+
lruTime = entry.timestamp;
|
|
98
|
+
lruCount = entry.accessCount;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (lruKey) {
|
|
102
|
+
this.cache.delete(lruKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Clean expired entries
|
|
107
|
+
* Should be called periodically
|
|
108
|
+
*/
|
|
109
|
+
cleanExpired() {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
let cleaned = 0;
|
|
112
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
113
|
+
if (now - entry.timestamp >= this.ttl) {
|
|
114
|
+
this.cache.delete(key);
|
|
115
|
+
cleaned++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return cleaned;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* LDAP User Editor - API Client
|
|
124
|
+
*/
|
|
125
|
+
class UserApiClient {
|
|
126
|
+
constructor(baseUrl = '', cacheOptions) {
|
|
127
|
+
this.baseUrl =
|
|
128
|
+
baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
|
|
129
|
+
this.cache = new CacheManager(cacheOptions);
|
|
130
|
+
// Clean expired entries every 5 minutes (only in browser context)
|
|
131
|
+
if (typeof window !== 'undefined') {
|
|
132
|
+
window.setInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
getApiBase() {
|
|
136
|
+
return `${this.baseUrl}/api/v1`;
|
|
137
|
+
}
|
|
138
|
+
getFirstValue(value) {
|
|
139
|
+
if (!value)
|
|
140
|
+
return '';
|
|
141
|
+
if (Array.isArray(value))
|
|
142
|
+
return value[0] || '';
|
|
143
|
+
return String(value);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Fetch with cache support for GET requests
|
|
147
|
+
*/
|
|
148
|
+
async cachedFetch(url, options) {
|
|
149
|
+
const method = options?.method || 'GET';
|
|
150
|
+
// Only cache GET requests
|
|
151
|
+
if (method === 'GET') {
|
|
152
|
+
const cached = this.cache.get(url);
|
|
153
|
+
if (cached !== null) {
|
|
154
|
+
return cached;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const res = await fetch(url, options);
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
160
|
+
}
|
|
161
|
+
const data = (await res.json());
|
|
162
|
+
// Cache GET responses
|
|
163
|
+
if (method === 'GET') {
|
|
164
|
+
this.cache.set(url, data);
|
|
165
|
+
}
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
async getConfig() {
|
|
169
|
+
return this.cachedFetch(`${this.getApiBase()}/config`);
|
|
170
|
+
}
|
|
171
|
+
async getSchema(schemaUrl) {
|
|
172
|
+
// Handle relative URLs by prepending baseUrl
|
|
173
|
+
const url = schemaUrl.startsWith('http')
|
|
174
|
+
? schemaUrl
|
|
175
|
+
: `${this.baseUrl}${schemaUrl}`;
|
|
176
|
+
return this.cachedFetch(url);
|
|
177
|
+
}
|
|
178
|
+
async getUsers(search = '') {
|
|
179
|
+
const url = search
|
|
180
|
+
? `${this.getApiBase()}/ldap/users?match=${encodeURIComponent(search)}&attribute=uid`
|
|
181
|
+
: `${this.getApiBase()}/ldap/users`;
|
|
182
|
+
const data = await this.cachedFetch(url);
|
|
183
|
+
// Convert from object format {uid: entry} to array format
|
|
184
|
+
return Array.isArray(data) ? data : Object.values(data);
|
|
185
|
+
}
|
|
186
|
+
async getUser(dn) {
|
|
187
|
+
const url = `${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`;
|
|
188
|
+
return this.cachedFetch(url);
|
|
189
|
+
}
|
|
190
|
+
async updateUser(dn, data) {
|
|
191
|
+
const res = await fetch(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`, {
|
|
192
|
+
method: 'PUT',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify(data),
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
const error = await res.text();
|
|
198
|
+
throw new Error(error || 'Failed to update user');
|
|
199
|
+
}
|
|
200
|
+
const result = await res.json();
|
|
201
|
+
// Invalidate cache for this user and user lists
|
|
202
|
+
this.cache.invalidate(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`);
|
|
203
|
+
this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
async createUser(data) {
|
|
207
|
+
const res = await fetch(`${this.getApiBase()}/ldap/users`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(data),
|
|
211
|
+
});
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
const error = await res.text();
|
|
214
|
+
throw new Error(error || 'Failed to create user');
|
|
215
|
+
}
|
|
216
|
+
const result = await res.json();
|
|
217
|
+
// Invalidate cache for user lists
|
|
218
|
+
this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
async deleteUser(dn) {
|
|
222
|
+
const res = await fetch(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`, {
|
|
223
|
+
method: 'DELETE',
|
|
224
|
+
});
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
const error = await res.text();
|
|
227
|
+
throw new Error(error || 'Failed to delete user');
|
|
228
|
+
}
|
|
229
|
+
// Invalidate cache for this user and user lists
|
|
230
|
+
this.cache.invalidate(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`);
|
|
231
|
+
this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
|
|
232
|
+
}
|
|
233
|
+
async moveUser(dn, targetOrgDn) {
|
|
234
|
+
const res = await fetch(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}/move`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify({ targetOrgDn }),
|
|
238
|
+
});
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
const error = await res.text();
|
|
241
|
+
throw new Error(error || 'Failed to move user');
|
|
242
|
+
}
|
|
243
|
+
const result = await res.json();
|
|
244
|
+
// Invalidate cache for this user and user lists
|
|
245
|
+
this.cache.invalidate(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`);
|
|
246
|
+
this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
async getPointerOptions(branch) {
|
|
250
|
+
try {
|
|
251
|
+
// Load config to find the right endpoint by matching base
|
|
252
|
+
const config = await this.getConfig();
|
|
253
|
+
// Find resource that matches this branch by comparing base
|
|
254
|
+
const resource = config.features?.ldapFlatGeneric?.flatResources?.find(r => {
|
|
255
|
+
// Match if branch equals base or branch starts with base
|
|
256
|
+
return branch === r.base || branch.startsWith(r.base);
|
|
257
|
+
});
|
|
258
|
+
if (resource && resource.endpoints?.list) {
|
|
259
|
+
// Use the endpoint from config
|
|
260
|
+
const url = `${this.baseUrl}${resource.endpoints.list}`;
|
|
261
|
+
const data = await this.cachedFetch(url);
|
|
262
|
+
// Convert from object format {key: entry} to array
|
|
263
|
+
const items = Array.isArray(data) ? data : Object.values(data);
|
|
264
|
+
// Load schema to get displayName field
|
|
265
|
+
let displayNameField = 'cn'; // Default fallback
|
|
266
|
+
const identifierField = resource.mainAttribute;
|
|
267
|
+
if (resource.schemaUrl) {
|
|
268
|
+
try {
|
|
269
|
+
const schema = await this.getSchema(resource.schemaUrl);
|
|
270
|
+
// Find field with displayName role
|
|
271
|
+
const displayField = Object.entries(schema.attributes).find(([, attr]) => attr.role === 'displayName');
|
|
272
|
+
if (displayField) {
|
|
273
|
+
displayNameField = displayField[0];
|
|
274
|
+
}
|
|
275
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
// Use defaults if schema fails
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return items.map((item) => {
|
|
282
|
+
const displayName = this.getFirstValue(item[displayNameField]);
|
|
283
|
+
const identifier = this.getFirstValue(item[identifierField]);
|
|
284
|
+
return {
|
|
285
|
+
dn: item.dn,
|
|
286
|
+
label: displayName || identifier || item.dn,
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// No matching resource found in config
|
|
291
|
+
throw new Error(`No flatResource found in config for branch: ${branch}`);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.error('Failed to load pointer options for branch:', branch, error);
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Cache management methods
|
|
300
|
+
*/
|
|
301
|
+
/**
|
|
302
|
+
* Clear all cache
|
|
303
|
+
*/
|
|
304
|
+
clearCache() {
|
|
305
|
+
this.cache.clear();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Invalidate cache for a specific URL pattern
|
|
309
|
+
*/
|
|
310
|
+
invalidateCache(pattern) {
|
|
311
|
+
this.cache.invalidatePattern(pattern);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get cache statistics
|
|
315
|
+
*/
|
|
316
|
+
getCacheStats() {
|
|
317
|
+
return this.cache.getStats();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get cache instance (for advanced usage)
|
|
321
|
+
*/
|
|
322
|
+
getCache() {
|
|
323
|
+
return this.cache;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* LDAP User Editor - Organization Tree Component
|
|
329
|
+
* Shows only LDAP organization tree (filters out users/groups)
|
|
330
|
+
*/
|
|
331
|
+
class UserTree {
|
|
332
|
+
constructor(container, baseUrl, onSelectOrg) {
|
|
333
|
+
this.expandedNodes = new Set();
|
|
334
|
+
this.selectedDn = null;
|
|
335
|
+
this.rootDn = null;
|
|
336
|
+
this.container = container;
|
|
337
|
+
this.baseUrl = baseUrl;
|
|
338
|
+
this.onSelectOrg = onSelectOrg;
|
|
339
|
+
this.cache = new CacheManager();
|
|
340
|
+
// Clean expired entries every 5 minutes
|
|
341
|
+
window.setInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Fetch with cache support
|
|
345
|
+
*/
|
|
346
|
+
async cachedFetch(url) {
|
|
347
|
+
const cached = this.cache.get(url);
|
|
348
|
+
if (cached !== null) {
|
|
349
|
+
return cached;
|
|
350
|
+
}
|
|
351
|
+
const res = await fetch(url);
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
354
|
+
}
|
|
355
|
+
const data = (await res.json());
|
|
356
|
+
this.cache.set(url, data);
|
|
357
|
+
return data;
|
|
358
|
+
}
|
|
359
|
+
async init() {
|
|
360
|
+
this.container.innerHTML = `
|
|
361
|
+
<div class="sidebar">
|
|
362
|
+
<div class="sidebar-header">
|
|
363
|
+
<h2>
|
|
364
|
+
<span class="material-icons">account_tree</span>
|
|
365
|
+
Organizations
|
|
366
|
+
</h2>
|
|
367
|
+
</div>
|
|
368
|
+
<div id="org-tree-viewer" class="user-tree">
|
|
369
|
+
<div class="loading"><div class="spinner"></div></div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
`;
|
|
373
|
+
try {
|
|
374
|
+
// Load top organization
|
|
375
|
+
const topOrg = await this.cachedFetch(`${this.baseUrl}/api/v1/ldap/organizations/top`);
|
|
376
|
+
this.rootDn = topOrg.dn;
|
|
377
|
+
await this.renderTree();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
console.error('Failed to init tree:', error);
|
|
381
|
+
const treeEl = this.container.querySelector('#org-tree-viewer');
|
|
382
|
+
if (treeEl) {
|
|
383
|
+
treeEl.innerHTML =
|
|
384
|
+
'<div class="empty-state"><span class="material-icons">error</span><p>Failed to load organization tree</p></div>';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async renderTree() {
|
|
389
|
+
const treeEl = this.container.querySelector('#org-tree-viewer');
|
|
390
|
+
if (!treeEl || !this.rootDn)
|
|
391
|
+
return;
|
|
392
|
+
try {
|
|
393
|
+
const html = await this.renderNode(this.rootDn, 0);
|
|
394
|
+
treeEl.innerHTML = html;
|
|
395
|
+
this.attachEventListeners();
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.error('Failed to render tree:', error);
|
|
399
|
+
treeEl.innerHTML =
|
|
400
|
+
'<div class="empty-state"><span class="material-icons">error</span><p>Error rendering tree</p></div>';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async renderNode(dn, level) {
|
|
404
|
+
const isExpanded = this.expandedNodes.has(dn);
|
|
405
|
+
const isSelected = this.selectedDn === dn;
|
|
406
|
+
const indent = level * 20;
|
|
407
|
+
// Get node info
|
|
408
|
+
const node = await this.cachedFetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
|
|
409
|
+
// Extract display name - handle both string and array
|
|
410
|
+
let displayName = dn;
|
|
411
|
+
if (node.ou) {
|
|
412
|
+
displayName = Array.isArray(node.ou) ? node.ou[0] : node.ou;
|
|
413
|
+
}
|
|
414
|
+
else if (node.cn) {
|
|
415
|
+
displayName = Array.isArray(node.cn) ? node.cn[0] : node.cn;
|
|
416
|
+
}
|
|
417
|
+
let html = `
|
|
418
|
+
<div class="tree-node ${isSelected ? 'active' : ''}" style="padding-left: ${indent}px">
|
|
419
|
+
<span class="material-icons tree-node-toggle" data-dn="${dn}">
|
|
420
|
+
${isExpanded ? 'expand_more' : 'chevron_right'}
|
|
421
|
+
</span>
|
|
422
|
+
<span class="material-icons">business</span>
|
|
423
|
+
<span class="tree-node-label" data-dn="${dn}">${displayName}</span>
|
|
424
|
+
</div>
|
|
425
|
+
`;
|
|
426
|
+
if (isExpanded) {
|
|
427
|
+
try {
|
|
428
|
+
// Load subnodes and filter only organizations
|
|
429
|
+
const subnodes = await this.cachedFetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes`);
|
|
430
|
+
const orgs = subnodes.filter((n) => {
|
|
431
|
+
const classes = Array.isArray(n.objectClass)
|
|
432
|
+
? n.objectClass
|
|
433
|
+
: n.objectClass
|
|
434
|
+
? [n.objectClass]
|
|
435
|
+
: [];
|
|
436
|
+
return (classes.includes('organizationalUnit') ||
|
|
437
|
+
classes.includes('organization'));
|
|
438
|
+
});
|
|
439
|
+
for (const subnode of orgs) {
|
|
440
|
+
html += await this.renderNode(subnode.dn, level + 1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
console.error('Failed to load subnodes for', dn, error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return html;
|
|
448
|
+
}
|
|
449
|
+
attachEventListeners() {
|
|
450
|
+
// Toggle expand/collapse
|
|
451
|
+
this.container.querySelectorAll('.tree-node-toggle').forEach(el => {
|
|
452
|
+
el.addEventListener('click', async (e) => {
|
|
453
|
+
e.stopPropagation();
|
|
454
|
+
const dn = el.getAttribute('data-dn');
|
|
455
|
+
if (!dn)
|
|
456
|
+
return;
|
|
457
|
+
if (this.expandedNodes.has(dn)) {
|
|
458
|
+
this.expandedNodes.delete(dn);
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
this.expandedNodes.add(dn);
|
|
462
|
+
}
|
|
463
|
+
await this.renderTree();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
// Select organization
|
|
467
|
+
this.container.querySelectorAll('.tree-node-label').forEach(el => {
|
|
468
|
+
el.addEventListener('click', e => {
|
|
469
|
+
e.stopPropagation();
|
|
470
|
+
const dn = el.getAttribute('data-dn');
|
|
471
|
+
if (!dn)
|
|
472
|
+
return;
|
|
473
|
+
this.selectedDn = dn;
|
|
474
|
+
this.renderTree();
|
|
475
|
+
this.onSelectOrg(dn);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
async refresh() {
|
|
480
|
+
await this.renderTree();
|
|
481
|
+
}
|
|
482
|
+
destroy() {
|
|
483
|
+
this.container.innerHTML = '';
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* LDAP User Editor - Pointer Field Component
|
|
489
|
+
*/
|
|
490
|
+
class PointerField {
|
|
491
|
+
constructor(api, fieldName, attribute, value, onChange) {
|
|
492
|
+
this.options = [];
|
|
493
|
+
this.api = api;
|
|
494
|
+
this.fieldName = fieldName;
|
|
495
|
+
this.attribute = attribute;
|
|
496
|
+
this.value = value;
|
|
497
|
+
this.onChange = onChange;
|
|
498
|
+
}
|
|
499
|
+
async init() {
|
|
500
|
+
await this.loadOptions();
|
|
501
|
+
}
|
|
502
|
+
async loadOptions() {
|
|
503
|
+
try {
|
|
504
|
+
const branch = this.attribute.type === 'array'
|
|
505
|
+
? this.attribute.items?.branch?.[0]
|
|
506
|
+
: this.attribute.branch?.[0];
|
|
507
|
+
if (!branch) {
|
|
508
|
+
this.options = [];
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
this.options = await this.api.getPointerOptions(branch);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
console.error('Failed to load pointer options:', error);
|
|
515
|
+
this.options = [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
render() {
|
|
519
|
+
const isArray = this.attribute.type === 'array';
|
|
520
|
+
const isRequired = this.attribute.required || false;
|
|
521
|
+
if (isArray) {
|
|
522
|
+
return this.renderArray(isRequired);
|
|
523
|
+
}
|
|
524
|
+
return this.renderSingle(isRequired);
|
|
525
|
+
}
|
|
526
|
+
renderSingle(required) {
|
|
527
|
+
const currentValue = Array.isArray(this.value)
|
|
528
|
+
? this.value[0] || ''
|
|
529
|
+
: this.value || '';
|
|
530
|
+
return `
|
|
531
|
+
<div class="form-group">
|
|
532
|
+
<label class="form-label">
|
|
533
|
+
${this.fieldName}
|
|
534
|
+
${required ? '<span class="required">*</span>' : ''}
|
|
535
|
+
</label>
|
|
536
|
+
<select
|
|
537
|
+
class="form-select pointer-field-select"
|
|
538
|
+
data-field="${this.fieldName}"
|
|
539
|
+
${required ? 'required' : ''}
|
|
540
|
+
>
|
|
541
|
+
<option value="">Select...</option>
|
|
542
|
+
${this.options
|
|
543
|
+
.map(opt => `
|
|
544
|
+
<option value="${opt.dn}" ${currentValue === opt.dn ? 'selected' : ''}>
|
|
545
|
+
${opt.label}
|
|
546
|
+
</option>
|
|
547
|
+
`)
|
|
548
|
+
.join('')}
|
|
549
|
+
</select>
|
|
550
|
+
</div>
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
renderArray(required) {
|
|
554
|
+
const values = Array.isArray(this.value) ? this.value : [];
|
|
555
|
+
return `
|
|
556
|
+
<div class="form-group">
|
|
557
|
+
<label class="form-label">
|
|
558
|
+
${this.fieldName}
|
|
559
|
+
${required ? '<span class="required">*</span>' : ''}
|
|
560
|
+
</label>
|
|
561
|
+
<div class="array-field" data-field="${this.fieldName}">
|
|
562
|
+
${values
|
|
563
|
+
.map((val, idx) => `
|
|
564
|
+
<div class="array-item" data-index="${idx}">
|
|
565
|
+
<select class="form-select pointer-array-item">
|
|
566
|
+
<option value="">Select...</option>
|
|
567
|
+
${this.options
|
|
568
|
+
.map(opt => `
|
|
569
|
+
<option value="${opt.dn}" ${val === opt.dn ? 'selected' : ''}>
|
|
570
|
+
${opt.label}
|
|
571
|
+
</option>
|
|
572
|
+
`)
|
|
573
|
+
.join('')}
|
|
574
|
+
</select>
|
|
575
|
+
<button type="button" class="btn btn-secondary btn-icon remove-array-item">
|
|
576
|
+
<span class="material-icons">remove</span>
|
|
577
|
+
</button>
|
|
578
|
+
</div>
|
|
579
|
+
`)
|
|
580
|
+
.join('')}
|
|
581
|
+
<button type="button" class="btn btn-secondary add-array-item">
|
|
582
|
+
<span class="material-icons">add</span>
|
|
583
|
+
Add ${this.fieldName}
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
attachHandlers(container) {
|
|
590
|
+
if (this.attribute.type === 'array') {
|
|
591
|
+
this.attachArrayHandlers(container);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
this.attachSingleHandlers(container);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
attachSingleHandlers(container) {
|
|
598
|
+
const select = container.querySelector(`select[data-field="${this.fieldName}"]`);
|
|
599
|
+
select?.addEventListener('change', () => {
|
|
600
|
+
this.onChange(select.value);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
attachArrayHandlers(container) {
|
|
604
|
+
const arrayContainer = container.querySelector(`.array-field[data-field="${this.fieldName}"]`);
|
|
605
|
+
if (!arrayContainer)
|
|
606
|
+
return;
|
|
607
|
+
// Handle item change
|
|
608
|
+
arrayContainer
|
|
609
|
+
.querySelectorAll('.pointer-array-item')
|
|
610
|
+
.forEach((select, idx) => {
|
|
611
|
+
select.addEventListener('change', () => {
|
|
612
|
+
const values = Array.isArray(this.value) ? [...this.value] : [];
|
|
613
|
+
values[idx] = select.value;
|
|
614
|
+
this.onChange(values);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
// Handle remove
|
|
618
|
+
arrayContainer
|
|
619
|
+
.querySelectorAll('.remove-array-item')
|
|
620
|
+
.forEach((btn, idx) => {
|
|
621
|
+
btn.addEventListener('click', () => {
|
|
622
|
+
const values = Array.isArray(this.value) ? [...this.value] : [];
|
|
623
|
+
values.splice(idx, 1);
|
|
624
|
+
this.onChange(values);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
// Handle add
|
|
628
|
+
const addBtn = arrayContainer.querySelector('.add-array-item');
|
|
629
|
+
addBtn?.addEventListener('click', () => {
|
|
630
|
+
const values = Array.isArray(this.value) ? [...this.value] : [];
|
|
631
|
+
values.push('');
|
|
632
|
+
this.onChange(values);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* LDAP User Editor - Move User Modal Component
|
|
639
|
+
* Modal dialog for selecting target organization when moving a user
|
|
640
|
+
*/
|
|
641
|
+
class MoveUserModal {
|
|
642
|
+
constructor(api, currentOrgDn, onMove) {
|
|
643
|
+
this.modal = null;
|
|
644
|
+
this.selectedOrgDn = null;
|
|
645
|
+
this.expandedNodes = new Set();
|
|
646
|
+
this.rootDn = null;
|
|
647
|
+
this.api = api;
|
|
648
|
+
this.currentOrgDn = currentOrgDn;
|
|
649
|
+
this.onMove = onMove;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Escape HTML special characters to prevent XSS attacks
|
|
653
|
+
*/
|
|
654
|
+
static escapeHtml(text) {
|
|
655
|
+
if (text == null)
|
|
656
|
+
return '';
|
|
657
|
+
const map = {
|
|
658
|
+
'&': '&',
|
|
659
|
+
'<': '<',
|
|
660
|
+
'>': '>',
|
|
661
|
+
'"': '"',
|
|
662
|
+
"'": ''',
|
|
663
|
+
'/': '/',
|
|
664
|
+
};
|
|
665
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
666
|
+
}
|
|
667
|
+
async show() {
|
|
668
|
+
// Create modal container
|
|
669
|
+
this.modal = document.createElement('div');
|
|
670
|
+
this.modal.className = 'modal-overlay active';
|
|
671
|
+
this.modal.innerHTML = `
|
|
672
|
+
<div class="modal-container" style="max-width: 600px;">
|
|
673
|
+
<div class="modal-header">
|
|
674
|
+
<h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
|
|
675
|
+
<span class="material-icons">folder_open</span>
|
|
676
|
+
Move User to Organization
|
|
677
|
+
</h3>
|
|
678
|
+
<button class="modal-close" id="modal-close-btn">
|
|
679
|
+
<span class="material-icons">close</span>
|
|
680
|
+
</button>
|
|
681
|
+
</div>
|
|
682
|
+
<div class="modal-body">
|
|
683
|
+
<div id="move-modal-alert"></div>
|
|
684
|
+
<div style="margin-bottom: 1rem; color: #666; font-size: 0.9rem;">
|
|
685
|
+
Select the target organization where you want to move this user.
|
|
686
|
+
</div>
|
|
687
|
+
<div id="organization-tree-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 0.5rem;">
|
|
688
|
+
<div class="loading">
|
|
689
|
+
<div class="spinner"></div>
|
|
690
|
+
<p style="margin-top: 1rem">Loading organizations...</p>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="modal-footer">
|
|
695
|
+
<button type="button" class="btn btn-secondary" id="modal-cancel-btn">
|
|
696
|
+
Cancel
|
|
697
|
+
</button>
|
|
698
|
+
<button type="button" class="btn btn-primary" id="modal-move-btn" disabled>
|
|
699
|
+
<span class="material-icons">drive_file_move</span>
|
|
700
|
+
Move User
|
|
701
|
+
</button>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
`;
|
|
705
|
+
document.body.appendChild(this.modal);
|
|
706
|
+
this.attachHandlers();
|
|
707
|
+
await this.loadOrganizationTree();
|
|
708
|
+
}
|
|
709
|
+
attachHandlers() {
|
|
710
|
+
if (!this.modal)
|
|
711
|
+
return;
|
|
712
|
+
// Close button
|
|
713
|
+
const closeBtn = this.modal.querySelector('#modal-close-btn');
|
|
714
|
+
closeBtn?.addEventListener('click', () => this.close());
|
|
715
|
+
// Cancel button
|
|
716
|
+
const cancelBtn = this.modal.querySelector('#modal-cancel-btn');
|
|
717
|
+
cancelBtn?.addEventListener('click', () => this.close());
|
|
718
|
+
// Move button
|
|
719
|
+
const moveBtn = this.modal.querySelector('#modal-move-btn');
|
|
720
|
+
moveBtn?.addEventListener('click', () => this.handleMove());
|
|
721
|
+
// Close on overlay click
|
|
722
|
+
this.modal.addEventListener('click', e => {
|
|
723
|
+
if (e.target === this.modal) {
|
|
724
|
+
this.close();
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
// Close on Escape key
|
|
728
|
+
const handleEscape = (e) => {
|
|
729
|
+
if (e.key === 'Escape') {
|
|
730
|
+
this.close();
|
|
731
|
+
document.removeEventListener('keydown', handleEscape);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
document.addEventListener('keydown', handleEscape);
|
|
735
|
+
}
|
|
736
|
+
async loadOrganizationTree() {
|
|
737
|
+
console.log('loadOrganizationTree called');
|
|
738
|
+
const container = this.modal?.querySelector('#organization-tree-container');
|
|
739
|
+
if (!container) {
|
|
740
|
+
console.log('Container not found');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
console.log('Fetching top organization...');
|
|
745
|
+
// Get top organization
|
|
746
|
+
const topOrg = await this.api['cachedFetch'](`${this.api['baseUrl'] || window.location.origin}/api/v1/ldap/organizations/top`);
|
|
747
|
+
console.log('Top org:', topOrg);
|
|
748
|
+
this.rootDn = topOrg.dn;
|
|
749
|
+
// Expand root by default
|
|
750
|
+
this.expandedNodes.add(this.rootDn);
|
|
751
|
+
console.log('Calling renderTree...');
|
|
752
|
+
await this.renderTree();
|
|
753
|
+
console.log('renderTree completed');
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
console.error('Error in loadOrganizationTree:', error);
|
|
757
|
+
container.innerHTML = `
|
|
758
|
+
<div class="alert alert-error">
|
|
759
|
+
<span class="material-icons">error</span>
|
|
760
|
+
<div>Failed to load organizations: ${MoveUserModal.escapeHtml(error.message)}</div>
|
|
761
|
+
</div>
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async renderTree() {
|
|
766
|
+
console.log('renderTree called');
|
|
767
|
+
const container = this.modal?.querySelector('#organization-tree-container');
|
|
768
|
+
if (!container || !this.rootDn) {
|
|
769
|
+
console.log('renderTree: no container or rootDn', {
|
|
770
|
+
container,
|
|
771
|
+
rootDn: this.rootDn,
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
console.log('renderTree: calling renderNode for', this.rootDn);
|
|
777
|
+
const html = await this.renderNode(this.rootDn, 0);
|
|
778
|
+
console.log('renderTree: got html, length:', html.length);
|
|
779
|
+
container.innerHTML = html;
|
|
780
|
+
console.log('renderTree: html set, attaching handlers');
|
|
781
|
+
this.attachTreeHandlers();
|
|
782
|
+
console.log('renderTree: handlers attached');
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
console.error('Error in renderTree:', error);
|
|
786
|
+
container.innerHTML = `
|
|
787
|
+
<div class="alert alert-error">
|
|
788
|
+
<span class="material-icons">error</span>
|
|
789
|
+
<div>Error rendering tree: ${MoveUserModal.escapeHtml(error.message)}</div>
|
|
790
|
+
</div>
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async renderNode(dn, level) {
|
|
795
|
+
console.log('renderNode called for', dn, 'level', level);
|
|
796
|
+
const isExpanded = this.expandedNodes.has(dn);
|
|
797
|
+
const isCurrentOrg = dn === this.currentOrgDn;
|
|
798
|
+
const isSelected = dn === this.selectedOrgDn;
|
|
799
|
+
const indent = level * 20;
|
|
800
|
+
// Get node info
|
|
801
|
+
const baseUrl = this.api['baseUrl'] || window.location.origin;
|
|
802
|
+
console.log('Fetching node details from:', `${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
|
|
803
|
+
const node = await this.api['cachedFetch'](`${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
|
|
804
|
+
console.log('Got node:', node);
|
|
805
|
+
// Extract display name
|
|
806
|
+
const ou = Array.isArray(node.ou) ? node.ou[0] : node.ou;
|
|
807
|
+
const path = node.twakeDepartmentPath
|
|
808
|
+
? Array.isArray(node.twakeDepartmentPath)
|
|
809
|
+
? node.twakeDepartmentPath[0]
|
|
810
|
+
: node.twakeDepartmentPath
|
|
811
|
+
: undefined;
|
|
812
|
+
const displayName = path || ou || dn;
|
|
813
|
+
let html = `
|
|
814
|
+
<div
|
|
815
|
+
class="org-node ${isCurrentOrg ? 'org-current' : ''} ${isSelected ? 'org-selected' : ''}"
|
|
816
|
+
data-org-dn="${MoveUserModal.escapeHtml(dn)}"
|
|
817
|
+
style="
|
|
818
|
+
padding: 0.5rem;
|
|
819
|
+
padding-left: ${indent + 8}px;
|
|
820
|
+
cursor: ${isCurrentOrg ? 'not-allowed' : 'pointer'};
|
|
821
|
+
background: ${isCurrentOrg ? '#f5f5f5' : isSelected ? '#e3f2fd' : 'white'};
|
|
822
|
+
opacity: ${isCurrentOrg ? '0.6' : '1'};
|
|
823
|
+
display: flex;
|
|
824
|
+
align-items: center;
|
|
825
|
+
gap: 0.5rem;
|
|
826
|
+
border-left: 2px solid ${isSelected ? '#2196f3' : 'transparent'};
|
|
827
|
+
"
|
|
828
|
+
>
|
|
829
|
+
<span class="material-icons tree-toggle" data-dn="${MoveUserModal.escapeHtml(dn)}" style="cursor: pointer; font-size: 1.2rem;">
|
|
830
|
+
${isExpanded ? 'expand_more' : 'chevron_right'}
|
|
831
|
+
</span>
|
|
832
|
+
<span class="material-icons" style="font-size: 1.2rem; color: ${isCurrentOrg ? '#999' : '#2196f3'};">
|
|
833
|
+
${isCurrentOrg ? 'business' : 'folder'}
|
|
834
|
+
</span>
|
|
835
|
+
<span style="flex: 1; font-weight: ${isSelected ? '600' : '500'};">
|
|
836
|
+
${MoveUserModal.escapeHtml(displayName)}
|
|
837
|
+
${isCurrentOrg ? '<span style="font-size: 0.85rem; color: #666; margin-left: 0.5rem;">(current)</span>' : ''}
|
|
838
|
+
</span>
|
|
839
|
+
${isSelected ? '<span class="material-icons" style="color: #2196f3;">check_circle</span>' : ''}
|
|
840
|
+
</div>
|
|
841
|
+
`;
|
|
842
|
+
// If expanded, load and render subnodes
|
|
843
|
+
if (isExpanded) {
|
|
844
|
+
try {
|
|
845
|
+
const subnodes = await this.api['cachedFetch'](`${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes?objectClass=organizationalUnit`);
|
|
846
|
+
for (const subnode of subnodes) {
|
|
847
|
+
html += await this.renderNode(subnode.dn, level + 1);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
// No subnodes or error loading them
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return html;
|
|
855
|
+
}
|
|
856
|
+
attachTreeHandlers() {
|
|
857
|
+
// Handle tree toggle (expand/collapse)
|
|
858
|
+
const toggles = this.modal?.querySelectorAll('.tree-toggle');
|
|
859
|
+
toggles?.forEach(toggle => {
|
|
860
|
+
toggle.addEventListener('click', async (e) => {
|
|
861
|
+
e.stopPropagation();
|
|
862
|
+
const dn = toggle.getAttribute('data-dn');
|
|
863
|
+
if (!dn)
|
|
864
|
+
return;
|
|
865
|
+
if (this.expandedNodes.has(dn)) {
|
|
866
|
+
this.expandedNodes.delete(dn);
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
this.expandedNodes.add(dn);
|
|
870
|
+
}
|
|
871
|
+
await this.renderTree();
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
// Handle node selection
|
|
875
|
+
const nodes = this.modal?.querySelectorAll('.org-node');
|
|
876
|
+
nodes?.forEach(node => {
|
|
877
|
+
const orgDn = node.getAttribute('data-org-dn');
|
|
878
|
+
if (!orgDn || orgDn === this.currentOrgDn)
|
|
879
|
+
return;
|
|
880
|
+
node.addEventListener('click', () => {
|
|
881
|
+
this.selectOrganization(orgDn);
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
async selectOrganization(orgDn) {
|
|
886
|
+
this.selectedOrgDn = orgDn;
|
|
887
|
+
// Re-render tree to update selection state
|
|
888
|
+
await this.renderTree();
|
|
889
|
+
// Enable move button
|
|
890
|
+
const moveBtn = this.modal?.querySelector('#modal-move-btn');
|
|
891
|
+
if (moveBtn) {
|
|
892
|
+
moveBtn.disabled = false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async handleMove() {
|
|
896
|
+
if (!this.selectedOrgDn)
|
|
897
|
+
return;
|
|
898
|
+
const moveBtn = this.modal?.querySelector('#modal-move-btn');
|
|
899
|
+
const alertContainer = this.modal?.querySelector('#move-modal-alert');
|
|
900
|
+
if (moveBtn) {
|
|
901
|
+
moveBtn.disabled = true;
|
|
902
|
+
moveBtn.innerHTML = `
|
|
903
|
+
<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px;"></span>
|
|
904
|
+
Moving...
|
|
905
|
+
`;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
this.onMove(this.selectedOrgDn);
|
|
909
|
+
this.close();
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
if (alertContainer) {
|
|
913
|
+
alertContainer.innerHTML = `
|
|
914
|
+
<div class="alert alert-error" style="margin-bottom: 1rem;">
|
|
915
|
+
<span class="material-icons">error</span>
|
|
916
|
+
<div>Move failed: ${MoveUserModal.escapeHtml(error.message)}</div>
|
|
917
|
+
</div>
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
if (moveBtn) {
|
|
921
|
+
moveBtn.disabled = false;
|
|
922
|
+
moveBtn.innerHTML = `
|
|
923
|
+
<span class="material-icons">drive_file_move</span>
|
|
924
|
+
Move User
|
|
925
|
+
`;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
close() {
|
|
930
|
+
if (this.modal) {
|
|
931
|
+
this.modal.remove();
|
|
932
|
+
this.modal = null;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* LDAP User Editor - User Editor Component
|
|
939
|
+
*/
|
|
940
|
+
class UserEditor {
|
|
941
|
+
/**
|
|
942
|
+
* Escape HTML special characters to prevent XSS attacks
|
|
943
|
+
* Reference: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html
|
|
944
|
+
*/
|
|
945
|
+
static escapeHtml(text) {
|
|
946
|
+
if (text == null)
|
|
947
|
+
return '';
|
|
948
|
+
const map = {
|
|
949
|
+
'&': '&',
|
|
950
|
+
'<': '<',
|
|
951
|
+
'>': '>',
|
|
952
|
+
'"': '"',
|
|
953
|
+
"'": ''',
|
|
954
|
+
'/': '/',
|
|
955
|
+
};
|
|
956
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
957
|
+
}
|
|
958
|
+
constructor(container, api, userDn, onSaved, onDeleted) {
|
|
959
|
+
this.user = null;
|
|
960
|
+
this.schema = null;
|
|
961
|
+
this.formData = {};
|
|
962
|
+
this.pointerFields = new Map();
|
|
963
|
+
this.ldapBase = '';
|
|
964
|
+
this.container = container;
|
|
965
|
+
this.api = api;
|
|
966
|
+
this.userDn = userDn;
|
|
967
|
+
this.onSaved = onSaved;
|
|
968
|
+
this.onDeleted = onDeleted;
|
|
969
|
+
}
|
|
970
|
+
async init() {
|
|
971
|
+
try {
|
|
972
|
+
this.showLoading();
|
|
973
|
+
await this.loadUserAndSchema();
|
|
974
|
+
this.render();
|
|
975
|
+
await this.initPointerFields();
|
|
976
|
+
this.attachHandlers();
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
this.showError(error.message);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
showLoading() {
|
|
983
|
+
this.container.innerHTML = `
|
|
984
|
+
<div class="loading">
|
|
985
|
+
<div class="spinner"></div>
|
|
986
|
+
<p style="margin-top: 1rem">Loading user data...</p>
|
|
987
|
+
</div>
|
|
988
|
+
`;
|
|
989
|
+
}
|
|
990
|
+
showError(message) {
|
|
991
|
+
this.container.innerHTML = `
|
|
992
|
+
<div class="alert alert-error">
|
|
993
|
+
<span class="material-icons">error</span>
|
|
994
|
+
<div>${UserEditor.escapeHtml(message)}</div>
|
|
995
|
+
</div>
|
|
996
|
+
`;
|
|
997
|
+
}
|
|
998
|
+
async loadUserAndSchema() {
|
|
999
|
+
const config = await this.api.getConfig();
|
|
1000
|
+
this.ldapBase = config.ldapBase;
|
|
1001
|
+
// Find users resource in flatResources array
|
|
1002
|
+
const usersResource = config.features?.ldapFlatGeneric?.flatResources?.find(r => r.pluralName === 'users' || r.name === 'users');
|
|
1003
|
+
const schemaUrl = usersResource?.schemaUrl;
|
|
1004
|
+
if (!schemaUrl) {
|
|
1005
|
+
throw new Error('Users schema not configured');
|
|
1006
|
+
}
|
|
1007
|
+
[this.schema, this.user] = await Promise.all([
|
|
1008
|
+
this.api.getSchema(schemaUrl),
|
|
1009
|
+
this.api.getUser(this.userDn),
|
|
1010
|
+
]);
|
|
1011
|
+
// Replace __LDAP_BASE__ placeholder in schema branches
|
|
1012
|
+
if (this.schema) {
|
|
1013
|
+
this.replacePlaceholders(this.schema);
|
|
1014
|
+
}
|
|
1015
|
+
this.formData = { ...this.user };
|
|
1016
|
+
}
|
|
1017
|
+
replacePlaceholders(schema) {
|
|
1018
|
+
for (const attr of Object.values(schema.attributes)) {
|
|
1019
|
+
if (attr.branch) {
|
|
1020
|
+
attr.branch = attr.branch.map(b => b.replace(/__LDAP_BASE__/g, this.ldapBase));
|
|
1021
|
+
}
|
|
1022
|
+
if (attr.items?.branch) {
|
|
1023
|
+
attr.items.branch = attr.items.branch.map(b => b.replace(/__LDAP_BASE__/g, this.ldapBase));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
getFirstValue(value) {
|
|
1028
|
+
if (!value)
|
|
1029
|
+
return '';
|
|
1030
|
+
if (Array.isArray(value))
|
|
1031
|
+
return value[0] || '';
|
|
1032
|
+
return String(value);
|
|
1033
|
+
}
|
|
1034
|
+
getFieldByRole(role) {
|
|
1035
|
+
if (!this.schema || !this.user)
|
|
1036
|
+
return '';
|
|
1037
|
+
const field = Object.entries(this.schema.attributes).find(([, attr]) => attr.role === role);
|
|
1038
|
+
if (!field)
|
|
1039
|
+
return '';
|
|
1040
|
+
const [fieldName] = field;
|
|
1041
|
+
return this.getFirstValue(this.user[fieldName]);
|
|
1042
|
+
}
|
|
1043
|
+
render() {
|
|
1044
|
+
if (!this.user || !this.schema)
|
|
1045
|
+
return;
|
|
1046
|
+
const displayName = this.getFieldByRole('displayName');
|
|
1047
|
+
const identifier = this.getFirstValue(this.user[this.schema.entity.mainAttribute]);
|
|
1048
|
+
const userName = displayName || identifier || 'User';
|
|
1049
|
+
this.container.innerHTML = `
|
|
1050
|
+
<div class="editor-container">
|
|
1051
|
+
<div class="editor-header">
|
|
1052
|
+
<span class="material-icons">person</span>
|
|
1053
|
+
<div class="editor-title">
|
|
1054
|
+
<h3>${UserEditor.escapeHtml(userName)}</h3>
|
|
1055
|
+
<div class="dn">${UserEditor.escapeHtml(this.userDn)}</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
|
|
1059
|
+
<div id="alert-container"></div>
|
|
1060
|
+
|
|
1061
|
+
<form id="user-edit-form" class="editor-form">
|
|
1062
|
+
${this.renderFormSections()}
|
|
1063
|
+
|
|
1064
|
+
<div class="editor-actions" style="margin-top: 2rem; display: flex; justify-content: space-between; align-items: center;">
|
|
1065
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
1066
|
+
<button type="button" class="btn btn-secondary" id="move-user-btn">
|
|
1067
|
+
<span class="material-icons">drive_file_move</span>
|
|
1068
|
+
Move User
|
|
1069
|
+
</button>
|
|
1070
|
+
<button type="button" class="btn btn-secondary" id="delete-user-btn" style="background: #d32f2f; color: white;">
|
|
1071
|
+
<span class="material-icons">person_remove</span>
|
|
1072
|
+
Delete User
|
|
1073
|
+
</button>
|
|
1074
|
+
</div>
|
|
1075
|
+
<button type="submit" class="btn btn-primary">
|
|
1076
|
+
<span class="material-icons">save</span>
|
|
1077
|
+
Save Changes
|
|
1078
|
+
</button>
|
|
1079
|
+
</div>
|
|
1080
|
+
</form>
|
|
1081
|
+
</div>
|
|
1082
|
+
`;
|
|
1083
|
+
}
|
|
1084
|
+
renderFormSections() {
|
|
1085
|
+
if (!this.schema)
|
|
1086
|
+
return '';
|
|
1087
|
+
// Group fields by the 'group' attribute from schema
|
|
1088
|
+
const groupedFields = {};
|
|
1089
|
+
const ungroupedFields = [];
|
|
1090
|
+
for (const [fieldName, attr] of Object.entries(this.schema.attributes)) {
|
|
1091
|
+
// Skip objectClass (it's fixed)
|
|
1092
|
+
if (fieldName === 'objectClass')
|
|
1093
|
+
continue;
|
|
1094
|
+
if (attr.group) {
|
|
1095
|
+
if (!groupedFields[attr.group]) {
|
|
1096
|
+
groupedFields[attr.group] = [];
|
|
1097
|
+
}
|
|
1098
|
+
groupedFields[attr.group].push(fieldName);
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
ungroupedFields.push(fieldName);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// Render grouped sections
|
|
1105
|
+
let html = Object.entries(groupedFields)
|
|
1106
|
+
.map(([title, fields]) => `
|
|
1107
|
+
<div class="form-section">
|
|
1108
|
+
<div class="form-section-title">${UserEditor.escapeHtml(title)}</div>
|
|
1109
|
+
<div class="form-row">
|
|
1110
|
+
${fields
|
|
1111
|
+
.map(field => this.renderField(field))
|
|
1112
|
+
.filter(f => f)
|
|
1113
|
+
.join('')}
|
|
1114
|
+
</div>
|
|
1115
|
+
</div>
|
|
1116
|
+
`)
|
|
1117
|
+
.join('');
|
|
1118
|
+
// Render ungrouped fields in a default section if any
|
|
1119
|
+
if (ungroupedFields.length > 0) {
|
|
1120
|
+
html += `
|
|
1121
|
+
<div class="form-section">
|
|
1122
|
+
<div class="form-section-title">Other Fields</div>
|
|
1123
|
+
<div class="form-row">
|
|
1124
|
+
${ungroupedFields
|
|
1125
|
+
.map(field => this.renderField(field))
|
|
1126
|
+
.filter(f => f)
|
|
1127
|
+
.join('')}
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
`;
|
|
1131
|
+
}
|
|
1132
|
+
return html;
|
|
1133
|
+
}
|
|
1134
|
+
renderField(fieldName) {
|
|
1135
|
+
const attr = this.schema?.attributes[fieldName];
|
|
1136
|
+
if (!attr)
|
|
1137
|
+
return '';
|
|
1138
|
+
// Skip fixed fields - they cannot be edited
|
|
1139
|
+
if (attr.fixed) {
|
|
1140
|
+
return '';
|
|
1141
|
+
}
|
|
1142
|
+
// Hide organizationLink and organizationPath fields - use Move button instead
|
|
1143
|
+
if (attr.role === 'organizationLink' || attr.role === 'organizationPath') {
|
|
1144
|
+
return '';
|
|
1145
|
+
}
|
|
1146
|
+
// Show identifier fields as disabled
|
|
1147
|
+
if (attr.role === 'identifier') {
|
|
1148
|
+
return this.renderIdentifierField(fieldName);
|
|
1149
|
+
}
|
|
1150
|
+
// Pointer fields
|
|
1151
|
+
if (attr.type === 'pointer' || attr.items?.type === 'pointer') {
|
|
1152
|
+
return `<div id="pointer-${UserEditor.escapeHtml(fieldName)}"></div>`;
|
|
1153
|
+
}
|
|
1154
|
+
// Array fields
|
|
1155
|
+
if (attr.type === 'array') {
|
|
1156
|
+
return this.renderArrayField(fieldName, attr);
|
|
1157
|
+
}
|
|
1158
|
+
// Number fields
|
|
1159
|
+
if (attr.type === 'number' || attr.type === 'integer') {
|
|
1160
|
+
return this.renderNumberField(fieldName, attr);
|
|
1161
|
+
}
|
|
1162
|
+
// String fields
|
|
1163
|
+
return this.renderStringField(fieldName, attr);
|
|
1164
|
+
}
|
|
1165
|
+
renderIdentifierField(fieldName) {
|
|
1166
|
+
const value = this.formData[fieldName];
|
|
1167
|
+
const displayValue = Array.isArray(value) ? value[0] : value || '';
|
|
1168
|
+
return `
|
|
1169
|
+
<div class="form-group">
|
|
1170
|
+
<label class="form-label">${UserEditor.escapeHtml(fieldName)}</label>
|
|
1171
|
+
<input
|
|
1172
|
+
type="text"
|
|
1173
|
+
class="form-input"
|
|
1174
|
+
value="${UserEditor.escapeHtml(displayValue)}"
|
|
1175
|
+
disabled
|
|
1176
|
+
/>
|
|
1177
|
+
</div>
|
|
1178
|
+
`;
|
|
1179
|
+
}
|
|
1180
|
+
renderStringField(fieldName, attr) {
|
|
1181
|
+
const value = this.formData[fieldName] || '';
|
|
1182
|
+
const inputType = attr.test?.includes('@')
|
|
1183
|
+
? 'email'
|
|
1184
|
+
: fieldName === 'userPassword'
|
|
1185
|
+
? 'password'
|
|
1186
|
+
: 'text';
|
|
1187
|
+
return `
|
|
1188
|
+
<div class="form-group">
|
|
1189
|
+
<label class="form-label">
|
|
1190
|
+
${UserEditor.escapeHtml(fieldName)}
|
|
1191
|
+
${attr.required ? '<span class="required">*</span>' : ''}
|
|
1192
|
+
</label>
|
|
1193
|
+
<input
|
|
1194
|
+
type="${UserEditor.escapeHtml(inputType)}"
|
|
1195
|
+
class="form-input"
|
|
1196
|
+
name="${UserEditor.escapeHtml(fieldName)}"
|
|
1197
|
+
value="${UserEditor.escapeHtml(value)}"
|
|
1198
|
+
${attr.required ? 'required' : ''}
|
|
1199
|
+
/>
|
|
1200
|
+
</div>
|
|
1201
|
+
`;
|
|
1202
|
+
}
|
|
1203
|
+
renderNumberField(fieldName, attr) {
|
|
1204
|
+
const value = this.formData[fieldName] || '';
|
|
1205
|
+
return `
|
|
1206
|
+
<div class="form-group">
|
|
1207
|
+
<label class="form-label">
|
|
1208
|
+
${UserEditor.escapeHtml(fieldName)}
|
|
1209
|
+
${attr.required ? '<span class="required">*</span>' : ''}
|
|
1210
|
+
</label>
|
|
1211
|
+
<input
|
|
1212
|
+
type="number"
|
|
1213
|
+
class="form-input"
|
|
1214
|
+
name="${UserEditor.escapeHtml(fieldName)}"
|
|
1215
|
+
value="${UserEditor.escapeHtml(value)}"
|
|
1216
|
+
${attr.required ? 'required' : ''}
|
|
1217
|
+
/>
|
|
1218
|
+
</div>
|
|
1219
|
+
`;
|
|
1220
|
+
}
|
|
1221
|
+
renderArrayField(fieldName, attr) {
|
|
1222
|
+
const values = Array.isArray(this.formData[fieldName])
|
|
1223
|
+
? this.formData[fieldName]
|
|
1224
|
+
: [];
|
|
1225
|
+
const inputType = attr.items?.type === 'string' && attr.items?.test?.includes('@')
|
|
1226
|
+
? 'email'
|
|
1227
|
+
: 'text';
|
|
1228
|
+
return `
|
|
1229
|
+
<div class="form-group">
|
|
1230
|
+
<label class="form-label">
|
|
1231
|
+
${UserEditor.escapeHtml(fieldName)}
|
|
1232
|
+
${attr.required ? '<span class="required">*</span>' : ''}
|
|
1233
|
+
</label>
|
|
1234
|
+
<div class="array-field" data-field="${UserEditor.escapeHtml(fieldName)}">
|
|
1235
|
+
${values
|
|
1236
|
+
.map((val, idx) => `
|
|
1237
|
+
<div class="array-item" data-index="${idx}">
|
|
1238
|
+
<input
|
|
1239
|
+
type="${UserEditor.escapeHtml(inputType)}"
|
|
1240
|
+
class="form-input array-string-item"
|
|
1241
|
+
value="${UserEditor.escapeHtml(val)}"
|
|
1242
|
+
/>
|
|
1243
|
+
<button type="button" class="btn btn-secondary btn-icon remove-array-item">
|
|
1244
|
+
<span class="material-icons">remove</span>
|
|
1245
|
+
</button>
|
|
1246
|
+
</div>
|
|
1247
|
+
`)
|
|
1248
|
+
.join('')}
|
|
1249
|
+
<button type="button" class="btn btn-secondary add-array-item" data-field="${UserEditor.escapeHtml(fieldName)}">
|
|
1250
|
+
<span class="material-icons">add</span>
|
|
1251
|
+
Add ${UserEditor.escapeHtml(fieldName)}
|
|
1252
|
+
</button>
|
|
1253
|
+
</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
`;
|
|
1256
|
+
}
|
|
1257
|
+
async initPointerFields() {
|
|
1258
|
+
if (!this.schema)
|
|
1259
|
+
return;
|
|
1260
|
+
const pointerFieldNames = Object.entries(this.schema.attributes)
|
|
1261
|
+
.filter(([, attr]) => (attr.type === 'pointer' || attr.items?.type === 'pointer') &&
|
|
1262
|
+
!attr.fixed)
|
|
1263
|
+
.map(([name]) => name);
|
|
1264
|
+
for (const fieldName of pointerFieldNames) {
|
|
1265
|
+
const attr = this.schema.attributes[fieldName];
|
|
1266
|
+
const value = this.formData[fieldName];
|
|
1267
|
+
const pointerField = new PointerField(this.api, fieldName, attr, value, newValue => {
|
|
1268
|
+
this.formData[fieldName] = newValue;
|
|
1269
|
+
this.rerenderPointerField(fieldName);
|
|
1270
|
+
});
|
|
1271
|
+
await pointerField.init();
|
|
1272
|
+
this.pointerFields.set(fieldName, pointerField);
|
|
1273
|
+
// Render into placeholder
|
|
1274
|
+
const placeholder = this.container.querySelector(`#pointer-${fieldName}`);
|
|
1275
|
+
if (placeholder) {
|
|
1276
|
+
placeholder.innerHTML = pointerField.render();
|
|
1277
|
+
pointerField.attachHandlers(placeholder);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
rerenderPointerField(fieldName) {
|
|
1282
|
+
const pointerField = this.pointerFields.get(fieldName);
|
|
1283
|
+
if (!pointerField)
|
|
1284
|
+
return;
|
|
1285
|
+
const placeholder = this.container.querySelector(`#pointer-${fieldName}`);
|
|
1286
|
+
if (placeholder) {
|
|
1287
|
+
placeholder.innerHTML = pointerField.render();
|
|
1288
|
+
pointerField.attachHandlers(placeholder);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
attachHandlers() {
|
|
1292
|
+
const form = this.container.querySelector('#user-edit-form');
|
|
1293
|
+
form?.addEventListener('submit', e => this.handleSubmit(e));
|
|
1294
|
+
// Move button
|
|
1295
|
+
const moveBtn = this.container.querySelector('#move-user-btn');
|
|
1296
|
+
moveBtn?.addEventListener('click', () => this.handleMove());
|
|
1297
|
+
// Delete button
|
|
1298
|
+
const deleteBtn = this.container.querySelector('#delete-user-btn');
|
|
1299
|
+
deleteBtn?.addEventListener('click', () => this.handleDelete());
|
|
1300
|
+
// String/number inputs
|
|
1301
|
+
this.container
|
|
1302
|
+
.querySelectorAll('input[name]')
|
|
1303
|
+
.forEach(input => {
|
|
1304
|
+
input.addEventListener('input', () => {
|
|
1305
|
+
const fieldName = input.name;
|
|
1306
|
+
const attr = this.schema?.attributes[fieldName];
|
|
1307
|
+
if (attr?.type === 'number' || attr?.type === 'integer') {
|
|
1308
|
+
this.formData[fieldName] = input.value
|
|
1309
|
+
? parseInt(input.value, 10)
|
|
1310
|
+
: '';
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
this.formData[fieldName] = input.value;
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
// Array fields
|
|
1318
|
+
this.attachArrayFieldHandlers();
|
|
1319
|
+
}
|
|
1320
|
+
attachArrayFieldHandlers() {
|
|
1321
|
+
// Handle array item changes
|
|
1322
|
+
this.container
|
|
1323
|
+
.querySelectorAll('.array-field')
|
|
1324
|
+
.forEach((arrayContainer) => {
|
|
1325
|
+
const fieldName = arrayContainer.getAttribute('data-field');
|
|
1326
|
+
if (!fieldName)
|
|
1327
|
+
return;
|
|
1328
|
+
arrayContainer
|
|
1329
|
+
.querySelectorAll('.array-string-item')
|
|
1330
|
+
.forEach((input, idx) => {
|
|
1331
|
+
input.addEventListener('input', () => {
|
|
1332
|
+
const values = Array.isArray(this.formData[fieldName])
|
|
1333
|
+
? [...this.formData[fieldName]]
|
|
1334
|
+
: [];
|
|
1335
|
+
values[idx] = input.value;
|
|
1336
|
+
this.formData[fieldName] = values;
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
arrayContainer
|
|
1340
|
+
.querySelectorAll('.remove-array-item')
|
|
1341
|
+
.forEach((btn, idx) => {
|
|
1342
|
+
btn.addEventListener('click', () => {
|
|
1343
|
+
const values = Array.isArray(this.formData[fieldName])
|
|
1344
|
+
? [...this.formData[fieldName]]
|
|
1345
|
+
: [];
|
|
1346
|
+
values.splice(idx, 1);
|
|
1347
|
+
this.formData[fieldName] = values;
|
|
1348
|
+
this.rerenderArrayField(fieldName);
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
// Handle add buttons
|
|
1353
|
+
this.container.querySelectorAll('.add-array-item').forEach(btn => {
|
|
1354
|
+
btn.addEventListener('click', () => {
|
|
1355
|
+
const fieldName = btn.getAttribute('data-field');
|
|
1356
|
+
if (!fieldName)
|
|
1357
|
+
return;
|
|
1358
|
+
const values = Array.isArray(this.formData[fieldName])
|
|
1359
|
+
? [...this.formData[fieldName]]
|
|
1360
|
+
: [];
|
|
1361
|
+
values.push('');
|
|
1362
|
+
this.formData[fieldName] = values;
|
|
1363
|
+
this.rerenderArrayField(fieldName);
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
rerenderArrayField(fieldName) {
|
|
1368
|
+
const attr = this.schema?.attributes[fieldName];
|
|
1369
|
+
if (!attr)
|
|
1370
|
+
return;
|
|
1371
|
+
const arrayContainer = this.container.querySelector(`.array-field[data-field="${fieldName}"]`)?.parentElement;
|
|
1372
|
+
if (arrayContainer) {
|
|
1373
|
+
const newHtml = attr.type === 'pointer' || attr.items?.type === 'pointer'
|
|
1374
|
+
? this.pointerFields.get(fieldName)?.render() || ''
|
|
1375
|
+
: this.renderArrayField(fieldName, attr);
|
|
1376
|
+
arrayContainer.outerHTML = newHtml;
|
|
1377
|
+
// Reattach handlers
|
|
1378
|
+
if (attr.type === 'pointer' || attr.items?.type === 'pointer') {
|
|
1379
|
+
const placeholder = this.container.querySelector(`#pointer-${fieldName}`);
|
|
1380
|
+
if (placeholder) {
|
|
1381
|
+
const pointerField = this.pointerFields.get(fieldName);
|
|
1382
|
+
pointerField?.attachHandlers(placeholder);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
this.attachArrayFieldHandlers();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async handleSubmit(e) {
|
|
1391
|
+
e.preventDefault();
|
|
1392
|
+
const alertContainer = this.container.querySelector('#alert-container');
|
|
1393
|
+
const submitBtn = this.container.querySelector('button[type="submit"]');
|
|
1394
|
+
if (alertContainer)
|
|
1395
|
+
alertContainer.innerHTML = '';
|
|
1396
|
+
if (submitBtn)
|
|
1397
|
+
submitBtn.disabled = true;
|
|
1398
|
+
try {
|
|
1399
|
+
await this.api.updateUser(this.userDn, this.formData);
|
|
1400
|
+
if (alertContainer) {
|
|
1401
|
+
alertContainer.innerHTML = `
|
|
1402
|
+
<div class="alert alert-success">
|
|
1403
|
+
<span class="material-icons">check_circle</span>
|
|
1404
|
+
<div>User updated successfully!</div>
|
|
1405
|
+
</div>
|
|
1406
|
+
`;
|
|
1407
|
+
}
|
|
1408
|
+
if (this.onSaved)
|
|
1409
|
+
this.onSaved();
|
|
1410
|
+
}
|
|
1411
|
+
catch (error) {
|
|
1412
|
+
if (alertContainer) {
|
|
1413
|
+
alertContainer.innerHTML = `
|
|
1414
|
+
<div class="alert alert-error">
|
|
1415
|
+
<span class="material-icons">error</span>
|
|
1416
|
+
<div>${UserEditor.escapeHtml(error.message)}</div>
|
|
1417
|
+
</div>
|
|
1418
|
+
`;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
finally {
|
|
1422
|
+
if (submitBtn)
|
|
1423
|
+
submitBtn.disabled = false;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
async handleMove() {
|
|
1427
|
+
if (!this.user || !this.schema)
|
|
1428
|
+
return;
|
|
1429
|
+
// Find the organizationLink field to get current organization
|
|
1430
|
+
const orgLinkField = Object.entries(this.schema.attributes).find(([, attr]) => attr.role === 'organizationLink');
|
|
1431
|
+
if (!orgLinkField) {
|
|
1432
|
+
alert('This user type does not support moving between organizations');
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const [orgLinkFieldName] = orgLinkField;
|
|
1436
|
+
const currentOrgDn = Array.isArray(this.user[orgLinkFieldName])
|
|
1437
|
+
? String(this.user[orgLinkFieldName][0])
|
|
1438
|
+
: String(this.user[orgLinkFieldName] || '');
|
|
1439
|
+
if (!currentOrgDn) {
|
|
1440
|
+
alert('User does not have an organization link');
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const modal = new MoveUserModal(this.api, currentOrgDn, async (targetOrgDn) => {
|
|
1444
|
+
await this.performMove(targetOrgDn);
|
|
1445
|
+
});
|
|
1446
|
+
await modal.show();
|
|
1447
|
+
}
|
|
1448
|
+
async performMove(targetOrgDn) {
|
|
1449
|
+
const alertContainer = this.container.querySelector('#alert-container');
|
|
1450
|
+
const moveBtn = this.container.querySelector('#move-user-btn');
|
|
1451
|
+
if (alertContainer)
|
|
1452
|
+
alertContainer.innerHTML = '';
|
|
1453
|
+
if (moveBtn) {
|
|
1454
|
+
moveBtn.disabled = true;
|
|
1455
|
+
moveBtn.innerHTML = `
|
|
1456
|
+
<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px;"></span>
|
|
1457
|
+
Moving...
|
|
1458
|
+
`;
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
const result = await this.api.moveUser(this.userDn, targetOrgDn);
|
|
1462
|
+
if (alertContainer) {
|
|
1463
|
+
alertContainer.innerHTML = `
|
|
1464
|
+
<div class="alert alert-success">
|
|
1465
|
+
<span class="material-icons">check_circle</span>
|
|
1466
|
+
<div>User moved successfully!</div>
|
|
1467
|
+
</div>
|
|
1468
|
+
`;
|
|
1469
|
+
}
|
|
1470
|
+
// Update the userDn if it changed
|
|
1471
|
+
if (result.newDn) {
|
|
1472
|
+
this.userDn = result.newDn;
|
|
1473
|
+
}
|
|
1474
|
+
// Reload the user data to show updated organization
|
|
1475
|
+
await this.init();
|
|
1476
|
+
if (this.onSaved)
|
|
1477
|
+
this.onSaved();
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
if (alertContainer) {
|
|
1481
|
+
alertContainer.innerHTML = `
|
|
1482
|
+
<div class="alert alert-error">
|
|
1483
|
+
<span class="material-icons">error</span>
|
|
1484
|
+
<div>${UserEditor.escapeHtml(error.message)}</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
`;
|
|
1487
|
+
}
|
|
1488
|
+
// Re-enable button on error
|
|
1489
|
+
if (moveBtn) {
|
|
1490
|
+
moveBtn.disabled = false;
|
|
1491
|
+
moveBtn.innerHTML = `
|
|
1492
|
+
<span class="material-icons">drive_file_move</span>
|
|
1493
|
+
Move User
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
throw error;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async handleDelete() {
|
|
1500
|
+
if (!confirm(`Are you sure you want to delete this user?\n\n${this.userDn}`)) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const alertContainer = this.container.querySelector('#alert-container');
|
|
1504
|
+
const deleteBtn = this.container.querySelector('#delete-user-btn');
|
|
1505
|
+
if (alertContainer)
|
|
1506
|
+
alertContainer.innerHTML = '';
|
|
1507
|
+
if (deleteBtn)
|
|
1508
|
+
deleteBtn.disabled = true;
|
|
1509
|
+
try {
|
|
1510
|
+
await this.api.deleteUser(this.userDn);
|
|
1511
|
+
if (alertContainer) {
|
|
1512
|
+
alertContainer.innerHTML = `
|
|
1513
|
+
<div class="alert alert-success">
|
|
1514
|
+
<span class="material-icons">check_circle</span>
|
|
1515
|
+
<div>User deleted successfully!</div>
|
|
1516
|
+
</div>
|
|
1517
|
+
`;
|
|
1518
|
+
}
|
|
1519
|
+
if (this.onDeleted)
|
|
1520
|
+
this.onDeleted();
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
if (alertContainer) {
|
|
1524
|
+
alertContainer.innerHTML = `
|
|
1525
|
+
<div class="alert alert-error">
|
|
1526
|
+
<span class="material-icons">error</span>
|
|
1527
|
+
<div>${UserEditor.escapeHtml(error.message)}</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
`;
|
|
1530
|
+
}
|
|
1531
|
+
if (deleteBtn)
|
|
1532
|
+
deleteBtn.disabled = false;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async refresh() {
|
|
1536
|
+
await this.init();
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* LDAP User Editor - User List Component
|
|
1542
|
+
* Shows users in a selected organization
|
|
1543
|
+
*/
|
|
1544
|
+
class UserList {
|
|
1545
|
+
constructor(container, api, orgDn, onSelectUser) {
|
|
1546
|
+
this.users = [];
|
|
1547
|
+
this.searchQuery = '';
|
|
1548
|
+
this.schema = null;
|
|
1549
|
+
this.config = null;
|
|
1550
|
+
this.container = container;
|
|
1551
|
+
this.api = api;
|
|
1552
|
+
this.orgDn = orgDn;
|
|
1553
|
+
this.onSelectUser = onSelectUser;
|
|
1554
|
+
}
|
|
1555
|
+
async init() {
|
|
1556
|
+
// Load config and schema first
|
|
1557
|
+
this.config = await this.api.getConfig();
|
|
1558
|
+
const usersResource = this.config.features?.ldapFlatGeneric?.flatResources?.find(r => r.pluralName === 'users' || r.name === 'users');
|
|
1559
|
+
if (usersResource?.schemaUrl) {
|
|
1560
|
+
this.schema = await this.api.getSchema(usersResource.schemaUrl);
|
|
1561
|
+
}
|
|
1562
|
+
this.render();
|
|
1563
|
+
await this.loadUsers();
|
|
1564
|
+
}
|
|
1565
|
+
render() {
|
|
1566
|
+
this.container.innerHTML = `
|
|
1567
|
+
<div class="user-list-container">
|
|
1568
|
+
<div class="user-list-header">
|
|
1569
|
+
<h2>
|
|
1570
|
+
<span class="material-icons">people</span>
|
|
1571
|
+
Users
|
|
1572
|
+
</h2>
|
|
1573
|
+
<div class="search-box">
|
|
1574
|
+
<span class="material-icons">search</span>
|
|
1575
|
+
<input
|
|
1576
|
+
type="text"
|
|
1577
|
+
placeholder="Search users in this organization..."
|
|
1578
|
+
id="user-list-search-input"
|
|
1579
|
+
/>
|
|
1580
|
+
</div>
|
|
1581
|
+
</div>
|
|
1582
|
+
<div class="user-list" id="user-list-items">
|
|
1583
|
+
<div class="loading">
|
|
1584
|
+
<div class="spinner"></div>
|
|
1585
|
+
</div>
|
|
1586
|
+
</div>
|
|
1587
|
+
</div>
|
|
1588
|
+
`;
|
|
1589
|
+
const searchInput = this.container.querySelector('#user-list-search-input');
|
|
1590
|
+
searchInput?.addEventListener('input', e => {
|
|
1591
|
+
this.searchQuery = e.target.value;
|
|
1592
|
+
this.renderUserList();
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
async loadUsers() {
|
|
1596
|
+
const listEl = this.container.querySelector('#user-list-items');
|
|
1597
|
+
if (!listEl)
|
|
1598
|
+
return;
|
|
1599
|
+
try {
|
|
1600
|
+
listEl.innerHTML =
|
|
1601
|
+
'<div class="loading"><div class="spinner"></div></div>';
|
|
1602
|
+
// Use organization subnodes API to get users in this org
|
|
1603
|
+
// Filter by the first objectClass from schema to get only users (not groups/OUs)
|
|
1604
|
+
const baseUrl = this.api['baseUrl'] || window.location.origin;
|
|
1605
|
+
const objectClass = this.schema?.entity?.objectClass?.[1] || 'twakeAccount'; // Use second objectClass (not 'top')
|
|
1606
|
+
const res = await fetch(`${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(this.orgDn)}/subnodes?objectClass=${encodeURIComponent(objectClass)}`);
|
|
1607
|
+
if (!res.ok)
|
|
1608
|
+
throw new Error('Failed to fetch organization users');
|
|
1609
|
+
const items = (await res.json());
|
|
1610
|
+
// Filter only user entries using schema objectClass
|
|
1611
|
+
// An item is a user if it has ALL the expected objectClasses
|
|
1612
|
+
const expectedObjectClasses = this.schema?.entity?.objectClass || [];
|
|
1613
|
+
const mainAttribute = this.schema?.entity?.mainAttribute || '';
|
|
1614
|
+
this.users = items.filter(item => {
|
|
1615
|
+
const objectClass = Array.isArray(item.objectClass)
|
|
1616
|
+
? item.objectClass
|
|
1617
|
+
: [item.objectClass];
|
|
1618
|
+
// Must have the mainAttribute field (uid, sAMAccountName, etc.)
|
|
1619
|
+
if (!item[mainAttribute])
|
|
1620
|
+
return false;
|
|
1621
|
+
// Check if item has ALL the expected objectClasses
|
|
1622
|
+
return expectedObjectClasses.every(expected => objectClass.includes(expected));
|
|
1623
|
+
});
|
|
1624
|
+
this.renderUserList();
|
|
1625
|
+
}
|
|
1626
|
+
catch (error) {
|
|
1627
|
+
console.error('Failed to load users:', error);
|
|
1628
|
+
listEl.innerHTML =
|
|
1629
|
+
'<div class="empty-state"><span class="material-icons">error</span><p>Failed to load users</p></div>';
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
getFirstValue(value) {
|
|
1633
|
+
if (!value)
|
|
1634
|
+
return '';
|
|
1635
|
+
if (Array.isArray(value))
|
|
1636
|
+
return value[0] || '';
|
|
1637
|
+
return String(value);
|
|
1638
|
+
}
|
|
1639
|
+
getFieldNameByRole(role) {
|
|
1640
|
+
if (!this.schema)
|
|
1641
|
+
return null;
|
|
1642
|
+
const field = Object.entries(this.schema.attributes).find(([, attr]) => attr.role === role);
|
|
1643
|
+
return field ? field[0] : null;
|
|
1644
|
+
}
|
|
1645
|
+
getFieldValueByRole(user, role) {
|
|
1646
|
+
const fieldName = this.getFieldNameByRole(role);
|
|
1647
|
+
if (!fieldName)
|
|
1648
|
+
return '';
|
|
1649
|
+
return this.getFirstValue(user[fieldName]);
|
|
1650
|
+
}
|
|
1651
|
+
renderUserList() {
|
|
1652
|
+
const listEl = this.container.querySelector('#user-list-items');
|
|
1653
|
+
if (!listEl)
|
|
1654
|
+
return;
|
|
1655
|
+
// Get field names from schema roles
|
|
1656
|
+
const displayNameField = this.getFieldNameByRole('displayName');
|
|
1657
|
+
const identifierField = this.schema?.entity?.mainAttribute || '';
|
|
1658
|
+
const emailField = this.getFieldNameByRole('primaryEmail');
|
|
1659
|
+
// Filter users by search query
|
|
1660
|
+
const filteredUsers = this.searchQuery
|
|
1661
|
+
? this.users.filter(user => {
|
|
1662
|
+
const displayName = displayNameField
|
|
1663
|
+
? this.getFirstValue(user[displayNameField]).toLowerCase()
|
|
1664
|
+
: '';
|
|
1665
|
+
const identifier = this.getFirstValue(user[identifierField]).toLowerCase();
|
|
1666
|
+
const email = emailField
|
|
1667
|
+
? this.getFirstValue(user[emailField]).toLowerCase()
|
|
1668
|
+
: '';
|
|
1669
|
+
const query = this.searchQuery.toLowerCase();
|
|
1670
|
+
return (displayName.includes(query) ||
|
|
1671
|
+
identifier.includes(query) ||
|
|
1672
|
+
email.includes(query));
|
|
1673
|
+
})
|
|
1674
|
+
: this.users;
|
|
1675
|
+
if (filteredUsers.length === 0) {
|
|
1676
|
+
listEl.innerHTML =
|
|
1677
|
+
'<div class="empty-state"><span class="material-icons">person_search</span><p>No users found</p></div>';
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
listEl.innerHTML = filteredUsers
|
|
1681
|
+
.map(user => {
|
|
1682
|
+
const displayName = displayNameField
|
|
1683
|
+
? this.getFirstValue(user[displayNameField])
|
|
1684
|
+
: '';
|
|
1685
|
+
const identifier = this.getFirstValue(user[identifierField]);
|
|
1686
|
+
const email = emailField ? this.getFirstValue(user[emailField]) : '';
|
|
1687
|
+
const name = displayName || identifier || 'Unknown';
|
|
1688
|
+
return `
|
|
1689
|
+
<div
|
|
1690
|
+
class="tree-node"
|
|
1691
|
+
data-dn="${user.dn}"
|
|
1692
|
+
>
|
|
1693
|
+
<span class="material-icons">person</span>
|
|
1694
|
+
<div class="tree-node-content">
|
|
1695
|
+
<div class="tree-node-name">
|
|
1696
|
+
${name}
|
|
1697
|
+
</div>
|
|
1698
|
+
${email ? `<div class="tree-node-email">${email}</div>` : ''}
|
|
1699
|
+
</div>
|
|
1700
|
+
</div>
|
|
1701
|
+
`;
|
|
1702
|
+
})
|
|
1703
|
+
.join('');
|
|
1704
|
+
// Add click handlers
|
|
1705
|
+
listEl.querySelectorAll('.tree-node').forEach(node => {
|
|
1706
|
+
node.addEventListener('click', () => {
|
|
1707
|
+
const dn = node.getAttribute('data-dn');
|
|
1708
|
+
if (dn) {
|
|
1709
|
+
this.onSelectUser(dn);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
async refresh() {
|
|
1715
|
+
await this.loadUsers();
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* LDAP User Editor - Main class
|
|
1721
|
+
* @author Xavier Guimard
|
|
1722
|
+
*/
|
|
1723
|
+
class LdapUserEditor {
|
|
1724
|
+
constructor(options) {
|
|
1725
|
+
this.container = null;
|
|
1726
|
+
this.userTree = null;
|
|
1727
|
+
this.userEditor = null;
|
|
1728
|
+
this.userList = null;
|
|
1729
|
+
this.currentUserDn = null;
|
|
1730
|
+
this.currentOrgDn = null;
|
|
1731
|
+
this.config = null;
|
|
1732
|
+
this.options = {
|
|
1733
|
+
apiBaseUrl: typeof window !== 'undefined' ? window.location.origin : '',
|
|
1734
|
+
...options,
|
|
1735
|
+
};
|
|
1736
|
+
this.api = new UserApiClient(this.options.apiBaseUrl);
|
|
1737
|
+
}
|
|
1738
|
+
async init() {
|
|
1739
|
+
this.container = document.getElementById(this.options.containerId);
|
|
1740
|
+
if (!this.container) {
|
|
1741
|
+
throw new Error(`Container #${this.options.containerId} not found`);
|
|
1742
|
+
}
|
|
1743
|
+
// Load config first
|
|
1744
|
+
this.config = await this.api.getConfig();
|
|
1745
|
+
this.render();
|
|
1746
|
+
await this.initComponents();
|
|
1747
|
+
}
|
|
1748
|
+
render() {
|
|
1749
|
+
if (!this.container)
|
|
1750
|
+
return;
|
|
1751
|
+
this.container.innerHTML = `
|
|
1752
|
+
<div class="ldap-user-editor">
|
|
1753
|
+
<div class="editor-layout">
|
|
1754
|
+
<div id="user-tree-container" class="demo-panel"></div>
|
|
1755
|
+
<div id="user-editor-container" class="demo-panel">
|
|
1756
|
+
<div class="empty-state">
|
|
1757
|
+
<span class="material-icons">person_search</span>
|
|
1758
|
+
<p>Select a user from the list to edit</p>
|
|
1759
|
+
</div>
|
|
1760
|
+
</div>
|
|
1761
|
+
</div>
|
|
1762
|
+
</div>
|
|
1763
|
+
`;
|
|
1764
|
+
}
|
|
1765
|
+
async initComponents() {
|
|
1766
|
+
const treeContainer = document.getElementById('user-tree-container');
|
|
1767
|
+
if (!treeContainer)
|
|
1768
|
+
return;
|
|
1769
|
+
this.userTree = new UserTree(treeContainer, this.options.apiBaseUrl || window.location.origin, orgDn => this.onOrganizationSelected(orgDn));
|
|
1770
|
+
try {
|
|
1771
|
+
await this.userTree.init();
|
|
1772
|
+
}
|
|
1773
|
+
catch (error) {
|
|
1774
|
+
this.handleError(error);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
async onOrganizationSelected(dn) {
|
|
1778
|
+
const editorContainer = document.getElementById('user-editor-container');
|
|
1779
|
+
if (!editorContainer)
|
|
1780
|
+
return;
|
|
1781
|
+
this.currentOrgDn = dn;
|
|
1782
|
+
this.currentUserDn = null;
|
|
1783
|
+
try {
|
|
1784
|
+
// Show user list for this organization
|
|
1785
|
+
this.userList = new UserList(editorContainer, this.api, dn, userDn => this.onUserSelected(userDn));
|
|
1786
|
+
await this.userList.init();
|
|
1787
|
+
}
|
|
1788
|
+
catch (error) {
|
|
1789
|
+
this.handleError(error);
|
|
1790
|
+
editorContainer.innerHTML = `
|
|
1791
|
+
<div class="empty-state">
|
|
1792
|
+
<span class="material-icons">error</span>
|
|
1793
|
+
<p>Failed to load users for this organization</p>
|
|
1794
|
+
</div>
|
|
1795
|
+
`;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async onUserSelected(dn) {
|
|
1799
|
+
this.currentUserDn = dn;
|
|
1800
|
+
const editorContainer = document.getElementById('user-editor-container');
|
|
1801
|
+
if (!editorContainer)
|
|
1802
|
+
return;
|
|
1803
|
+
this.userEditor = new UserEditor(editorContainer, this.api, dn, () => {
|
|
1804
|
+
if (this.options.onUserSaved) {
|
|
1805
|
+
this.options.onUserSaved(dn);
|
|
1806
|
+
}
|
|
1807
|
+
// Refresh the user list if we have one
|
|
1808
|
+
if (this.currentOrgDn && this.userList) {
|
|
1809
|
+
this.userList.refresh();
|
|
1810
|
+
}
|
|
1811
|
+
}, () => {
|
|
1812
|
+
// On user deleted
|
|
1813
|
+
this.deleteUser(dn);
|
|
1814
|
+
});
|
|
1815
|
+
try {
|
|
1816
|
+
await this.userEditor.init();
|
|
1817
|
+
}
|
|
1818
|
+
catch (error) {
|
|
1819
|
+
this.handleError(error);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
handleError(error) {
|
|
1823
|
+
console.error('LDAP User Editor error:', error);
|
|
1824
|
+
if (this.options.onError) {
|
|
1825
|
+
this.options.onError(error);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
async refresh() {
|
|
1829
|
+
await this.userTree?.refresh();
|
|
1830
|
+
if (this.currentUserDn) {
|
|
1831
|
+
await this.userEditor?.refresh();
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
destroy() {
|
|
1835
|
+
if (this.container) {
|
|
1836
|
+
this.container.innerHTML = '';
|
|
1837
|
+
}
|
|
1838
|
+
this.userTree = null;
|
|
1839
|
+
this.userEditor = null;
|
|
1840
|
+
this.currentUserDn = null;
|
|
1841
|
+
}
|
|
1842
|
+
async createUser(userData) {
|
|
1843
|
+
try {
|
|
1844
|
+
const result = await this.api.createUser(userData);
|
|
1845
|
+
// Refresh the user list if we have one
|
|
1846
|
+
if (this.userList) {
|
|
1847
|
+
await this.userList.refresh();
|
|
1848
|
+
}
|
|
1849
|
+
return result;
|
|
1850
|
+
}
|
|
1851
|
+
catch (error) {
|
|
1852
|
+
this.handleError(error);
|
|
1853
|
+
throw error;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async deleteUser(dn) {
|
|
1857
|
+
try {
|
|
1858
|
+
await this.api.deleteUser(dn);
|
|
1859
|
+
// Clear current selection if we deleted the selected user
|
|
1860
|
+
if (this.currentUserDn === dn) {
|
|
1861
|
+
this.currentUserDn = null;
|
|
1862
|
+
const editorContainer = document.getElementById('user-editor-container');
|
|
1863
|
+
if (editorContainer) {
|
|
1864
|
+
editorContainer.innerHTML = `
|
|
1865
|
+
<div class="empty-state">
|
|
1866
|
+
<span class="material-icons">person_search</span>
|
|
1867
|
+
<p>Select a user from the list to edit</p>
|
|
1868
|
+
</div>
|
|
1869
|
+
`;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
// Refresh the user list
|
|
1873
|
+
if (this.userList) {
|
|
1874
|
+
await this.userList.refresh();
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
catch (error) {
|
|
1878
|
+
this.handleError(error);
|
|
1879
|
+
throw error;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
getConfig() {
|
|
1883
|
+
return this.config;
|
|
1884
|
+
}
|
|
1885
|
+
getApi() {
|
|
1886
|
+
return this.api;
|
|
1887
|
+
}
|
|
1888
|
+
getCurrentOrgDn() {
|
|
1889
|
+
return this.currentOrgDn;
|
|
1890
|
+
}
|
|
1891
|
+
getCurrentUserDn() {
|
|
1892
|
+
return this.currentUserDn;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
export { LdapUserEditor };
|
|
1897
|
+
//# sourceMappingURL=ldap-user-editor.esm.js.map
|