ldap-rest 0.1.2 → 0.1.4

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 (49) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/plugin.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/lib/errors.js +1 -0
  6. package/dist/lib/expressFormatedResponses.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/plugins/auth/trustedProxy.js +1 -0
  10. package/dist/plugins/ldap/groups.js +1 -1
  11. package/dist/plugins/ldap/organization.js +1 -1
  12. package/dist/plugins/priority.json.js +1 -1
  13. package/dist/plugins/twake/james.js +1 -1
  14. package/dist/plugins/weblogs.js +1 -1
  15. package/dist/src/abstract/ldapFlat.d.ts +9 -0
  16. package/dist/src/abstract/plugin.d.ts +1 -0
  17. package/dist/src/bin/index.d.ts +5 -0
  18. package/dist/src/browser/ldap-user-editor/components/UserTree.d.ts +2 -1
  19. package/dist/src/browser/shared/components/DisposableComponent.d.ts +29 -0
  20. package/dist/src/browser/shared/components/Modal.d.ts +6 -1
  21. package/dist/src/config/args.d.ts +2 -0
  22. package/dist/src/lib/errors.d.ts +47 -0
  23. package/dist/src/lib/ldapActions.d.ts +9 -0
  24. package/dist/src/lib/utils.d.ts +17 -0
  25. package/dist/src/plugins/auth/trustedProxy.d.ts +67 -0
  26. package/dist/src/plugins/ldap/groups.d.ts +9 -0
  27. package/dist/src/plugins/twake/james.d.ts +9 -0
  28. package/dist/src/plugins/weblogs.d.ts +1 -0
  29. package/docs/authentication.md +133 -0
  30. package/docs/twakeJames.md +61 -0
  31. package/package.json +12 -1
  32. package/static/browser/ldap-user-editor/components/UserTree.js +6 -3
  33. package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -1
  34. package/static/browser/ldap-user-editor.esm.js +62 -3
  35. package/static/browser/ldap-user-editor.esm.js.map +1 -1
  36. package/static/browser/ldap-user-editor.js +62 -3
  37. package/static/browser/ldap-user-editor.js.map +1 -1
  38. package/static/browser/ldap-user-editor.min.js +1 -1
  39. package/static/browser/ldap-user-editor.min.js.map +1 -1
  40. package/static/browser/shared/components/DisposableComponent.js +59 -0
  41. package/static/browser/shared/components/DisposableComponent.js.map +1 -0
  42. package/static/browser/shared/components/Modal.js +16 -6
  43. package/static/browser/shared/components/Modal.js.map +1 -1
  44. package/static/browser/shared.esm.js +72 -6
  45. package/static/browser/shared.esm.js.map +1 -1
  46. package/static/browser/shared.js +72 -6
  47. package/static/browser/shared.js.map +1 -1
  48. package/static/browser/shared.min.js +1 -1
  49. package/static/browser/shared.min.js.map +1 -1
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Custom HTTP Error classes with status codes
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ */
5
+ /**
6
+ * Base HTTP Error class with status code
7
+ */
8
+ export declare class HttpError extends Error {
9
+ statusCode: number;
10
+ constructor(message: string, statusCode?: number);
11
+ }
12
+ /**
13
+ * Client errors (4xx)
14
+ */
15
+ export declare class BadRequestError extends HttpError {
16
+ constructor(message?: string);
17
+ }
18
+ export declare class UnauthorizedError extends HttpError {
19
+ constructor(message?: string);
20
+ }
21
+ export declare class ForbiddenError extends HttpError {
22
+ constructor(message?: string);
23
+ }
24
+ export declare class NotFoundError extends HttpError {
25
+ constructor(message?: string);
26
+ }
27
+ export declare class ConflictError extends HttpError {
28
+ constructor(message?: string);
29
+ }
30
+ export declare class UriTooLongError extends HttpError {
31
+ constructor(message?: string);
32
+ }
33
+ export declare class TooManyRequestsError extends HttpError {
34
+ constructor(message?: string);
35
+ }
36
+ /**
37
+ * Server errors (5xx)
38
+ */
39
+ export declare class BadGatewayError extends HttpError {
40
+ constructor(message?: string);
41
+ }
42
+ export declare class ServiceUnavailableError extends HttpError {
43
+ constructor(message?: string);
44
+ }
45
+ export declare class GatewayTimeoutError extends HttpError {
46
+ constructor(message?: string);
47
+ }
@@ -32,6 +32,9 @@ declare class ldapActions {
32
32
  private connectionTtl;
33
33
  private ldapUrls;
34
34
  private currentUrlIndex;
35
+ private attrSignatureCache;
36
+ private waitingResolvers;
37
+ private availableConnections;
35
38
  constructor(server: DM);
36
39
  /**
37
40
  * Create a new LDAP connection with failover support
@@ -43,12 +46,18 @@ declare class ldapActions {
43
46
  private cleanupExpiredConnections;
44
47
  /**
45
48
  * Acquire a connection from the pool or create a new one
49
+ * Optimized for O(1) lookup using separate available connections queue
46
50
  */
47
51
  private acquireConnection;
48
52
  /**
49
53
  * Release a connection back to the pool
54
+ * If there are waiting requests, hand off the connection directly
50
55
  */
51
56
  private releaseConnection;
57
+ /**
58
+ * Get a sorted signature for attribute list, using cache for performance
59
+ */
60
+ private getAttributeSignature;
52
61
  /**
53
62
  * Generate cache key for LDAP search
54
63
  */
@@ -7,6 +7,23 @@ import type { Config } from '../bin';
7
7
  export declare const launchHooks: (hooks: Function[] | undefined, ...args: unknown[]) => Promise<void>;
8
8
  export declare const launchHooksChained: <T>(hooks: Function[] | undefined, args: T) => Promise<T>;
9
9
  export declare const transformSchemas: (schemas: string | Buffer, config: Config) => string;
10
+ /**
11
+ * Escape special characters in LDAP filter values according to RFC 4515
12
+ * Prevents LDAP injection attacks by escaping characters that have special meaning in LDAP filters
13
+ *
14
+ * @param value - The value to escape
15
+ * @returns The escaped value safe for use in LDAP filters
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * escapeLdapFilter('user*')
20
+ * // => 'user\\2a'
21
+ *
22
+ * escapeLdapFilter('Smith, John (admin)')
23
+ * // => 'Smith, John \\28admin\\29'
24
+ * ```
25
+ */
26
+ export declare function escapeLdapFilter(value: string): string;
10
27
  /**
11
28
  * Parse a Distinguished Name (DN) into its component parts (RDNs)
12
29
  * Handles escaped commas and other special characters
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @module core/auth/trustedProxy
3
+ * Trusted proxy plugin to validate X-Forwarded-For headers
4
+ *
5
+ * This plugin validates that X-Forwarded-For headers only come from trusted
6
+ * upstream reverse proxies. If the request comes from an untrusted IP,
7
+ * the X-Forwarded-For header is removed to prevent IP spoofing attacks.
8
+ *
9
+ * This plugin MUST be loaded BEFORE any plugin that uses X-Forwarded-For
10
+ * (like crowdsec and rateLimit) to ensure they receive sanitized headers.
11
+ *
12
+ * Configuration:
13
+ * - trusted_proxy: Array of trusted proxy IPs or CIDR ranges (required)
14
+ * Example: ["127.0.0.1", "10.0.0.0/8", "192.168.1.0/24", "::1"]
15
+ * - trusted_proxy_auth_header: Header name containing the authenticated user
16
+ * from trusted proxy (optional, default: "Auth-User")
17
+ *
18
+ * When a request comes from a trusted proxy:
19
+ * - X-Forwarded-For header is preserved
20
+ * - The request is marked as trusted (req.trustedProxy = true)
21
+ * - If trusted_proxy_auth_header is set and the header is present,
22
+ * req.proxyAuthUser is set to its value for use by other plugins (e.g., weblogs)
23
+ *
24
+ * @author Xavier Guimard <xguimard@linagora.com>
25
+ */
26
+ import type { Express } from 'express';
27
+ import DmPlugin, { type Role } from '../../abstract/plugin';
28
+ declare global {
29
+ namespace Express {
30
+ interface Request {
31
+ trustedProxy?: boolean;
32
+ proxyAuthUser?: string;
33
+ }
34
+ }
35
+ }
36
+ export default class TrustedProxy extends DmPlugin {
37
+ name: string;
38
+ roles: Role[];
39
+ private trustedNetworks;
40
+ private authHeader;
41
+ constructor(...args: ConstructorParameters<typeof DmPlugin>);
42
+ api(app: Express): void;
43
+ /**
44
+ * Check if an IP address belongs to a trusted proxy network
45
+ */
46
+ private isTrustedProxy;
47
+ /**
48
+ * Normalize IP address (handle IPv4-mapped IPv6 addresses)
49
+ */
50
+ private normalizeIP;
51
+ /**
52
+ * Parse an IP address into a bigint for comparison
53
+ */
54
+ private parseIP;
55
+ /**
56
+ * Parse a CIDR notation string into network address and mask
57
+ */
58
+ private parseCIDR;
59
+ /**
60
+ * Parse an IPv4 address into a bigint
61
+ */
62
+ private parseIPv4;
63
+ /**
64
+ * Parse an IPv6 address into a bigint
65
+ */
66
+ private parseIPv6;
67
+ }
@@ -17,6 +17,15 @@ export default class LdapGroups extends DmPlugin {
17
17
  ldap: ldapActions;
18
18
  cn: string;
19
19
  schema?: Schema;
20
+ private regexCache;
21
+ /**
22
+ * Get a compiled RegExp from cache, or compile and cache it
23
+ */
24
+ private getCompiledRegex;
25
+ /**
26
+ * Escape special regex characters in a string
27
+ */
28
+ private escapeRegex;
20
29
  constructor(server: DM);
21
30
  /**
22
31
  * Catch all deletion to remove deleted users from groups
@@ -1,3 +1,4 @@
1
+ import type { Express } from 'express';
1
2
  import TwakePlugin from '../../abstract/twakePlugin';
2
3
  import { type Role } from '../../abstract/plugin';
3
4
  import type { DM } from '../../bin';
@@ -16,6 +17,14 @@ export default class James extends TwakePlugin {
16
17
  private mailboxTypeAttr;
17
18
  private initDelay;
18
19
  constructor(server: DM);
20
+ /**
21
+ * Register API routes
22
+ */
23
+ api(app: Express): void;
24
+ /**
25
+ * Get quota information for a user
26
+ */
27
+ private getUserQuota;
19
28
  hooks: Hooks;
20
29
  /**
21
30
  * Handle mailbox type transitions when twakeMailboxType changes
@@ -5,4 +5,5 @@ export default class WebLogs extends DmPlugin {
5
5
  roles: Role[];
6
6
  api(app: Express): void;
7
7
  log(req: Request, res: Response, start: number, log: Record<string, string | number>): void;
8
+ private getClientIp;
8
9
  }
@@ -11,6 +11,139 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
11
11
  5. **OpenID Connect** (`core/auth/openidconnect`) - OAuth 2.0 / OpenID Connect authentication
12
12
  6. **Authorization Per Branch** (`core/auth/authzPerBranch`) - Branch-level access control (see [authzPerBranch.md](authzPerBranch.md))
13
13
 
14
+ ## Security Plugins
15
+
16
+ 1. **Trusted Proxy** (`core/auth/trustedProxy`) - Validate X-Forwarded-For headers from reverse proxies
17
+ 2. **Rate Limiting** (`core/auth/rateLimit`) - Prevent brute force attacks
18
+ 3. **CrowdSec** (`core/auth/crowdsec`) - IP reputation and blocking
19
+
20
+ ## Trusted Proxy
21
+
22
+ When running behind a reverse proxy (nginx, Apache, HAProxy, etc.), client IP addresses are typically passed via `X-Forwarded-For` headers. The `trustedProxy` plugin validates these headers to prevent IP spoofing attacks.
23
+
24
+ ### Configuration
25
+
26
+ ```bash
27
+ --plugin core/auth/trustedProxy \
28
+ --trusted-proxy "127.0.0.1" \
29
+ --trusted-proxy "10.0.0.0/8" \
30
+ --trusted-proxy "192.168.0.0/16"
31
+ ```
32
+
33
+ **Environment Variables:**
34
+
35
+ ```bash
36
+ # Comma-separated list of trusted proxy IPs or CIDR ranges
37
+ DM_TRUSTED_PROXIES="127.0.0.1,10.0.0.0/8,192.168.0.0/16,::1"
38
+
39
+ # Optional: Header name for authenticated user from proxy (default: Auth-User)
40
+ DM_TRUSTED_PROXY_AUTH_HEADER="Auth-User"
41
+ ```
42
+
43
+ ### How It Works
44
+
45
+ 1. **Request arrives** from reverse proxy
46
+ 2. **Plugin checks** if `req.socket.remoteAddress` matches a trusted proxy
47
+ 3. **If trusted:**
48
+ - `X-Forwarded-For` header is preserved
49
+ - `Auth-User` header (or custom header) is extracted for logging
50
+ - Request is marked as `req.trustedProxy = true`
51
+ 4. **If untrusted:**
52
+ - `X-Forwarded-For` header is **removed** to prevent IP spoofing
53
+ - Request is marked as `req.trustedProxy = false`
54
+ 5. **Other plugins** (rate limiting, CrowdSec, weblogs) see sanitized headers
55
+
56
+ ### Supported Formats
57
+
58
+ - **IPv4 addresses:** `192.168.1.1`
59
+ - **IPv6 addresses:** `::1`, `fe80::1`
60
+ - **CIDR ranges:** `10.0.0.0/8`, `192.168.0.0/16`, `2001:db8::/32`
61
+ - **IPv4-mapped IPv6:** Automatically handled (e.g., `::ffff:127.0.0.1` matches `127.0.0.1`)
62
+
63
+ ### Auth-User Header
64
+
65
+ When requests come from a trusted proxy, the plugin can extract an authenticated username from a header set by the proxy. This is useful for:
66
+
67
+ - Logging who made the request (even if LDAP-Rest doesn't handle authentication)
68
+ - Passing identity from an upstream authentication system
69
+
70
+ ```bash
71
+ # Custom header name (default: Auth-User)
72
+ DM_TRUSTED_PROXY_AUTH_HEADER="X-Remote-User"
73
+ ```
74
+
75
+ The extracted username is:
76
+ - Available as `req.proxyAuthUser` for other plugins
77
+ - Used by the `weblogs` plugin to log the user field
78
+
79
+ ### Example: nginx Configuration
80
+
81
+ ```nginx
82
+ upstream ldap_rest {
83
+ server 127.0.0.1:8081;
84
+ }
85
+
86
+ server {
87
+ listen 443 ssl;
88
+ server_name api.example.com;
89
+
90
+ location / {
91
+ proxy_pass http://ldap_rest;
92
+ proxy_set_header Host $host;
93
+ proxy_set_header X-Real-IP $remote_addr;
94
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
95
+ proxy_set_header X-Forwarded-Proto $scheme;
96
+
97
+ # Optional: Pass authenticated user from nginx auth
98
+ proxy_set_header Auth-User $remote_user;
99
+ }
100
+ }
101
+ ```
102
+
103
+ LDAP-Rest configuration:
104
+
105
+ ```bash
106
+ DM_TRUSTED_PROXIES="127.0.0.1"
107
+ ```
108
+
109
+ ### Example: HAProxy Configuration
110
+
111
+ ```haproxy
112
+ frontend https
113
+ bind *:443 ssl crt /etc/ssl/cert.pem
114
+ default_backend ldap_rest
115
+
116
+ backend ldap_rest
117
+ option forwardfor
118
+ http-request set-header X-Forwarded-Proto https
119
+ server ldap_rest 127.0.0.1:8081
120
+ ```
121
+
122
+ ### Use Cases
123
+
124
+ - **Reverse Proxy Deployments:** Ensure correct client IP for rate limiting and logging
125
+ - **Load Balancers:** Trust headers from known load balancer IPs
126
+ - **CDN Integration:** Trust headers from CDN edge servers
127
+ - **Kubernetes:** Trust headers from ingress controllers
128
+ - **Security:** Prevent attackers from spoofing `X-Forwarded-For` to bypass rate limits
129
+
130
+ ### Security Considerations
131
+
132
+ - **Only trust known proxies:** Never use `0.0.0.0/0` or trust all IPs
133
+ - **Use specific IPs/ranges:** Limit to your actual proxy infrastructure
134
+ - **HTTPS between proxy and LDAP-Rest:** Prevent header injection attacks
135
+ - **Monitor logs:** Watch for warnings about removed X-Forwarded-For headers
136
+
137
+ ### Plugin Load Order
138
+
139
+ The `trustedProxy` plugin is automatically loaded **first** (via `priority.json`) to ensure all other plugins see sanitized headers:
140
+
141
+ 1. `core/auth/trustedProxy` - Sanitizes X-Forwarded-For headers
142
+ 2. `core/weblogs` - Logs requests with correct client IP
143
+ 3. `core/auth/crowdsec` - Checks IP reputation
144
+ 4. `core/auth/rateLimit` - Rate limits by IP
145
+ 5. Authentication plugins...
146
+
14
147
  ## Token Authentication
15
148
 
16
149
  Simple stateless authentication using bearer tokens.
@@ -243,6 +243,66 @@ proxyAddresses: SMTP:john.doe@company.com
243
243
 
244
244
  The `smtp:` prefix is automatically stripped. The capitalization (SMTP vs smtp) is ignored - only the email address after the colon is used.
245
245
 
246
+ ## REST API
247
+
248
+ The James plugin exposes a REST API to retrieve quota usage and limits for users.
249
+
250
+ ### GET /api/v1/users/:user/quota-usage
251
+
252
+ Retrieve quota usage and limits for a user from James. This endpoint returns both the quota limits configured for the user and their current mailbox usage.
253
+
254
+ **Request:**
255
+
256
+ ```bash
257
+ GET /api/v1/users/jdoe/quota-usage
258
+ ```
259
+
260
+ **Response (200 OK):**
261
+
262
+ ```json
263
+ {
264
+ "global": { "count": 1000, "size": 1000000000 },
265
+ "domain": { "count": 800, "size": 800000000 },
266
+ "user": { "count": 500, "size": 75000000 },
267
+ "computed": { "count": 500, "size": 75000000 },
268
+ "occupation": {
269
+ "size": 50000000,
270
+ "count": 250,
271
+ "ratio": {
272
+ "size": 0.67,
273
+ "count": 0.5,
274
+ "max": 0.67
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ **Response Fields:**
281
+
282
+ - `global`: Global quota limit (server-wide)
283
+ - `domain`: Domain-specific quota limit
284
+ - `user`: User-specific quota limit from LDAP
285
+ - `computed`: Effective quota limit applied (resolved from upper values)
286
+ - `occupation`: **Current mailbox usage statistics**
287
+ - `size`: Storage currently used (bytes)
288
+ - `count`: Number of messages in mailbox
289
+ - `ratio`: Usage ratios (used / limit)
290
+
291
+ **Error Responses:**
292
+
293
+ - `404`: User not found in LDAP or quota not found in James
294
+ - `400`: User has no mail attribute
295
+ - `502`: Failed to retrieve quota from James
296
+ - `500`: Internal server error
297
+
298
+ **Example:**
299
+
300
+ ```bash
301
+ # Get quota usage for user 'jdoe'
302
+ curl http://localhost:8081/api/v1/users/jdoe/quota-usage \
303
+ -H "Authorization: Bearer admin-token"
304
+ ```
305
+
246
306
  ## James WebAdmin API
247
307
 
248
308
  ### Endpoints Used
@@ -251,6 +311,7 @@ The `smtp:` prefix is automatically stripped. The capitalization (SMTP vs smtp)
251
311
  | ------------------- | ------------------------------------------ | ------ |
252
312
  | Rename account | `/users/{old}/rename/{new}?action=rename` | POST |
253
313
  | Update quota | `/quota/users/{mail}/size` | PUT |
314
+ | Get quota | `/quota/users/{mail}` | GET |
254
315
  | Add delegation | `/users/{mail}/authorizedUsers/{delegate}` | PUT |
255
316
  | Remove delegation | `/users/{mail}/authorizedUsers/{delegate}` | DELETE |
256
317
  | Add alias | `/address/aliases/{mail}/sources/{alias}` | PUT |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -64,6 +64,10 @@
64
64
  "import": "./dist/plugins/auth/totp.js",
65
65
  "types": "./dist/src/plugins/auth/totp.d.ts"
66
66
  },
67
+ "./plugin-auth-trustedproxy": {
68
+ "import": "./dist/plugins/auth/trustedProxy.js",
69
+ "types": "./dist/src/plugins/auth/trustedProxy.d.ts"
70
+ },
67
71
  "./plugin-configapi": {
68
72
  "import": "./dist/plugins/configApi.js",
69
73
  "types": "./dist/src/plugins/configApi.d.ts"
@@ -292,6 +296,10 @@
292
296
  "import": "./static/browser/ldap-user-editor/types.js",
293
297
  "types": "./dist/src/browser/ldap-user-editor/types.d.ts"
294
298
  },
299
+ "./browser-shared-components-disposablecomponent": {
300
+ "import": "./static/browser/shared/components/DisposableComponent.js",
301
+ "types": "./dist/src/browser/shared/components/DisposableComponent.d.ts"
302
+ },
295
303
  "./browser-shared-components-modal": {
296
304
  "import": "./static/browser/shared/components/Modal.js",
297
305
  "types": "./dist/src/browser/shared/components/Modal.d.ts"
@@ -450,6 +458,9 @@
450
458
  "test:dev": "./.dev.mk test",
451
459
  "test:one": "mocha --config .mocharc-one.json"
452
460
  },
461
+ "overrides": {
462
+ "glob": ">=11.0.4"
463
+ },
453
464
  "dependencies": {
454
465
  "body-parser": "^2.2.0",
455
466
  "csv-parse": "^6.1.0",
@@ -1,11 +1,13 @@
1
1
  import { CacheManager } from '../cache/CacheManager.js';
2
+ import { DisposableComponent } from '../../shared/components/DisposableComponent.js';
2
3
 
3
4
  /**
4
5
  * LDAP User Editor - Organization Tree Component
5
6
  * Shows only LDAP organization tree (filters out users/groups)
6
7
  */
7
- class UserTree {
8
+ class UserTree extends DisposableComponent {
8
9
  constructor(container, baseUrl, onSelectOrg) {
10
+ super();
9
11
  this.expandedNodes = new Set();
10
12
  this.selectedDn = null;
11
13
  this.rootDn = null;
@@ -13,8 +15,8 @@ class UserTree {
13
15
  this.baseUrl = baseUrl;
14
16
  this.onSelectOrg = onSelectOrg;
15
17
  this.cache = new CacheManager();
16
- // Clean expired entries every 5 minutes
17
- window.setInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
18
+ // Clean expired entries every 5 minutes - now properly cleaned up
19
+ this.addManagedInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
18
20
  }
19
21
  /**
20
22
  * Fetch with cache support
@@ -157,6 +159,7 @@ class UserTree {
157
159
  }
158
160
  destroy() {
159
161
  this.container.innerHTML = '';
162
+ super.destroy();
160
163
  }
161
164
  }
162
165
 
@@ -1 +1 @@
1
- {"version":3,"file":"UserTree.js","sources":["../../../../../src/browser/ldap-user-editor/components/UserTree.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"UserTree.js","sources":["../../../../../src/browser/ldap-user-editor/components/UserTree.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -324,12 +324,70 @@ class UserApiClient {
324
324
  }
325
325
  }
326
326
 
327
+ /**
328
+ * Base class for browser components with proper lifecycle management
329
+ * Ensures all event listeners and intervals are cleaned up on destroy
330
+ * @module browser/shared/components/DisposableComponent
331
+ */
332
+ class DisposableComponent {
333
+ constructor() {
334
+ this._eventListeners = [];
335
+ this._intervals = [];
336
+ this._isDestroyed = false;
337
+ }
338
+ /**
339
+ * Register an event listener for automatic cleanup
340
+ * Use this instead of element.addEventListener directly
341
+ */
342
+ addManagedEventListener(target, type, listener, options) {
343
+ if (this._isDestroyed)
344
+ return;
345
+ target.addEventListener(type, listener, options);
346
+ this._eventListeners.push({ target, type, listener, options });
347
+ }
348
+ /**
349
+ * Register an interval for automatic cleanup
350
+ * Use this instead of setInterval directly
351
+ */
352
+ addManagedInterval(callback, ms) {
353
+ const id = setInterval(callback, ms);
354
+ this._intervals.push(id);
355
+ return id;
356
+ }
357
+ /**
358
+ * Check if the component has been destroyed
359
+ */
360
+ get isDestroyed() {
361
+ return this._isDestroyed;
362
+ }
363
+ /**
364
+ * Clean up all managed resources
365
+ * Override in subclasses but ALWAYS call super.destroy()
366
+ */
367
+ destroy() {
368
+ if (this._isDestroyed)
369
+ return;
370
+ this._isDestroyed = true;
371
+ // Remove all event listeners
372
+ for (const { target, type, listener, options } of this._eventListeners) {
373
+ target.removeEventListener(type, listener, options);
374
+ }
375
+ this._eventListeners = [];
376
+ // Clear all intervals
377
+ for (const id of this._intervals) {
378
+ clearInterval(id);
379
+ }
380
+ this._intervals = [];
381
+ }
382
+ }
383
+
327
384
  /**
328
385
  * LDAP User Editor - Organization Tree Component
329
386
  * Shows only LDAP organization tree (filters out users/groups)
330
387
  */
331
- class UserTree {
388
+ class UserTree extends DisposableComponent {
332
389
  constructor(container, baseUrl, onSelectOrg) {
390
+ super();
333
391
  this.expandedNodes = new Set();
334
392
  this.selectedDn = null;
335
393
  this.rootDn = null;
@@ -337,8 +395,8 @@ class UserTree {
337
395
  this.baseUrl = baseUrl;
338
396
  this.onSelectOrg = onSelectOrg;
339
397
  this.cache = new CacheManager();
340
- // Clean expired entries every 5 minutes
341
- window.setInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
398
+ // Clean expired entries every 5 minutes - now properly cleaned up
399
+ this.addManagedInterval(() => this.cache.cleanExpired(), 5 * 60 * 1000);
342
400
  }
343
401
  /**
344
402
  * Fetch with cache support
@@ -481,6 +539,7 @@ class UserTree {
481
539
  }
482
540
  destroy() {
483
541
  this.container.innerHTML = '';
542
+ super.destroy();
484
543
  }
485
544
  }
486
545