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