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