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,1659 @@
1
+ # Integration Examples - LDAP-Rest
2
+
3
+ This guide provides complete, production-ready examples for integrating LDAP-Rest into your applications using React, Vue.js, and Vanilla JavaScript.
4
+
5
+ ## Table of Contents
6
+
7
+ - [React Application Example](#react-application-example)
8
+ - [Vue.js Application Example](#vuejs-application-example)
9
+ - [Vanilla JavaScript with TreeViewer](#vanilla-javascript-with-treeviewer)
10
+ - [Group Management Application](#group-management-application)
11
+
12
+ ---
13
+
14
+ ## React Application Example
15
+
16
+ This example demonstrates a complete React functional component that fetches users from LDAP-Rest and displays them in a list.
17
+
18
+ ### Complete React Component
19
+
20
+ ```jsx
21
+ import React, { useState, useEffect } from 'react';
22
+
23
+ const UserManager = () => {
24
+ const [users, setUsers] = useState([]);
25
+ const [config, setConfig] = useState(null);
26
+ const [usersEndpoint, setUsersEndpoint] = useState(null);
27
+ const [loading, setLoading] = useState(true);
28
+ const [error, setError] = useState(null);
29
+
30
+ // Load configuration on component mount
31
+ useEffect(() => {
32
+ const loadConfig = async () => {
33
+ try {
34
+ const response = await fetch('http://localhost:8081/api/v1/config');
35
+ if (!response.ok) {
36
+ throw new Error(`HTTP error! status: ${response.status}`);
37
+ }
38
+ const configData = await response.json();
39
+ setConfig(configData);
40
+
41
+ // Discover users endpoint from config
42
+ const userResource = configData.features?.flatResources?.find(
43
+ r => r.pluralName === 'users'
44
+ );
45
+
46
+ if (!userResource) {
47
+ throw new Error('Users endpoint not found in configuration');
48
+ }
49
+
50
+ setUsersEndpoint(userResource.endpoints.list);
51
+ } catch (err) {
52
+ console.error('Failed to load config:', err);
53
+ setError(err.message);
54
+ setLoading(false);
55
+ }
56
+ };
57
+
58
+ loadConfig();
59
+ }, []);
60
+
61
+ // Load users when endpoint is discovered
62
+ useEffect(() => {
63
+ if (!usersEndpoint) return;
64
+
65
+ const loadUsers = async () => {
66
+ try {
67
+ setLoading(true);
68
+ const response = await fetch(usersEndpoint);
69
+ if (!response.ok) {
70
+ throw new Error(`HTTP error! status: ${response.status}`);
71
+ }
72
+ const usersData = await response.json();
73
+ setUsers(usersData);
74
+ setError(null);
75
+ } catch (err) {
76
+ console.error('Failed to load users:', err);
77
+ setError(err.message);
78
+ } finally {
79
+ setLoading(false);
80
+ }
81
+ };
82
+
83
+ loadUsers();
84
+ }, [usersEndpoint]);
85
+
86
+ // Error state
87
+ if (error) {
88
+ return (
89
+ <div className="error-container">
90
+ <h2>Error</h2>
91
+ <p>{error}</p>
92
+ <button onClick={() => window.location.reload()}>Retry</button>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ // Loading state
98
+ if (loading) {
99
+ return (
100
+ <div className="loading-container">
101
+ <div className="spinner"></div>
102
+ <p>Loading users...</p>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // Success state - display users
108
+ return (
109
+ <div className="user-manager">
110
+ <header className="header">
111
+ <h1>User Management</h1>
112
+ <p className="subtitle">
113
+ Managing {users.length} users in {config?.ldapBase}
114
+ </p>
115
+ </header>
116
+
117
+ <div className="user-list">
118
+ {users.length === 0 ? (
119
+ <div className="empty-state">
120
+ <p>No users found</p>
121
+ </div>
122
+ ) : (
123
+ <table className="user-table">
124
+ <thead>
125
+ <tr>
126
+ <th>Username</th>
127
+ <th>Full Name</th>
128
+ <th>Email</th>
129
+ <th>DN</th>
130
+ </tr>
131
+ </thead>
132
+ <tbody>
133
+ {users.map(user => (
134
+ <tr key={user.dn}>
135
+ <td className="username">{user.uid}</td>
136
+ <td>{user.cn}</td>
137
+ <td>
138
+ {user.mail ? (
139
+ <a href={`mailto:${user.mail}`}>{user.mail}</a>
140
+ ) : (
141
+ <span className="no-email">No email</span>
142
+ )}
143
+ </td>
144
+ <td className="dn">{user.dn}</td>
145
+ </tr>
146
+ ))}
147
+ </tbody>
148
+ </table>
149
+ )}
150
+ </div>
151
+ </div>
152
+ );
153
+ };
154
+
155
+ export default UserManager;
156
+ ```
157
+
158
+ ### Styles (Optional)
159
+
160
+ ```css
161
+ .user-manager {
162
+ max-width: 1200px;
163
+ margin: 0 auto;
164
+ padding: 2rem;
165
+ }
166
+
167
+ .header {
168
+ margin-bottom: 2rem;
169
+ border-bottom: 2px solid #e2e8f0;
170
+ padding-bottom: 1rem;
171
+ }
172
+
173
+ .header h1 {
174
+ margin: 0 0 0.5rem 0;
175
+ color: #1e293b;
176
+ }
177
+
178
+ .subtitle {
179
+ color: #64748b;
180
+ margin: 0;
181
+ }
182
+
183
+ .loading-container,
184
+ .error-container {
185
+ display: flex;
186
+ flex-direction: column;
187
+ align-items: center;
188
+ justify-content: center;
189
+ min-height: 400px;
190
+ gap: 1rem;
191
+ }
192
+
193
+ .spinner {
194
+ width: 50px;
195
+ height: 50px;
196
+ border: 4px solid #e2e8f0;
197
+ border-top-color: #6200ee;
198
+ border-radius: 50%;
199
+ animation: spin 1s linear infinite;
200
+ }
201
+
202
+ @keyframes spin {
203
+ to {
204
+ transform: rotate(360deg);
205
+ }
206
+ }
207
+
208
+ .error-container {
209
+ color: #c62828;
210
+ }
211
+
212
+ .error-container button {
213
+ padding: 0.5rem 1rem;
214
+ background: #6200ee;
215
+ color: white;
216
+ border: none;
217
+ border-radius: 4px;
218
+ cursor: pointer;
219
+ }
220
+
221
+ .user-table {
222
+ width: 100%;
223
+ border-collapse: collapse;
224
+ background: white;
225
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
226
+ }
227
+
228
+ .user-table th,
229
+ .user-table td {
230
+ padding: 1rem;
231
+ text-align: left;
232
+ border-bottom: 1px solid #e2e8f0;
233
+ }
234
+
235
+ .user-table th {
236
+ background: #f8fafc;
237
+ font-weight: 600;
238
+ color: #1e293b;
239
+ }
240
+
241
+ .user-table tr:hover {
242
+ background: #f8fafc;
243
+ }
244
+
245
+ .username {
246
+ font-family: monospace;
247
+ font-weight: 600;
248
+ }
249
+
250
+ .dn {
251
+ font-family: monospace;
252
+ font-size: 0.875rem;
253
+ color: #64748b;
254
+ }
255
+
256
+ .no-email {
257
+ color: #94a3b8;
258
+ font-style: italic;
259
+ }
260
+
261
+ .empty-state {
262
+ text-align: center;
263
+ padding: 3rem;
264
+ color: #64748b;
265
+ }
266
+ ```
267
+
268
+ ### Usage
269
+
270
+ ```jsx
271
+ import React from 'react';
272
+ import ReactDOM from 'react-dom/client';
273
+ import UserManager from './UserManager';
274
+
275
+ const root = ReactDOM.createRoot(document.getElementById('root'));
276
+ root.render(
277
+ <React.StrictMode>
278
+ <UserManager />
279
+ </React.StrictMode>
280
+ );
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Vue.js Application Example
286
+
287
+ This example shows a complete Vue.js component using the LdapUserEditor library with proper lifecycle management and toast notifications.
288
+
289
+ ### Complete Vue Component
290
+
291
+ ```vue
292
+ <template>
293
+ <div class="ldap-editor-wrapper">
294
+ <header class="app-header">
295
+ <h1>LDAP User Editor</h1>
296
+ <p class="subtitle">Manage your organization's users</p>
297
+ </header>
298
+
299
+ <div v-if="loading" class="loading-state">
300
+ <div class="spinner"></div>
301
+ <p>Initializing editor...</p>
302
+ </div>
303
+
304
+ <div v-else-if="error" class="error-state">
305
+ <span class="material-icons">error_outline</span>
306
+ <h2>Failed to Initialize</h2>
307
+ <p>{{ error }}</p>
308
+ <button @click="retryInit" class="retry-btn">Retry</button>
309
+ </div>
310
+
311
+ <div v-else id="editor-container" class="editor-container"></div>
312
+
313
+ <!-- Toast Notifications -->
314
+ <Transition name="toast">
315
+ <div v-if="toast.visible" :class="['toast', `toast-${toast.type}`]">
316
+ <span class="material-icons">{{ getToastIcon(toast.type) }}</span>
317
+ <span class="toast-message">{{ toast.message }}</span>
318
+ </div>
319
+ </Transition>
320
+ </div>
321
+ </template>
322
+
323
+ <script>
324
+ export default {
325
+ name: 'LdapEditorApp',
326
+
327
+ data() {
328
+ return {
329
+ editor: null,
330
+ loading: true,
331
+ error: null,
332
+ toast: {
333
+ visible: false,
334
+ message: '',
335
+ type: 'info', // 'success', 'error', 'info', 'warning'
336
+ },
337
+ };
338
+ },
339
+
340
+ mounted() {
341
+ this.initEditor();
342
+ },
343
+
344
+ beforeUnmount() {
345
+ if (this.editor) {
346
+ this.editor.destroy();
347
+ this.editor = null;
348
+ }
349
+ },
350
+
351
+ methods: {
352
+ async initEditor() {
353
+ try {
354
+ this.loading = true;
355
+ this.error = null;
356
+
357
+ // Load LdapUserEditor library
358
+ const { LdapUserEditor } = window.LdapUserEditor;
359
+
360
+ if (!LdapUserEditor) {
361
+ throw new Error('LdapUserEditor library not loaded');
362
+ }
363
+
364
+ // Create editor instance
365
+ this.editor = new LdapUserEditor({
366
+ containerId: 'editor-container',
367
+ apiBaseUrl: window.location.origin,
368
+ onUserSaved: this.handleUserSaved,
369
+ onError: this.handleError,
370
+ });
371
+
372
+ // Initialize the editor
373
+ await this.editor.init();
374
+
375
+ this.loading = false;
376
+ this.showToast('Editor initialized successfully', 'success');
377
+ } catch (err) {
378
+ console.error('Failed to initialize editor:', err);
379
+ this.error = err.message || 'Unknown error occurred';
380
+ this.loading = false;
381
+ this.showToast('Failed to initialize editor', 'error');
382
+ }
383
+ },
384
+
385
+ handleUserSaved(userDn) {
386
+ console.log('User saved:', userDn);
387
+ this.showToast('User saved successfully!', 'success');
388
+
389
+ // Optional: Track analytics
390
+ if (window.gtag) {
391
+ window.gtag('event', 'user_updated', {
392
+ event_category: 'ldap',
393
+ event_label: userDn,
394
+ });
395
+ }
396
+ },
397
+
398
+ handleError(error) {
399
+ console.error('Editor error:', error);
400
+ this.showToast(error.message || 'An error occurred', 'error');
401
+ },
402
+
403
+ retryInit() {
404
+ this.initEditor();
405
+ },
406
+
407
+ showToast(message, type = 'info') {
408
+ this.toast = {
409
+ visible: true,
410
+ message,
411
+ type,
412
+ };
413
+
414
+ // Auto-hide after 3 seconds
415
+ setTimeout(() => {
416
+ this.toast.visible = false;
417
+ }, 3000);
418
+ },
419
+
420
+ getToastIcon(type) {
421
+ const icons = {
422
+ success: 'check_circle',
423
+ error: 'error',
424
+ warning: 'warning',
425
+ info: 'info',
426
+ };
427
+ return icons[type] || 'info';
428
+ },
429
+ },
430
+ };
431
+ </script>
432
+
433
+ <style scoped>
434
+ .ldap-editor-wrapper {
435
+ min-height: 100vh;
436
+ background: #f5f5f5;
437
+ }
438
+
439
+ .app-header {
440
+ background: white;
441
+ padding: 2rem;
442
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
443
+ margin-bottom: 2rem;
444
+ }
445
+
446
+ .app-header h1 {
447
+ margin: 0 0 0.5rem 0;
448
+ color: #1e293b;
449
+ font-size: 2rem;
450
+ }
451
+
452
+ .subtitle {
453
+ margin: 0;
454
+ color: #64748b;
455
+ }
456
+
457
+ .loading-state,
458
+ .error-state {
459
+ display: flex;
460
+ flex-direction: column;
461
+ align-items: center;
462
+ justify-content: center;
463
+ min-height: 400px;
464
+ gap: 1rem;
465
+ text-align: center;
466
+ }
467
+
468
+ .spinner {
469
+ width: 50px;
470
+ height: 50px;
471
+ border: 4px solid #e2e8f0;
472
+ border-top-color: #6200ee;
473
+ border-radius: 50%;
474
+ animation: spin 1s linear infinite;
475
+ }
476
+
477
+ @keyframes spin {
478
+ to {
479
+ transform: rotate(360deg);
480
+ }
481
+ }
482
+
483
+ .error-state {
484
+ color: #c62828;
485
+ }
486
+
487
+ .error-state .material-icons {
488
+ font-size: 64px;
489
+ }
490
+
491
+ .retry-btn {
492
+ padding: 0.75rem 1.5rem;
493
+ background: #6200ee;
494
+ color: white;
495
+ border: none;
496
+ border-radius: 4px;
497
+ font-size: 1rem;
498
+ cursor: pointer;
499
+ transition: background 0.2s;
500
+ }
501
+
502
+ .retry-btn:hover {
503
+ background: #3700b3;
504
+ }
505
+
506
+ .editor-container {
507
+ padding: 2rem;
508
+ }
509
+
510
+ /* Toast Notifications */
511
+ .toast {
512
+ position: fixed;
513
+ bottom: 2rem;
514
+ right: 2rem;
515
+ display: flex;
516
+ align-items: center;
517
+ gap: 0.75rem;
518
+ padding: 1rem 1.5rem;
519
+ background: white;
520
+ border-radius: 8px;
521
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
522
+ z-index: 1000;
523
+ min-width: 300px;
524
+ }
525
+
526
+ .toast-success {
527
+ border-left: 4px solid #2e7d32;
528
+ }
529
+
530
+ .toast-success .material-icons {
531
+ color: #2e7d32;
532
+ }
533
+
534
+ .toast-error {
535
+ border-left: 4px solid #c62828;
536
+ }
537
+
538
+ .toast-error .material-icons {
539
+ color: #c62828;
540
+ }
541
+
542
+ .toast-warning {
543
+ border-left: 4px solid #f57c00;
544
+ }
545
+
546
+ .toast-warning .material-icons {
547
+ color: #f57c00;
548
+ }
549
+
550
+ .toast-info {
551
+ border-left: 4px solid #1976d2;
552
+ }
553
+
554
+ .toast-info .material-icons {
555
+ color: #1976d2;
556
+ }
557
+
558
+ .toast-message {
559
+ flex: 1;
560
+ color: #1e293b;
561
+ }
562
+
563
+ /* Toast Transitions */
564
+ .toast-enter-active,
565
+ .toast-leave-active {
566
+ transition: all 0.3s ease;
567
+ }
568
+
569
+ .toast-enter-from,
570
+ .toast-leave-to {
571
+ transform: translateX(400px);
572
+ opacity: 0;
573
+ }
574
+ </style>
575
+ ```
576
+
577
+ ### HTML Template
578
+
579
+ ```html
580
+ <!DOCTYPE html>
581
+ <html lang="en">
582
+ <head>
583
+ <meta charset="UTF-8" />
584
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
585
+ <title>LDAP User Editor - Vue.js</title>
586
+
587
+ <!-- Google Fonts -->
588
+ <link
589
+ href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
590
+ rel="stylesheet"
591
+ />
592
+ <link
593
+ href="https://fonts.googleapis.com/icon?family=Material+Icons"
594
+ rel="stylesheet"
595
+ />
596
+
597
+ <!-- LdapUserEditor CSS -->
598
+ <link
599
+ rel="stylesheet"
600
+ href="/static/browser/ldap-user-editor/LdapUserEditor.css"
601
+ />
602
+
603
+ <!-- Vue.js (Development version) -->
604
+ <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
605
+
606
+ <style>
607
+ body {
608
+ margin: 0;
609
+ font-family: 'Roboto', sans-serif;
610
+ }
611
+ </style>
612
+ </head>
613
+ <body>
614
+ <div id="app"></div>
615
+
616
+ <!-- LdapUserEditor JavaScript -->
617
+ <script src="/static/browser/ldap-user-editor/LdapUserEditor.js"></script>
618
+
619
+ <!-- Your Vue App -->
620
+ <script src="./app.js"></script>
621
+ </body>
622
+ </html>
623
+ ```
624
+
625
+ ### App Entry Point (app.js)
626
+
627
+ ```javascript
628
+ const { createApp } = Vue;
629
+ import LdapEditorApp from './LdapEditorApp.vue';
630
+
631
+ createApp(LdapEditorApp).mount('#app');
632
+ ```
633
+
634
+ ---
635
+
636
+ ## Vanilla JavaScript with TreeViewer
637
+
638
+ This example demonstrates a complete HTML page with sidebar tree navigation and content area for displaying node details.
639
+
640
+ ### Complete HTML Page
641
+
642
+ ```html
643
+ <!DOCTYPE html>
644
+ <html lang="en">
645
+ <head>
646
+ <meta charset="UTF-8" />
647
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
648
+ <title>Organization Browser - LDAP-Rest</title>
649
+
650
+ <!-- Material Icons -->
651
+ <link
652
+ href="https://fonts.googleapis.com/icon?family=Material+Icons"
653
+ rel="stylesheet"
654
+ />
655
+
656
+ <!-- LdapTreeViewer CSS -->
657
+ <link
658
+ rel="stylesheet"
659
+ href="/static/browser/ldap-tree-viewer/LdapTreeViewer.css"
660
+ />
661
+
662
+ <style>
663
+ * {
664
+ margin: 0;
665
+ padding: 0;
666
+ box-sizing: border-box;
667
+ }
668
+
669
+ body {
670
+ font-family:
671
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
672
+ Cantarell, sans-serif;
673
+ background: #f5f5f5;
674
+ color: #333;
675
+ }
676
+
677
+ .app-container {
678
+ display: flex;
679
+ height: 100vh;
680
+ overflow: hidden;
681
+ }
682
+
683
+ /* Sidebar with Tree */
684
+ .sidebar {
685
+ width: 350px;
686
+ background: white;
687
+ border-right: 1px solid #e0e0e0;
688
+ display: flex;
689
+ flex-direction: column;
690
+ }
691
+
692
+ .sidebar-header {
693
+ padding: 1.5rem;
694
+ background: #6200ee;
695
+ color: white;
696
+ }
697
+
698
+ .sidebar-header h1 {
699
+ font-size: 1.25rem;
700
+ margin-bottom: 0.25rem;
701
+ }
702
+
703
+ .sidebar-header p {
704
+ font-size: 0.875rem;
705
+ opacity: 0.9;
706
+ }
707
+
708
+ .tree-container {
709
+ flex: 1;
710
+ overflow-y: auto;
711
+ padding: 1rem;
712
+ }
713
+
714
+ /* Main Content Area */
715
+ .content-area {
716
+ flex: 1;
717
+ display: flex;
718
+ flex-direction: column;
719
+ overflow: hidden;
720
+ }
721
+
722
+ .content-header {
723
+ padding: 1.5rem 2rem;
724
+ background: white;
725
+ border-bottom: 1px solid #e0e0e0;
726
+ }
727
+
728
+ .content-header h2 {
729
+ font-size: 1.5rem;
730
+ color: #1e293b;
731
+ }
732
+
733
+ .content-body {
734
+ flex: 1;
735
+ overflow-y: auto;
736
+ padding: 2rem;
737
+ }
738
+
739
+ .node-details {
740
+ background: white;
741
+ border-radius: 8px;
742
+ padding: 2rem;
743
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
744
+ }
745
+
746
+ .empty-state {
747
+ display: flex;
748
+ flex-direction: column;
749
+ align-items: center;
750
+ justify-content: center;
751
+ height: 100%;
752
+ color: #64748b;
753
+ text-align: center;
754
+ }
755
+
756
+ .empty-state .material-icons {
757
+ font-size: 80px;
758
+ margin-bottom: 1rem;
759
+ opacity: 0.5;
760
+ }
761
+
762
+ .node-type-badge {
763
+ display: inline-block;
764
+ padding: 0.25rem 0.75rem;
765
+ border-radius: 12px;
766
+ font-size: 0.75rem;
767
+ font-weight: 600;
768
+ text-transform: uppercase;
769
+ margin-bottom: 1rem;
770
+ }
771
+
772
+ .node-type-organization {
773
+ background: #e3f2fd;
774
+ color: #1976d2;
775
+ }
776
+
777
+ .node-type-user {
778
+ background: #f3e5f5;
779
+ color: #7b1fa2;
780
+ }
781
+
782
+ .node-type-group {
783
+ background: #e8f5e9;
784
+ color: #388e3c;
785
+ }
786
+
787
+ .detail-section {
788
+ margin-bottom: 2rem;
789
+ }
790
+
791
+ .detail-section h3 {
792
+ font-size: 1rem;
793
+ color: #64748b;
794
+ margin-bottom: 1rem;
795
+ text-transform: uppercase;
796
+ letter-spacing: 0.05em;
797
+ font-weight: 600;
798
+ }
799
+
800
+ .detail-row {
801
+ display: flex;
802
+ padding: 0.75rem 0;
803
+ border-bottom: 1px solid #f1f5f9;
804
+ }
805
+
806
+ .detail-row:last-child {
807
+ border-bottom: none;
808
+ }
809
+
810
+ .detail-label {
811
+ width: 200px;
812
+ font-weight: 600;
813
+ color: #475569;
814
+ }
815
+
816
+ .detail-value {
817
+ flex: 1;
818
+ color: #1e293b;
819
+ word-break: break-all;
820
+ }
821
+
822
+ .detail-value code {
823
+ background: #f1f5f9;
824
+ padding: 0.125rem 0.375rem;
825
+ border-radius: 3px;
826
+ font-family: 'Courier New', monospace;
827
+ font-size: 0.875rem;
828
+ }
829
+
830
+ .children-list {
831
+ list-style: none;
832
+ }
833
+
834
+ .children-list li {
835
+ padding: 0.5rem 0;
836
+ color: #1e293b;
837
+ }
838
+
839
+ .children-list li::before {
840
+ content: '→';
841
+ margin-right: 0.5rem;
842
+ color: #6200ee;
843
+ font-weight: bold;
844
+ }
845
+
846
+ .loading-overlay {
847
+ position: absolute;
848
+ top: 0;
849
+ left: 0;
850
+ right: 0;
851
+ bottom: 0;
852
+ background: rgba(255, 255, 255, 0.9);
853
+ display: flex;
854
+ align-items: center;
855
+ justify-content: center;
856
+ z-index: 10;
857
+ }
858
+
859
+ .spinner {
860
+ width: 50px;
861
+ height: 50px;
862
+ border: 4px solid #e0e0e0;
863
+ border-top-color: #6200ee;
864
+ border-radius: 50%;
865
+ animation: spin 1s linear infinite;
866
+ }
867
+
868
+ @keyframes spin {
869
+ to {
870
+ transform: rotate(360deg);
871
+ }
872
+ }
873
+ </style>
874
+ </head>
875
+ <body>
876
+ <div class="app-container">
877
+ <!-- Sidebar with Tree -->
878
+ <aside class="sidebar">
879
+ <div class="sidebar-header">
880
+ <h1>Organizations</h1>
881
+ <p>Browse your LDAP directory</p>
882
+ </div>
883
+ <div id="tree" class="tree-container"></div>
884
+ </aside>
885
+
886
+ <!-- Main Content Area -->
887
+ <main class="content-area">
888
+ <div class="content-header">
889
+ <h2>Node Details</h2>
890
+ </div>
891
+ <div class="content-body">
892
+ <div id="details" class="empty-state">
893
+ <span class="material-icons">account_tree</span>
894
+ <p>Select a node from the tree to view details</p>
895
+ </div>
896
+ </div>
897
+ </main>
898
+ </div>
899
+
900
+ <!-- LdapTreeViewer JavaScript -->
901
+ <script src="/static/browser/ldap-tree-viewer/LdapTreeViewer.js"></script>
902
+
903
+ <script>
904
+ const { LdapTreeViewer } = window.LdapTreeViewer;
905
+
906
+ // Initialize the tree viewer
907
+ const viewer = new LdapTreeViewer({
908
+ containerId: 'tree',
909
+ apiBaseUrl: window.location.origin,
910
+ onNodeClick: handleNodeClick,
911
+ onNodeExpand: handleNodeExpand,
912
+ });
913
+
914
+ // Handle node clicks
915
+ async function handleNodeClick(node) {
916
+ const detailsContainer = document.getElementById('details');
917
+
918
+ // Show loading state
919
+ detailsContainer.innerHTML = `
920
+ <div class="loading-overlay">
921
+ <div class="spinner"></div>
922
+ </div>
923
+ `;
924
+
925
+ try {
926
+ // Fetch full node details
927
+ const apiUrl = `${viewer.options.apiBaseUrl}/api/v1/ldap/organizations/${encodeURIComponent(node.dn)}`;
928
+ const response = await fetch(apiUrl);
929
+
930
+ if (!response.ok) {
931
+ throw new Error(`HTTP error! status: ${response.status}`);
932
+ }
933
+
934
+ const nodeData = await response.json();
935
+
936
+ // Display node details
937
+ displayNodeDetails(node, nodeData);
938
+ } catch (error) {
939
+ console.error('Failed to load node details:', error);
940
+ detailsContainer.innerHTML = `
941
+ <div class="empty-state">
942
+ <span class="material-icons">error_outline</span>
943
+ <p>Failed to load node details</p>
944
+ <p style="font-size: 0.875rem; margin-top: 0.5rem;">${error.message}</p>
945
+ </div>
946
+ `;
947
+ }
948
+ }
949
+
950
+ // Handle node expansion
951
+ function handleNodeExpand(node) {
952
+ console.log(
953
+ 'Node expanded:',
954
+ node.displayName,
955
+ '(',
956
+ node.childrenDns.length,
957
+ 'children)'
958
+ );
959
+ }
960
+
961
+ // Display node details in the content area
962
+ function displayNodeDetails(node, fullData) {
963
+ const detailsContainer = document.getElementById('details');
964
+
965
+ // Build attributes list
966
+ const attributesHtml = Object.entries(fullData)
967
+ .filter(([key]) => key !== 'dn')
968
+ .map(([key, value]) => {
969
+ const displayValue = Array.isArray(value)
970
+ ? value.join(', ')
971
+ : String(value);
972
+
973
+ return `
974
+ <div class="detail-row">
975
+ <div class="detail-label">${key}</div>
976
+ <div class="detail-value"><code>${escapeHtml(displayValue)}</code></div>
977
+ </div>
978
+ `;
979
+ })
980
+ .join('');
981
+
982
+ // Build children list
983
+ const childrenHtml =
984
+ node.childrenDns.length > 0
985
+ ? `
986
+ <div class="detail-section">
987
+ <h3>Children (${node.childrenDns.length})</h3>
988
+ <ul class="children-list">
989
+ ${node.childrenDns
990
+ .slice(0, 10)
991
+ .map(dn => `<li>${escapeHtml(dn)}</li>`)
992
+ .join('')}
993
+ ${node.childrenDns.length > 10 ? `<li>... and ${node.childrenDns.length - 10} more</li>` : ''}
994
+ </ul>
995
+ </div>
996
+ `
997
+ : '';
998
+
999
+ detailsContainer.innerHTML = `
1000
+ <div class="node-details">
1001
+ <span class="node-type-badge node-type-${node.type}">${node.type}</span>
1002
+ <h2 style="margin-bottom: 0.5rem;">${escapeHtml(node.displayName)}</h2>
1003
+ <p style="color: #64748b; font-family: monospace; font-size: 0.875rem; margin-bottom: 2rem;">
1004
+ ${escapeHtml(node.dn)}
1005
+ </p>
1006
+
1007
+ <div class="detail-section">
1008
+ <h3>Attributes</h3>
1009
+ ${attributesHtml}
1010
+ </div>
1011
+
1012
+ ${childrenHtml}
1013
+ </div>
1014
+ `;
1015
+ }
1016
+
1017
+ // Utility function to escape HTML
1018
+ function escapeHtml(text) {
1019
+ const div = document.createElement('div');
1020
+ div.textContent = text;
1021
+ return div.innerHTML;
1022
+ }
1023
+
1024
+ // Initialize the viewer
1025
+ viewer.init().catch(error => {
1026
+ console.error('Failed to initialize tree viewer:', error);
1027
+ document.getElementById('tree').innerHTML = `
1028
+ <div style="padding: 1rem; color: #c62828; text-align: center;">
1029
+ <p><strong>Failed to initialize</strong></p>
1030
+ <p style="font-size: 0.875rem; margin-top: 0.5rem;">${error.message}</p>
1031
+ </div>
1032
+ `;
1033
+ });
1034
+ </script>
1035
+ </body>
1036
+ </html>
1037
+ ```
1038
+
1039
+ ---
1040
+
1041
+ ## Group Management Application
1042
+
1043
+ This example demonstrates a complete class-based group management application with initialization, group operations, and member management.
1044
+
1045
+ ### Complete GroupManager Class
1046
+
1047
+ ```javascript
1048
+ /**
1049
+ * GroupManager - Complete group management application
1050
+ * Provides methods for listing, creating, and managing LDAP groups
1051
+ */
1052
+ class GroupManager {
1053
+ constructor(apiBaseUrl = window.location.origin) {
1054
+ this.apiBaseUrl = apiBaseUrl;
1055
+ this.config = null;
1056
+ this.groupsEndpoint = null;
1057
+ this.groupsBase = null;
1058
+ }
1059
+
1060
+ /**
1061
+ * Initialize the manager by loading configuration
1062
+ */
1063
+ async init() {
1064
+ try {
1065
+ // Load configuration from API
1066
+ const response = await fetch(`${this.apiBaseUrl}/api/v1/config`);
1067
+ if (!response.ok) {
1068
+ throw new Error(`Failed to load config: ${response.status}`);
1069
+ }
1070
+
1071
+ this.config = await response.json();
1072
+
1073
+ // Discover groups endpoints from config
1074
+ const groupsConfig = this.config.features?.groups;
1075
+ if (!groupsConfig || !groupsConfig.enabled) {
1076
+ throw new Error(
1077
+ 'Groups feature is not enabled in LDAP-Rest configuration'
1078
+ );
1079
+ }
1080
+
1081
+ this.groupsEndpoint = groupsConfig.endpoints;
1082
+ this.groupsBase = groupsConfig.base;
1083
+
1084
+ console.log('GroupManager initialized successfully');
1085
+ console.log('Groups base:', this.groupsBase);
1086
+
1087
+ return true;
1088
+ } catch (error) {
1089
+ console.error('Failed to initialize GroupManager:', error);
1090
+ throw error;
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * List all groups or filter by name pattern
1096
+ * @param {string} matchPattern - Optional pattern to filter groups (e.g., "admin*")
1097
+ * @returns {Promise<Object>} Object with group CNs as keys and group data as values
1098
+ */
1099
+ async listGroups(matchPattern = null) {
1100
+ if (!this.groupsEndpoint) {
1101
+ throw new Error('GroupManager not initialized. Call init() first.');
1102
+ }
1103
+
1104
+ try {
1105
+ let url = this.groupsEndpoint.list;
1106
+
1107
+ // Add filter if provided
1108
+ if (matchPattern) {
1109
+ url += `?match=${encodeURIComponent(matchPattern)}`;
1110
+ }
1111
+
1112
+ const response = await fetch(url);
1113
+ if (!response.ok) {
1114
+ throw new Error(`Failed to list groups: ${response.status}`);
1115
+ }
1116
+
1117
+ const groups = await response.json();
1118
+ console.log(`Found ${Object.keys(groups).length} groups`);
1119
+
1120
+ return groups;
1121
+ } catch (error) {
1122
+ console.error('Error listing groups:', error);
1123
+ throw error;
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Get a specific group by CN
1129
+ * @param {string} cn - Group common name
1130
+ * @returns {Promise<Object>} Group data
1131
+ */
1132
+ async getGroup(cn) {
1133
+ if (!this.groupsEndpoint) {
1134
+ throw new Error('GroupManager not initialized. Call init() first.');
1135
+ }
1136
+
1137
+ try {
1138
+ const url = this.groupsEndpoint.get.replace(
1139
+ ':cn',
1140
+ encodeURIComponent(cn)
1141
+ );
1142
+ const response = await fetch(url);
1143
+
1144
+ if (!response.ok) {
1145
+ if (response.status === 404) {
1146
+ throw new Error(`Group "${cn}" not found`);
1147
+ }
1148
+ throw new Error(`Failed to get group: ${response.status}`);
1149
+ }
1150
+
1151
+ return await response.json();
1152
+ } catch (error) {
1153
+ console.error(`Error getting group "${cn}":`, error);
1154
+ throw error;
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Create a new group
1160
+ * @param {string} cn - Group common name
1161
+ * @param {Object} options - Additional group options
1162
+ * @param {string[]} options.members - Array of member DNs
1163
+ * @param {string} options.description - Group description
1164
+ * @returns {Promise<Object>} Response with success status and DN
1165
+ */
1166
+ async createGroup(cn, options = {}) {
1167
+ if (!this.groupsEndpoint) {
1168
+ throw new Error('GroupManager not initialized. Call init() first.');
1169
+ }
1170
+
1171
+ try {
1172
+ const groupData = {
1173
+ cn: cn,
1174
+ ...options,
1175
+ };
1176
+
1177
+ const response = await fetch(this.groupsEndpoint.create, {
1178
+ method: 'POST',
1179
+ headers: {
1180
+ 'Content-Type': 'application/json',
1181
+ Accept: 'application/json',
1182
+ },
1183
+ body: JSON.stringify(groupData),
1184
+ });
1185
+
1186
+ if (!response.ok) {
1187
+ const errorData = await response.json().catch(() => ({}));
1188
+ throw new Error(
1189
+ errorData.error || `Failed to create group: ${response.status}`
1190
+ );
1191
+ }
1192
+
1193
+ const result = await response.json();
1194
+ console.log(`Group "${cn}" created successfully:`, result.dn);
1195
+
1196
+ return result;
1197
+ } catch (error) {
1198
+ console.error(`Error creating group "${cn}":`, error);
1199
+ throw error;
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * Update a group's attributes
1205
+ * @param {string} cn - Group common name
1206
+ * @param {Object} changes - LDAP modify operations (replace, add, delete)
1207
+ * @returns {Promise<Object>} Response with success status
1208
+ */
1209
+ async updateGroup(cn, changes) {
1210
+ if (!this.groupsEndpoint) {
1211
+ throw new Error('GroupManager not initialized. Call init() first.');
1212
+ }
1213
+
1214
+ try {
1215
+ const url = this.groupsEndpoint.update.replace(
1216
+ ':cn',
1217
+ encodeURIComponent(cn)
1218
+ );
1219
+ const response = await fetch(url, {
1220
+ method: 'PUT',
1221
+ headers: {
1222
+ 'Content-Type': 'application/json',
1223
+ Accept: 'application/json',
1224
+ },
1225
+ body: JSON.stringify(changes),
1226
+ });
1227
+
1228
+ if (!response.ok) {
1229
+ const errorData = await response.json().catch(() => ({}));
1230
+ throw new Error(
1231
+ errorData.error || `Failed to update group: ${response.status}`
1232
+ );
1233
+ }
1234
+
1235
+ const result = await response.json();
1236
+ console.log(`Group "${cn}" updated successfully`);
1237
+
1238
+ return result;
1239
+ } catch (error) {
1240
+ console.error(`Error updating group "${cn}":`, error);
1241
+ throw error;
1242
+ }
1243
+ }
1244
+
1245
+ /**
1246
+ * Delete a group
1247
+ * @param {string} cn - Group common name
1248
+ * @returns {Promise<Object>} Response with success status
1249
+ */
1250
+ async deleteGroup(cn) {
1251
+ if (!this.groupsEndpoint) {
1252
+ throw new Error('GroupManager not initialized. Call init() first.');
1253
+ }
1254
+
1255
+ try {
1256
+ const url = this.groupsEndpoint.delete.replace(
1257
+ ':cn',
1258
+ encodeURIComponent(cn)
1259
+ );
1260
+ const response = await fetch(url, {
1261
+ method: 'DELETE',
1262
+ headers: {
1263
+ Accept: 'application/json',
1264
+ },
1265
+ });
1266
+
1267
+ if (!response.ok) {
1268
+ const errorData = await response.json().catch(() => ({}));
1269
+ throw new Error(
1270
+ errorData.error || `Failed to delete group: ${response.status}`
1271
+ );
1272
+ }
1273
+
1274
+ const result = await response.json();
1275
+ console.log(`Group "${cn}" deleted successfully`);
1276
+
1277
+ return result;
1278
+ } catch (error) {
1279
+ console.error(`Error deleting group "${cn}":`, error);
1280
+ throw error;
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Add a member to a group
1286
+ * @param {string} groupCn - Group common name
1287
+ * @param {string|string[]} memberDn - Member DN(s) to add
1288
+ * @returns {Promise<Object>} Response with success status
1289
+ */
1290
+ async addMember(groupCn, memberDn) {
1291
+ if (!this.groupsEndpoint) {
1292
+ throw new Error('GroupManager not initialized. Call init() first.');
1293
+ }
1294
+
1295
+ try {
1296
+ const url = this.groupsEndpoint.addMember.replace(
1297
+ ':id',
1298
+ encodeURIComponent(groupCn)
1299
+ );
1300
+ const response = await fetch(url, {
1301
+ method: 'POST',
1302
+ headers: {
1303
+ 'Content-Type': 'application/json',
1304
+ Accept: 'application/json',
1305
+ },
1306
+ body: JSON.stringify({ member: memberDn }),
1307
+ });
1308
+
1309
+ if (!response.ok) {
1310
+ const errorData = await response.json().catch(() => ({}));
1311
+ throw new Error(
1312
+ errorData.error || `Failed to add member: ${response.status}`
1313
+ );
1314
+ }
1315
+
1316
+ const result = await response.json();
1317
+ const memberCount = Array.isArray(memberDn) ? memberDn.length : 1;
1318
+ console.log(`Added ${memberCount} member(s) to group "${groupCn}"`);
1319
+
1320
+ return result;
1321
+ } catch (error) {
1322
+ console.error(`Error adding member to group "${groupCn}":`, error);
1323
+ throw error;
1324
+ }
1325
+ }
1326
+
1327
+ /**
1328
+ * Remove a member from a group
1329
+ * @param {string} groupCn - Group common name
1330
+ * @param {string} memberDn - Member DN to remove
1331
+ * @returns {Promise<Object>} Response with success status
1332
+ */
1333
+ async removeMember(groupCn, memberDn) {
1334
+ if (!this.groupsEndpoint) {
1335
+ throw new Error('GroupManager not initialized. Call init() first.');
1336
+ }
1337
+
1338
+ try {
1339
+ const url = this.groupsEndpoint.removeMember
1340
+ .replace(':cn', encodeURIComponent(groupCn))
1341
+ .replace(':memberId', encodeURIComponent(memberDn));
1342
+
1343
+ const response = await fetch(url, {
1344
+ method: 'DELETE',
1345
+ headers: {
1346
+ Accept: 'application/json',
1347
+ },
1348
+ });
1349
+
1350
+ if (!response.ok) {
1351
+ const errorData = await response.json().catch(() => ({}));
1352
+ throw new Error(
1353
+ errorData.error || `Failed to remove member: ${response.status}`
1354
+ );
1355
+ }
1356
+
1357
+ const result = await response.json();
1358
+ console.log(`Removed member from group "${groupCn}"`);
1359
+
1360
+ return result;
1361
+ } catch (error) {
1362
+ console.error(`Error removing member from group "${groupCn}":`, error);
1363
+ throw error;
1364
+ }
1365
+ }
1366
+
1367
+ /**
1368
+ * Get all members of a group
1369
+ * @param {string} cn - Group common name
1370
+ * @returns {Promise<string[]>} Array of member DNs
1371
+ */
1372
+ async getMembers(cn) {
1373
+ try {
1374
+ const group = await this.getGroup(cn);
1375
+ const members = group.member || [];
1376
+
1377
+ // LDAP attributes are always arrays, ensure we return an array
1378
+ return Array.isArray(members) ? members : [members];
1379
+ } catch (error) {
1380
+ console.error(`Error getting members of group "${cn}":`, error);
1381
+ throw error;
1382
+ }
1383
+ }
1384
+
1385
+ /**
1386
+ * Check if a user is a member of a group
1387
+ * @param {string} groupCn - Group common name
1388
+ * @param {string} userDn - User DN to check
1389
+ * @returns {Promise<boolean>} True if user is a member
1390
+ */
1391
+ async isMember(groupCn, userDn) {
1392
+ try {
1393
+ const members = await this.getMembers(groupCn);
1394
+ return members.includes(userDn);
1395
+ } catch (error) {
1396
+ console.error(`Error checking membership in group "${groupCn}":`, error);
1397
+ throw error;
1398
+ }
1399
+ }
1400
+ }
1401
+
1402
+ // Export for use in modules
1403
+ if (typeof module !== 'undefined' && module.exports) {
1404
+ module.exports = GroupManager;
1405
+ }
1406
+ ```
1407
+
1408
+ ### Usage Example
1409
+
1410
+ ```javascript
1411
+ // Initialize the GroupManager
1412
+ const manager = new GroupManager('http://localhost:8081');
1413
+
1414
+ async function demonstrateGroupManagement() {
1415
+ try {
1416
+ // Initialize
1417
+ console.log('Initializing GroupManager...');
1418
+ await manager.init();
1419
+
1420
+ // List all groups
1421
+ console.log('\n--- Listing all groups ---');
1422
+ const allGroups = await manager.listGroups();
1423
+ console.log('All groups:', Object.keys(allGroups));
1424
+
1425
+ // Create a new group
1426
+ console.log('\n--- Creating a new group ---');
1427
+ const newGroup = await manager.createGroup('developers', {
1428
+ description: 'Development Team',
1429
+ member: ['uid=alice,ou=users,dc=example,dc=com'],
1430
+ });
1431
+ console.log('Created group:', newGroup.dn);
1432
+
1433
+ // Get group details
1434
+ console.log('\n--- Getting group details ---');
1435
+ const groupDetails = await manager.getGroup('developers');
1436
+ console.log('Group details:', groupDetails);
1437
+
1438
+ // Add members to group
1439
+ console.log('\n--- Adding members ---');
1440
+ await manager.addMember('developers', 'uid=bob,ou=users,dc=example,dc=com');
1441
+ await manager.addMember('developers', [
1442
+ 'uid=carol,ou=users,dc=example,dc=com',
1443
+ 'uid=dave,ou=users,dc=example,dc=com',
1444
+ ]);
1445
+
1446
+ // Get all members
1447
+ console.log('\n--- Getting all members ---');
1448
+ const members = await manager.getMembers('developers');
1449
+ console.log('Group members:', members);
1450
+
1451
+ // Check membership
1452
+ console.log('\n--- Checking membership ---');
1453
+ const isBobMember = await manager.isMember(
1454
+ 'developers',
1455
+ 'uid=bob,ou=users,dc=example,dc=com'
1456
+ );
1457
+ console.log('Is Bob a member?', isBobMember);
1458
+
1459
+ // Update group description
1460
+ console.log('\n--- Updating group ---');
1461
+ await manager.updateGroup('developers', {
1462
+ replace: {
1463
+ description: 'Software Development Team',
1464
+ },
1465
+ });
1466
+
1467
+ // Remove a member
1468
+ console.log('\n--- Removing member ---');
1469
+ await manager.removeMember(
1470
+ 'developers',
1471
+ 'uid=dave,ou=users,dc=example,dc=com'
1472
+ );
1473
+
1474
+ // List updated members
1475
+ const updatedMembers = await manager.getMembers('developers');
1476
+ console.log('Updated members:', updatedMembers);
1477
+
1478
+ // Filter groups
1479
+ console.log('\n--- Filtering groups ---');
1480
+ const filteredGroups = await manager.listGroups('dev*');
1481
+ console.log('Groups matching "dev*":', Object.keys(filteredGroups));
1482
+ } catch (error) {
1483
+ console.error('Error in demonstration:', error);
1484
+ }
1485
+ }
1486
+
1487
+ // Run the demonstration
1488
+ demonstrateGroupManagement();
1489
+ ```
1490
+
1491
+ ### HTML Usage Example
1492
+
1493
+ ```html
1494
+ <!DOCTYPE html>
1495
+ <html lang="en">
1496
+ <head>
1497
+ <meta charset="UTF-8" />
1498
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1499
+ <title>Group Management - LDAP-Rest</title>
1500
+ <style>
1501
+ body {
1502
+ font-family: Arial, sans-serif;
1503
+ max-width: 1200px;
1504
+ margin: 0 auto;
1505
+ padding: 2rem;
1506
+ background: #f5f5f5;
1507
+ }
1508
+
1509
+ .container {
1510
+ background: white;
1511
+ padding: 2rem;
1512
+ border-radius: 8px;
1513
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1514
+ }
1515
+
1516
+ h1 {
1517
+ margin-bottom: 2rem;
1518
+ }
1519
+
1520
+ .button {
1521
+ padding: 0.5rem 1rem;
1522
+ margin: 0.25rem;
1523
+ background: #6200ee;
1524
+ color: white;
1525
+ border: none;
1526
+ border-radius: 4px;
1527
+ cursor: pointer;
1528
+ }
1529
+
1530
+ .button:hover {
1531
+ background: #3700b3;
1532
+ }
1533
+
1534
+ #output {
1535
+ margin-top: 2rem;
1536
+ padding: 1rem;
1537
+ background: #f8f9fa;
1538
+ border-radius: 4px;
1539
+ font-family: monospace;
1540
+ white-space: pre-wrap;
1541
+ max-height: 600px;
1542
+ overflow-y: auto;
1543
+ }
1544
+ </style>
1545
+ </head>
1546
+ <body>
1547
+ <div class="container">
1548
+ <h1>Group Management</h1>
1549
+
1550
+ <div>
1551
+ <button class="button" onclick="listAllGroups()">
1552
+ List All Groups
1553
+ </button>
1554
+ <button class="button" onclick="createTestGroup()">
1555
+ Create Test Group
1556
+ </button>
1557
+ <button class="button" onclick="addTestMember()">Add Member</button>
1558
+ <button class="button" onclick="removeTestMember()">
1559
+ Remove Member
1560
+ </button>
1561
+ </div>
1562
+
1563
+ <div id="output">Ready. Click a button to perform an operation.</div>
1564
+ </div>
1565
+
1566
+ <script src="./GroupManager.js"></script>
1567
+ <script>
1568
+ const manager = new GroupManager();
1569
+ const output = document.getElementById('output');
1570
+
1571
+ // Initialize on page load
1572
+ manager
1573
+ .init()
1574
+ .then(() => {
1575
+ output.textContent =
1576
+ 'GroupManager initialized successfully!\n\nClick a button to perform an operation.';
1577
+ })
1578
+ .catch(error => {
1579
+ output.textContent = 'Failed to initialize:\n' + error.message;
1580
+ });
1581
+
1582
+ async function listAllGroups() {
1583
+ try {
1584
+ output.textContent = 'Loading groups...';
1585
+ const groups = await manager.listGroups();
1586
+ output.textContent = 'Groups:\n\n' + JSON.stringify(groups, null, 2);
1587
+ } catch (error) {
1588
+ output.textContent = 'Error:\n' + error.message;
1589
+ }
1590
+ }
1591
+
1592
+ async function createTestGroup() {
1593
+ try {
1594
+ output.textContent = 'Creating test group...';
1595
+ const result = await manager.createGroup('test-group', {
1596
+ description: 'Test Group',
1597
+ member: ['uid=testuser,ou=users,dc=example,dc=com'],
1598
+ });
1599
+ output.textContent =
1600
+ 'Group created:\n\n' + JSON.stringify(result, null, 2);
1601
+ } catch (error) {
1602
+ output.textContent = 'Error:\n' + error.message;
1603
+ }
1604
+ }
1605
+
1606
+ async function addTestMember() {
1607
+ try {
1608
+ output.textContent = 'Adding member...';
1609
+ const result = await manager.addMember(
1610
+ 'test-group',
1611
+ 'uid=newuser,ou=users,dc=example,dc=com'
1612
+ );
1613
+ output.textContent =
1614
+ 'Member added:\n\n' + JSON.stringify(result, null, 2);
1615
+ } catch (error) {
1616
+ output.textContent = 'Error:\n' + error.message;
1617
+ }
1618
+ }
1619
+
1620
+ async function removeTestMember() {
1621
+ try {
1622
+ output.textContent = 'Removing member...';
1623
+ const result = await manager.removeMember(
1624
+ 'test-group',
1625
+ 'uid=newuser,ou=users,dc=example,dc=com'
1626
+ );
1627
+ output.textContent =
1628
+ 'Member removed:\n\n' + JSON.stringify(result, null, 2);
1629
+ } catch (error) {
1630
+ output.textContent = 'Error:\n' + error.message;
1631
+ }
1632
+ }
1633
+ </script>
1634
+ </body>
1635
+ </html>
1636
+ ```
1637
+
1638
+ ---
1639
+
1640
+ ## Next Steps
1641
+
1642
+ - **[REST API Documentation](../api/REST_API.md)** - Learn about all available API endpoints
1643
+ - **[Browser Libraries Guide](../browser/LIBRARIES.md)** - Detailed documentation for LdapTreeViewer and LdapUserEditor
1644
+ - **[JSON Schemas Guide](../schemas/SCHEMAS.md)** - Understanding schema-driven architecture
1645
+ - **[Developer Guide](../DEVELOPER_GUIDE.md)** - Complete developer documentation
1646
+
1647
+ ---
1648
+
1649
+ ## Resources
1650
+
1651
+ - [GitHub Repository](https://github.com/linagora/ldap-rest)
1652
+ - [API Reference](../api/REFERENCE.md)
1653
+ - [Plugin Development](../plugins/DEVELOPMENT.md)
1654
+
1655
+ ---
1656
+
1657
+ ## License
1658
+
1659
+ AGPL-3.0 - Copyright 2025-present LINAGORA