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.
Files changed (296) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +48 -0
  3. package/bin/cleanup-external-users.mjs +219 -0
  4. package/bin/index.mjs +54 -0
  5. package/bin/sync-james.mjs +190 -0
  6. package/dist/abstract/ldapFlat.js +1 -0
  7. package/dist/abstract/plugin.js +1 -0
  8. package/dist/abstract/twakePlugin.js +1 -0
  9. package/dist/bin/index.js +1 -0
  10. package/dist/config/args.js +1 -0
  11. package/dist/lib/auth/base.js +1 -0
  12. package/dist/lib/authz/base.js +1 -0
  13. package/dist/lib/expressFormatedResponses.js +1 -0
  14. package/dist/lib/ldapActions.js +1 -0
  15. package/dist/lib/parseConfig.js +1 -0
  16. package/dist/lib/utils.js +1 -0
  17. package/dist/logger/winston.js +1 -0
  18. package/dist/plugins/auth/authzLinid1.js +1 -0
  19. package/dist/plugins/auth/authzPerBranch.js +1 -0
  20. package/dist/plugins/auth/crowdsec.js +1 -0
  21. package/dist/plugins/auth/llng.js +1 -0
  22. package/dist/plugins/auth/openidconnect.js +1 -0
  23. package/dist/plugins/auth/rateLimit.js +1 -0
  24. package/dist/plugins/auth/token.js +1 -0
  25. package/dist/plugins/configApi.js +1 -0
  26. package/dist/plugins/demo/helloworld.js +1 -0
  27. package/dist/plugins/ldap/bulkImport.js +1 -0
  28. package/dist/plugins/ldap/departmentSync.js +1 -0
  29. package/dist/plugins/ldap/externalUsersInGroups.js +1 -0
  30. package/dist/plugins/ldap/flatGeneric.js +1 -0
  31. package/dist/plugins/ldap/groups.js +1 -0
  32. package/dist/plugins/ldap/onChange.js +1 -0
  33. package/dist/plugins/ldap/organization.js +1 -0
  34. package/dist/plugins/ldap/trash.js +1 -0
  35. package/dist/plugins/priority.json.js +1 -0
  36. package/dist/plugins/static.js +1 -0
  37. package/dist/plugins/twake/calendarResources.js +1 -0
  38. package/dist/plugins/twake/james.js +1 -0
  39. package/dist/plugins/weblogs.js +1 -0
  40. package/dist/src/abstract/ldapFlat.d.ts +94 -0
  41. package/dist/src/abstract/plugin.d.ts +43 -0
  42. package/dist/src/abstract/twakePlugin.d.ts +90 -0
  43. package/dist/src/bin/index.d.ts +43 -0
  44. package/dist/src/browser/ldap-group-editor/LdapGroupEditor.d.ts +31 -0
  45. package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +23 -0
  46. package/dist/src/browser/ldap-group-editor/components/CreateGroupModal.d.ts +19 -0
  47. package/dist/src/browser/ldap-group-editor/components/GroupPropertyEditor.d.ts +30 -0
  48. package/dist/src/browser/ldap-group-editor/components/GroupTree.d.ts +17 -0
  49. package/dist/src/browser/ldap-group-editor/components/MoveGroupModal.d.ts +20 -0
  50. package/dist/src/browser/ldap-group-editor/index.d.ts +6 -0
  51. package/dist/src/browser/ldap-group-editor/types.d.ts +58 -0
  52. package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +53 -0
  53. package/dist/src/browser/ldap-resource-editor/cache/CacheManager.d.ts +65 -0
  54. package/dist/src/browser/ldap-resource-editor/index.d.ts +6 -0
  55. package/dist/src/browser/ldap-resource-editor/types.d.ts +78 -0
  56. package/dist/src/browser/ldap-tree-viewer/LdapTreeViewer.d.ts +26 -0
  57. package/dist/src/browser/ldap-tree-viewer/api/LdapApiClient.d.ts +30 -0
  58. package/dist/src/browser/ldap-tree-viewer/components/TreeNode.d.ts +17 -0
  59. package/dist/src/browser/ldap-tree-viewer/components/TreeRoot.d.ts +14 -0
  60. package/dist/src/browser/ldap-tree-viewer/index.d.ts +7 -0
  61. package/dist/src/browser/ldap-tree-viewer/store/Store.d.ts +18 -0
  62. package/dist/src/browser/ldap-tree-viewer/store/actions.d.ts +28 -0
  63. package/dist/src/browser/ldap-tree-viewer/store/reducers.d.ts +7 -0
  64. package/dist/src/browser/ldap-tree-viewer/types.d.ts +34 -0
  65. package/dist/src/browser/ldap-unit-editor/LdapUnitEditor.d.ts +27 -0
  66. package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +24 -0
  67. package/dist/src/browser/ldap-unit-editor/components/MoveUnitModal.d.ts +28 -0
  68. package/dist/src/browser/ldap-unit-editor/components/UnitPropertyEditor.d.ts +28 -0
  69. package/dist/src/browser/ldap-unit-editor/components/UnitTree.d.ts +18 -0
  70. package/dist/src/browser/ldap-unit-editor/index.d.ts +6 -0
  71. package/dist/src/browser/ldap-unit-editor/types.d.ts +55 -0
  72. package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +32 -0
  73. package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +55 -0
  74. package/dist/src/browser/ldap-user-editor/cache/CacheManager.d.ts +65 -0
  75. package/dist/src/browser/ldap-user-editor/components/MoveUserModal.d.ts +28 -0
  76. package/dist/src/browser/ldap-user-editor/components/PointerField.d.ts +22 -0
  77. package/dist/src/browser/ldap-user-editor/components/UserEditor.d.ts +46 -0
  78. package/dist/src/browser/ldap-user-editor/components/UserList.d.ts +24 -0
  79. package/dist/src/browser/ldap-user-editor/components/UserTree.d.ts +24 -0
  80. package/dist/src/browser/ldap-user-editor/index.d.ts +7 -0
  81. package/dist/src/browser/ldap-user-editor/types.d.ts +67 -0
  82. package/dist/src/browser/shared/components/Modal.d.ts +38 -0
  83. package/dist/src/browser/shared/components/StatusMessage.d.ts +48 -0
  84. package/dist/src/browser/shared/index.d.ts +10 -0
  85. package/dist/src/browser/shared/types/index.d.ts +65 -0
  86. package/dist/src/browser/shared/utils/dom.d.ts +33 -0
  87. package/dist/src/browser/shared/utils/form.d.ts +28 -0
  88. package/dist/src/browser/shared/utils/schema.d.ts +29 -0
  89. package/dist/src/config/args.d.ts +128 -0
  90. package/dist/src/config/schema.d.ts +18 -0
  91. package/dist/src/hooks.d.ts +76 -0
  92. package/dist/src/lib/auth/base.d.ts +9 -0
  93. package/dist/src/lib/authz/base.d.ts +63 -0
  94. package/dist/src/lib/expressFormatedResponses.d.ts +26 -0
  95. package/dist/src/lib/ldapActions.d.ts +91 -0
  96. package/dist/src/lib/parseConfig.d.ts +23 -0
  97. package/dist/src/lib/utils.d.ts +73 -0
  98. package/dist/src/logger/winston.d.ts +3 -0
  99. package/dist/src/plugins/auth/authzLinid1.d.ts +46 -0
  100. package/dist/src/plugins/auth/authzPerBranch.d.ts +59 -0
  101. package/dist/src/plugins/auth/crowdsec.d.ts +29 -0
  102. package/dist/src/plugins/auth/llng.d.ts +8 -0
  103. package/dist/src/plugins/auth/openidconnect.d.ts +11 -0
  104. package/dist/src/plugins/auth/rateLimit.d.ts +24 -0
  105. package/dist/src/plugins/auth/token.d.ts +17 -0
  106. package/dist/src/plugins/configApi.d.ts +24 -0
  107. package/dist/src/plugins/demo/helloworld.d.ts +12 -0
  108. package/dist/src/plugins/ldap/bulkImport.d.ts +48 -0
  109. package/dist/src/plugins/ldap/departmentSync.d.ts +35 -0
  110. package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +21 -0
  111. package/dist/src/plugins/ldap/flatGeneric.d.ts +31 -0
  112. package/dist/src/plugins/ldap/groups.d.ts +95 -0
  113. package/dist/src/plugins/ldap/onChange.d.ts +32 -0
  114. package/dist/src/plugins/ldap/organization.d.ts +65 -0
  115. package/dist/src/plugins/ldap/trash.d.ts +52 -0
  116. package/dist/src/plugins/static.d.ts +7 -0
  117. package/dist/src/plugins/twake/calendarResources.d.ts +31 -0
  118. package/dist/src/plugins/twake/james.d.ts +83 -0
  119. package/dist/src/plugins/weblogs.d.ts +8 -0
  120. package/docs/DEVELOPER_GUIDE.md +298 -0
  121. package/docs/OPENAPI.md +440 -0
  122. package/docs/PLUGIN_DEPENDENCIES.md +612 -0
  123. package/docs/README.md +466 -0
  124. package/docs/api/REFERENCE.md +983 -0
  125. package/docs/api/REST_API.md +1237 -0
  126. package/docs/authentication.md +469 -0
  127. package/docs/authzLinid1.md +521 -0
  128. package/docs/authzPerBranch.md +596 -0
  129. package/docs/browser/LIBRARIES.md +1196 -0
  130. package/docs/examples/EXAMPLES.md +1659 -0
  131. package/docs/examples/twake-mailbox-type-nomenclature.ldif +37 -0
  132. package/docs/examples/twake-mailbox-type-schema-amendment.ldif +30 -0
  133. package/docs/jamesTeamMailboxes.md +343 -0
  134. package/docs/ldapBulkImport.md +722 -0
  135. package/docs/ldapExternalUsersInGroups.md +325 -0
  136. package/docs/ldapFlatGeneric.md +444 -0
  137. package/docs/ldapGroups.md +589 -0
  138. package/docs/ldapOrganizations.md +659 -0
  139. package/docs/ldapTrash.md +260 -0
  140. package/docs/linagora.png +0 -0
  141. package/docs/onChange.md +318 -0
  142. package/docs/plugins/DEVELOPMENT.md +1219 -0
  143. package/docs/presenterm-fr.md +721 -0
  144. package/docs/presenterm.md +721 -0
  145. package/docs/schemas/SCHEMAS.md +1212 -0
  146. package/docs/static.md +410 -0
  147. package/docs/twakeCalendarResources.md +148 -0
  148. package/docs/twakeJames.md +661 -0
  149. package/docs/weblogs.md +399 -0
  150. package/examples/web/ldap-group-editor.html +113 -0
  151. package/examples/web/ldap-tree-viewer.html +368 -0
  152. package/examples/web/ldap-unit-editor.html +108 -0
  153. package/examples/web/ldap-user-editor.html +412 -0
  154. package/package.json +458 -0
  155. package/static/browser/common-demo.css +450 -0
  156. package/static/browser/ldap-group-editor/LdapGroupEditor.js +207 -0
  157. package/static/browser/ldap-group-editor/LdapGroupEditor.js.map +1 -0
  158. package/static/browser/ldap-group-editor/api/GroupApiClient.js +112 -0
  159. package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -0
  160. package/static/browser/ldap-group-editor/components/CreateGroupModal.js +212 -0
  161. package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -0
  162. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +497 -0
  163. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -0
  164. package/static/browser/ldap-group-editor/components/GroupTree.js +153 -0
  165. package/static/browser/ldap-group-editor/components/GroupTree.js.map +1 -0
  166. package/static/browser/ldap-group-editor/components/MoveGroupModal.js +235 -0
  167. package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -0
  168. package/static/browser/ldap-group-editor/index.js +2 -0
  169. package/static/browser/ldap-group-editor/index.js.map +1 -0
  170. package/static/browser/ldap-group-editor/types.js +2 -0
  171. package/static/browser/ldap-group-editor/types.js.map +1 -0
  172. package/static/browser/ldap-group-editor.esm.js +1399 -0
  173. package/static/browser/ldap-group-editor.esm.js.map +1 -0
  174. package/static/browser/ldap-group-editor.js +1407 -0
  175. package/static/browser/ldap-group-editor.js.map +1 -0
  176. package/static/browser/ldap-group-editor.min.js +2 -0
  177. package/static/browser/ldap-group-editor.min.js.map +1 -0
  178. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +226 -0
  179. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -0
  180. package/static/browser/ldap-resource-editor/cache/CacheManager.js +123 -0
  181. package/static/browser/ldap-resource-editor/cache/CacheManager.js.map +1 -0
  182. package/static/browser/ldap-resource-editor/index.js +2 -0
  183. package/static/browser/ldap-resource-editor/index.js.map +1 -0
  184. package/static/browser/ldap-resource-editor/types.js +2 -0
  185. package/static/browser/ldap-resource-editor/types.js.map +1 -0
  186. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js +187 -0
  187. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -0
  188. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js +50 -0
  189. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js.map +1 -0
  190. package/static/browser/ldap-tree-viewer/components/TreeNode.js +140 -0
  191. package/static/browser/ldap-tree-viewer/components/TreeNode.js.map +1 -0
  192. package/static/browser/ldap-tree-viewer/components/TreeRoot.js +98 -0
  193. package/static/browser/ldap-tree-viewer/components/TreeRoot.js.map +1 -0
  194. package/static/browser/ldap-tree-viewer/index.js +2 -0
  195. package/static/browser/ldap-tree-viewer/index.js.map +1 -0
  196. package/static/browser/ldap-tree-viewer/store/Store.js +29 -0
  197. package/static/browser/ldap-tree-viewer/store/Store.js.map +1 -0
  198. package/static/browser/ldap-tree-viewer/store/actions.js +53 -0
  199. package/static/browser/ldap-tree-viewer/store/actions.js.map +1 -0
  200. package/static/browser/ldap-tree-viewer/store/reducers.js +79 -0
  201. package/static/browser/ldap-tree-viewer/store/reducers.js.map +1 -0
  202. package/static/browser/ldap-tree-viewer/types.js +2 -0
  203. package/static/browser/ldap-tree-viewer/types.js.map +1 -0
  204. package/static/browser/ldap-tree-viewer.css +2 -0
  205. package/static/browser/ldap-tree-viewer.css.map +1 -0
  206. package/static/browser/ldap-tree-viewer.esm.js +606 -0
  207. package/static/browser/ldap-tree-viewer.esm.js.map +1 -0
  208. package/static/browser/ldap-tree-viewer.js +614 -0
  209. package/static/browser/ldap-tree-viewer.js.map +1 -0
  210. package/static/browser/ldap-tree-viewer.min.js +2 -0
  211. package/static/browser/ldap-tree-viewer.min.js.map +1 -0
  212. package/static/browser/ldap-unit-editor/LdapUnitEditor.js +119 -0
  213. package/static/browser/ldap-unit-editor/LdapUnitEditor.js.map +1 -0
  214. package/static/browser/ldap-unit-editor/api/UnitApiClient.js +95 -0
  215. package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -0
  216. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js +295 -0
  217. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js.map +1 -0
  218. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +321 -0
  219. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -0
  220. package/static/browser/ldap-unit-editor/components/UnitTree.js +197 -0
  221. package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -0
  222. package/static/browser/ldap-unit-editor/index.js +2 -0
  223. package/static/browser/ldap-unit-editor/index.js.map +1 -0
  224. package/static/browser/ldap-unit-editor/types.js +2 -0
  225. package/static/browser/ldap-unit-editor/types.js.map +1 -0
  226. package/static/browser/ldap-unit-editor.esm.js +1013 -0
  227. package/static/browser/ldap-unit-editor.esm.js.map +1 -0
  228. package/static/browser/ldap-unit-editor.js +1021 -0
  229. package/static/browser/ldap-unit-editor.js.map +1 -0
  230. package/static/browser/ldap-unit-editor.min.js +2 -0
  231. package/static/browser/ldap-unit-editor.min.js.map +1 -0
  232. package/static/browser/ldap-user-editor/LdapUserEditor.js +184 -0
  233. package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -0
  234. package/static/browser/ldap-user-editor/api/UserApiClient.js +209 -0
  235. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -0
  236. package/static/browser/ldap-user-editor/cache/CacheManager.js +123 -0
  237. package/static/browser/ldap-user-editor/cache/CacheManager.js.map +1 -0
  238. package/static/browser/ldap-user-editor/components/MoveUserModal.js +302 -0
  239. package/static/browser/ldap-user-editor/components/MoveUserModal.js.map +1 -0
  240. package/static/browser/ldap-user-editor/components/PointerField.js +152 -0
  241. package/static/browser/ldap-user-editor/components/PointerField.js.map +1 -0
  242. package/static/browser/ldap-user-editor/components/UserEditor.js +608 -0
  243. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -0
  244. package/static/browser/ldap-user-editor/components/UserList.js +181 -0
  245. package/static/browser/ldap-user-editor/components/UserList.js.map +1 -0
  246. package/static/browser/ldap-user-editor/components/UserTree.js +164 -0
  247. package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -0
  248. package/static/browser/ldap-user-editor/index.js +2 -0
  249. package/static/browser/ldap-user-editor/index.js.map +1 -0
  250. package/static/browser/ldap-user-editor/types.js +2 -0
  251. package/static/browser/ldap-user-editor/types.js.map +1 -0
  252. package/static/browser/ldap-user-editor.css +2 -0
  253. package/static/browser/ldap-user-editor.css.map +1 -0
  254. package/static/browser/ldap-user-editor.esm.js +1897 -0
  255. package/static/browser/ldap-user-editor.esm.js.map +1 -0
  256. package/static/browser/ldap-user-editor.js +1905 -0
  257. package/static/browser/ldap-user-editor.js.map +1 -0
  258. package/static/browser/ldap-user-editor.min.js +2 -0
  259. package/static/browser/ldap-user-editor.min.js.map +1 -0
  260. package/static/browser/shared/components/Modal.js +121 -0
  261. package/static/browser/shared/components/Modal.js.map +1 -0
  262. package/static/browser/shared/components/StatusMessage.js +119 -0
  263. package/static/browser/shared/components/StatusMessage.js.map +1 -0
  264. package/static/browser/shared/index.js +7 -0
  265. package/static/browser/shared/index.js.map +1 -0
  266. package/static/browser/shared/types/index.js +37 -0
  267. package/static/browser/shared/types/index.js.map +1 -0
  268. package/static/browser/shared/utils/dom.js +78 -0
  269. package/static/browser/shared/utils/dom.js.map +1 -0
  270. package/static/browser/shared/utils/form.js +234 -0
  271. package/static/browser/shared/utils/form.js.map +1 -0
  272. package/static/browser/shared/utils/schema.js +121 -0
  273. package/static/browser/shared/utils/schema.js.map +1 -0
  274. package/static/browser/shared.esm.js +694 -0
  275. package/static/browser/shared.esm.js.map +1 -0
  276. package/static/browser/shared.js +723 -0
  277. package/static/browser/shared.js.map +1 -0
  278. package/static/browser/shared.min.js +2 -0
  279. package/static/browser/shared.min.js.map +1 -0
  280. package/static/schemas/ad/computers.json +92 -0
  281. package/static/schemas/ad/groups.json +55 -0
  282. package/static/schemas/ad/organizations.json +66 -0
  283. package/static/schemas/ad/users.json +116 -0
  284. package/static/schemas/standard/devices.json +72 -0
  285. package/static/schemas/standard/groups.json +54 -0
  286. package/static/schemas/standard/organizations.json +49 -0
  287. package/static/schemas/standard/users.json +86 -0
  288. package/static/schemas/twake/groups.json +75 -0
  289. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +31 -0
  290. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +31 -0
  291. package/static/schemas/twake/nomenclature/twakeListType.json +31 -0
  292. package/static/schemas/twake/nomenclature/twakeMailboxType.json +31 -0
  293. package/static/schemas/twake/nomenclature/twakeTitle.json +31 -0
  294. package/static/schemas/twake/organizations.json +52 -0
  295. package/static/schemas/twake/positions.json +32 -0
  296. 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
+ '&': '&amp;',
258
+ '<': '&lt;',
259
+ '>': '&gt;',
260
+ '"': '&quot;',
261
+ "'": '&#x27;',
262
+ '/': '&#x2F;',
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
+ '&': '&amp;',
491
+ '<': '&lt;',
492
+ '>': '&gt;',
493
+ '"': '&quot;',
494
+ "'": '&#x27;',
495
+ '/': '&#x2F;',
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
+ '&': '&amp;',
984
+ '<': '&lt;',
985
+ '>': '&gt;',
986
+ '"': '&quot;',
987
+ "'": '&#x27;',
988
+ '/': '&#x2F;',
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
+ '&': '&amp;',
1194
+ '<': '&lt;',
1195
+ '>': '&gt;',
1196
+ '"': '&quot;',
1197
+ "'": '&#x27;',
1198
+ '/': '&#x2F;',
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
+ '&': '&amp;',
1366
+ '<': '&lt;',
1367
+ '>': '&gt;',
1368
+ '"': '&quot;',
1369
+ "'": '&#x27;',
1370
+ '/': '&#x2F;',
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