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