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,1407 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LdapGroupEditor = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* API Client for LDAP Group operations
|
|
9
|
+
*/
|
|
10
|
+
class GroupApiClient {
|
|
11
|
+
constructor(baseUrl) {
|
|
12
|
+
this.baseUrl = baseUrl;
|
|
13
|
+
}
|
|
14
|
+
async getConfig() {
|
|
15
|
+
const response = await fetch(`${this.baseUrl}/api/v1/config`);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Failed to load config: ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
return response.json();
|
|
20
|
+
}
|
|
21
|
+
async getOrganizations() {
|
|
22
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/top`);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Failed to load organizations: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
async getGroups(orgDn) {
|
|
29
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(orgDn)}/subnodes?objectClass=groupOfNames`);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Failed to load groups: ${response.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
const subnodes = await response.json();
|
|
34
|
+
// Filter to keep only groups
|
|
35
|
+
return subnodes.filter((node) => {
|
|
36
|
+
const classes = Array.isArray(node.objectClass)
|
|
37
|
+
? node.objectClass
|
|
38
|
+
: [node.objectClass];
|
|
39
|
+
return classes.includes('groupOfNames') || classes.includes('group');
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async getGroup(dn) {
|
|
43
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/groups/${encodeURIComponent(dn)}`);
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(`Failed to load group: ${response.statusText}`);
|
|
46
|
+
}
|
|
47
|
+
return response.json();
|
|
48
|
+
}
|
|
49
|
+
async updateGroup(dn, data) {
|
|
50
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/groups/${encodeURIComponent(dn)}`, {
|
|
51
|
+
method: 'PUT',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify(data),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const error = await response.text();
|
|
57
|
+
throw new Error(`Failed to update group: ${error}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async createEntry(dn, data) {
|
|
61
|
+
// Use the groups API for creating groups
|
|
62
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/groups`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify(data),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const error = await response.text();
|
|
69
|
+
throw new Error(`Failed to create entry: ${error}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async deleteEntry(dn) {
|
|
73
|
+
// Use the groups API for deleting groups
|
|
74
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/groups/${encodeURIComponent(dn)}`, {
|
|
75
|
+
method: 'DELETE',
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const error = await response.text();
|
|
79
|
+
throw new Error(`Failed to delete entry: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async moveGroup(cn, targetOrgDn) {
|
|
83
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/groups/${encodeURIComponent(cn)}/move`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ targetOrgDn }),
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const error = await response.text();
|
|
90
|
+
throw new Error(`Failed to move group: ${error}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async renameGroup(cn, newCn) {
|
|
94
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/groups/${encodeURIComponent(cn)}/rename`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({ newCn }),
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const error = await response.text();
|
|
101
|
+
throw new Error(`Failed to rename group: ${error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async getPointerOptions(branch) {
|
|
105
|
+
const response = await fetch(`${this.baseUrl}/api/v1/ldap/search?base=${encodeURIComponent(branch)}&scope=one`);
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`Failed to load pointer options: ${response.statusText}`);
|
|
108
|
+
}
|
|
109
|
+
const entries = await response.json();
|
|
110
|
+
return entries.map((entry) => ({
|
|
111
|
+
dn: entry.dn,
|
|
112
|
+
label: entry.cn?.[0] || entry.ou?.[0] || entry.dn,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Group Tree Component - Shows organizations tree (same as UserTree)
|
|
119
|
+
*/
|
|
120
|
+
class GroupTree {
|
|
121
|
+
constructor(container, apiBaseUrl, onOrgSelect) {
|
|
122
|
+
this.expandedNodes = new Set();
|
|
123
|
+
this.selectedDn = null;
|
|
124
|
+
this.rootDn = null;
|
|
125
|
+
this.container = container;
|
|
126
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
127
|
+
this.onOrgSelect = onOrgSelect;
|
|
128
|
+
}
|
|
129
|
+
async init() {
|
|
130
|
+
this.container.innerHTML = `
|
|
131
|
+
<div class="group-tree">
|
|
132
|
+
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px;">
|
|
133
|
+
<span class="material-icons">account_tree</span>
|
|
134
|
+
Organizations
|
|
135
|
+
</h3>
|
|
136
|
+
<div id="tree-content">
|
|
137
|
+
<div class="loading"><div class="spinner"></div></div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations/top`);
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
throw new Error('Failed to load organizations');
|
|
145
|
+
const topOrg = await response.json();
|
|
146
|
+
this.rootDn = topOrg.dn;
|
|
147
|
+
await this.renderTree();
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error('Failed to init tree:', error);
|
|
151
|
+
const treeEl = this.container.querySelector('#tree-content');
|
|
152
|
+
if (treeEl) {
|
|
153
|
+
treeEl.innerHTML =
|
|
154
|
+
'<p style="color: #f44336;">Failed to load organizations</p>';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async renderTree() {
|
|
159
|
+
const treeEl = this.container.querySelector('#tree-content');
|
|
160
|
+
if (!treeEl || !this.rootDn)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
const html = await this.renderNode(this.rootDn, 0);
|
|
164
|
+
treeEl.innerHTML = html;
|
|
165
|
+
this.attachEventListeners();
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('Failed to render tree:', error);
|
|
169
|
+
treeEl.innerHTML = '<p style="color: #f44336;">Error rendering tree</p>';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async renderNode(dn, level) {
|
|
173
|
+
const isExpanded = this.expandedNodes.has(dn);
|
|
174
|
+
const isSelected = this.selectedDn === dn;
|
|
175
|
+
const indent = level * 20;
|
|
176
|
+
// Get node info
|
|
177
|
+
const response = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
|
|
178
|
+
if (!response.ok)
|
|
179
|
+
throw new Error('Failed to load node');
|
|
180
|
+
const node = await response.json();
|
|
181
|
+
// Extract display name
|
|
182
|
+
let displayName = dn;
|
|
183
|
+
if (node.ou) {
|
|
184
|
+
displayName = Array.isArray(node.ou) ? node.ou[0] : node.ou;
|
|
185
|
+
}
|
|
186
|
+
else if (node.cn) {
|
|
187
|
+
displayName = Array.isArray(node.cn) ? node.cn[0] : node.cn;
|
|
188
|
+
}
|
|
189
|
+
let html = `
|
|
190
|
+
<div class="tree-node ${isSelected ? 'active' : ''}" style="padding-left: ${indent}px; padding: 8px 8px 8px ${indent}px; cursor: pointer; display: flex; align-items: center; gap: 4px;">
|
|
191
|
+
<span class="material-icons tree-node-toggle" data-dn="${this.escapeHtml(dn)}" style="font-size: 18px; cursor: pointer;">
|
|
192
|
+
${isExpanded ? 'expand_more' : 'chevron_right'}
|
|
193
|
+
</span>
|
|
194
|
+
<span class="material-icons" style="font-size: 18px;">business</span>
|
|
195
|
+
<span class="tree-node-label" data-dn="${this.escapeHtml(dn)}">${this.escapeHtml(displayName)}</span>
|
|
196
|
+
</div>
|
|
197
|
+
`;
|
|
198
|
+
if (isExpanded) {
|
|
199
|
+
try {
|
|
200
|
+
const subnodesResponse = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes`);
|
|
201
|
+
if (!subnodesResponse.ok)
|
|
202
|
+
throw new Error('Failed to load subnodes');
|
|
203
|
+
const subnodes = await subnodesResponse.json();
|
|
204
|
+
const orgs = subnodes.filter((n) => {
|
|
205
|
+
const classes = Array.isArray(n.objectClass)
|
|
206
|
+
? n.objectClass
|
|
207
|
+
: n.objectClass
|
|
208
|
+
? [n.objectClass]
|
|
209
|
+
: [];
|
|
210
|
+
return (classes.includes('organizationalUnit') ||
|
|
211
|
+
classes.includes('organization'));
|
|
212
|
+
});
|
|
213
|
+
for (const subnode of orgs) {
|
|
214
|
+
html += await this.renderNode(subnode.dn, level + 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error('Failed to load subnodes for', dn, error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return html;
|
|
222
|
+
}
|
|
223
|
+
attachEventListeners() {
|
|
224
|
+
// Toggle expand/collapse
|
|
225
|
+
this.container.querySelectorAll('.tree-node-toggle').forEach(el => {
|
|
226
|
+
el.addEventListener('click', async (e) => {
|
|
227
|
+
e.stopPropagation();
|
|
228
|
+
const dn = el.getAttribute('data-dn');
|
|
229
|
+
if (!dn)
|
|
230
|
+
return;
|
|
231
|
+
if (this.expandedNodes.has(dn)) {
|
|
232
|
+
this.expandedNodes.delete(dn);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
this.expandedNodes.add(dn);
|
|
236
|
+
}
|
|
237
|
+
await this.renderTree();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// Select organization
|
|
241
|
+
this.container.querySelectorAll('.tree-node-label').forEach(el => {
|
|
242
|
+
el.addEventListener('click', e => {
|
|
243
|
+
e.stopPropagation();
|
|
244
|
+
const dn = el.getAttribute('data-dn');
|
|
245
|
+
if (!dn)
|
|
246
|
+
return;
|
|
247
|
+
this.selectedDn = dn;
|
|
248
|
+
this.onOrgSelect(dn);
|
|
249
|
+
this.renderTree();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
escapeHtml(text) {
|
|
254
|
+
if (text == null)
|
|
255
|
+
return '';
|
|
256
|
+
const map = {
|
|
257
|
+
'&': '&',
|
|
258
|
+
'<': '<',
|
|
259
|
+
'>': '>',
|
|
260
|
+
'"': '"',
|
|
261
|
+
"'": ''',
|
|
262
|
+
'/': '/',
|
|
263
|
+
};
|
|
264
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Move Group Modal Component - Modal for selecting target organization when moving a group
|
|
270
|
+
*/
|
|
271
|
+
class MoveGroupModal {
|
|
272
|
+
constructor(api, currentOrgDn, onMove) {
|
|
273
|
+
this.modal = null;
|
|
274
|
+
this.expandedNodes = new Set();
|
|
275
|
+
this.selectedOrgDn = null;
|
|
276
|
+
this.api = api;
|
|
277
|
+
this.currentOrgDn = currentOrgDn;
|
|
278
|
+
this.onMove = onMove;
|
|
279
|
+
}
|
|
280
|
+
async show() {
|
|
281
|
+
// Create modal backdrop
|
|
282
|
+
this.modal = document.createElement('div');
|
|
283
|
+
this.modal.style.cssText = `
|
|
284
|
+
position: fixed;
|
|
285
|
+
top: 0;
|
|
286
|
+
left: 0;
|
|
287
|
+
right: 0;
|
|
288
|
+
bottom: 0;
|
|
289
|
+
background: rgba(0, 0, 0, 0.5);
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
justify-content: center;
|
|
293
|
+
z-index: 1000;
|
|
294
|
+
`;
|
|
295
|
+
// Create modal content
|
|
296
|
+
const modalContent = document.createElement('div');
|
|
297
|
+
modalContent.style.cssText = `
|
|
298
|
+
background: white;
|
|
299
|
+
border-radius: 8px;
|
|
300
|
+
padding: 24px;
|
|
301
|
+
max-width: 600px;
|
|
302
|
+
width: 90%;
|
|
303
|
+
max-height: 80vh;
|
|
304
|
+
overflow-y: auto;
|
|
305
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
306
|
+
`;
|
|
307
|
+
modalContent.innerHTML = `
|
|
308
|
+
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px;">
|
|
309
|
+
<span class="material-icons">drive_file_move</span>
|
|
310
|
+
Move Group to Organization
|
|
311
|
+
</h3>
|
|
312
|
+
<p style="margin: 0 0 16px 0; color: #666;">Select the target organization:</p>
|
|
313
|
+
<div id="org-tree" style="margin-bottom: 24px; max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 12px;">
|
|
314
|
+
<div class="loading"><div class="spinner"></div></div>
|
|
315
|
+
</div>
|
|
316
|
+
<div style="display: flex; gap: 12px; justify-content: flex-end;">
|
|
317
|
+
<button type="button" id="cancel-move-btn" style="padding: 10px 20px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer;">Cancel</button>
|
|
318
|
+
<button type="button" id="confirm-move-btn" disabled style="padding: 10px 20px; background: var(--primary-color, #6200ee); color: white; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
319
|
+
<span class="material-icons" style="font-size: 18px;">drive_file_move</span>
|
|
320
|
+
Move Here
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
`;
|
|
324
|
+
this.modal.appendChild(modalContent);
|
|
325
|
+
document.body.appendChild(this.modal);
|
|
326
|
+
// Attach event listeners
|
|
327
|
+
const cancelBtn = this.modal.querySelector('#cancel-move-btn');
|
|
328
|
+
if (cancelBtn) {
|
|
329
|
+
cancelBtn.addEventListener('click', () => this.close());
|
|
330
|
+
}
|
|
331
|
+
const confirmBtn = this.modal.querySelector('#confirm-move-btn');
|
|
332
|
+
if (confirmBtn) {
|
|
333
|
+
confirmBtn.addEventListener('click', () => this.handleConfirm());
|
|
334
|
+
}
|
|
335
|
+
// Click outside to close
|
|
336
|
+
this.modal.addEventListener('click', e => {
|
|
337
|
+
if (e.target === this.modal) {
|
|
338
|
+
this.close();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// Load organization tree
|
|
342
|
+
await this.loadOrgTree();
|
|
343
|
+
}
|
|
344
|
+
async loadOrgTree() {
|
|
345
|
+
const treeContainer = this.modal?.querySelector('#org-tree');
|
|
346
|
+
if (!treeContainer)
|
|
347
|
+
return;
|
|
348
|
+
try {
|
|
349
|
+
// Get top organization
|
|
350
|
+
const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/organizations/top`);
|
|
351
|
+
if (!response.ok)
|
|
352
|
+
throw new Error('Failed to load organizations');
|
|
353
|
+
const topOrg = await response.json();
|
|
354
|
+
const rootDn = topOrg.dn;
|
|
355
|
+
// Render tree starting from root
|
|
356
|
+
const html = await this.renderNode(rootDn, 0);
|
|
357
|
+
treeContainer.innerHTML = html;
|
|
358
|
+
this.attachTreeEventListeners();
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
console.error('Failed to load organization tree:', error);
|
|
362
|
+
treeContainer.innerHTML = `<p style="color: #f44336;">Failed to load organizations</p>`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async renderNode(dn, level) {
|
|
366
|
+
const isExpanded = this.expandedNodes.has(dn);
|
|
367
|
+
const isSelected = this.selectedOrgDn === dn;
|
|
368
|
+
const isCurrent = this.currentOrgDn === dn;
|
|
369
|
+
const indent = level * 20;
|
|
370
|
+
// Get node info
|
|
371
|
+
const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
|
|
372
|
+
if (!response.ok)
|
|
373
|
+
throw new Error('Failed to load node');
|
|
374
|
+
const node = await response.json();
|
|
375
|
+
// Extract display name
|
|
376
|
+
let displayName = dn;
|
|
377
|
+
if (node.ou) {
|
|
378
|
+
displayName = Array.isArray(node.ou) ? node.ou[0] : node.ou;
|
|
379
|
+
}
|
|
380
|
+
else if (node.cn) {
|
|
381
|
+
displayName = Array.isArray(node.cn) ? node.cn[0] : node.cn;
|
|
382
|
+
}
|
|
383
|
+
const nodeClass = `tree-node ${isSelected ? 'selected' : ''} ${isCurrent ? 'current-org' : ''}`;
|
|
384
|
+
const nodeStyle = `padding-left: ${indent}px; padding: 8px 8px 8px ${indent}px; cursor: ${isCurrent ? 'not-allowed' : 'pointer'}; display: flex; align-items: center; gap: 4px; ${isCurrent ? 'opacity: 0.5; background: #f5f5f5;' : isSelected ? 'background: #e3f2fd;' : ''}`;
|
|
385
|
+
let html = `
|
|
386
|
+
<div class="${nodeClass}" style="${nodeStyle}" data-dn="${this.escapeHtml(dn)}" data-is-current="${isCurrent}">
|
|
387
|
+
<span class="material-icons tree-node-toggle" data-dn="${this.escapeHtml(dn)}" style="font-size: 18px; cursor: pointer;">
|
|
388
|
+
${isExpanded ? 'expand_more' : 'chevron_right'}
|
|
389
|
+
</span>
|
|
390
|
+
<span class="material-icons" style="font-size: 18px;">business</span>
|
|
391
|
+
<span class="tree-node-label" data-dn="${this.escapeHtml(dn)}">${this.escapeHtml(displayName)}</span>
|
|
392
|
+
${isCurrent ? '<span style="font-size: 12px; color: #666; margin-left: 8px;">(current)</span>' : ''}
|
|
393
|
+
</div>
|
|
394
|
+
`;
|
|
395
|
+
// If expanded, load and render subnodes
|
|
396
|
+
if (isExpanded) {
|
|
397
|
+
try {
|
|
398
|
+
const subnodesResponse = await fetch(`${this.api['baseUrl']}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes`);
|
|
399
|
+
if (!subnodesResponse.ok)
|
|
400
|
+
throw new Error('Failed to load subnodes');
|
|
401
|
+
const subnodes = await subnodesResponse.json();
|
|
402
|
+
const orgs = subnodes.filter((n) => {
|
|
403
|
+
const classes = Array.isArray(n.objectClass)
|
|
404
|
+
? n.objectClass
|
|
405
|
+
: n.objectClass
|
|
406
|
+
? [n.objectClass]
|
|
407
|
+
: [];
|
|
408
|
+
return (classes.includes('organizationalUnit') ||
|
|
409
|
+
classes.includes('organization'));
|
|
410
|
+
});
|
|
411
|
+
for (const subnode of orgs) {
|
|
412
|
+
html += await this.renderNode(subnode.dn, level + 1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('Failed to load subnodes for', dn, error);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return html;
|
|
420
|
+
}
|
|
421
|
+
attachTreeEventListeners() {
|
|
422
|
+
const treeContainer = this.modal?.querySelector('#org-tree');
|
|
423
|
+
if (!treeContainer)
|
|
424
|
+
return;
|
|
425
|
+
// Toggle expand/collapse
|
|
426
|
+
treeContainer.querySelectorAll('.tree-node-toggle').forEach(el => {
|
|
427
|
+
el.addEventListener('click', async (e) => {
|
|
428
|
+
e.stopPropagation();
|
|
429
|
+
const dn = el.getAttribute('data-dn');
|
|
430
|
+
if (!dn)
|
|
431
|
+
return;
|
|
432
|
+
if (this.expandedNodes.has(dn)) {
|
|
433
|
+
this.expandedNodes.delete(dn);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
this.expandedNodes.add(dn);
|
|
437
|
+
}
|
|
438
|
+
await this.loadOrgTree();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
// Select organization
|
|
442
|
+
treeContainer.querySelectorAll('.tree-node').forEach(el => {
|
|
443
|
+
el.addEventListener('click', () => {
|
|
444
|
+
const isCurrent = el.getAttribute('data-is-current') === 'true';
|
|
445
|
+
if (isCurrent)
|
|
446
|
+
return; // Cannot select current organization
|
|
447
|
+
const dn = el.getAttribute('data-dn');
|
|
448
|
+
if (!dn)
|
|
449
|
+
return;
|
|
450
|
+
this.selectedOrgDn = dn;
|
|
451
|
+
// Update UI
|
|
452
|
+
treeContainer.querySelectorAll('.tree-node').forEach(node => {
|
|
453
|
+
node.classList.remove('selected');
|
|
454
|
+
node.style.background = '';
|
|
455
|
+
});
|
|
456
|
+
el.classList.add('selected');
|
|
457
|
+
el.style.background = '#e3f2fd';
|
|
458
|
+
// Enable confirm button
|
|
459
|
+
const confirmBtn = this.modal?.querySelector('#confirm-move-btn');
|
|
460
|
+
if (confirmBtn) {
|
|
461
|
+
confirmBtn.disabled = false;
|
|
462
|
+
confirmBtn.style.opacity = '1';
|
|
463
|
+
confirmBtn.style.cursor = 'pointer';
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async handleConfirm() {
|
|
469
|
+
if (!this.selectedOrgDn)
|
|
470
|
+
return;
|
|
471
|
+
try {
|
|
472
|
+
await this.onMove(this.selectedOrgDn);
|
|
473
|
+
this.close();
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
console.error('Failed to move group:', error);
|
|
477
|
+
// Error will be shown by the parent component
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
close() {
|
|
481
|
+
if (this.modal) {
|
|
482
|
+
document.body.removeChild(this.modal);
|
|
483
|
+
this.modal = null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
escapeHtml(text) {
|
|
487
|
+
if (text == null)
|
|
488
|
+
return '';
|
|
489
|
+
const map = {
|
|
490
|
+
'&': '&',
|
|
491
|
+
'<': '<',
|
|
492
|
+
'>': '>',
|
|
493
|
+
'"': '"',
|
|
494
|
+
"'": ''',
|
|
495
|
+
'/': '/',
|
|
496
|
+
};
|
|
497
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Group Property Editor Component - Edits properties of a selected group
|
|
503
|
+
*/
|
|
504
|
+
class GroupPropertyEditor {
|
|
505
|
+
constructor(container, api, groupDn) {
|
|
506
|
+
this.group = null;
|
|
507
|
+
this.schema = null;
|
|
508
|
+
this.saveCallback = null;
|
|
509
|
+
this.container = container;
|
|
510
|
+
this.api = api;
|
|
511
|
+
this.groupDn = groupDn;
|
|
512
|
+
}
|
|
513
|
+
async init() {
|
|
514
|
+
try {
|
|
515
|
+
await this.loadGroup();
|
|
516
|
+
await this.loadSchema();
|
|
517
|
+
this.render();
|
|
518
|
+
this.attachEventListeners();
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
console.error('Failed to init GroupPropertyEditor:', error);
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async loadGroup() {
|
|
526
|
+
console.warn('[GroupPropertyEditor] Loading group:', this.groupDn);
|
|
527
|
+
this.group = await this.api.getGroup(this.groupDn);
|
|
528
|
+
console.warn('[GroupPropertyEditor] Group loaded:', this.group);
|
|
529
|
+
}
|
|
530
|
+
async loadSchema() {
|
|
531
|
+
console.warn('[GroupPropertyEditor] Loading schema...');
|
|
532
|
+
const config = await this.api.getConfig();
|
|
533
|
+
console.warn('[GroupPropertyEditor] Config loaded:', config);
|
|
534
|
+
// First check in features.ldapGroups (from ldapGroups plugin)
|
|
535
|
+
const groupsConfig = config.features?.ldapGroups;
|
|
536
|
+
console.warn('[GroupPropertyEditor] Groups config:', groupsConfig);
|
|
537
|
+
if (groupsConfig?.schemaUrl) {
|
|
538
|
+
const response = await fetch(groupsConfig.schemaUrl);
|
|
539
|
+
this.schema = await response.json();
|
|
540
|
+
console.warn('[GroupPropertyEditor] Schema loaded from URL:', this.schema);
|
|
541
|
+
}
|
|
542
|
+
else if (groupsConfig?.schema) {
|
|
543
|
+
this.schema = groupsConfig.schema;
|
|
544
|
+
console.warn('[GroupPropertyEditor] Schema loaded from config:', this.schema);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
// Fallback: check in flatResources
|
|
548
|
+
const groupsResource = config.features?.ldapFlatGeneric?.flatResources?.find(r => r.pluralName === 'groups' || r.name === 'groups');
|
|
549
|
+
console.warn('[GroupPropertyEditor] Groups resource (flatResources):', groupsResource);
|
|
550
|
+
if (groupsResource?.schemaUrl) {
|
|
551
|
+
const response = await fetch(groupsResource.schemaUrl);
|
|
552
|
+
this.schema = await response.json();
|
|
553
|
+
console.warn('[GroupPropertyEditor] Schema loaded from URL (flatResources):', this.schema);
|
|
554
|
+
}
|
|
555
|
+
else if (groupsResource?.schema) {
|
|
556
|
+
this.schema = groupsResource.schema;
|
|
557
|
+
console.warn('[GroupPropertyEditor] Schema loaded from config (flatResources):', this.schema);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// No schema configured - create a default schema from the group's attributes
|
|
561
|
+
console.warn('[GroupPropertyEditor] No schema configured, creating default schema from group attributes');
|
|
562
|
+
this.schema = this.createDefaultSchema();
|
|
563
|
+
console.warn('[GroupPropertyEditor] Default schema created:', this.schema);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
createDefaultSchema() {
|
|
568
|
+
if (!this.group) {
|
|
569
|
+
return { entity: { objectClass: [] }, attributes: {} };
|
|
570
|
+
}
|
|
571
|
+
const attributes = {};
|
|
572
|
+
// Create schema attributes from the group's actual attributes
|
|
573
|
+
for (const [key, value] of Object.entries(this.group)) {
|
|
574
|
+
if (key === 'dn' || key === '*' || key === 'objectClass')
|
|
575
|
+
continue;
|
|
576
|
+
const isArray = Array.isArray(value);
|
|
577
|
+
attributes[key] = {
|
|
578
|
+
type: isArray ? 'array' : 'string',
|
|
579
|
+
required: false,
|
|
580
|
+
fixed: key === 'dn',
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
entity: {
|
|
585
|
+
objectClass: Array.isArray(this.group.objectClass)
|
|
586
|
+
? this.group.objectClass
|
|
587
|
+
: [this.group.objectClass],
|
|
588
|
+
},
|
|
589
|
+
attributes,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
onSave(callback) {
|
|
593
|
+
this.saveCallback = callback;
|
|
594
|
+
}
|
|
595
|
+
render() {
|
|
596
|
+
if (!this.group)
|
|
597
|
+
return;
|
|
598
|
+
this.container.innerHTML = `
|
|
599
|
+
<div class="group-property-editor">
|
|
600
|
+
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px;">
|
|
601
|
+
<span class="material-icons">group</span>
|
|
602
|
+
Edit Group
|
|
603
|
+
</h3>
|
|
604
|
+
<form id="group-edit-form">
|
|
605
|
+
${this.renderFields()}
|
|
606
|
+
<div class="form-actions" style="margin-top: 24px; padding-top: 24px; border-top: 1px solid #e0e0e0; display: flex; gap: 12px; justify-content: space-between;">
|
|
607
|
+
<button type="button" id="move-group-btn" class="btn btn-secondary" style="background: #757575; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
608
|
+
<span class="material-icons" style="font-size: 18px;">drive_file_move</span>
|
|
609
|
+
Move
|
|
610
|
+
</button>
|
|
611
|
+
<div style="display: flex; gap: 12px;">
|
|
612
|
+
<button type="button" id="delete-group-btn" class="btn btn-danger" style="background: #d32f2f; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
613
|
+
<span class="material-icons" style="font-size: 18px;">delete</span>
|
|
614
|
+
Delete Group
|
|
615
|
+
</button>
|
|
616
|
+
<button type="submit" class="btn btn-primary" style="background: var(--primary-color, #6200ee); color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
617
|
+
<span class="material-icons" style="font-size: 18px;">save</span>
|
|
618
|
+
Save Changes
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</form>
|
|
623
|
+
</div>
|
|
624
|
+
`;
|
|
625
|
+
}
|
|
626
|
+
renderFields() {
|
|
627
|
+
if (!this.group || !this.schema) {
|
|
628
|
+
return '<div>Loading...</div>';
|
|
629
|
+
}
|
|
630
|
+
let html = '';
|
|
631
|
+
// Group fields by category
|
|
632
|
+
const basicFields = [];
|
|
633
|
+
const memberFields = [];
|
|
634
|
+
const mailFields = [];
|
|
635
|
+
for (const [fieldName, attribute] of Object.entries(this.schema.attributes)) {
|
|
636
|
+
if (attribute.fixed || fieldName === 'objectClass')
|
|
637
|
+
continue;
|
|
638
|
+
if (fieldName === 'member' || fieldName === 'owner') {
|
|
639
|
+
memberFields.push([fieldName, attribute]);
|
|
640
|
+
}
|
|
641
|
+
else if (fieldName.includes('mail') || fieldName.includes('Mail')) {
|
|
642
|
+
mailFields.push([fieldName, attribute]);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
basicFields.push([fieldName, attribute]);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Basic Information
|
|
649
|
+
if (basicFields.length > 0) {
|
|
650
|
+
html +=
|
|
651
|
+
'<div class="field-group" style="margin-bottom: 20px; padding: 16px; background: #f9f9f9; border-radius: 4px;">';
|
|
652
|
+
html +=
|
|
653
|
+
'<div class="field-group-title" style="font-weight: 500; color: var(--primary-color, #6200ee); margin-bottom: 12px; font-size: 14px; text-transform: uppercase;">Basic Information</div>';
|
|
654
|
+
for (const [fieldName, attribute] of basicFields) {
|
|
655
|
+
html += this.renderField(fieldName, attribute);
|
|
656
|
+
}
|
|
657
|
+
html += '</div>';
|
|
658
|
+
}
|
|
659
|
+
// Email/Mailbox Settings
|
|
660
|
+
if (mailFields.length > 0) {
|
|
661
|
+
html +=
|
|
662
|
+
'<div class="field-group" style="margin-bottom: 20px; padding: 16px; background: #f9f9f9; border-radius: 4px;">';
|
|
663
|
+
html +=
|
|
664
|
+
'<div class="field-group-title" style="font-weight: 500; color: var(--primary-color, #6200ee); margin-bottom: 12px; font-size: 14px; text-transform: uppercase;">Email/Mailbox Settings</div>';
|
|
665
|
+
for (const [fieldName, attribute] of mailFields) {
|
|
666
|
+
html += this.renderField(fieldName, attribute);
|
|
667
|
+
}
|
|
668
|
+
html += '</div>';
|
|
669
|
+
}
|
|
670
|
+
// Members & Owners
|
|
671
|
+
if (memberFields.length > 0) {
|
|
672
|
+
html +=
|
|
673
|
+
'<div class="field-group" style="margin-bottom: 20px; padding: 16px; background: #f9f9f9; border-radius: 4px;">';
|
|
674
|
+
html +=
|
|
675
|
+
'<div class="field-group-title" style="font-weight: 500; color: var(--primary-color, #6200ee); margin-bottom: 12px; font-size: 14px; text-transform: uppercase;">Members & Owners</div>';
|
|
676
|
+
for (const [fieldName, attribute] of memberFields) {
|
|
677
|
+
html += this.renderField(fieldName, attribute);
|
|
678
|
+
}
|
|
679
|
+
html += '</div>';
|
|
680
|
+
}
|
|
681
|
+
return html;
|
|
682
|
+
}
|
|
683
|
+
renderField(fieldName, attribute) {
|
|
684
|
+
const value = this.group?.[fieldName];
|
|
685
|
+
const label = this.getFieldLabel(fieldName);
|
|
686
|
+
const required = attribute.required ? 'required' : '';
|
|
687
|
+
if (attribute.type === 'array') {
|
|
688
|
+
const arrayValue = Array.isArray(value) ? value.join('\n') : '';
|
|
689
|
+
// Special handling for member/owner fields with add/delete buttons
|
|
690
|
+
if (fieldName === 'member' || fieldName === 'owner') {
|
|
691
|
+
return `
|
|
692
|
+
<div class="form-group" style="margin-bottom: 16px;">
|
|
693
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
|
|
694
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
|
695
|
+
<input type="text" id="${fieldName}-add-input" placeholder="Search DN or email..." style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" list="${fieldName}-suggestions" />
|
|
696
|
+
<datalist id="${fieldName}-suggestions"></datalist>
|
|
697
|
+
<button type="button" class="btn-add-${fieldName}" style="padding: 8px 16px; background: var(--primary-color, #6200ee); color: white; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 4px;">
|
|
698
|
+
<span class="material-icons" style="font-size: 16px;">add</span>
|
|
699
|
+
Add
|
|
700
|
+
</button>
|
|
701
|
+
</div>
|
|
702
|
+
<textarea name="${this.escapeHtml(fieldName)}" id="${fieldName}-list" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; min-height: 120px; font-family: 'Roboto', sans-serif;" ${required}>${this.escapeHtml(arrayValue)}</textarea>
|
|
703
|
+
<div style="font-size: 12px; color: #666; margin-top: 4px;">
|
|
704
|
+
One DN per line. Type to search for users, or enter DN/email manually. Select a line and press Delete to remove.
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
return `
|
|
710
|
+
<div class="form-group" style="margin-bottom: 16px;">
|
|
711
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
|
|
712
|
+
<textarea name="${this.escapeHtml(fieldName)}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; min-height: 80px; font-family: 'Roboto', sans-serif;" ${required}>${this.escapeHtml(arrayValue)}</textarea>
|
|
713
|
+
<div style="font-size: 12px; color: #666; margin-top: 4px;">Enter one value per line</div>
|
|
714
|
+
</div>
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
if (attribute.type === 'number' || attribute.type === 'integer') {
|
|
718
|
+
return `
|
|
719
|
+
<div class="form-group" style="margin-bottom: 16px;">
|
|
720
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
|
|
721
|
+
<input type="number" name="${this.escapeHtml(fieldName)}" value="${value || ''}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" ${required} />
|
|
722
|
+
</div>
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
// String field
|
|
726
|
+
const type = fieldName.toLowerCase().includes('mail') ? 'email' : 'text';
|
|
727
|
+
return `
|
|
728
|
+
<div class="form-group" style="margin-bottom: 16px;">
|
|
729
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
|
|
730
|
+
<input type="${type}" name="${this.escapeHtml(fieldName)}" value="${this.escapeHtml(String(value || ''))}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" ${required} />
|
|
731
|
+
</div>
|
|
732
|
+
`;
|
|
733
|
+
}
|
|
734
|
+
getFieldLabel(fieldName) {
|
|
735
|
+
const labelMap = {
|
|
736
|
+
cn: 'Group Name',
|
|
737
|
+
description: 'Description',
|
|
738
|
+
member: 'Members',
|
|
739
|
+
owner: 'Owners',
|
|
740
|
+
mail: 'Email Address',
|
|
741
|
+
twakeMailboxType: 'Mailbox Type',
|
|
742
|
+
};
|
|
743
|
+
return (labelMap[fieldName] ||
|
|
744
|
+
fieldName
|
|
745
|
+
.replace(/([A-Z])/g, ' $1')
|
|
746
|
+
.replace(/_/g, ' ')
|
|
747
|
+
.replace(/^./, str => str.toUpperCase()));
|
|
748
|
+
}
|
|
749
|
+
attachEventListeners() {
|
|
750
|
+
const form = document.getElementById('group-edit-form');
|
|
751
|
+
if (form) {
|
|
752
|
+
form.addEventListener('submit', e => this.handleSubmit(e));
|
|
753
|
+
}
|
|
754
|
+
const deleteBtn = document.getElementById('delete-group-btn');
|
|
755
|
+
if (deleteBtn) {
|
|
756
|
+
deleteBtn.addEventListener('click', () => this.handleDelete());
|
|
757
|
+
}
|
|
758
|
+
const moveBtn = document.getElementById('move-group-btn');
|
|
759
|
+
if (moveBtn) {
|
|
760
|
+
moveBtn.addEventListener('click', () => this.handleMove());
|
|
761
|
+
}
|
|
762
|
+
// Attach listeners for member/owner add buttons
|
|
763
|
+
const addMemberBtn = this.container.querySelector('.btn-add-member');
|
|
764
|
+
if (addMemberBtn) {
|
|
765
|
+
addMemberBtn.addEventListener('click', () => this.handleAddMember('member'));
|
|
766
|
+
}
|
|
767
|
+
const addOwnerBtn = this.container.querySelector('.btn-add-owner');
|
|
768
|
+
if (addOwnerBtn) {
|
|
769
|
+
addOwnerBtn.addEventListener('click', () => this.handleAddMember('owner'));
|
|
770
|
+
}
|
|
771
|
+
// Handle Delete key for removing members/owners
|
|
772
|
+
['member', 'owner'].forEach(fieldName => {
|
|
773
|
+
const textarea = document.getElementById(`${fieldName}-list`);
|
|
774
|
+
if (textarea) {
|
|
775
|
+
textarea.addEventListener('keydown', e => {
|
|
776
|
+
if (e.key === 'Delete') {
|
|
777
|
+
this.handleDeleteMember(textarea, e);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
// Add autocomplete for member/owner inputs
|
|
782
|
+
const input = document.getElementById(`${fieldName}-add-input`);
|
|
783
|
+
if (input) {
|
|
784
|
+
let searchTimeout;
|
|
785
|
+
input.addEventListener('input', () => {
|
|
786
|
+
clearTimeout(searchTimeout);
|
|
787
|
+
searchTimeout = window.setTimeout(() => {
|
|
788
|
+
this.searchUsers(input.value, fieldName);
|
|
789
|
+
}, 300);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async searchUsers(query, fieldName) {
|
|
795
|
+
if (!query || query.length < 2)
|
|
796
|
+
return;
|
|
797
|
+
try {
|
|
798
|
+
// Search for users by CN, UID, or mail
|
|
799
|
+
const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/search?filter=(|(cn=*${encodeURIComponent(query)}*)(uid=*${encodeURIComponent(query)}*)(mail=*${encodeURIComponent(query)}*))`);
|
|
800
|
+
if (!response.ok)
|
|
801
|
+
return;
|
|
802
|
+
const results = await response.json();
|
|
803
|
+
const datalist = document.getElementById(`${fieldName}-suggestions`);
|
|
804
|
+
if (!datalist)
|
|
805
|
+
return;
|
|
806
|
+
datalist.innerHTML = results
|
|
807
|
+
.slice(0, 10)
|
|
808
|
+
.map((user) => {
|
|
809
|
+
const label = user.cn?.[0] || user.uid?.[0] || user.mail?.[0] || user.dn;
|
|
810
|
+
return `<option value="${this.escapeHtml(user.dn)}">${this.escapeHtml(label)}</option>`;
|
|
811
|
+
})
|
|
812
|
+
.join('');
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
console.error('Failed to search users:', error);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async handleAddMember(fieldName) {
|
|
819
|
+
const input = document.getElementById(`${fieldName}-add-input`);
|
|
820
|
+
const textarea = document.getElementById(`${fieldName}-list`);
|
|
821
|
+
if (!input || !textarea)
|
|
822
|
+
return;
|
|
823
|
+
const valueToAdd = input.value.trim();
|
|
824
|
+
if (!valueToAdd) {
|
|
825
|
+
alert('Please enter a DN or email address');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
// Get current members/owners
|
|
829
|
+
const currentValues = textarea.value
|
|
830
|
+
.split('\n')
|
|
831
|
+
.map(v => v.trim())
|
|
832
|
+
.filter(v => v);
|
|
833
|
+
// Check if already exists
|
|
834
|
+
if (currentValues.includes(valueToAdd)) {
|
|
835
|
+
alert(`This ${fieldName} already exists`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
// Add to list
|
|
839
|
+
currentValues.push(valueToAdd);
|
|
840
|
+
textarea.value = currentValues.join('\n');
|
|
841
|
+
// Clear input
|
|
842
|
+
input.value = '';
|
|
843
|
+
input.focus();
|
|
844
|
+
}
|
|
845
|
+
handleDeleteMember(textarea, e) {
|
|
846
|
+
const start = textarea.selectionStart;
|
|
847
|
+
const end = textarea.selectionEnd;
|
|
848
|
+
const value = textarea.value;
|
|
849
|
+
// Find the line(s) being selected
|
|
850
|
+
const lines = value.split('\n');
|
|
851
|
+
let currentPos = 0;
|
|
852
|
+
let startLine = 0;
|
|
853
|
+
let endLine = 0;
|
|
854
|
+
for (let i = 0; i < lines.length; i++) {
|
|
855
|
+
const lineLength = lines[i].length + 1; // +1 for newline
|
|
856
|
+
if (currentPos <= start && start < currentPos + lineLength) {
|
|
857
|
+
startLine = i;
|
|
858
|
+
}
|
|
859
|
+
if (currentPos <= end && end <= currentPos + lineLength) {
|
|
860
|
+
endLine = i;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
currentPos += lineLength;
|
|
864
|
+
}
|
|
865
|
+
// Remove selected lines
|
|
866
|
+
e.preventDefault();
|
|
867
|
+
const newLines = lines.filter((_, i) => i < startLine || i > endLine);
|
|
868
|
+
textarea.value = newLines.join('\n');
|
|
869
|
+
}
|
|
870
|
+
async handleSubmit(e) {
|
|
871
|
+
e.preventDefault();
|
|
872
|
+
const form = e.target;
|
|
873
|
+
const formData = new FormData(form);
|
|
874
|
+
const updates = {};
|
|
875
|
+
for (const [key, value] of formData.entries()) {
|
|
876
|
+
if (this.schema?.attributes[key]?.type === 'array') {
|
|
877
|
+
updates[key] = value
|
|
878
|
+
.split('\n')
|
|
879
|
+
.map(v => v.trim())
|
|
880
|
+
.filter(v => v);
|
|
881
|
+
}
|
|
882
|
+
else if (this.schema?.attributes[key]?.type === 'number' ||
|
|
883
|
+
this.schema?.attributes[key]?.type === 'integer') {
|
|
884
|
+
updates[key] = Number(value);
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
updates[key] = value;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
// Check if cn has changed - if so, use rename API instead
|
|
892
|
+
if (updates.cn && this.group?.cn) {
|
|
893
|
+
const oldCn = Array.isArray(this.group.cn)
|
|
894
|
+
? this.group.cn[0]
|
|
895
|
+
: this.group.cn;
|
|
896
|
+
const newCn = updates.cn;
|
|
897
|
+
if (oldCn !== newCn) {
|
|
898
|
+
// Remove cn from updates
|
|
899
|
+
delete updates.cn;
|
|
900
|
+
// First rename the group
|
|
901
|
+
await this.api.renameGroup(this.groupDn, newCn);
|
|
902
|
+
// Update the groupDn for subsequent operations
|
|
903
|
+
const newDn = this.groupDn.replace(new RegExp(`^cn=${oldCn},`), `cn=${newCn},`);
|
|
904
|
+
this.groupDn = newDn;
|
|
905
|
+
// If there are other updates, apply them to the renamed group
|
|
906
|
+
if (Object.keys(updates).length > 0) {
|
|
907
|
+
await this.api.updateGroup(this.groupDn, updates);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
// cn hasn't changed, just do a normal update
|
|
912
|
+
await this.api.updateGroup(this.groupDn, updates);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
// No cn change, normal update
|
|
917
|
+
await this.api.updateGroup(this.groupDn, updates);
|
|
918
|
+
}
|
|
919
|
+
alert('Group updated successfully!');
|
|
920
|
+
if (this.saveCallback) {
|
|
921
|
+
this.saveCallback();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
console.error('Failed to update group:', error);
|
|
926
|
+
alert(`Failed to update group: ${error.message}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async handleMove() {
|
|
930
|
+
if (!this.group)
|
|
931
|
+
return;
|
|
932
|
+
// Get current organization DN from twakeDepartmentLink attribute
|
|
933
|
+
const currentOrgDn = this.group.twakeDepartmentLink;
|
|
934
|
+
if (!currentOrgDn) {
|
|
935
|
+
alert('Cannot move group: no organization link attribute found. This group may not support move operation.');
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
// Extract group cn from DN
|
|
939
|
+
const cnMatch = this.groupDn.match(/^cn=([^,]+)/);
|
|
940
|
+
const cn = cnMatch ? cnMatch[1] : this.groupDn;
|
|
941
|
+
const modal = new MoveGroupModal(this.api, currentOrgDn, async (targetOrgDn) => {
|
|
942
|
+
try {
|
|
943
|
+
await this.api.moveGroup(cn, targetOrgDn);
|
|
944
|
+
alert('Group moved successfully!');
|
|
945
|
+
// Reload the group
|
|
946
|
+
await this.loadGroup();
|
|
947
|
+
this.render();
|
|
948
|
+
this.attachEventListeners();
|
|
949
|
+
if (this.saveCallback) {
|
|
950
|
+
this.saveCallback();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
console.error('Failed to move group:', error);
|
|
955
|
+
alert(`Failed to move group: ${error.message}`);
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
await modal.show();
|
|
959
|
+
}
|
|
960
|
+
async handleDelete() {
|
|
961
|
+
if (!confirm(`Are you sure you want to delete this group?\n\n${this.groupDn}\n\nThis action cannot be undone.`)) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
await this.api.deleteEntry(this.groupDn);
|
|
966
|
+
alert('Group deleted successfully!');
|
|
967
|
+
this.container.innerHTML = `
|
|
968
|
+
<div class="empty-state">
|
|
969
|
+
<span class="material-icons">group</span>
|
|
970
|
+
<p>Group deleted. Select another group to edit.</p>
|
|
971
|
+
</div>
|
|
972
|
+
`;
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
console.error('Failed to delete group:', error);
|
|
976
|
+
alert(`Failed to delete group: ${error.message}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
escapeHtml(text) {
|
|
980
|
+
if (text == null)
|
|
981
|
+
return '';
|
|
982
|
+
const map = {
|
|
983
|
+
'&': '&',
|
|
984
|
+
'<': '<',
|
|
985
|
+
'>': '>',
|
|
986
|
+
'"': '"',
|
|
987
|
+
"'": ''',
|
|
988
|
+
'/': '/',
|
|
989
|
+
};
|
|
990
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Create Group Modal Component - Modal for creating a new group
|
|
996
|
+
*/
|
|
997
|
+
class CreateGroupModal {
|
|
998
|
+
constructor(api, config, orgDn, onCreated) {
|
|
999
|
+
this.modal = null;
|
|
1000
|
+
this.api = api;
|
|
1001
|
+
this.config = config;
|
|
1002
|
+
this.orgDn = orgDn;
|
|
1003
|
+
this.onCreated = onCreated;
|
|
1004
|
+
}
|
|
1005
|
+
async show() {
|
|
1006
|
+
// Create modal backdrop
|
|
1007
|
+
this.modal = document.createElement('div');
|
|
1008
|
+
this.modal.style.cssText = `
|
|
1009
|
+
position: fixed;
|
|
1010
|
+
top: 0;
|
|
1011
|
+
left: 0;
|
|
1012
|
+
right: 0;
|
|
1013
|
+
bottom: 0;
|
|
1014
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1015
|
+
display: flex;
|
|
1016
|
+
align-items: center;
|
|
1017
|
+
justify-content: center;
|
|
1018
|
+
z-index: 1000;
|
|
1019
|
+
`;
|
|
1020
|
+
// Get organization path for the new group
|
|
1021
|
+
const orgPath = await this.getOrgPath(this.orgDn);
|
|
1022
|
+
// Create modal content
|
|
1023
|
+
const modalContent = document.createElement('div');
|
|
1024
|
+
modalContent.style.cssText = `
|
|
1025
|
+
background: white;
|
|
1026
|
+
border-radius: 8px;
|
|
1027
|
+
padding: 24px;
|
|
1028
|
+
max-width: 500px;
|
|
1029
|
+
width: 90%;
|
|
1030
|
+
max-height: 80vh;
|
|
1031
|
+
overflow-y: auto;
|
|
1032
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1033
|
+
`;
|
|
1034
|
+
modalContent.innerHTML = `
|
|
1035
|
+
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px;">
|
|
1036
|
+
<span class="material-icons">group_add</span>
|
|
1037
|
+
Create New Group
|
|
1038
|
+
</h3>
|
|
1039
|
+
<form id="create-group-form" style="display: flex; flex-direction: column; gap: 16px;">
|
|
1040
|
+
<div>
|
|
1041
|
+
<label for="group-cn" style="display: block; margin-bottom: 4px; font-weight: 500;">Group Name (cn) *</label>
|
|
1042
|
+
<input type="text" id="group-cn" required pattern="[a-zA-Z][a-zA-Z0-9-]{0,254}"
|
|
1043
|
+
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
|
|
1044
|
+
placeholder="e.g., developers" />
|
|
1045
|
+
<small style="color: #666; font-size: 12px;">Must start with a letter, contain only letters, numbers, and hyphens</small>
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
<div>
|
|
1049
|
+
<label for="group-mail" style="display: block; margin-bottom: 4px; font-weight: 500;">Email Address</label>
|
|
1050
|
+
<input type="email" id="group-mail"
|
|
1051
|
+
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
|
|
1052
|
+
placeholder="e.g., developers@example.com" />
|
|
1053
|
+
</div>
|
|
1054
|
+
|
|
1055
|
+
<div>
|
|
1056
|
+
<label for="group-description" style="display: block; margin-bottom: 4px; font-weight: 500;">Description</label>
|
|
1057
|
+
<textarea id="group-description" rows="3"
|
|
1058
|
+
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;"
|
|
1059
|
+
placeholder="Group description..."></textarea>
|
|
1060
|
+
</div>
|
|
1061
|
+
|
|
1062
|
+
<div style="padding: 12px; background: #f5f5f5; border-radius: 4px;">
|
|
1063
|
+
<p style="margin: 0 0 8px 0; font-size: 14px; color: #666;">Organization:</p>
|
|
1064
|
+
<p style="margin: 0; font-weight: 500;">${this.escapeHtml(orgPath || this.orgDn)}</p>
|
|
1065
|
+
</div>
|
|
1066
|
+
|
|
1067
|
+
<div id="error-message" style="display: none; padding: 12px; background: #ffebee; border-radius: 4px; color: #c62828;"></div>
|
|
1068
|
+
|
|
1069
|
+
<div style="display: flex; gap: 12px; justify-content: flex-end;">
|
|
1070
|
+
<button type="button" id="cancel-create-btn" style="padding: 10px 20px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer;">Cancel</button>
|
|
1071
|
+
<button type="submit" style="padding: 10px 20px; background: var(--primary-color, #6200ee); color: white; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
1072
|
+
<span class="material-icons" style="font-size: 18px;">group_add</span>
|
|
1073
|
+
Create Group
|
|
1074
|
+
</button>
|
|
1075
|
+
</div>
|
|
1076
|
+
</form>
|
|
1077
|
+
`;
|
|
1078
|
+
this.modal.appendChild(modalContent);
|
|
1079
|
+
document.body.appendChild(this.modal);
|
|
1080
|
+
// Attach event listeners
|
|
1081
|
+
const cancelBtn = this.modal.querySelector('#cancel-create-btn');
|
|
1082
|
+
if (cancelBtn) {
|
|
1083
|
+
cancelBtn.addEventListener('click', () => this.close());
|
|
1084
|
+
}
|
|
1085
|
+
const form = this.modal.querySelector('#create-group-form');
|
|
1086
|
+
if (form) {
|
|
1087
|
+
form.addEventListener('submit', e => this.handleSubmit(e));
|
|
1088
|
+
}
|
|
1089
|
+
// Click outside to close
|
|
1090
|
+
this.modal.addEventListener('click', e => {
|
|
1091
|
+
if (e.target === this.modal) {
|
|
1092
|
+
this.close();
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async getOrgPath(orgDn) {
|
|
1097
|
+
try {
|
|
1098
|
+
const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/organizations/${encodeURIComponent(orgDn)}`);
|
|
1099
|
+
if (!response.ok)
|
|
1100
|
+
return null;
|
|
1101
|
+
const org = await response.json();
|
|
1102
|
+
const pathAttr = this.config.features?.organizations
|
|
1103
|
+
?.pathAttribute;
|
|
1104
|
+
if (pathAttr && org[pathAttr]) {
|
|
1105
|
+
return Array.isArray(org[pathAttr]) ? org[pathAttr][0] : org[pathAttr];
|
|
1106
|
+
}
|
|
1107
|
+
// If no path attribute, use the DN as fallback (for root organization)
|
|
1108
|
+
return orgDn;
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
console.error('Failed to load organization path:', error);
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async handleSubmit(e) {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
const cnInput = this.modal?.querySelector('#group-cn');
|
|
1118
|
+
const mailInput = this.modal?.querySelector('#group-mail');
|
|
1119
|
+
const descInput = this.modal?.querySelector('#group-description');
|
|
1120
|
+
const errorDiv = this.modal?.querySelector('#error-message');
|
|
1121
|
+
if (!cnInput)
|
|
1122
|
+
return;
|
|
1123
|
+
const cn = cnInput.value.trim();
|
|
1124
|
+
if (!cn) {
|
|
1125
|
+
this.showError(errorDiv, 'Group name is required');
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
// Get the group branch from config
|
|
1130
|
+
// const groupBranch = this.config.ldap_groups_branch as string;
|
|
1131
|
+
// const dn = `cn=${cn},${groupBranch}`;
|
|
1132
|
+
// Get organization path for the group
|
|
1133
|
+
const orgPath = await this.getOrgPath(this.orgDn);
|
|
1134
|
+
// Build group data - only send cn, the backend will add objectClass, member, and default attributes
|
|
1135
|
+
const groupData = {
|
|
1136
|
+
cn: cn,
|
|
1137
|
+
};
|
|
1138
|
+
// Add optional fields
|
|
1139
|
+
if (mailInput?.value.trim()) {
|
|
1140
|
+
groupData.mail = mailInput.value.trim();
|
|
1141
|
+
}
|
|
1142
|
+
if (descInput?.value.trim()) {
|
|
1143
|
+
groupData.description = descInput.value.trim();
|
|
1144
|
+
}
|
|
1145
|
+
// Add organization link and path (required for twake groups)
|
|
1146
|
+
const linkAttr = this.config.features?.organizations
|
|
1147
|
+
?.linkAttribute;
|
|
1148
|
+
const pathAttr = this.config.features?.organizations
|
|
1149
|
+
?.pathAttribute;
|
|
1150
|
+
if (linkAttr && pathAttr) {
|
|
1151
|
+
if (!orgPath) {
|
|
1152
|
+
this.showError(errorDiv, 'Failed to get organization path');
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
groupData[linkAttr] = this.orgDn;
|
|
1156
|
+
groupData[pathAttr] = orgPath;
|
|
1157
|
+
}
|
|
1158
|
+
// Create the group using the proper API
|
|
1159
|
+
const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/groups`, {
|
|
1160
|
+
method: 'POST',
|
|
1161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1162
|
+
body: JSON.stringify(groupData),
|
|
1163
|
+
});
|
|
1164
|
+
if (!response.ok) {
|
|
1165
|
+
const error = await response.text();
|
|
1166
|
+
throw new Error(error || 'Failed to create group');
|
|
1167
|
+
}
|
|
1168
|
+
// Call success callback and close
|
|
1169
|
+
await this.onCreated();
|
|
1170
|
+
this.close();
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
console.error('Failed to create group:', error);
|
|
1174
|
+
this.showError(errorDiv, error.message);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
showError(errorDiv, message) {
|
|
1178
|
+
if (errorDiv) {
|
|
1179
|
+
errorDiv.textContent = `Error: ${message}`;
|
|
1180
|
+
errorDiv.style.display = 'block';
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
close() {
|
|
1184
|
+
if (this.modal) {
|
|
1185
|
+
document.body.removeChild(this.modal);
|
|
1186
|
+
this.modal = null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
escapeHtml(text) {
|
|
1190
|
+
if (text == null)
|
|
1191
|
+
return '';
|
|
1192
|
+
const map = {
|
|
1193
|
+
'&': '&',
|
|
1194
|
+
'<': '<',
|
|
1195
|
+
'>': '>',
|
|
1196
|
+
'"': '"',
|
|
1197
|
+
"'": ''',
|
|
1198
|
+
'/': '/',
|
|
1199
|
+
};
|
|
1200
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* LDAP Group Editor - Main class for editing group properties
|
|
1206
|
+
* @author Xavier Guimard
|
|
1207
|
+
*/
|
|
1208
|
+
class LdapGroupEditor {
|
|
1209
|
+
constructor(options) {
|
|
1210
|
+
this.container = null;
|
|
1211
|
+
this.groupTree = null;
|
|
1212
|
+
this.groupEditor = null;
|
|
1213
|
+
this.currentGroupDn = null;
|
|
1214
|
+
this.currentOrgDn = null;
|
|
1215
|
+
this.config = null;
|
|
1216
|
+
this.options = {
|
|
1217
|
+
apiBaseUrl: typeof window !== 'undefined' ? window.location.origin : '',
|
|
1218
|
+
...options,
|
|
1219
|
+
};
|
|
1220
|
+
this.api = new GroupApiClient(this.options.apiBaseUrl || '');
|
|
1221
|
+
}
|
|
1222
|
+
async init() {
|
|
1223
|
+
this.container = document.getElementById(this.options.containerId);
|
|
1224
|
+
if (!this.container) {
|
|
1225
|
+
throw new Error(`Container #${this.options.containerId} not found`);
|
|
1226
|
+
}
|
|
1227
|
+
// Load config first
|
|
1228
|
+
this.config = await this.api.getConfig();
|
|
1229
|
+
this.render();
|
|
1230
|
+
await this.initComponents();
|
|
1231
|
+
}
|
|
1232
|
+
render() {
|
|
1233
|
+
if (!this.container)
|
|
1234
|
+
return;
|
|
1235
|
+
this.container.innerHTML = `
|
|
1236
|
+
<div class="ldap-group-editor">
|
|
1237
|
+
<div class="editor-layout">
|
|
1238
|
+
<div id="group-tree-container" class="demo-panel"></div>
|
|
1239
|
+
<div id="group-editor-container" class="demo-panel">
|
|
1240
|
+
<div class="empty-state">
|
|
1241
|
+
<span class="material-icons">group</span>
|
|
1242
|
+
<p>Select a group from the tree to edit its properties</p>
|
|
1243
|
+
</div>
|
|
1244
|
+
</div>
|
|
1245
|
+
</div>
|
|
1246
|
+
</div>
|
|
1247
|
+
`;
|
|
1248
|
+
}
|
|
1249
|
+
async initComponents() {
|
|
1250
|
+
const treeContainer = document.getElementById('group-tree-container');
|
|
1251
|
+
if (!treeContainer)
|
|
1252
|
+
return;
|
|
1253
|
+
this.groupTree = new GroupTree(treeContainer, this.options.apiBaseUrl || window.location.origin, orgDn => this.onOrgSelected(orgDn));
|
|
1254
|
+
try {
|
|
1255
|
+
await this.groupTree.init();
|
|
1256
|
+
}
|
|
1257
|
+
catch (error) {
|
|
1258
|
+
this.handleError(error);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async onOrgSelected(orgDn) {
|
|
1262
|
+
const editorContainer = document.getElementById('group-editor-container');
|
|
1263
|
+
if (!editorContainer)
|
|
1264
|
+
return;
|
|
1265
|
+
this.currentOrgDn = orgDn;
|
|
1266
|
+
try {
|
|
1267
|
+
// Load groups for this organization
|
|
1268
|
+
const groups = await this.api.getGroups(orgDn);
|
|
1269
|
+
// Show list of groups with create button
|
|
1270
|
+
editorContainer.innerHTML = `
|
|
1271
|
+
<div style="padding: 20px;">
|
|
1272
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
1273
|
+
<h3 style="margin: 0;">Groups</h3>
|
|
1274
|
+
<button type="button" id="create-group-btn" style="padding: 8px 16px; background: var(--primary-color, #6200ee); color: white; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
1275
|
+
<span class="material-icons" style="font-size: 18px;">group_add</span>
|
|
1276
|
+
Create
|
|
1277
|
+
</button>
|
|
1278
|
+
</div>
|
|
1279
|
+
${groups.length === 0
|
|
1280
|
+
? `
|
|
1281
|
+
<div class="empty-state">
|
|
1282
|
+
<span class="material-icons">group</span>
|
|
1283
|
+
<p>No groups in this organization</p>
|
|
1284
|
+
</div>
|
|
1285
|
+
`
|
|
1286
|
+
: `
|
|
1287
|
+
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
1288
|
+
${groups
|
|
1289
|
+
.map(group => `
|
|
1290
|
+
<div class="group-item" data-dn="${this.escapeHtml(group.dn)}" style="padding: 12px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
1291
|
+
<span class="material-icons">group</span>
|
|
1292
|
+
<span>${this.escapeHtml(group.cn || group.dn)}</span>
|
|
1293
|
+
</div>
|
|
1294
|
+
`)
|
|
1295
|
+
.join('')}
|
|
1296
|
+
</div>
|
|
1297
|
+
`}
|
|
1298
|
+
</div>
|
|
1299
|
+
`;
|
|
1300
|
+
// Attach click handlers for group items
|
|
1301
|
+
editorContainer.querySelectorAll('.group-item').forEach(item => {
|
|
1302
|
+
item.addEventListener('click', () => {
|
|
1303
|
+
const groupDn = item.dataset.dn;
|
|
1304
|
+
this.onGroupSelected(groupDn);
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
// Attach click handler for create button
|
|
1308
|
+
const createBtn = editorContainer.querySelector('#create-group-btn');
|
|
1309
|
+
if (createBtn) {
|
|
1310
|
+
createBtn.addEventListener('click', () => this.handleCreateGroup());
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
catch (error) {
|
|
1314
|
+
this.handleError(error);
|
|
1315
|
+
editorContainer.innerHTML = `
|
|
1316
|
+
<div class="empty-state">
|
|
1317
|
+
<span class="material-icons">error</span>
|
|
1318
|
+
<p>Failed to load groups</p>
|
|
1319
|
+
</div>
|
|
1320
|
+
`;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async onGroupSelected(groupDn) {
|
|
1324
|
+
const editorContainer = document.getElementById('group-editor-container');
|
|
1325
|
+
if (!editorContainer)
|
|
1326
|
+
return;
|
|
1327
|
+
this.currentGroupDn = groupDn;
|
|
1328
|
+
try {
|
|
1329
|
+
// Show group property editor
|
|
1330
|
+
this.groupEditor = new GroupPropertyEditor(editorContainer, this.api, groupDn);
|
|
1331
|
+
await this.groupEditor.init();
|
|
1332
|
+
// Notify parent
|
|
1333
|
+
if (this.options.onGroupSaved) {
|
|
1334
|
+
this.groupEditor.onSave(() => {
|
|
1335
|
+
this.options.onGroupSaved?.(groupDn);
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
this.handleError(error);
|
|
1341
|
+
editorContainer.innerHTML = `
|
|
1342
|
+
<div class="empty-state">
|
|
1343
|
+
<span class="material-icons">error</span>
|
|
1344
|
+
<p>Failed to load group properties</p>
|
|
1345
|
+
</div>
|
|
1346
|
+
`;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async handleCreateGroup() {
|
|
1350
|
+
if (!this.currentOrgDn || !this.config)
|
|
1351
|
+
return;
|
|
1352
|
+
const modal = new CreateGroupModal(this.api, this.config, this.currentOrgDn, async () => {
|
|
1353
|
+
// Refresh the group list after creation
|
|
1354
|
+
await this.onOrgSelected(this.currentOrgDn);
|
|
1355
|
+
if (this.options.onGroupSaved) {
|
|
1356
|
+
this.options.onGroupSaved('');
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
await modal.show();
|
|
1360
|
+
}
|
|
1361
|
+
escapeHtml(text) {
|
|
1362
|
+
if (text == null)
|
|
1363
|
+
return '';
|
|
1364
|
+
const map = {
|
|
1365
|
+
'&': '&',
|
|
1366
|
+
'<': '<',
|
|
1367
|
+
'>': '>',
|
|
1368
|
+
'"': '"',
|
|
1369
|
+
"'": ''',
|
|
1370
|
+
'/': '/',
|
|
1371
|
+
};
|
|
1372
|
+
return String(text).replace(/[&<>"'/]/g, m => map[m]);
|
|
1373
|
+
}
|
|
1374
|
+
handleError(error) {
|
|
1375
|
+
console.error('LdapGroupEditor error:', error);
|
|
1376
|
+
if (this.options.onError) {
|
|
1377
|
+
this.options.onError(error);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
// Public API
|
|
1381
|
+
getApi() {
|
|
1382
|
+
return this.api;
|
|
1383
|
+
}
|
|
1384
|
+
getConfig() {
|
|
1385
|
+
return this.config;
|
|
1386
|
+
}
|
|
1387
|
+
getCurrentOrgDn() {
|
|
1388
|
+
return this.currentOrgDn;
|
|
1389
|
+
}
|
|
1390
|
+
getCurrentUserDn() {
|
|
1391
|
+
// For compatibility with HTML that calls getCurrentUserDn
|
|
1392
|
+
return this.currentGroupDn;
|
|
1393
|
+
}
|
|
1394
|
+
async createUser(data) {
|
|
1395
|
+
// For compatibility - actually creates a group
|
|
1396
|
+
return this.api.createEntry(data.dn, data);
|
|
1397
|
+
}
|
|
1398
|
+
async deleteUser(dn) {
|
|
1399
|
+
// For compatibility - actually deletes a group
|
|
1400
|
+
return this.api.deleteEntry(dn);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
exports.LdapGroupEditor = LdapGroupEditor;
|
|
1405
|
+
|
|
1406
|
+
}));
|
|
1407
|
+
//# sourceMappingURL=ldap-group-editor.js.map
|