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,1013 @@
1
+ /**
2
+ * Unit Tree Component - Shows organizations tree (same as UserTree)
3
+ */
4
+ class UnitTree {
5
+ constructor(container, apiBaseUrl, onOrgSelect) {
6
+ this.expandedNodes = new Set();
7
+ this.selectedDn = null;
8
+ this.rootDn = null;
9
+ this.container = container;
10
+ this.apiBaseUrl = apiBaseUrl;
11
+ this.onOrgSelect = onOrgSelect;
12
+ }
13
+ async init() {
14
+ this.container.innerHTML = `
15
+ <div class="unit-tree">
16
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
17
+ <h3 style="margin: 0; display: flex; align-items: center; gap: 8px;">
18
+ <span class="material-icons">account_tree</span>
19
+ Organizations
20
+ </h3>
21
+ <button id="create-unit-btn" style="background: var(--primary-color, #6200ee); color: white; border: none; border-radius: 4px; padding: 8px 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; font-size: 14px;">
22
+ <span class="material-icons" style="font-size: 18px;">add</span>
23
+ Create
24
+ </button>
25
+ </div>
26
+ <div id="tree-content">
27
+ <div class="loading"><div class="spinner"></div></div>
28
+ </div>
29
+ </div>
30
+ `;
31
+ try {
32
+ const response = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations/top`);
33
+ if (!response.ok)
34
+ throw new Error('Failed to load organizations');
35
+ const topOrg = await response.json();
36
+ this.rootDn = topOrg.dn;
37
+ await this.renderTree();
38
+ // Attach create button listener
39
+ const createBtn = this.container.querySelector('#create-unit-btn');
40
+ if (createBtn) {
41
+ createBtn.addEventListener('click', () => this.handleCreateUnit());
42
+ }
43
+ }
44
+ catch (error) {
45
+ console.error('Failed to init tree:', error);
46
+ const treeEl = this.container.querySelector('#tree-content');
47
+ if (treeEl) {
48
+ treeEl.innerHTML =
49
+ '<p style="color: #f44336;">Failed to load organizations</p>';
50
+ }
51
+ }
52
+ }
53
+ async handleCreateUnit() {
54
+ const parentDn = this.selectedDn || this.rootDn;
55
+ if (!parentDn) {
56
+ alert('Please select a parent organization first');
57
+ return;
58
+ }
59
+ // eslint-disable-next-line no-undef
60
+ const ouName = prompt('Enter the name for the new organizational unit (ou):');
61
+ if (!ouName || !ouName.trim())
62
+ return;
63
+ try {
64
+ const response = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations`, {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({
68
+ ou: ouName.trim(),
69
+ parentDn,
70
+ }),
71
+ });
72
+ if (!response.ok) {
73
+ const error = await response.text();
74
+ throw new Error(error || 'Failed to create organizational unit');
75
+ }
76
+ alert(`Organizational unit "${ouName}" created successfully!`);
77
+ // Expand the parent node and refresh tree
78
+ this.expandedNodes.add(parentDn);
79
+ await this.renderTree();
80
+ }
81
+ catch (error) {
82
+ console.error('Failed to create unit:', error);
83
+ alert(`Failed to create unit: ${error.message}`);
84
+ }
85
+ }
86
+ async renderTree() {
87
+ const treeEl = this.container.querySelector('#tree-content');
88
+ if (!treeEl || !this.rootDn)
89
+ return;
90
+ try {
91
+ const html = await this.renderNode(this.rootDn, 0);
92
+ treeEl.innerHTML = html;
93
+ this.attachEventListeners();
94
+ }
95
+ catch (error) {
96
+ console.error('Failed to render tree:', error);
97
+ treeEl.innerHTML = '<p style="color: #f44336;">Error rendering tree</p>';
98
+ }
99
+ }
100
+ async renderNode(dn, level) {
101
+ const isExpanded = this.expandedNodes.has(dn);
102
+ const isSelected = this.selectedDn === dn;
103
+ const indent = level * 20;
104
+ // Get node info
105
+ const response = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
106
+ if (!response.ok)
107
+ throw new Error('Failed to load node');
108
+ const node = await response.json();
109
+ // Extract display name
110
+ let displayName = dn;
111
+ if (node.ou) {
112
+ displayName = Array.isArray(node.ou) ? node.ou[0] : node.ou;
113
+ }
114
+ else if (node.cn) {
115
+ displayName = Array.isArray(node.cn) ? node.cn[0] : node.cn;
116
+ }
117
+ let html = `
118
+ <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;">
119
+ <span class="material-icons tree-node-toggle" data-dn="${this.escapeHtml(dn)}" style="font-size: 18px; cursor: pointer;">
120
+ ${isExpanded ? 'expand_more' : 'chevron_right'}
121
+ </span>
122
+ <span class="material-icons" style="font-size: 18px;">business</span>
123
+ <span class="tree-node-label" data-dn="${this.escapeHtml(dn)}">${this.escapeHtml(displayName)}</span>
124
+ </div>
125
+ `;
126
+ if (isExpanded) {
127
+ try {
128
+ const subnodesResponse = await fetch(`${this.apiBaseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes`);
129
+ if (!subnodesResponse.ok)
130
+ throw new Error('Failed to load subnodes');
131
+ const subnodes = await subnodesResponse.json();
132
+ const orgs = subnodes.filter((n) => {
133
+ const classes = Array.isArray(n.objectClass)
134
+ ? n.objectClass
135
+ : n.objectClass
136
+ ? [n.objectClass]
137
+ : [];
138
+ return (classes.includes('organizationalUnit') ||
139
+ classes.includes('organization'));
140
+ });
141
+ for (const subnode of orgs) {
142
+ html += await this.renderNode(subnode.dn, level + 1);
143
+ }
144
+ }
145
+ catch (error) {
146
+ console.error('Failed to load subnodes for', dn, error);
147
+ }
148
+ }
149
+ return html;
150
+ }
151
+ attachEventListeners() {
152
+ // Toggle expand/collapse
153
+ this.container.querySelectorAll('.tree-node-toggle').forEach(el => {
154
+ el.addEventListener('click', async (e) => {
155
+ e.stopPropagation();
156
+ const dn = el.getAttribute('data-dn');
157
+ if (!dn)
158
+ return;
159
+ if (this.expandedNodes.has(dn)) {
160
+ this.expandedNodes.delete(dn);
161
+ }
162
+ else {
163
+ this.expandedNodes.add(dn);
164
+ }
165
+ await this.renderTree();
166
+ });
167
+ });
168
+ // Select organization
169
+ this.container.querySelectorAll('.tree-node-label').forEach(el => {
170
+ el.addEventListener('click', e => {
171
+ e.stopPropagation();
172
+ const dn = el.getAttribute('data-dn');
173
+ if (!dn)
174
+ return;
175
+ this.selectedDn = dn;
176
+ this.onOrgSelect(dn);
177
+ this.renderTree();
178
+ });
179
+ });
180
+ }
181
+ escapeHtml(text) {
182
+ if (text == null)
183
+ return '';
184
+ const map = {
185
+ '&': '&amp;',
186
+ '<': '&lt;',
187
+ '>': '&gt;',
188
+ '"': '&quot;',
189
+ "'": '&#x27;',
190
+ '/': '&#x2F;',
191
+ };
192
+ return String(text).replace(/[&<>"'/]/g, m => map[m]);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * LDAP Unit Editor - Move Unit Modal Component
198
+ * Modal dialog for selecting target organization when moving a unit
199
+ */
200
+ class MoveUnitModal {
201
+ constructor(api, currentOrgDn, onMove) {
202
+ this.modal = null;
203
+ this.selectedOrgDn = null;
204
+ this.expandedNodes = new Set();
205
+ this.rootDn = null;
206
+ this.api = api;
207
+ this.currentOrgDn = currentOrgDn;
208
+ this.onMove = onMove;
209
+ }
210
+ /**
211
+ * Escape HTML special characters to prevent XSS attacks
212
+ */
213
+ static escapeHtml(text) {
214
+ if (text == null)
215
+ return '';
216
+ const map = {
217
+ '&': '&amp;',
218
+ '<': '&lt;',
219
+ '>': '&gt;',
220
+ '"': '&quot;',
221
+ "'": '&#x27;',
222
+ '/': '&#x2F;',
223
+ };
224
+ return String(text).replace(/[&<>"'/]/g, m => map[m]);
225
+ }
226
+ async show() {
227
+ // Create modal container
228
+ this.modal = document.createElement('div');
229
+ this.modal.className = 'modal-overlay active';
230
+ this.modal.innerHTML = `
231
+ <div class="modal-container" style="max-width: 600px;">
232
+ <div class="modal-header">
233
+ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
234
+ <span class="material-icons">folder_open</span>
235
+ Move Unit to Organization
236
+ </h3>
237
+ <button class="modal-close" id="modal-close-btn">
238
+ <span class="material-icons">close</span>
239
+ </button>
240
+ </div>
241
+ <div class="modal-body">
242
+ <div id="move-modal-alert"></div>
243
+ <div style="margin-bottom: 1rem; color: #666; font-size: 0.9rem;">
244
+ Select the target organization where you want to move this unit.
245
+ </div>
246
+ <div id="organization-tree-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 0.5rem;">
247
+ <div class="loading">
248
+ <div class="spinner"></div>
249
+ <p style="margin-top: 1rem">Loading organizations...</p>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ <div class="modal-footer">
254
+ <button type="button" class="btn btn-secondary" id="modal-cancel-btn">
255
+ Cancel
256
+ </button>
257
+ <button type="button" class="btn btn-primary" id="modal-move-btn" disabled>
258
+ <span class="material-icons">drive_file_move</span>
259
+ Move Unit
260
+ </button>
261
+ </div>
262
+ </div>
263
+ `;
264
+ document.body.appendChild(this.modal);
265
+ this.attachHandlers();
266
+ await this.loadOrganizationTree();
267
+ }
268
+ attachHandlers() {
269
+ if (!this.modal)
270
+ return;
271
+ // Close button
272
+ const closeBtn = this.modal.querySelector('#modal-close-btn');
273
+ closeBtn?.addEventListener('click', () => this.close());
274
+ // Cancel button
275
+ const cancelBtn = this.modal.querySelector('#modal-cancel-btn');
276
+ cancelBtn?.addEventListener('click', () => this.close());
277
+ // Move button
278
+ const moveBtn = this.modal.querySelector('#modal-move-btn');
279
+ moveBtn?.addEventListener('click', () => this.handleMove());
280
+ // Close on overlay click
281
+ this.modal.addEventListener('click', e => {
282
+ if (e.target === this.modal) {
283
+ this.close();
284
+ }
285
+ });
286
+ // Close on Escape key
287
+ const handleEscape = (e) => {
288
+ if (e.key === 'Escape') {
289
+ this.close();
290
+ document.removeEventListener('keydown', handleEscape);
291
+ }
292
+ };
293
+ document.addEventListener('keydown', handleEscape);
294
+ }
295
+ async loadOrganizationTree() {
296
+ const container = this.modal?.querySelector('#organization-tree-container');
297
+ if (!container)
298
+ return;
299
+ try {
300
+ // Get top organization
301
+ const topOrg = await this.api.getOrganizations();
302
+ this.rootDn = topOrg.dn;
303
+ // Expand root by default
304
+ this.expandedNodes.add(this.rootDn);
305
+ await this.renderTree();
306
+ }
307
+ catch (error) {
308
+ container.innerHTML = `
309
+ <div class="alert alert-error">
310
+ <span class="material-icons">error</span>
311
+ <div>Failed to load organizations: ${MoveUnitModal.escapeHtml(error.message)}</div>
312
+ </div>
313
+ `;
314
+ }
315
+ }
316
+ async renderTree() {
317
+ const container = this.modal?.querySelector('#organization-tree-container');
318
+ if (!container || !this.rootDn)
319
+ return;
320
+ try {
321
+ const html = await this.renderNode(this.rootDn, 0);
322
+ container.innerHTML = html;
323
+ this.attachTreeHandlers();
324
+ }
325
+ catch (error) {
326
+ container.innerHTML = `
327
+ <div class="alert alert-error">
328
+ <span class="material-icons">error</span>
329
+ <div>Error rendering tree: ${MoveUnitModal.escapeHtml(error.message)}</div>
330
+ </div>
331
+ `;
332
+ }
333
+ }
334
+ async renderNode(dn, level) {
335
+ const isExpanded = this.expandedNodes.has(dn);
336
+ const isCurrentOrg = dn === this.currentOrgDn;
337
+ const isSelected = dn === this.selectedOrgDn;
338
+ const indent = level * 20;
339
+ // Get node info
340
+ const node = await this.api.getUnit(dn);
341
+ // Extract display name
342
+ const ou = Array.isArray(node.ou) ? node.ou[0] : node.ou;
343
+ const path = node.twakeDepartmentPath
344
+ ? Array.isArray(node.twakeDepartmentPath)
345
+ ? node.twakeDepartmentPath[0]
346
+ : node.twakeDepartmentPath
347
+ : undefined;
348
+ const displayName = path || ou || dn;
349
+ let html = `
350
+ <div
351
+ class="org-node ${isCurrentOrg ? 'org-current' : ''} ${isSelected ? 'org-selected' : ''}"
352
+ data-org-dn="${MoveUnitModal.escapeHtml(dn)}"
353
+ style="
354
+ padding: 0.5rem;
355
+ padding-left: ${indent + 8}px;
356
+ cursor: ${isCurrentOrg ? 'not-allowed' : 'pointer'};
357
+ background: ${isCurrentOrg ? '#f5f5f5' : isSelected ? '#e3f2fd' : 'white'};
358
+ opacity: ${isCurrentOrg ? '0.6' : '1'};
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 0.5rem;
362
+ border-left: 2px solid ${isSelected ? '#2196f3' : 'transparent'};
363
+ "
364
+ >
365
+ <span class="material-icons tree-toggle" data-dn="${MoveUnitModal.escapeHtml(dn)}" style="cursor: pointer; font-size: 1.2rem;">
366
+ ${isExpanded ? 'expand_more' : 'chevron_right'}
367
+ </span>
368
+ <span class="material-icons" style="font-size: 1.2rem; color: ${isCurrentOrg ? '#999' : '#2196f3'};">
369
+ ${isCurrentOrg ? 'business' : 'folder'}
370
+ </span>
371
+ <span style="flex: 1; font-weight: ${isSelected ? '600' : '500'};">
372
+ ${MoveUnitModal.escapeHtml(displayName)}
373
+ ${isCurrentOrg ? '<span style="font-size: 0.85rem; color: #666; margin-left: 0.5rem;">(current)</span>' : ''}
374
+ </span>
375
+ ${isSelected ? '<span class="material-icons" style="color: #2196f3;">check_circle</span>' : ''}
376
+ </div>
377
+ `;
378
+ // If expanded, load and render subnodes
379
+ if (isExpanded) {
380
+ try {
381
+ // Use the organizations subnodes API to get only organizational units
382
+ const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes`);
383
+ if (!response.ok) {
384
+ throw new Error('Failed to load subnodes');
385
+ }
386
+ const subnodes = await response.json();
387
+ // Filter for organizational units only
388
+ const orgs = subnodes.filter((n) => {
389
+ const classes = Array.isArray(n.objectClass)
390
+ ? n.objectClass
391
+ : n.objectClass
392
+ ? [n.objectClass]
393
+ : [];
394
+ return (classes.includes('organizationalUnit') ||
395
+ classes.includes('organization'));
396
+ });
397
+ for (const subnode of orgs) {
398
+ html += await this.renderNode(subnode.dn, level + 1);
399
+ }
400
+ }
401
+ catch (error) {
402
+ console.error('Failed to load subnodes for', dn, error);
403
+ // No subnodes or error loading them
404
+ }
405
+ }
406
+ return html;
407
+ }
408
+ attachTreeHandlers() {
409
+ // Handle tree toggle (expand/collapse)
410
+ const toggles = this.modal?.querySelectorAll('.tree-toggle');
411
+ toggles?.forEach(toggle => {
412
+ toggle.addEventListener('click', async (e) => {
413
+ e.stopPropagation();
414
+ const dn = toggle.getAttribute('data-dn');
415
+ if (!dn)
416
+ return;
417
+ if (this.expandedNodes.has(dn)) {
418
+ this.expandedNodes.delete(dn);
419
+ }
420
+ else {
421
+ this.expandedNodes.add(dn);
422
+ }
423
+ await this.renderTree();
424
+ });
425
+ });
426
+ // Handle node selection
427
+ const nodes = this.modal?.querySelectorAll('.org-node');
428
+ nodes?.forEach(node => {
429
+ const orgDn = node.getAttribute('data-org-dn');
430
+ if (!orgDn || orgDn === this.currentOrgDn)
431
+ return;
432
+ node.addEventListener('click', () => {
433
+ this.selectOrganization(orgDn);
434
+ });
435
+ });
436
+ }
437
+ async selectOrganization(orgDn) {
438
+ this.selectedOrgDn = orgDn;
439
+ // Re-render tree to update selection state
440
+ await this.renderTree();
441
+ // Enable move button
442
+ const moveBtn = this.modal?.querySelector('#modal-move-btn');
443
+ if (moveBtn) {
444
+ moveBtn.disabled = false;
445
+ }
446
+ }
447
+ async handleMove() {
448
+ if (!this.selectedOrgDn)
449
+ return;
450
+ const moveBtn = this.modal?.querySelector('#modal-move-btn');
451
+ const alertContainer = this.modal?.querySelector('#move-modal-alert');
452
+ if (moveBtn) {
453
+ moveBtn.disabled = true;
454
+ moveBtn.innerHTML = `
455
+ <span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px;"></span>
456
+ Moving...
457
+ `;
458
+ }
459
+ try {
460
+ this.onMove(this.selectedOrgDn);
461
+ this.close();
462
+ }
463
+ catch (error) {
464
+ if (alertContainer) {
465
+ alertContainer.innerHTML = `
466
+ <div class="alert alert-error" style="margin-bottom: 1rem;">
467
+ <span class="material-icons">error</span>
468
+ <div>Move failed: ${MoveUnitModal.escapeHtml(error.message)}</div>
469
+ </div>
470
+ `;
471
+ }
472
+ if (moveBtn) {
473
+ moveBtn.disabled = false;
474
+ moveBtn.innerHTML = `
475
+ <span class="material-icons">drive_file_move</span>
476
+ Move Unit
477
+ `;
478
+ }
479
+ }
480
+ }
481
+ close() {
482
+ if (this.modal) {
483
+ this.modal.remove();
484
+ this.modal = null;
485
+ }
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Unit Property Editor Component - Edits properties of a selected organizational unit
491
+ */
492
+ class UnitPropertyEditor {
493
+ constructor(container, api, unitDn) {
494
+ this.unit = null;
495
+ this.schema = null;
496
+ this.saveCallback = null;
497
+ this.hasSubnodes = false;
498
+ this.container = container;
499
+ this.api = api;
500
+ this.unitDn = unitDn;
501
+ }
502
+ async init() {
503
+ await this.loadUnit();
504
+ await this.loadSchema();
505
+ await this.checkSubnodes();
506
+ this.render();
507
+ this.attachEventListeners();
508
+ }
509
+ async loadUnit() {
510
+ this.unit = await this.api.getUnit(this.unitDn);
511
+ }
512
+ async checkSubnodes() {
513
+ try {
514
+ const response = await fetch(`${this.api['baseUrl']}/api/v1/ldap/organizations/${encodeURIComponent(this.unitDn)}/subnodes`);
515
+ if (response.ok) {
516
+ const subnodes = await response.json();
517
+ // Check if there are any organizational subnodes
518
+ this.hasSubnodes = subnodes.some((node) => {
519
+ const classes = Array.isArray(node.objectClass)
520
+ ? node.objectClass
521
+ : node.objectClass
522
+ ? [node.objectClass]
523
+ : [];
524
+ return (classes.includes('organizationalUnit') ||
525
+ classes.includes('organization'));
526
+ });
527
+ }
528
+ }
529
+ catch (error) {
530
+ console.error('Failed to check subnodes:', error);
531
+ // On error, assume it might have subnodes to be safe
532
+ this.hasSubnodes = false;
533
+ }
534
+ }
535
+ async loadSchema() {
536
+ console.warn('[UnitPropertyEditor] Loading schema...');
537
+ const config = await this.api.getConfig();
538
+ // First check in features.ldapOrganizations (from ldapOrganizations plugin)
539
+ const orgsConfig = config.features?.ldapOrganizations;
540
+ console.warn('[UnitPropertyEditor] Organizations config:', orgsConfig);
541
+ if (orgsConfig?.schemaUrl) {
542
+ console.warn('[UnitPropertyEditor] Loading schema from URL:', orgsConfig.schemaUrl);
543
+ const response = await fetch(orgsConfig.schemaUrl);
544
+ this.schema = await response.json();
545
+ console.warn('[UnitPropertyEditor] Schema loaded from URL:', this.schema);
546
+ }
547
+ else if (orgsConfig?.schema) {
548
+ console.warn('[UnitPropertyEditor] Using schema from config');
549
+ this.schema = orgsConfig.schema;
550
+ }
551
+ else {
552
+ // Fallback: check in flatResources
553
+ console.warn('[UnitPropertyEditor] Falling back to flatResources');
554
+ const unitsResource = config.features?.ldapFlatGeneric?.flatResources?.find(r => r.pluralName === 'organizations' || r.name === 'organizations');
555
+ if (unitsResource?.schemaUrl) {
556
+ const response = await fetch(unitsResource.schemaUrl);
557
+ this.schema = await response.json();
558
+ }
559
+ else if (unitsResource?.schema) {
560
+ this.schema = unitsResource.schema;
561
+ }
562
+ else {
563
+ console.warn('[UnitPropertyEditor] No schema found, using default');
564
+ // Create a minimal default schema
565
+ this.schema = {
566
+ entity: {
567
+ objectClass: ['organizationalUnit'],
568
+ },
569
+ attributes: {
570
+ ou: { type: 'string', required: true },
571
+ description: { type: 'string' },
572
+ },
573
+ };
574
+ }
575
+ }
576
+ }
577
+ onSave(callback) {
578
+ this.saveCallback = callback;
579
+ }
580
+ render() {
581
+ if (!this.unit)
582
+ return;
583
+ this.container.innerHTML = `
584
+ <div class="unit-property-editor">
585
+ <h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px;">
586
+ <span class="material-icons">business</span>
587
+ Edit Organizational Unit
588
+ </h3>
589
+ <form id="unit-edit-form">
590
+ ${this.renderFields()}
591
+ <div class="form-actions" style="margin-top: 24px; padding-top: 24px; border-top: 1px solid #e0e0e0; display: flex; gap: 12px; justify-content: space-between;">
592
+ <button type="button" id="move-unit-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;">
593
+ <span class="material-icons" style="font-size: 18px;">drive_file_move</span>
594
+ Move
595
+ </button>
596
+ <div style="display: flex; gap: 12px;">
597
+ <button type="button" id="delete-unit-btn" class="btn btn-danger" ${this.hasSubnodes ? 'disabled title="Cannot delete: organization has sub-organizations"' : ''} style="background: ${this.hasSubnodes ? '#ccc' : '#d32f2f'}; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: ${this.hasSubnodes ? 'not-allowed' : 'pointer'}; display: flex; align-items: center; gap: 8px; opacity: ${this.hasSubnodes ? '0.5' : '1'};">
598
+ <span class="material-icons" style="font-size: 18px;">delete</span>
599
+ Delete Unit
600
+ </button>
601
+ <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;">
602
+ <span class="material-icons" style="font-size: 18px;">save</span>
603
+ Save Changes
604
+ </button>
605
+ </div>
606
+ </div>
607
+ </form>
608
+ </div>
609
+ `;
610
+ }
611
+ renderFields() {
612
+ if (!this.unit || !this.schema) {
613
+ return '<div>Loading...</div>';
614
+ }
615
+ let html = '';
616
+ // Group fields by category
617
+ const basicFields = [];
618
+ const otherFields = [];
619
+ for (const [fieldName, attribute] of Object.entries(this.schema.attributes)) {
620
+ if (attribute.fixed || fieldName === 'objectClass')
621
+ continue;
622
+ if (fieldName === 'ou' || fieldName === 'description') {
623
+ basicFields.push([fieldName, attribute]);
624
+ }
625
+ else {
626
+ otherFields.push([fieldName, attribute]);
627
+ }
628
+ }
629
+ // Basic Information
630
+ if (basicFields.length > 0) {
631
+ html +=
632
+ '<div class="field-group" style="margin-bottom: 20px; padding: 16px; background: #f9f9f9; border-radius: 4px;">';
633
+ html +=
634
+ '<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>';
635
+ for (const [fieldName, attribute] of basicFields) {
636
+ html += this.renderField(fieldName, attribute);
637
+ }
638
+ html += '</div>';
639
+ }
640
+ // Additional Properties
641
+ if (otherFields.length > 0) {
642
+ html +=
643
+ '<div class="field-group" style="margin-bottom: 20px; padding: 16px; background: #f9f9f9; border-radius: 4px;">';
644
+ html +=
645
+ '<div class="field-group-title" style="font-weight: 500; color: var(--primary-color, #6200ee); margin-bottom: 12px; font-size: 14px; text-transform: uppercase;">Additional Properties</div>';
646
+ for (const [fieldName, attribute] of otherFields) {
647
+ html += this.renderField(fieldName, attribute);
648
+ }
649
+ html += '</div>';
650
+ }
651
+ return html;
652
+ }
653
+ renderField(fieldName, attribute) {
654
+ const value = this.unit?.[fieldName];
655
+ const label = this.getFieldLabel(fieldName);
656
+ const required = attribute.required ? 'required' : '';
657
+ if (attribute.type === 'array') {
658
+ const arrayValue = Array.isArray(value) ? value.join('\n') : '';
659
+ return `
660
+ <div class="form-group" style="margin-bottom: 16px;">
661
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
662
+ <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>
663
+ <div style="font-size: 12px; color: #666; margin-top: 4px;">Enter one value per line</div>
664
+ </div>
665
+ `;
666
+ }
667
+ if (attribute.type === 'number' || attribute.type === 'integer') {
668
+ return `
669
+ <div class="form-group" style="margin-bottom: 16px;">
670
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
671
+ <input type="number" name="${this.escapeHtml(fieldName)}" value="${value || ''}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" ${required} />
672
+ </div>
673
+ `;
674
+ }
675
+ // String field
676
+ return `
677
+ <div class="form-group" style="margin-bottom: 16px;">
678
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;">${this.escapeHtml(label)}</label>
679
+ <input type="text" name="${this.escapeHtml(fieldName)}" value="${this.escapeHtml(String(value || ''))}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" ${required} />
680
+ </div>
681
+ `;
682
+ }
683
+ getFieldLabel(fieldName) {
684
+ const labelMap = {
685
+ ou: 'Organization Unit',
686
+ description: 'Description',
687
+ };
688
+ return (labelMap[fieldName] ||
689
+ fieldName
690
+ .replace(/([A-Z])/g, ' $1')
691
+ .replace(/_/g, ' ')
692
+ .replace(/^./, str => str.toUpperCase()));
693
+ }
694
+ attachEventListeners() {
695
+ const form = document.getElementById('unit-edit-form');
696
+ if (form) {
697
+ form.addEventListener('submit', e => this.handleSubmit(e));
698
+ }
699
+ const deleteBtn = document.getElementById('delete-unit-btn');
700
+ if (deleteBtn) {
701
+ deleteBtn.addEventListener('click', () => this.handleDelete());
702
+ }
703
+ const moveBtn = document.getElementById('move-unit-btn');
704
+ if (moveBtn) {
705
+ moveBtn.addEventListener('click', () => this.handleMove());
706
+ }
707
+ }
708
+ async handleSubmit(e) {
709
+ e.preventDefault();
710
+ const form = e.target;
711
+ const formData = new FormData(form);
712
+ const updates = {};
713
+ for (const [key, value] of formData.entries()) {
714
+ if (this.schema?.attributes[key]?.type === 'array') {
715
+ updates[key] = value
716
+ .split('\n')
717
+ .map(v => v.trim())
718
+ .filter(v => v);
719
+ }
720
+ else if (this.schema?.attributes[key]?.type === 'number' ||
721
+ this.schema?.attributes[key]?.type === 'integer') {
722
+ updates[key] = Number(value);
723
+ }
724
+ else {
725
+ updates[key] = value;
726
+ }
727
+ }
728
+ try {
729
+ await this.api.updateUnit(this.unitDn, updates);
730
+ alert('Organizational unit updated successfully!');
731
+ if (this.saveCallback) {
732
+ this.saveCallback();
733
+ }
734
+ }
735
+ catch (error) {
736
+ console.error('Failed to update unit:', error);
737
+ alert(`Failed to update unit: ${error.message}`);
738
+ }
739
+ }
740
+ async handleMove() {
741
+ if (!this.unit)
742
+ return;
743
+ // Extract parent DN from current unit DN
744
+ const parentDn = this.unitDn.split(',').slice(1).join(',');
745
+ const modal = new MoveUnitModal(this.api, parentDn, async (targetOrgDn) => {
746
+ try {
747
+ const result = await this.api.moveUnit(this.unitDn, targetOrgDn);
748
+ alert('Unit moved successfully!');
749
+ // Reload the unit with its new DN
750
+ if (result.newDn) {
751
+ this.unitDn = result.newDn;
752
+ await this.loadUnit();
753
+ this.render();
754
+ this.attachEventListeners();
755
+ }
756
+ if (this.saveCallback) {
757
+ this.saveCallback();
758
+ }
759
+ }
760
+ catch (error) {
761
+ console.error('Failed to move unit:', error);
762
+ alert(`Failed to move unit: ${error.message}`);
763
+ }
764
+ });
765
+ await modal.show();
766
+ }
767
+ async handleDelete() {
768
+ // Don't allow delete if there are subnodes
769
+ if (this.hasSubnodes) {
770
+ alert('Cannot delete this organizational unit because it contains sub-organizations. Please delete all sub-organizations first.');
771
+ return;
772
+ }
773
+ if (!confirm(`Are you sure you want to delete this organizational unit?\n\n${this.unitDn}\n\nThis action cannot be undone.`)) {
774
+ return;
775
+ }
776
+ try {
777
+ await this.api.deleteEntry(this.unitDn);
778
+ alert('Organizational unit deleted successfully!');
779
+ this.container.innerHTML = `
780
+ <div class="empty-state">
781
+ <span class="material-icons">business</span>
782
+ <p>Unit deleted. Select another unit to edit.</p>
783
+ </div>
784
+ `;
785
+ }
786
+ catch (error) {
787
+ console.error('Failed to delete unit:', error);
788
+ alert(`Failed to delete unit: ${error.message}`);
789
+ }
790
+ }
791
+ escapeHtml(text) {
792
+ if (text == null)
793
+ return '';
794
+ const map = {
795
+ '&': '&amp;',
796
+ '<': '&lt;',
797
+ '>': '&gt;',
798
+ '"': '&quot;',
799
+ "'": '&#x27;',
800
+ '/': '&#x2F;',
801
+ };
802
+ return String(text).replace(/[&<>"'/]/g, m => map[m]);
803
+ }
804
+ }
805
+
806
+ /**
807
+ * API Client for LDAP Unit operations
808
+ */
809
+ class UnitApiClient {
810
+ constructor(baseUrl) {
811
+ this.baseUrl = baseUrl;
812
+ }
813
+ async getConfig() {
814
+ const response = await fetch(`${this.baseUrl}/api/v1/config`);
815
+ if (!response.ok) {
816
+ throw new Error(`Failed to load config: ${response.statusText}`);
817
+ }
818
+ return response.json();
819
+ }
820
+ async getOrganizations() {
821
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/top`);
822
+ if (!response.ok) {
823
+ throw new Error(`Failed to load organizations: ${response.statusText}`);
824
+ }
825
+ return response.json();
826
+ }
827
+ async getUnit(dn) {
828
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
829
+ if (!response.ok) {
830
+ throw new Error(`Failed to load unit: ${response.statusText}`);
831
+ }
832
+ return response.json();
833
+ }
834
+ async updateUnit(dn, data) {
835
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`, {
836
+ method: 'PUT',
837
+ headers: { 'Content-Type': 'application/json' },
838
+ body: JSON.stringify(data),
839
+ });
840
+ if (!response.ok) {
841
+ const error = await response.text();
842
+ throw new Error(`Failed to update unit: ${error}`);
843
+ }
844
+ }
845
+ async createEntry(dn, data) {
846
+ // Use the organizations API for creating units
847
+ // Extract ou and parentDn from DN
848
+ // DN format: ou=value,parent,dn,parts
849
+ const match = dn.match(/^ou=([^,]+),(.+)$/);
850
+ if (!match) {
851
+ throw new Error(`Invalid DN format: ${dn}`);
852
+ }
853
+ const [, ou, parentDn] = match;
854
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations`, {
855
+ method: 'POST',
856
+ headers: { 'Content-Type': 'application/json' },
857
+ body: JSON.stringify({ ou, parentDn, ...data }),
858
+ });
859
+ if (!response.ok) {
860
+ const error = await response.text();
861
+ throw new Error(`Failed to create entry: ${error}`);
862
+ }
863
+ }
864
+ async deleteEntry(dn) {
865
+ // Use the organizations API for deleting units
866
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`, {
867
+ method: 'DELETE',
868
+ });
869
+ if (!response.ok) {
870
+ const error = await response.text();
871
+ throw new Error(`Failed to delete entry: ${error}`);
872
+ }
873
+ }
874
+ async getPointerOptions(branch) {
875
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/search?base=${encodeURIComponent(branch)}&scope=one`);
876
+ if (!response.ok) {
877
+ throw new Error(`Failed to load pointer options: ${response.statusText}`);
878
+ }
879
+ const entries = await response.json();
880
+ return entries.map((entry) => ({
881
+ dn: entry.dn,
882
+ label: entry.cn?.[0] || entry.ou?.[0] || entry.dn,
883
+ }));
884
+ }
885
+ async moveUnit(dn, targetOrgDn) {
886
+ const response = await fetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/move`, {
887
+ method: 'POST',
888
+ headers: { 'Content-Type': 'application/json' },
889
+ body: JSON.stringify({ targetOrgDn }),
890
+ });
891
+ if (!response.ok) {
892
+ const error = await response.text();
893
+ throw new Error(error || 'Failed to move unit');
894
+ }
895
+ return response.json();
896
+ }
897
+ }
898
+
899
+ /**
900
+ * LDAP Unit Editor - Main component for editing organizational units
901
+ */
902
+ class LdapUnitEditor {
903
+ constructor(options) {
904
+ this.container = null;
905
+ this.unitTree = null;
906
+ this.unitEditor = null;
907
+ this.currentUnitDn = null;
908
+ this.config = null;
909
+ this.options = options;
910
+ this.api = new UnitApiClient(options.apiBaseUrl || '');
911
+ }
912
+ async init() {
913
+ const container = document.getElementById(this.options.containerId);
914
+ if (!container) {
915
+ throw new Error(`Container element with id '${this.options.containerId}' not found`);
916
+ }
917
+ this.container = container;
918
+ // Load config first
919
+ this.config = await this.api.getConfig();
920
+ this.render();
921
+ await this.initializeComponents();
922
+ }
923
+ render() {
924
+ if (!this.container)
925
+ return;
926
+ this.container.innerHTML = `
927
+ <div class="ldap-unit-editor" style="display: flex; gap: 24px; height: 100%;">
928
+ <div id="unit-tree-container" style="flex: 0 0 300px; overflow-y: auto; border-right: 1px solid #e0e0e0; padding-right: 24px;"></div>
929
+ <div id="unit-editor-container" style="flex: 1; overflow-y: auto;">
930
+ <div class="empty-state" style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #999;">
931
+ <span class="material-icons" style="font-size: 64px; margin-bottom: 16px;">business</span>
932
+ <p style="font-size: 16px; margin: 0;">Select an organizational unit to edit</p>
933
+ </div>
934
+ </div>
935
+ </div>
936
+ `;
937
+ }
938
+ async initializeComponents() {
939
+ const treeContainer = document.getElementById('unit-tree-container');
940
+ if (!treeContainer)
941
+ return;
942
+ this.unitTree = new UnitTree(treeContainer, this.options.apiBaseUrl || '', unitDn => this.onUnitSelected(unitDn));
943
+ await this.unitTree.init();
944
+ }
945
+ async onUnitSelected(unitDn) {
946
+ this.currentUnitDn = unitDn;
947
+ const editorContainer = document.getElementById('unit-editor-container');
948
+ if (!editorContainer)
949
+ return;
950
+ try {
951
+ this.unitEditor = new UnitPropertyEditor(editorContainer, this.api, unitDn);
952
+ await this.unitEditor.init();
953
+ this.unitEditor.onSave(() => {
954
+ if (this.options.onUnitSaved) {
955
+ this.options.onUnitSaved(unitDn);
956
+ }
957
+ });
958
+ }
959
+ catch (error) {
960
+ console.error('Failed to load unit editor:', error);
961
+ editorContainer.innerHTML = `
962
+ <div class="error-state" style="padding: 24px; color: #f44336;">
963
+ <h3>Error loading unit</h3>
964
+ <p>${error.message}</p>
965
+ </div>
966
+ `;
967
+ if (this.options.onError) {
968
+ this.options.onError(error);
969
+ }
970
+ }
971
+ }
972
+ getCurrentUnitDn() {
973
+ return this.currentUnitDn;
974
+ }
975
+ async refresh() {
976
+ if (this.unitTree) {
977
+ await this.unitTree.init();
978
+ }
979
+ if (this.currentUnitDn && this.unitEditor) {
980
+ const editorContainer = document.getElementById('unit-editor-container');
981
+ if (editorContainer) {
982
+ this.unitEditor = new UnitPropertyEditor(editorContainer, this.api, this.currentUnitDn);
983
+ await this.unitEditor.init();
984
+ }
985
+ }
986
+ }
987
+ // Public API for compatibility with tests
988
+ getApi() {
989
+ return this.api;
990
+ }
991
+ getConfig() {
992
+ return this.config;
993
+ }
994
+ getCurrentOrgDn() {
995
+ // For units, the current org is the current unit
996
+ return this.currentUnitDn;
997
+ }
998
+ getCurrentUserDn() {
999
+ // For compatibility - in unit context, "user" means "unit"
1000
+ return this.currentUnitDn;
1001
+ }
1002
+ async createUser(data) {
1003
+ // For compatibility - actually creates an organizational unit
1004
+ return this.api.createEntry(data.dn, data);
1005
+ }
1006
+ async deleteUser(dn) {
1007
+ // For compatibility - actually deletes an organizational unit
1008
+ return this.api.deleteEntry(dn);
1009
+ }
1010
+ }
1011
+
1012
+ export { LdapUnitEditor };
1013
+ //# sourceMappingURL=ldap-unit-editor.esm.js.map