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,1905 @@
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.LdapUserEditor = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * Simple in-memory cache manager with LRU eviction
9
+ * Prevents excessive API calls when navigating the organization tree
10
+ */
11
+ class CacheManager {
12
+ constructor(options = {}) {
13
+ this.cache = new Map();
14
+ this.ttl = options.ttl ?? 5 * 60 * 1000; // 5 minutes default
15
+ this.maxEntries = options.maxEntries ?? 200; // 200 entries max
16
+ }
17
+ /**
18
+ * Get an item from cache
19
+ * Returns null if not found or expired
20
+ */
21
+ get(key) {
22
+ const entry = this.cache.get(key);
23
+ if (!entry)
24
+ return null;
25
+ // Check if expired
26
+ if (Date.now() - entry.timestamp >= this.ttl) {
27
+ this.cache.delete(key);
28
+ return null;
29
+ }
30
+ // Update access count for LRU
31
+ entry.accessCount++;
32
+ entry.timestamp = Date.now();
33
+ return entry.data;
34
+ }
35
+ /**
36
+ * Set an item in cache
37
+ * Evicts least recently used items if cache is full
38
+ */
39
+ set(key, data) {
40
+ // If cache is full, evict LRU entry
41
+ if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
42
+ this.evictLRU();
43
+ }
44
+ this.cache.set(key, {
45
+ data,
46
+ timestamp: Date.now(),
47
+ accessCount: 0,
48
+ });
49
+ }
50
+ /**
51
+ * Check if key exists and is not expired
52
+ */
53
+ has(key) {
54
+ return this.get(key) !== null;
55
+ }
56
+ /**
57
+ * Invalidate a specific key
58
+ */
59
+ invalidate(key) {
60
+ this.cache.delete(key);
61
+ }
62
+ /**
63
+ * Invalidate all keys matching a pattern
64
+ * Pattern can contain wildcards (*)
65
+ */
66
+ invalidatePattern(pattern) {
67
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
68
+ for (const key of this.cache.keys()) {
69
+ if (regex.test(key)) {
70
+ this.cache.delete(key);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Clear all cache
76
+ */
77
+ clear() {
78
+ this.cache.clear();
79
+ }
80
+ /**
81
+ * Get cache statistics
82
+ */
83
+ getStats() {
84
+ return {
85
+ size: this.cache.size,
86
+ maxSize: this.maxEntries,
87
+ ttl: this.ttl,
88
+ keys: Array.from(this.cache.keys()),
89
+ };
90
+ }
91
+ /**
92
+ * Evict least recently used entry
93
+ */
94
+ evictLRU() {
95
+ let lruKey = null;
96
+ let lruTime = Infinity;
97
+ let lruCount = Infinity;
98
+ for (const [key, entry] of this.cache.entries()) {
99
+ // Prioritize by access count, then by timestamp
100
+ if (entry.accessCount < lruCount ||
101
+ (entry.accessCount === lruCount && entry.timestamp < lruTime)) {
102
+ lruKey = key;
103
+ lruTime = entry.timestamp;
104
+ lruCount = entry.accessCount;
105
+ }
106
+ }
107
+ if (lruKey) {
108
+ this.cache.delete(lruKey);
109
+ }
110
+ }
111
+ /**
112
+ * Clean expired entries
113
+ * Should be called periodically
114
+ */
115
+ cleanExpired() {
116
+ const now = Date.now();
117
+ let cleaned = 0;
118
+ for (const [key, entry] of this.cache.entries()) {
119
+ if (now - entry.timestamp >= this.ttl) {
120
+ this.cache.delete(key);
121
+ cleaned++;
122
+ }
123
+ }
124
+ return cleaned;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * LDAP User Editor - API Client
130
+ */
131
+ class UserApiClient {
132
+ constructor(baseUrl = '', cacheOptions) {
133
+ this.baseUrl =
134
+ baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
135
+ this.cache = new CacheManager(cacheOptions);
136
+ // Clean expired entries every 5 minutes (only in browser context)
137
+ if (typeof window !== 'undefined') {
138
+ window.setInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
139
+ }
140
+ }
141
+ getApiBase() {
142
+ return `${this.baseUrl}/api/v1`;
143
+ }
144
+ getFirstValue(value) {
145
+ if (!value)
146
+ return '';
147
+ if (Array.isArray(value))
148
+ return value[0] || '';
149
+ return String(value);
150
+ }
151
+ /**
152
+ * Fetch with cache support for GET requests
153
+ */
154
+ async cachedFetch(url, options) {
155
+ const method = options?.method || 'GET';
156
+ // Only cache GET requests
157
+ if (method === 'GET') {
158
+ const cached = this.cache.get(url);
159
+ if (cached !== null) {
160
+ return cached;
161
+ }
162
+ }
163
+ const res = await fetch(url, options);
164
+ if (!res.ok) {
165
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
166
+ }
167
+ const data = (await res.json());
168
+ // Cache GET responses
169
+ if (method === 'GET') {
170
+ this.cache.set(url, data);
171
+ }
172
+ return data;
173
+ }
174
+ async getConfig() {
175
+ return this.cachedFetch(`${this.getApiBase()}/config`);
176
+ }
177
+ async getSchema(schemaUrl) {
178
+ // Handle relative URLs by prepending baseUrl
179
+ const url = schemaUrl.startsWith('http')
180
+ ? schemaUrl
181
+ : `${this.baseUrl}${schemaUrl}`;
182
+ return this.cachedFetch(url);
183
+ }
184
+ async getUsers(search = '') {
185
+ const url = search
186
+ ? `${this.getApiBase()}/ldap/users?match=${encodeURIComponent(search)}&attribute=uid`
187
+ : `${this.getApiBase()}/ldap/users`;
188
+ const data = await this.cachedFetch(url);
189
+ // Convert from object format {uid: entry} to array format
190
+ return Array.isArray(data) ? data : Object.values(data);
191
+ }
192
+ async getUser(dn) {
193
+ const url = `${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`;
194
+ return this.cachedFetch(url);
195
+ }
196
+ async updateUser(dn, data) {
197
+ const res = await fetch(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`, {
198
+ method: 'PUT',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify(data),
201
+ });
202
+ if (!res.ok) {
203
+ const error = await res.text();
204
+ throw new Error(error || 'Failed to update user');
205
+ }
206
+ const result = await res.json();
207
+ // Invalidate cache for this user and user lists
208
+ this.cache.invalidate(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`);
209
+ this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
210
+ return result;
211
+ }
212
+ async createUser(data) {
213
+ const res = await fetch(`${this.getApiBase()}/ldap/users`, {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify(data),
217
+ });
218
+ if (!res.ok) {
219
+ const error = await res.text();
220
+ throw new Error(error || 'Failed to create user');
221
+ }
222
+ const result = await res.json();
223
+ // Invalidate cache for user lists
224
+ this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
225
+ return result;
226
+ }
227
+ async deleteUser(dn) {
228
+ const res = await fetch(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`, {
229
+ method: 'DELETE',
230
+ });
231
+ if (!res.ok) {
232
+ const error = await res.text();
233
+ throw new Error(error || 'Failed to delete user');
234
+ }
235
+ // Invalidate cache for this user and user lists
236
+ this.cache.invalidate(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`);
237
+ this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
238
+ }
239
+ async moveUser(dn, targetOrgDn) {
240
+ const res = await fetch(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}/move`, {
241
+ method: 'POST',
242
+ headers: { 'Content-Type': 'application/json' },
243
+ body: JSON.stringify({ targetOrgDn }),
244
+ });
245
+ if (!res.ok) {
246
+ const error = await res.text();
247
+ throw new Error(error || 'Failed to move user');
248
+ }
249
+ const result = await res.json();
250
+ // Invalidate cache for this user and user lists
251
+ this.cache.invalidate(`${this.getApiBase()}/ldap/users/${encodeURIComponent(dn)}`);
252
+ this.cache.invalidatePattern(`${this.getApiBase()}/ldap/users*`);
253
+ return result;
254
+ }
255
+ async getPointerOptions(branch) {
256
+ try {
257
+ // Load config to find the right endpoint by matching base
258
+ const config = await this.getConfig();
259
+ // Find resource that matches this branch by comparing base
260
+ const resource = config.features?.ldapFlatGeneric?.flatResources?.find(r => {
261
+ // Match if branch equals base or branch starts with base
262
+ return branch === r.base || branch.startsWith(r.base);
263
+ });
264
+ if (resource && resource.endpoints?.list) {
265
+ // Use the endpoint from config
266
+ const url = `${this.baseUrl}${resource.endpoints.list}`;
267
+ const data = await this.cachedFetch(url);
268
+ // Convert from object format {key: entry} to array
269
+ const items = Array.isArray(data) ? data : Object.values(data);
270
+ // Load schema to get displayName field
271
+ let displayNameField = 'cn'; // Default fallback
272
+ const identifierField = resource.mainAttribute;
273
+ if (resource.schemaUrl) {
274
+ try {
275
+ const schema = await this.getSchema(resource.schemaUrl);
276
+ // Find field with displayName role
277
+ const displayField = Object.entries(schema.attributes).find(([, attr]) => attr.role === 'displayName');
278
+ if (displayField) {
279
+ displayNameField = displayField[0];
280
+ }
281
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
282
+ }
283
+ catch (e) {
284
+ // Use defaults if schema fails
285
+ }
286
+ }
287
+ return items.map((item) => {
288
+ const displayName = this.getFirstValue(item[displayNameField]);
289
+ const identifier = this.getFirstValue(item[identifierField]);
290
+ return {
291
+ dn: item.dn,
292
+ label: displayName || identifier || item.dn,
293
+ };
294
+ });
295
+ }
296
+ // No matching resource found in config
297
+ throw new Error(`No flatResource found in config for branch: ${branch}`);
298
+ }
299
+ catch (error) {
300
+ console.error('Failed to load pointer options for branch:', branch, error);
301
+ return [];
302
+ }
303
+ }
304
+ /**
305
+ * Cache management methods
306
+ */
307
+ /**
308
+ * Clear all cache
309
+ */
310
+ clearCache() {
311
+ this.cache.clear();
312
+ }
313
+ /**
314
+ * Invalidate cache for a specific URL pattern
315
+ */
316
+ invalidateCache(pattern) {
317
+ this.cache.invalidatePattern(pattern);
318
+ }
319
+ /**
320
+ * Get cache statistics
321
+ */
322
+ getCacheStats() {
323
+ return this.cache.getStats();
324
+ }
325
+ /**
326
+ * Get cache instance (for advanced usage)
327
+ */
328
+ getCache() {
329
+ return this.cache;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * LDAP User Editor - Organization Tree Component
335
+ * Shows only LDAP organization tree (filters out users/groups)
336
+ */
337
+ class UserTree {
338
+ constructor(container, baseUrl, onSelectOrg) {
339
+ this.expandedNodes = new Set();
340
+ this.selectedDn = null;
341
+ this.rootDn = null;
342
+ this.container = container;
343
+ this.baseUrl = baseUrl;
344
+ this.onSelectOrg = onSelectOrg;
345
+ this.cache = new CacheManager();
346
+ // Clean expired entries every 5 minutes
347
+ window.setInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
348
+ }
349
+ /**
350
+ * Fetch with cache support
351
+ */
352
+ async cachedFetch(url) {
353
+ const cached = this.cache.get(url);
354
+ if (cached !== null) {
355
+ return cached;
356
+ }
357
+ const res = await fetch(url);
358
+ if (!res.ok) {
359
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
360
+ }
361
+ const data = (await res.json());
362
+ this.cache.set(url, data);
363
+ return data;
364
+ }
365
+ async init() {
366
+ this.container.innerHTML = `
367
+ <div class="sidebar">
368
+ <div class="sidebar-header">
369
+ <h2>
370
+ <span class="material-icons">account_tree</span>
371
+ Organizations
372
+ </h2>
373
+ </div>
374
+ <div id="org-tree-viewer" class="user-tree">
375
+ <div class="loading"><div class="spinner"></div></div>
376
+ </div>
377
+ </div>
378
+ `;
379
+ try {
380
+ // Load top organization
381
+ const topOrg = await this.cachedFetch(`${this.baseUrl}/api/v1/ldap/organizations/top`);
382
+ this.rootDn = topOrg.dn;
383
+ await this.renderTree();
384
+ }
385
+ catch (error) {
386
+ console.error('Failed to init tree:', error);
387
+ const treeEl = this.container.querySelector('#org-tree-viewer');
388
+ if (treeEl) {
389
+ treeEl.innerHTML =
390
+ '<div class="empty-state"><span class="material-icons">error</span><p>Failed to load organization tree</p></div>';
391
+ }
392
+ }
393
+ }
394
+ async renderTree() {
395
+ const treeEl = this.container.querySelector('#org-tree-viewer');
396
+ if (!treeEl || !this.rootDn)
397
+ return;
398
+ try {
399
+ const html = await this.renderNode(this.rootDn, 0);
400
+ treeEl.innerHTML = html;
401
+ this.attachEventListeners();
402
+ }
403
+ catch (error) {
404
+ console.error('Failed to render tree:', error);
405
+ treeEl.innerHTML =
406
+ '<div class="empty-state"><span class="material-icons">error</span><p>Error rendering tree</p></div>';
407
+ }
408
+ }
409
+ async renderNode(dn, level) {
410
+ const isExpanded = this.expandedNodes.has(dn);
411
+ const isSelected = this.selectedDn === dn;
412
+ const indent = level * 20;
413
+ // Get node info
414
+ const node = await this.cachedFetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
415
+ // Extract display name - handle both string and array
416
+ let displayName = dn;
417
+ if (node.ou) {
418
+ displayName = Array.isArray(node.ou) ? node.ou[0] : node.ou;
419
+ }
420
+ else if (node.cn) {
421
+ displayName = Array.isArray(node.cn) ? node.cn[0] : node.cn;
422
+ }
423
+ let html = `
424
+ <div class="tree-node ${isSelected ? 'active' : ''}" style="padding-left: ${indent}px">
425
+ <span class="material-icons tree-node-toggle" data-dn="${dn}">
426
+ ${isExpanded ? 'expand_more' : 'chevron_right'}
427
+ </span>
428
+ <span class="material-icons">business</span>
429
+ <span class="tree-node-label" data-dn="${dn}">${displayName}</span>
430
+ </div>
431
+ `;
432
+ if (isExpanded) {
433
+ try {
434
+ // Load subnodes and filter only organizations
435
+ const subnodes = await this.cachedFetch(`${this.baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes`);
436
+ const orgs = subnodes.filter((n) => {
437
+ const classes = Array.isArray(n.objectClass)
438
+ ? n.objectClass
439
+ : n.objectClass
440
+ ? [n.objectClass]
441
+ : [];
442
+ return (classes.includes('organizationalUnit') ||
443
+ classes.includes('organization'));
444
+ });
445
+ for (const subnode of orgs) {
446
+ html += await this.renderNode(subnode.dn, level + 1);
447
+ }
448
+ }
449
+ catch (error) {
450
+ console.error('Failed to load subnodes for', dn, error);
451
+ }
452
+ }
453
+ return html;
454
+ }
455
+ attachEventListeners() {
456
+ // Toggle expand/collapse
457
+ this.container.querySelectorAll('.tree-node-toggle').forEach(el => {
458
+ el.addEventListener('click', async (e) => {
459
+ e.stopPropagation();
460
+ const dn = el.getAttribute('data-dn');
461
+ if (!dn)
462
+ return;
463
+ if (this.expandedNodes.has(dn)) {
464
+ this.expandedNodes.delete(dn);
465
+ }
466
+ else {
467
+ this.expandedNodes.add(dn);
468
+ }
469
+ await this.renderTree();
470
+ });
471
+ });
472
+ // Select organization
473
+ this.container.querySelectorAll('.tree-node-label').forEach(el => {
474
+ el.addEventListener('click', e => {
475
+ e.stopPropagation();
476
+ const dn = el.getAttribute('data-dn');
477
+ if (!dn)
478
+ return;
479
+ this.selectedDn = dn;
480
+ this.renderTree();
481
+ this.onSelectOrg(dn);
482
+ });
483
+ });
484
+ }
485
+ async refresh() {
486
+ await this.renderTree();
487
+ }
488
+ destroy() {
489
+ this.container.innerHTML = '';
490
+ }
491
+ }
492
+
493
+ /**
494
+ * LDAP User Editor - Pointer Field Component
495
+ */
496
+ class PointerField {
497
+ constructor(api, fieldName, attribute, value, onChange) {
498
+ this.options = [];
499
+ this.api = api;
500
+ this.fieldName = fieldName;
501
+ this.attribute = attribute;
502
+ this.value = value;
503
+ this.onChange = onChange;
504
+ }
505
+ async init() {
506
+ await this.loadOptions();
507
+ }
508
+ async loadOptions() {
509
+ try {
510
+ const branch = this.attribute.type === 'array'
511
+ ? this.attribute.items?.branch?.[0]
512
+ : this.attribute.branch?.[0];
513
+ if (!branch) {
514
+ this.options = [];
515
+ return;
516
+ }
517
+ this.options = await this.api.getPointerOptions(branch);
518
+ }
519
+ catch (error) {
520
+ console.error('Failed to load pointer options:', error);
521
+ this.options = [];
522
+ }
523
+ }
524
+ render() {
525
+ const isArray = this.attribute.type === 'array';
526
+ const isRequired = this.attribute.required || false;
527
+ if (isArray) {
528
+ return this.renderArray(isRequired);
529
+ }
530
+ return this.renderSingle(isRequired);
531
+ }
532
+ renderSingle(required) {
533
+ const currentValue = Array.isArray(this.value)
534
+ ? this.value[0] || ''
535
+ : this.value || '';
536
+ return `
537
+ <div class="form-group">
538
+ <label class="form-label">
539
+ ${this.fieldName}
540
+ ${required ? '<span class="required">*</span>' : ''}
541
+ </label>
542
+ <select
543
+ class="form-select pointer-field-select"
544
+ data-field="${this.fieldName}"
545
+ ${required ? 'required' : ''}
546
+ >
547
+ <option value="">Select...</option>
548
+ ${this.options
549
+ .map(opt => `
550
+ <option value="${opt.dn}" ${currentValue === opt.dn ? 'selected' : ''}>
551
+ ${opt.label}
552
+ </option>
553
+ `)
554
+ .join('')}
555
+ </select>
556
+ </div>
557
+ `;
558
+ }
559
+ renderArray(required) {
560
+ const values = Array.isArray(this.value) ? this.value : [];
561
+ return `
562
+ <div class="form-group">
563
+ <label class="form-label">
564
+ ${this.fieldName}
565
+ ${required ? '<span class="required">*</span>' : ''}
566
+ </label>
567
+ <div class="array-field" data-field="${this.fieldName}">
568
+ ${values
569
+ .map((val, idx) => `
570
+ <div class="array-item" data-index="${idx}">
571
+ <select class="form-select pointer-array-item">
572
+ <option value="">Select...</option>
573
+ ${this.options
574
+ .map(opt => `
575
+ <option value="${opt.dn}" ${val === opt.dn ? 'selected' : ''}>
576
+ ${opt.label}
577
+ </option>
578
+ `)
579
+ .join('')}
580
+ </select>
581
+ <button type="button" class="btn btn-secondary btn-icon remove-array-item">
582
+ <span class="material-icons">remove</span>
583
+ </button>
584
+ </div>
585
+ `)
586
+ .join('')}
587
+ <button type="button" class="btn btn-secondary add-array-item">
588
+ <span class="material-icons">add</span>
589
+ Add ${this.fieldName}
590
+ </button>
591
+ </div>
592
+ </div>
593
+ `;
594
+ }
595
+ attachHandlers(container) {
596
+ if (this.attribute.type === 'array') {
597
+ this.attachArrayHandlers(container);
598
+ }
599
+ else {
600
+ this.attachSingleHandlers(container);
601
+ }
602
+ }
603
+ attachSingleHandlers(container) {
604
+ const select = container.querySelector(`select[data-field="${this.fieldName}"]`);
605
+ select?.addEventListener('change', () => {
606
+ this.onChange(select.value);
607
+ });
608
+ }
609
+ attachArrayHandlers(container) {
610
+ const arrayContainer = container.querySelector(`.array-field[data-field="${this.fieldName}"]`);
611
+ if (!arrayContainer)
612
+ return;
613
+ // Handle item change
614
+ arrayContainer
615
+ .querySelectorAll('.pointer-array-item')
616
+ .forEach((select, idx) => {
617
+ select.addEventListener('change', () => {
618
+ const values = Array.isArray(this.value) ? [...this.value] : [];
619
+ values[idx] = select.value;
620
+ this.onChange(values);
621
+ });
622
+ });
623
+ // Handle remove
624
+ arrayContainer
625
+ .querySelectorAll('.remove-array-item')
626
+ .forEach((btn, idx) => {
627
+ btn.addEventListener('click', () => {
628
+ const values = Array.isArray(this.value) ? [...this.value] : [];
629
+ values.splice(idx, 1);
630
+ this.onChange(values);
631
+ });
632
+ });
633
+ // Handle add
634
+ const addBtn = arrayContainer.querySelector('.add-array-item');
635
+ addBtn?.addEventListener('click', () => {
636
+ const values = Array.isArray(this.value) ? [...this.value] : [];
637
+ values.push('');
638
+ this.onChange(values);
639
+ });
640
+ }
641
+ }
642
+
643
+ /**
644
+ * LDAP User Editor - Move User Modal Component
645
+ * Modal dialog for selecting target organization when moving a user
646
+ */
647
+ class MoveUserModal {
648
+ constructor(api, currentOrgDn, onMove) {
649
+ this.modal = null;
650
+ this.selectedOrgDn = null;
651
+ this.expandedNodes = new Set();
652
+ this.rootDn = null;
653
+ this.api = api;
654
+ this.currentOrgDn = currentOrgDn;
655
+ this.onMove = onMove;
656
+ }
657
+ /**
658
+ * Escape HTML special characters to prevent XSS attacks
659
+ */
660
+ static escapeHtml(text) {
661
+ if (text == null)
662
+ return '';
663
+ const map = {
664
+ '&': '&amp;',
665
+ '<': '&lt;',
666
+ '>': '&gt;',
667
+ '"': '&quot;',
668
+ "'": '&#x27;',
669
+ '/': '&#x2F;',
670
+ };
671
+ return String(text).replace(/[&<>"'/]/g, m => map[m]);
672
+ }
673
+ async show() {
674
+ // Create modal container
675
+ this.modal = document.createElement('div');
676
+ this.modal.className = 'modal-overlay active';
677
+ this.modal.innerHTML = `
678
+ <div class="modal-container" style="max-width: 600px;">
679
+ <div class="modal-header">
680
+ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
681
+ <span class="material-icons">folder_open</span>
682
+ Move User to Organization
683
+ </h3>
684
+ <button class="modal-close" id="modal-close-btn">
685
+ <span class="material-icons">close</span>
686
+ </button>
687
+ </div>
688
+ <div class="modal-body">
689
+ <div id="move-modal-alert"></div>
690
+ <div style="margin-bottom: 1rem; color: #666; font-size: 0.9rem;">
691
+ Select the target organization where you want to move this user.
692
+ </div>
693
+ <div id="organization-tree-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 0.5rem;">
694
+ <div class="loading">
695
+ <div class="spinner"></div>
696
+ <p style="margin-top: 1rem">Loading organizations...</p>
697
+ </div>
698
+ </div>
699
+ </div>
700
+ <div class="modal-footer">
701
+ <button type="button" class="btn btn-secondary" id="modal-cancel-btn">
702
+ Cancel
703
+ </button>
704
+ <button type="button" class="btn btn-primary" id="modal-move-btn" disabled>
705
+ <span class="material-icons">drive_file_move</span>
706
+ Move User
707
+ </button>
708
+ </div>
709
+ </div>
710
+ `;
711
+ document.body.appendChild(this.modal);
712
+ this.attachHandlers();
713
+ await this.loadOrganizationTree();
714
+ }
715
+ attachHandlers() {
716
+ if (!this.modal)
717
+ return;
718
+ // Close button
719
+ const closeBtn = this.modal.querySelector('#modal-close-btn');
720
+ closeBtn?.addEventListener('click', () => this.close());
721
+ // Cancel button
722
+ const cancelBtn = this.modal.querySelector('#modal-cancel-btn');
723
+ cancelBtn?.addEventListener('click', () => this.close());
724
+ // Move button
725
+ const moveBtn = this.modal.querySelector('#modal-move-btn');
726
+ moveBtn?.addEventListener('click', () => this.handleMove());
727
+ // Close on overlay click
728
+ this.modal.addEventListener('click', e => {
729
+ if (e.target === this.modal) {
730
+ this.close();
731
+ }
732
+ });
733
+ // Close on Escape key
734
+ const handleEscape = (e) => {
735
+ if (e.key === 'Escape') {
736
+ this.close();
737
+ document.removeEventListener('keydown', handleEscape);
738
+ }
739
+ };
740
+ document.addEventListener('keydown', handleEscape);
741
+ }
742
+ async loadOrganizationTree() {
743
+ console.log('loadOrganizationTree called');
744
+ const container = this.modal?.querySelector('#organization-tree-container');
745
+ if (!container) {
746
+ console.log('Container not found');
747
+ return;
748
+ }
749
+ try {
750
+ console.log('Fetching top organization...');
751
+ // Get top organization
752
+ const topOrg = await this.api['cachedFetch'](`${this.api['baseUrl'] || window.location.origin}/api/v1/ldap/organizations/top`);
753
+ console.log('Top org:', topOrg);
754
+ this.rootDn = topOrg.dn;
755
+ // Expand root by default
756
+ this.expandedNodes.add(this.rootDn);
757
+ console.log('Calling renderTree...');
758
+ await this.renderTree();
759
+ console.log('renderTree completed');
760
+ }
761
+ catch (error) {
762
+ console.error('Error in loadOrganizationTree:', error);
763
+ container.innerHTML = `
764
+ <div class="alert alert-error">
765
+ <span class="material-icons">error</span>
766
+ <div>Failed to load organizations: ${MoveUserModal.escapeHtml(error.message)}</div>
767
+ </div>
768
+ `;
769
+ }
770
+ }
771
+ async renderTree() {
772
+ console.log('renderTree called');
773
+ const container = this.modal?.querySelector('#organization-tree-container');
774
+ if (!container || !this.rootDn) {
775
+ console.log('renderTree: no container or rootDn', {
776
+ container,
777
+ rootDn: this.rootDn,
778
+ });
779
+ return;
780
+ }
781
+ try {
782
+ console.log('renderTree: calling renderNode for', this.rootDn);
783
+ const html = await this.renderNode(this.rootDn, 0);
784
+ console.log('renderTree: got html, length:', html.length);
785
+ container.innerHTML = html;
786
+ console.log('renderTree: html set, attaching handlers');
787
+ this.attachTreeHandlers();
788
+ console.log('renderTree: handlers attached');
789
+ }
790
+ catch (error) {
791
+ console.error('Error in renderTree:', error);
792
+ container.innerHTML = `
793
+ <div class="alert alert-error">
794
+ <span class="material-icons">error</span>
795
+ <div>Error rendering tree: ${MoveUserModal.escapeHtml(error.message)}</div>
796
+ </div>
797
+ `;
798
+ }
799
+ }
800
+ async renderNode(dn, level) {
801
+ console.log('renderNode called for', dn, 'level', level);
802
+ const isExpanded = this.expandedNodes.has(dn);
803
+ const isCurrentOrg = dn === this.currentOrgDn;
804
+ const isSelected = dn === this.selectedOrgDn;
805
+ const indent = level * 20;
806
+ // Get node info
807
+ const baseUrl = this.api['baseUrl'] || window.location.origin;
808
+ console.log('Fetching node details from:', `${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
809
+ const node = await this.api['cachedFetch'](`${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}`);
810
+ console.log('Got node:', node);
811
+ // Extract display name
812
+ const ou = Array.isArray(node.ou) ? node.ou[0] : node.ou;
813
+ const path = node.twakeDepartmentPath
814
+ ? Array.isArray(node.twakeDepartmentPath)
815
+ ? node.twakeDepartmentPath[0]
816
+ : node.twakeDepartmentPath
817
+ : undefined;
818
+ const displayName = path || ou || dn;
819
+ let html = `
820
+ <div
821
+ class="org-node ${isCurrentOrg ? 'org-current' : ''} ${isSelected ? 'org-selected' : ''}"
822
+ data-org-dn="${MoveUserModal.escapeHtml(dn)}"
823
+ style="
824
+ padding: 0.5rem;
825
+ padding-left: ${indent + 8}px;
826
+ cursor: ${isCurrentOrg ? 'not-allowed' : 'pointer'};
827
+ background: ${isCurrentOrg ? '#f5f5f5' : isSelected ? '#e3f2fd' : 'white'};
828
+ opacity: ${isCurrentOrg ? '0.6' : '1'};
829
+ display: flex;
830
+ align-items: center;
831
+ gap: 0.5rem;
832
+ border-left: 2px solid ${isSelected ? '#2196f3' : 'transparent'};
833
+ "
834
+ >
835
+ <span class="material-icons tree-toggle" data-dn="${MoveUserModal.escapeHtml(dn)}" style="cursor: pointer; font-size: 1.2rem;">
836
+ ${isExpanded ? 'expand_more' : 'chevron_right'}
837
+ </span>
838
+ <span class="material-icons" style="font-size: 1.2rem; color: ${isCurrentOrg ? '#999' : '#2196f3'};">
839
+ ${isCurrentOrg ? 'business' : 'folder'}
840
+ </span>
841
+ <span style="flex: 1; font-weight: ${isSelected ? '600' : '500'};">
842
+ ${MoveUserModal.escapeHtml(displayName)}
843
+ ${isCurrentOrg ? '<span style="font-size: 0.85rem; color: #666; margin-left: 0.5rem;">(current)</span>' : ''}
844
+ </span>
845
+ ${isSelected ? '<span class="material-icons" style="color: #2196f3;">check_circle</span>' : ''}
846
+ </div>
847
+ `;
848
+ // If expanded, load and render subnodes
849
+ if (isExpanded) {
850
+ try {
851
+ const subnodes = await this.api['cachedFetch'](`${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(dn)}/subnodes?objectClass=organizationalUnit`);
852
+ for (const subnode of subnodes) {
853
+ html += await this.renderNode(subnode.dn, level + 1);
854
+ }
855
+ }
856
+ catch {
857
+ // No subnodes or error loading them
858
+ }
859
+ }
860
+ return html;
861
+ }
862
+ attachTreeHandlers() {
863
+ // Handle tree toggle (expand/collapse)
864
+ const toggles = this.modal?.querySelectorAll('.tree-toggle');
865
+ toggles?.forEach(toggle => {
866
+ toggle.addEventListener('click', async (e) => {
867
+ e.stopPropagation();
868
+ const dn = toggle.getAttribute('data-dn');
869
+ if (!dn)
870
+ return;
871
+ if (this.expandedNodes.has(dn)) {
872
+ this.expandedNodes.delete(dn);
873
+ }
874
+ else {
875
+ this.expandedNodes.add(dn);
876
+ }
877
+ await this.renderTree();
878
+ });
879
+ });
880
+ // Handle node selection
881
+ const nodes = this.modal?.querySelectorAll('.org-node');
882
+ nodes?.forEach(node => {
883
+ const orgDn = node.getAttribute('data-org-dn');
884
+ if (!orgDn || orgDn === this.currentOrgDn)
885
+ return;
886
+ node.addEventListener('click', () => {
887
+ this.selectOrganization(orgDn);
888
+ });
889
+ });
890
+ }
891
+ async selectOrganization(orgDn) {
892
+ this.selectedOrgDn = orgDn;
893
+ // Re-render tree to update selection state
894
+ await this.renderTree();
895
+ // Enable move button
896
+ const moveBtn = this.modal?.querySelector('#modal-move-btn');
897
+ if (moveBtn) {
898
+ moveBtn.disabled = false;
899
+ }
900
+ }
901
+ async handleMove() {
902
+ if (!this.selectedOrgDn)
903
+ return;
904
+ const moveBtn = this.modal?.querySelector('#modal-move-btn');
905
+ const alertContainer = this.modal?.querySelector('#move-modal-alert');
906
+ if (moveBtn) {
907
+ moveBtn.disabled = true;
908
+ moveBtn.innerHTML = `
909
+ <span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px;"></span>
910
+ Moving...
911
+ `;
912
+ }
913
+ try {
914
+ this.onMove(this.selectedOrgDn);
915
+ this.close();
916
+ }
917
+ catch (error) {
918
+ if (alertContainer) {
919
+ alertContainer.innerHTML = `
920
+ <div class="alert alert-error" style="margin-bottom: 1rem;">
921
+ <span class="material-icons">error</span>
922
+ <div>Move failed: ${MoveUserModal.escapeHtml(error.message)}</div>
923
+ </div>
924
+ `;
925
+ }
926
+ if (moveBtn) {
927
+ moveBtn.disabled = false;
928
+ moveBtn.innerHTML = `
929
+ <span class="material-icons">drive_file_move</span>
930
+ Move User
931
+ `;
932
+ }
933
+ }
934
+ }
935
+ close() {
936
+ if (this.modal) {
937
+ this.modal.remove();
938
+ this.modal = null;
939
+ }
940
+ }
941
+ }
942
+
943
+ /**
944
+ * LDAP User Editor - User Editor Component
945
+ */
946
+ class UserEditor {
947
+ /**
948
+ * Escape HTML special characters to prevent XSS attacks
949
+ * Reference: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html
950
+ */
951
+ static escapeHtml(text) {
952
+ if (text == null)
953
+ return '';
954
+ const map = {
955
+ '&': '&amp;',
956
+ '<': '&lt;',
957
+ '>': '&gt;',
958
+ '"': '&quot;',
959
+ "'": '&#x27;',
960
+ '/': '&#x2F;',
961
+ };
962
+ return String(text).replace(/[&<>"'/]/g, m => map[m]);
963
+ }
964
+ constructor(container, api, userDn, onSaved, onDeleted) {
965
+ this.user = null;
966
+ this.schema = null;
967
+ this.formData = {};
968
+ this.pointerFields = new Map();
969
+ this.ldapBase = '';
970
+ this.container = container;
971
+ this.api = api;
972
+ this.userDn = userDn;
973
+ this.onSaved = onSaved;
974
+ this.onDeleted = onDeleted;
975
+ }
976
+ async init() {
977
+ try {
978
+ this.showLoading();
979
+ await this.loadUserAndSchema();
980
+ this.render();
981
+ await this.initPointerFields();
982
+ this.attachHandlers();
983
+ }
984
+ catch (error) {
985
+ this.showError(error.message);
986
+ }
987
+ }
988
+ showLoading() {
989
+ this.container.innerHTML = `
990
+ <div class="loading">
991
+ <div class="spinner"></div>
992
+ <p style="margin-top: 1rem">Loading user data...</p>
993
+ </div>
994
+ `;
995
+ }
996
+ showError(message) {
997
+ this.container.innerHTML = `
998
+ <div class="alert alert-error">
999
+ <span class="material-icons">error</span>
1000
+ <div>${UserEditor.escapeHtml(message)}</div>
1001
+ </div>
1002
+ `;
1003
+ }
1004
+ async loadUserAndSchema() {
1005
+ const config = await this.api.getConfig();
1006
+ this.ldapBase = config.ldapBase;
1007
+ // Find users resource in flatResources array
1008
+ const usersResource = config.features?.ldapFlatGeneric?.flatResources?.find(r => r.pluralName === 'users' || r.name === 'users');
1009
+ const schemaUrl = usersResource?.schemaUrl;
1010
+ if (!schemaUrl) {
1011
+ throw new Error('Users schema not configured');
1012
+ }
1013
+ [this.schema, this.user] = await Promise.all([
1014
+ this.api.getSchema(schemaUrl),
1015
+ this.api.getUser(this.userDn),
1016
+ ]);
1017
+ // Replace __LDAP_BASE__ placeholder in schema branches
1018
+ if (this.schema) {
1019
+ this.replacePlaceholders(this.schema);
1020
+ }
1021
+ this.formData = { ...this.user };
1022
+ }
1023
+ replacePlaceholders(schema) {
1024
+ for (const attr of Object.values(schema.attributes)) {
1025
+ if (attr.branch) {
1026
+ attr.branch = attr.branch.map(b => b.replace(/__LDAP_BASE__/g, this.ldapBase));
1027
+ }
1028
+ if (attr.items?.branch) {
1029
+ attr.items.branch = attr.items.branch.map(b => b.replace(/__LDAP_BASE__/g, this.ldapBase));
1030
+ }
1031
+ }
1032
+ }
1033
+ getFirstValue(value) {
1034
+ if (!value)
1035
+ return '';
1036
+ if (Array.isArray(value))
1037
+ return value[0] || '';
1038
+ return String(value);
1039
+ }
1040
+ getFieldByRole(role) {
1041
+ if (!this.schema || !this.user)
1042
+ return '';
1043
+ const field = Object.entries(this.schema.attributes).find(([, attr]) => attr.role === role);
1044
+ if (!field)
1045
+ return '';
1046
+ const [fieldName] = field;
1047
+ return this.getFirstValue(this.user[fieldName]);
1048
+ }
1049
+ render() {
1050
+ if (!this.user || !this.schema)
1051
+ return;
1052
+ const displayName = this.getFieldByRole('displayName');
1053
+ const identifier = this.getFirstValue(this.user[this.schema.entity.mainAttribute]);
1054
+ const userName = displayName || identifier || 'User';
1055
+ this.container.innerHTML = `
1056
+ <div class="editor-container">
1057
+ <div class="editor-header">
1058
+ <span class="material-icons">person</span>
1059
+ <div class="editor-title">
1060
+ <h3>${UserEditor.escapeHtml(userName)}</h3>
1061
+ <div class="dn">${UserEditor.escapeHtml(this.userDn)}</div>
1062
+ </div>
1063
+ </div>
1064
+
1065
+ <div id="alert-container"></div>
1066
+
1067
+ <form id="user-edit-form" class="editor-form">
1068
+ ${this.renderFormSections()}
1069
+
1070
+ <div class="editor-actions" style="margin-top: 2rem; display: flex; justify-content: space-between; align-items: center;">
1071
+ <div style="display: flex; gap: 0.5rem;">
1072
+ <button type="button" class="btn btn-secondary" id="move-user-btn">
1073
+ <span class="material-icons">drive_file_move</span>
1074
+ Move User
1075
+ </button>
1076
+ <button type="button" class="btn btn-secondary" id="delete-user-btn" style="background: #d32f2f; color: white;">
1077
+ <span class="material-icons">person_remove</span>
1078
+ Delete User
1079
+ </button>
1080
+ </div>
1081
+ <button type="submit" class="btn btn-primary">
1082
+ <span class="material-icons">save</span>
1083
+ Save Changes
1084
+ </button>
1085
+ </div>
1086
+ </form>
1087
+ </div>
1088
+ `;
1089
+ }
1090
+ renderFormSections() {
1091
+ if (!this.schema)
1092
+ return '';
1093
+ // Group fields by the 'group' attribute from schema
1094
+ const groupedFields = {};
1095
+ const ungroupedFields = [];
1096
+ for (const [fieldName, attr] of Object.entries(this.schema.attributes)) {
1097
+ // Skip objectClass (it's fixed)
1098
+ if (fieldName === 'objectClass')
1099
+ continue;
1100
+ if (attr.group) {
1101
+ if (!groupedFields[attr.group]) {
1102
+ groupedFields[attr.group] = [];
1103
+ }
1104
+ groupedFields[attr.group].push(fieldName);
1105
+ }
1106
+ else {
1107
+ ungroupedFields.push(fieldName);
1108
+ }
1109
+ }
1110
+ // Render grouped sections
1111
+ let html = Object.entries(groupedFields)
1112
+ .map(([title, fields]) => `
1113
+ <div class="form-section">
1114
+ <div class="form-section-title">${UserEditor.escapeHtml(title)}</div>
1115
+ <div class="form-row">
1116
+ ${fields
1117
+ .map(field => this.renderField(field))
1118
+ .filter(f => f)
1119
+ .join('')}
1120
+ </div>
1121
+ </div>
1122
+ `)
1123
+ .join('');
1124
+ // Render ungrouped fields in a default section if any
1125
+ if (ungroupedFields.length > 0) {
1126
+ html += `
1127
+ <div class="form-section">
1128
+ <div class="form-section-title">Other Fields</div>
1129
+ <div class="form-row">
1130
+ ${ungroupedFields
1131
+ .map(field => this.renderField(field))
1132
+ .filter(f => f)
1133
+ .join('')}
1134
+ </div>
1135
+ </div>
1136
+ `;
1137
+ }
1138
+ return html;
1139
+ }
1140
+ renderField(fieldName) {
1141
+ const attr = this.schema?.attributes[fieldName];
1142
+ if (!attr)
1143
+ return '';
1144
+ // Skip fixed fields - they cannot be edited
1145
+ if (attr.fixed) {
1146
+ return '';
1147
+ }
1148
+ // Hide organizationLink and organizationPath fields - use Move button instead
1149
+ if (attr.role === 'organizationLink' || attr.role === 'organizationPath') {
1150
+ return '';
1151
+ }
1152
+ // Show identifier fields as disabled
1153
+ if (attr.role === 'identifier') {
1154
+ return this.renderIdentifierField(fieldName);
1155
+ }
1156
+ // Pointer fields
1157
+ if (attr.type === 'pointer' || attr.items?.type === 'pointer') {
1158
+ return `<div id="pointer-${UserEditor.escapeHtml(fieldName)}"></div>`;
1159
+ }
1160
+ // Array fields
1161
+ if (attr.type === 'array') {
1162
+ return this.renderArrayField(fieldName, attr);
1163
+ }
1164
+ // Number fields
1165
+ if (attr.type === 'number' || attr.type === 'integer') {
1166
+ return this.renderNumberField(fieldName, attr);
1167
+ }
1168
+ // String fields
1169
+ return this.renderStringField(fieldName, attr);
1170
+ }
1171
+ renderIdentifierField(fieldName) {
1172
+ const value = this.formData[fieldName];
1173
+ const displayValue = Array.isArray(value) ? value[0] : value || '';
1174
+ return `
1175
+ <div class="form-group">
1176
+ <label class="form-label">${UserEditor.escapeHtml(fieldName)}</label>
1177
+ <input
1178
+ type="text"
1179
+ class="form-input"
1180
+ value="${UserEditor.escapeHtml(displayValue)}"
1181
+ disabled
1182
+ />
1183
+ </div>
1184
+ `;
1185
+ }
1186
+ renderStringField(fieldName, attr) {
1187
+ const value = this.formData[fieldName] || '';
1188
+ const inputType = attr.test?.includes('@')
1189
+ ? 'email'
1190
+ : fieldName === 'userPassword'
1191
+ ? 'password'
1192
+ : 'text';
1193
+ return `
1194
+ <div class="form-group">
1195
+ <label class="form-label">
1196
+ ${UserEditor.escapeHtml(fieldName)}
1197
+ ${attr.required ? '<span class="required">*</span>' : ''}
1198
+ </label>
1199
+ <input
1200
+ type="${UserEditor.escapeHtml(inputType)}"
1201
+ class="form-input"
1202
+ name="${UserEditor.escapeHtml(fieldName)}"
1203
+ value="${UserEditor.escapeHtml(value)}"
1204
+ ${attr.required ? 'required' : ''}
1205
+ />
1206
+ </div>
1207
+ `;
1208
+ }
1209
+ renderNumberField(fieldName, attr) {
1210
+ const value = this.formData[fieldName] || '';
1211
+ return `
1212
+ <div class="form-group">
1213
+ <label class="form-label">
1214
+ ${UserEditor.escapeHtml(fieldName)}
1215
+ ${attr.required ? '<span class="required">*</span>' : ''}
1216
+ </label>
1217
+ <input
1218
+ type="number"
1219
+ class="form-input"
1220
+ name="${UserEditor.escapeHtml(fieldName)}"
1221
+ value="${UserEditor.escapeHtml(value)}"
1222
+ ${attr.required ? 'required' : ''}
1223
+ />
1224
+ </div>
1225
+ `;
1226
+ }
1227
+ renderArrayField(fieldName, attr) {
1228
+ const values = Array.isArray(this.formData[fieldName])
1229
+ ? this.formData[fieldName]
1230
+ : [];
1231
+ const inputType = attr.items?.type === 'string' && attr.items?.test?.includes('@')
1232
+ ? 'email'
1233
+ : 'text';
1234
+ return `
1235
+ <div class="form-group">
1236
+ <label class="form-label">
1237
+ ${UserEditor.escapeHtml(fieldName)}
1238
+ ${attr.required ? '<span class="required">*</span>' : ''}
1239
+ </label>
1240
+ <div class="array-field" data-field="${UserEditor.escapeHtml(fieldName)}">
1241
+ ${values
1242
+ .map((val, idx) => `
1243
+ <div class="array-item" data-index="${idx}">
1244
+ <input
1245
+ type="${UserEditor.escapeHtml(inputType)}"
1246
+ class="form-input array-string-item"
1247
+ value="${UserEditor.escapeHtml(val)}"
1248
+ />
1249
+ <button type="button" class="btn btn-secondary btn-icon remove-array-item">
1250
+ <span class="material-icons">remove</span>
1251
+ </button>
1252
+ </div>
1253
+ `)
1254
+ .join('')}
1255
+ <button type="button" class="btn btn-secondary add-array-item" data-field="${UserEditor.escapeHtml(fieldName)}">
1256
+ <span class="material-icons">add</span>
1257
+ Add ${UserEditor.escapeHtml(fieldName)}
1258
+ </button>
1259
+ </div>
1260
+ </div>
1261
+ `;
1262
+ }
1263
+ async initPointerFields() {
1264
+ if (!this.schema)
1265
+ return;
1266
+ const pointerFieldNames = Object.entries(this.schema.attributes)
1267
+ .filter(([, attr]) => (attr.type === 'pointer' || attr.items?.type === 'pointer') &&
1268
+ !attr.fixed)
1269
+ .map(([name]) => name);
1270
+ for (const fieldName of pointerFieldNames) {
1271
+ const attr = this.schema.attributes[fieldName];
1272
+ const value = this.formData[fieldName];
1273
+ const pointerField = new PointerField(this.api, fieldName, attr, value, newValue => {
1274
+ this.formData[fieldName] = newValue;
1275
+ this.rerenderPointerField(fieldName);
1276
+ });
1277
+ await pointerField.init();
1278
+ this.pointerFields.set(fieldName, pointerField);
1279
+ // Render into placeholder
1280
+ const placeholder = this.container.querySelector(`#pointer-${fieldName}`);
1281
+ if (placeholder) {
1282
+ placeholder.innerHTML = pointerField.render();
1283
+ pointerField.attachHandlers(placeholder);
1284
+ }
1285
+ }
1286
+ }
1287
+ rerenderPointerField(fieldName) {
1288
+ const pointerField = this.pointerFields.get(fieldName);
1289
+ if (!pointerField)
1290
+ return;
1291
+ const placeholder = this.container.querySelector(`#pointer-${fieldName}`);
1292
+ if (placeholder) {
1293
+ placeholder.innerHTML = pointerField.render();
1294
+ pointerField.attachHandlers(placeholder);
1295
+ }
1296
+ }
1297
+ attachHandlers() {
1298
+ const form = this.container.querySelector('#user-edit-form');
1299
+ form?.addEventListener('submit', e => this.handleSubmit(e));
1300
+ // Move button
1301
+ const moveBtn = this.container.querySelector('#move-user-btn');
1302
+ moveBtn?.addEventListener('click', () => this.handleMove());
1303
+ // Delete button
1304
+ const deleteBtn = this.container.querySelector('#delete-user-btn');
1305
+ deleteBtn?.addEventListener('click', () => this.handleDelete());
1306
+ // String/number inputs
1307
+ this.container
1308
+ .querySelectorAll('input[name]')
1309
+ .forEach(input => {
1310
+ input.addEventListener('input', () => {
1311
+ const fieldName = input.name;
1312
+ const attr = this.schema?.attributes[fieldName];
1313
+ if (attr?.type === 'number' || attr?.type === 'integer') {
1314
+ this.formData[fieldName] = input.value
1315
+ ? parseInt(input.value, 10)
1316
+ : '';
1317
+ }
1318
+ else {
1319
+ this.formData[fieldName] = input.value;
1320
+ }
1321
+ });
1322
+ });
1323
+ // Array fields
1324
+ this.attachArrayFieldHandlers();
1325
+ }
1326
+ attachArrayFieldHandlers() {
1327
+ // Handle array item changes
1328
+ this.container
1329
+ .querySelectorAll('.array-field')
1330
+ .forEach((arrayContainer) => {
1331
+ const fieldName = arrayContainer.getAttribute('data-field');
1332
+ if (!fieldName)
1333
+ return;
1334
+ arrayContainer
1335
+ .querySelectorAll('.array-string-item')
1336
+ .forEach((input, idx) => {
1337
+ input.addEventListener('input', () => {
1338
+ const values = Array.isArray(this.formData[fieldName])
1339
+ ? [...this.formData[fieldName]]
1340
+ : [];
1341
+ values[idx] = input.value;
1342
+ this.formData[fieldName] = values;
1343
+ });
1344
+ });
1345
+ arrayContainer
1346
+ .querySelectorAll('.remove-array-item')
1347
+ .forEach((btn, idx) => {
1348
+ btn.addEventListener('click', () => {
1349
+ const values = Array.isArray(this.formData[fieldName])
1350
+ ? [...this.formData[fieldName]]
1351
+ : [];
1352
+ values.splice(idx, 1);
1353
+ this.formData[fieldName] = values;
1354
+ this.rerenderArrayField(fieldName);
1355
+ });
1356
+ });
1357
+ });
1358
+ // Handle add buttons
1359
+ this.container.querySelectorAll('.add-array-item').forEach(btn => {
1360
+ btn.addEventListener('click', () => {
1361
+ const fieldName = btn.getAttribute('data-field');
1362
+ if (!fieldName)
1363
+ return;
1364
+ const values = Array.isArray(this.formData[fieldName])
1365
+ ? [...this.formData[fieldName]]
1366
+ : [];
1367
+ values.push('');
1368
+ this.formData[fieldName] = values;
1369
+ this.rerenderArrayField(fieldName);
1370
+ });
1371
+ });
1372
+ }
1373
+ rerenderArrayField(fieldName) {
1374
+ const attr = this.schema?.attributes[fieldName];
1375
+ if (!attr)
1376
+ return;
1377
+ const arrayContainer = this.container.querySelector(`.array-field[data-field="${fieldName}"]`)?.parentElement;
1378
+ if (arrayContainer) {
1379
+ const newHtml = attr.type === 'pointer' || attr.items?.type === 'pointer'
1380
+ ? this.pointerFields.get(fieldName)?.render() || ''
1381
+ : this.renderArrayField(fieldName, attr);
1382
+ arrayContainer.outerHTML = newHtml;
1383
+ // Reattach handlers
1384
+ if (attr.type === 'pointer' || attr.items?.type === 'pointer') {
1385
+ const placeholder = this.container.querySelector(`#pointer-${fieldName}`);
1386
+ if (placeholder) {
1387
+ const pointerField = this.pointerFields.get(fieldName);
1388
+ pointerField?.attachHandlers(placeholder);
1389
+ }
1390
+ }
1391
+ else {
1392
+ this.attachArrayFieldHandlers();
1393
+ }
1394
+ }
1395
+ }
1396
+ async handleSubmit(e) {
1397
+ e.preventDefault();
1398
+ const alertContainer = this.container.querySelector('#alert-container');
1399
+ const submitBtn = this.container.querySelector('button[type="submit"]');
1400
+ if (alertContainer)
1401
+ alertContainer.innerHTML = '';
1402
+ if (submitBtn)
1403
+ submitBtn.disabled = true;
1404
+ try {
1405
+ await this.api.updateUser(this.userDn, this.formData);
1406
+ if (alertContainer) {
1407
+ alertContainer.innerHTML = `
1408
+ <div class="alert alert-success">
1409
+ <span class="material-icons">check_circle</span>
1410
+ <div>User updated successfully!</div>
1411
+ </div>
1412
+ `;
1413
+ }
1414
+ if (this.onSaved)
1415
+ this.onSaved();
1416
+ }
1417
+ catch (error) {
1418
+ if (alertContainer) {
1419
+ alertContainer.innerHTML = `
1420
+ <div class="alert alert-error">
1421
+ <span class="material-icons">error</span>
1422
+ <div>${UserEditor.escapeHtml(error.message)}</div>
1423
+ </div>
1424
+ `;
1425
+ }
1426
+ }
1427
+ finally {
1428
+ if (submitBtn)
1429
+ submitBtn.disabled = false;
1430
+ }
1431
+ }
1432
+ async handleMove() {
1433
+ if (!this.user || !this.schema)
1434
+ return;
1435
+ // Find the organizationLink field to get current organization
1436
+ const orgLinkField = Object.entries(this.schema.attributes).find(([, attr]) => attr.role === 'organizationLink');
1437
+ if (!orgLinkField) {
1438
+ alert('This user type does not support moving between organizations');
1439
+ return;
1440
+ }
1441
+ const [orgLinkFieldName] = orgLinkField;
1442
+ const currentOrgDn = Array.isArray(this.user[orgLinkFieldName])
1443
+ ? String(this.user[orgLinkFieldName][0])
1444
+ : String(this.user[orgLinkFieldName] || '');
1445
+ if (!currentOrgDn) {
1446
+ alert('User does not have an organization link');
1447
+ return;
1448
+ }
1449
+ const modal = new MoveUserModal(this.api, currentOrgDn, async (targetOrgDn) => {
1450
+ await this.performMove(targetOrgDn);
1451
+ });
1452
+ await modal.show();
1453
+ }
1454
+ async performMove(targetOrgDn) {
1455
+ const alertContainer = this.container.querySelector('#alert-container');
1456
+ const moveBtn = this.container.querySelector('#move-user-btn');
1457
+ if (alertContainer)
1458
+ alertContainer.innerHTML = '';
1459
+ if (moveBtn) {
1460
+ moveBtn.disabled = true;
1461
+ moveBtn.innerHTML = `
1462
+ <span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px;"></span>
1463
+ Moving...
1464
+ `;
1465
+ }
1466
+ try {
1467
+ const result = await this.api.moveUser(this.userDn, targetOrgDn);
1468
+ if (alertContainer) {
1469
+ alertContainer.innerHTML = `
1470
+ <div class="alert alert-success">
1471
+ <span class="material-icons">check_circle</span>
1472
+ <div>User moved successfully!</div>
1473
+ </div>
1474
+ `;
1475
+ }
1476
+ // Update the userDn if it changed
1477
+ if (result.newDn) {
1478
+ this.userDn = result.newDn;
1479
+ }
1480
+ // Reload the user data to show updated organization
1481
+ await this.init();
1482
+ if (this.onSaved)
1483
+ this.onSaved();
1484
+ }
1485
+ catch (error) {
1486
+ if (alertContainer) {
1487
+ alertContainer.innerHTML = `
1488
+ <div class="alert alert-error">
1489
+ <span class="material-icons">error</span>
1490
+ <div>${UserEditor.escapeHtml(error.message)}</div>
1491
+ </div>
1492
+ `;
1493
+ }
1494
+ // Re-enable button on error
1495
+ if (moveBtn) {
1496
+ moveBtn.disabled = false;
1497
+ moveBtn.innerHTML = `
1498
+ <span class="material-icons">drive_file_move</span>
1499
+ Move User
1500
+ `;
1501
+ }
1502
+ throw error;
1503
+ }
1504
+ }
1505
+ async handleDelete() {
1506
+ if (!confirm(`Are you sure you want to delete this user?\n\n${this.userDn}`)) {
1507
+ return;
1508
+ }
1509
+ const alertContainer = this.container.querySelector('#alert-container');
1510
+ const deleteBtn = this.container.querySelector('#delete-user-btn');
1511
+ if (alertContainer)
1512
+ alertContainer.innerHTML = '';
1513
+ if (deleteBtn)
1514
+ deleteBtn.disabled = true;
1515
+ try {
1516
+ await this.api.deleteUser(this.userDn);
1517
+ if (alertContainer) {
1518
+ alertContainer.innerHTML = `
1519
+ <div class="alert alert-success">
1520
+ <span class="material-icons">check_circle</span>
1521
+ <div>User deleted successfully!</div>
1522
+ </div>
1523
+ `;
1524
+ }
1525
+ if (this.onDeleted)
1526
+ this.onDeleted();
1527
+ }
1528
+ catch (error) {
1529
+ if (alertContainer) {
1530
+ alertContainer.innerHTML = `
1531
+ <div class="alert alert-error">
1532
+ <span class="material-icons">error</span>
1533
+ <div>${UserEditor.escapeHtml(error.message)}</div>
1534
+ </div>
1535
+ `;
1536
+ }
1537
+ if (deleteBtn)
1538
+ deleteBtn.disabled = false;
1539
+ }
1540
+ }
1541
+ async refresh() {
1542
+ await this.init();
1543
+ }
1544
+ }
1545
+
1546
+ /**
1547
+ * LDAP User Editor - User List Component
1548
+ * Shows users in a selected organization
1549
+ */
1550
+ class UserList {
1551
+ constructor(container, api, orgDn, onSelectUser) {
1552
+ this.users = [];
1553
+ this.searchQuery = '';
1554
+ this.schema = null;
1555
+ this.config = null;
1556
+ this.container = container;
1557
+ this.api = api;
1558
+ this.orgDn = orgDn;
1559
+ this.onSelectUser = onSelectUser;
1560
+ }
1561
+ async init() {
1562
+ // Load config and schema first
1563
+ this.config = await this.api.getConfig();
1564
+ const usersResource = this.config.features?.ldapFlatGeneric?.flatResources?.find(r => r.pluralName === 'users' || r.name === 'users');
1565
+ if (usersResource?.schemaUrl) {
1566
+ this.schema = await this.api.getSchema(usersResource.schemaUrl);
1567
+ }
1568
+ this.render();
1569
+ await this.loadUsers();
1570
+ }
1571
+ render() {
1572
+ this.container.innerHTML = `
1573
+ <div class="user-list-container">
1574
+ <div class="user-list-header">
1575
+ <h2>
1576
+ <span class="material-icons">people</span>
1577
+ Users
1578
+ </h2>
1579
+ <div class="search-box">
1580
+ <span class="material-icons">search</span>
1581
+ <input
1582
+ type="text"
1583
+ placeholder="Search users in this organization..."
1584
+ id="user-list-search-input"
1585
+ />
1586
+ </div>
1587
+ </div>
1588
+ <div class="user-list" id="user-list-items">
1589
+ <div class="loading">
1590
+ <div class="spinner"></div>
1591
+ </div>
1592
+ </div>
1593
+ </div>
1594
+ `;
1595
+ const searchInput = this.container.querySelector('#user-list-search-input');
1596
+ searchInput?.addEventListener('input', e => {
1597
+ this.searchQuery = e.target.value;
1598
+ this.renderUserList();
1599
+ });
1600
+ }
1601
+ async loadUsers() {
1602
+ const listEl = this.container.querySelector('#user-list-items');
1603
+ if (!listEl)
1604
+ return;
1605
+ try {
1606
+ listEl.innerHTML =
1607
+ '<div class="loading"><div class="spinner"></div></div>';
1608
+ // Use organization subnodes API to get users in this org
1609
+ // Filter by the first objectClass from schema to get only users (not groups/OUs)
1610
+ const baseUrl = this.api['baseUrl'] || window.location.origin;
1611
+ const objectClass = this.schema?.entity?.objectClass?.[1] || 'twakeAccount'; // Use second objectClass (not 'top')
1612
+ const res = await fetch(`${baseUrl}/api/v1/ldap/organizations/${encodeURIComponent(this.orgDn)}/subnodes?objectClass=${encodeURIComponent(objectClass)}`);
1613
+ if (!res.ok)
1614
+ throw new Error('Failed to fetch organization users');
1615
+ const items = (await res.json());
1616
+ // Filter only user entries using schema objectClass
1617
+ // An item is a user if it has ALL the expected objectClasses
1618
+ const expectedObjectClasses = this.schema?.entity?.objectClass || [];
1619
+ const mainAttribute = this.schema?.entity?.mainAttribute || '';
1620
+ this.users = items.filter(item => {
1621
+ const objectClass = Array.isArray(item.objectClass)
1622
+ ? item.objectClass
1623
+ : [item.objectClass];
1624
+ // Must have the mainAttribute field (uid, sAMAccountName, etc.)
1625
+ if (!item[mainAttribute])
1626
+ return false;
1627
+ // Check if item has ALL the expected objectClasses
1628
+ return expectedObjectClasses.every(expected => objectClass.includes(expected));
1629
+ });
1630
+ this.renderUserList();
1631
+ }
1632
+ catch (error) {
1633
+ console.error('Failed to load users:', error);
1634
+ listEl.innerHTML =
1635
+ '<div class="empty-state"><span class="material-icons">error</span><p>Failed to load users</p></div>';
1636
+ }
1637
+ }
1638
+ getFirstValue(value) {
1639
+ if (!value)
1640
+ return '';
1641
+ if (Array.isArray(value))
1642
+ return value[0] || '';
1643
+ return String(value);
1644
+ }
1645
+ getFieldNameByRole(role) {
1646
+ if (!this.schema)
1647
+ return null;
1648
+ const field = Object.entries(this.schema.attributes).find(([, attr]) => attr.role === role);
1649
+ return field ? field[0] : null;
1650
+ }
1651
+ getFieldValueByRole(user, role) {
1652
+ const fieldName = this.getFieldNameByRole(role);
1653
+ if (!fieldName)
1654
+ return '';
1655
+ return this.getFirstValue(user[fieldName]);
1656
+ }
1657
+ renderUserList() {
1658
+ const listEl = this.container.querySelector('#user-list-items');
1659
+ if (!listEl)
1660
+ return;
1661
+ // Get field names from schema roles
1662
+ const displayNameField = this.getFieldNameByRole('displayName');
1663
+ const identifierField = this.schema?.entity?.mainAttribute || '';
1664
+ const emailField = this.getFieldNameByRole('primaryEmail');
1665
+ // Filter users by search query
1666
+ const filteredUsers = this.searchQuery
1667
+ ? this.users.filter(user => {
1668
+ const displayName = displayNameField
1669
+ ? this.getFirstValue(user[displayNameField]).toLowerCase()
1670
+ : '';
1671
+ const identifier = this.getFirstValue(user[identifierField]).toLowerCase();
1672
+ const email = emailField
1673
+ ? this.getFirstValue(user[emailField]).toLowerCase()
1674
+ : '';
1675
+ const query = this.searchQuery.toLowerCase();
1676
+ return (displayName.includes(query) ||
1677
+ identifier.includes(query) ||
1678
+ email.includes(query));
1679
+ })
1680
+ : this.users;
1681
+ if (filteredUsers.length === 0) {
1682
+ listEl.innerHTML =
1683
+ '<div class="empty-state"><span class="material-icons">person_search</span><p>No users found</p></div>';
1684
+ return;
1685
+ }
1686
+ listEl.innerHTML = filteredUsers
1687
+ .map(user => {
1688
+ const displayName = displayNameField
1689
+ ? this.getFirstValue(user[displayNameField])
1690
+ : '';
1691
+ const identifier = this.getFirstValue(user[identifierField]);
1692
+ const email = emailField ? this.getFirstValue(user[emailField]) : '';
1693
+ const name = displayName || identifier || 'Unknown';
1694
+ return `
1695
+ <div
1696
+ class="tree-node"
1697
+ data-dn="${user.dn}"
1698
+ >
1699
+ <span class="material-icons">person</span>
1700
+ <div class="tree-node-content">
1701
+ <div class="tree-node-name">
1702
+ ${name}
1703
+ </div>
1704
+ ${email ? `<div class="tree-node-email">${email}</div>` : ''}
1705
+ </div>
1706
+ </div>
1707
+ `;
1708
+ })
1709
+ .join('');
1710
+ // Add click handlers
1711
+ listEl.querySelectorAll('.tree-node').forEach(node => {
1712
+ node.addEventListener('click', () => {
1713
+ const dn = node.getAttribute('data-dn');
1714
+ if (dn) {
1715
+ this.onSelectUser(dn);
1716
+ }
1717
+ });
1718
+ });
1719
+ }
1720
+ async refresh() {
1721
+ await this.loadUsers();
1722
+ }
1723
+ }
1724
+
1725
+ /**
1726
+ * LDAP User Editor - Main class
1727
+ * @author Xavier Guimard
1728
+ */
1729
+ class LdapUserEditor {
1730
+ constructor(options) {
1731
+ this.container = null;
1732
+ this.userTree = null;
1733
+ this.userEditor = null;
1734
+ this.userList = null;
1735
+ this.currentUserDn = null;
1736
+ this.currentOrgDn = null;
1737
+ this.config = null;
1738
+ this.options = {
1739
+ apiBaseUrl: typeof window !== 'undefined' ? window.location.origin : '',
1740
+ ...options,
1741
+ };
1742
+ this.api = new UserApiClient(this.options.apiBaseUrl);
1743
+ }
1744
+ async init() {
1745
+ this.container = document.getElementById(this.options.containerId);
1746
+ if (!this.container) {
1747
+ throw new Error(`Container #${this.options.containerId} not found`);
1748
+ }
1749
+ // Load config first
1750
+ this.config = await this.api.getConfig();
1751
+ this.render();
1752
+ await this.initComponents();
1753
+ }
1754
+ render() {
1755
+ if (!this.container)
1756
+ return;
1757
+ this.container.innerHTML = `
1758
+ <div class="ldap-user-editor">
1759
+ <div class="editor-layout">
1760
+ <div id="user-tree-container" class="demo-panel"></div>
1761
+ <div id="user-editor-container" class="demo-panel">
1762
+ <div class="empty-state">
1763
+ <span class="material-icons">person_search</span>
1764
+ <p>Select a user from the list to edit</p>
1765
+ </div>
1766
+ </div>
1767
+ </div>
1768
+ </div>
1769
+ `;
1770
+ }
1771
+ async initComponents() {
1772
+ const treeContainer = document.getElementById('user-tree-container');
1773
+ if (!treeContainer)
1774
+ return;
1775
+ this.userTree = new UserTree(treeContainer, this.options.apiBaseUrl || window.location.origin, orgDn => this.onOrganizationSelected(orgDn));
1776
+ try {
1777
+ await this.userTree.init();
1778
+ }
1779
+ catch (error) {
1780
+ this.handleError(error);
1781
+ }
1782
+ }
1783
+ async onOrganizationSelected(dn) {
1784
+ const editorContainer = document.getElementById('user-editor-container');
1785
+ if (!editorContainer)
1786
+ return;
1787
+ this.currentOrgDn = dn;
1788
+ this.currentUserDn = null;
1789
+ try {
1790
+ // Show user list for this organization
1791
+ this.userList = new UserList(editorContainer, this.api, dn, userDn => this.onUserSelected(userDn));
1792
+ await this.userList.init();
1793
+ }
1794
+ catch (error) {
1795
+ this.handleError(error);
1796
+ editorContainer.innerHTML = `
1797
+ <div class="empty-state">
1798
+ <span class="material-icons">error</span>
1799
+ <p>Failed to load users for this organization</p>
1800
+ </div>
1801
+ `;
1802
+ }
1803
+ }
1804
+ async onUserSelected(dn) {
1805
+ this.currentUserDn = dn;
1806
+ const editorContainer = document.getElementById('user-editor-container');
1807
+ if (!editorContainer)
1808
+ return;
1809
+ this.userEditor = new UserEditor(editorContainer, this.api, dn, () => {
1810
+ if (this.options.onUserSaved) {
1811
+ this.options.onUserSaved(dn);
1812
+ }
1813
+ // Refresh the user list if we have one
1814
+ if (this.currentOrgDn && this.userList) {
1815
+ this.userList.refresh();
1816
+ }
1817
+ }, () => {
1818
+ // On user deleted
1819
+ this.deleteUser(dn);
1820
+ });
1821
+ try {
1822
+ await this.userEditor.init();
1823
+ }
1824
+ catch (error) {
1825
+ this.handleError(error);
1826
+ }
1827
+ }
1828
+ handleError(error) {
1829
+ console.error('LDAP User Editor error:', error);
1830
+ if (this.options.onError) {
1831
+ this.options.onError(error);
1832
+ }
1833
+ }
1834
+ async refresh() {
1835
+ await this.userTree?.refresh();
1836
+ if (this.currentUserDn) {
1837
+ await this.userEditor?.refresh();
1838
+ }
1839
+ }
1840
+ destroy() {
1841
+ if (this.container) {
1842
+ this.container.innerHTML = '';
1843
+ }
1844
+ this.userTree = null;
1845
+ this.userEditor = null;
1846
+ this.currentUserDn = null;
1847
+ }
1848
+ async createUser(userData) {
1849
+ try {
1850
+ const result = await this.api.createUser(userData);
1851
+ // Refresh the user list if we have one
1852
+ if (this.userList) {
1853
+ await this.userList.refresh();
1854
+ }
1855
+ return result;
1856
+ }
1857
+ catch (error) {
1858
+ this.handleError(error);
1859
+ throw error;
1860
+ }
1861
+ }
1862
+ async deleteUser(dn) {
1863
+ try {
1864
+ await this.api.deleteUser(dn);
1865
+ // Clear current selection if we deleted the selected user
1866
+ if (this.currentUserDn === dn) {
1867
+ this.currentUserDn = null;
1868
+ const editorContainer = document.getElementById('user-editor-container');
1869
+ if (editorContainer) {
1870
+ editorContainer.innerHTML = `
1871
+ <div class="empty-state">
1872
+ <span class="material-icons">person_search</span>
1873
+ <p>Select a user from the list to edit</p>
1874
+ </div>
1875
+ `;
1876
+ }
1877
+ }
1878
+ // Refresh the user list
1879
+ if (this.userList) {
1880
+ await this.userList.refresh();
1881
+ }
1882
+ }
1883
+ catch (error) {
1884
+ this.handleError(error);
1885
+ throw error;
1886
+ }
1887
+ }
1888
+ getConfig() {
1889
+ return this.config;
1890
+ }
1891
+ getApi() {
1892
+ return this.api;
1893
+ }
1894
+ getCurrentOrgDn() {
1895
+ return this.currentOrgDn;
1896
+ }
1897
+ getCurrentUserDn() {
1898
+ return this.currentUserDn;
1899
+ }
1900
+ }
1901
+
1902
+ exports.LdapUserEditor = LdapUserEditor;
1903
+
1904
+ }));
1905
+ //# sourceMappingURL=ldap-user-editor.js.map