@igxjs/node-components 1.0.10 → 1.0.12

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.
package/README.md CHANGED
@@ -12,7 +12,8 @@ npm install @igxjs/node-components
12
12
 
13
13
  | Component | Description | Documentation |
14
14
  |-----------|-------------|---------------|
15
- | **SessionManager** | SSO session management with Redis/memory storage | [View docs](./docs/session-manager.md) |
15
+ | **SessionManager** | SSO session management with Redis/memory storage, supporting both session and token-based authentication | [View docs](./docs/session-manager.md) |
16
+ | **Logger** | High-performance logging utility with zero dependencies and smart color detection | [View docs](./docs/logger.md) |
16
17
  | **FlexRouter** | Flexible routing with context paths and middleware | [View docs](./docs/flex-router.md) |
17
18
  | **RedisManager** | Redis connection management with TLS support | [View docs](./docs/redis-manager.md) |
18
19
  | **JWT Manager** | Secure JWT encryption/decryption with JWE | [View docs](./docs/jwt-manager.md) |
@@ -23,9 +24,9 @@ npm install @igxjs/node-components
23
24
  ### SessionManager
24
25
 
25
26
  ```javascript
26
- import { SessionManager } from '@igxjs/node-components';
27
+ import { SessionManager, SessionMode } from '@igxjs/node-components';
27
28
 
28
- // Create singleton instance
29
+ // Create singleton instance with SESSION authentication (default)
29
30
  export const session = new SessionManager({
30
31
  SSO_ENDPOINT_URL: process.env.SSO_ENDPOINT_URL,
31
32
  SSO_CLIENT_ID: process.env.SSO_CLIENT_ID,
@@ -34,6 +35,16 @@ export const session = new SessionManager({
34
35
  REDIS_URL: process.env.REDIS_URL
35
36
  });
36
37
 
38
+ // Create singleton instance with TOKEN authentication
39
+ export const tokenSession = new SessionManager({
40
+ SESSION_MODE: SessionMode.TOKEN, // Use token-based authentication
41
+ SSO_ENDPOINT_URL: process.env.SSO_ENDPOINT_URL,
42
+ SSO_CLIENT_ID: process.env.SSO_CLIENT_ID,
43
+ SSO_CLIENT_SECRET: process.env.SSO_CLIENT_SECRET,
44
+ SESSION_SECRET: process.env.SESSION_SECRET,
45
+ REDIS_URL: process.env.REDIS_URL,
46
+ });
47
+
37
48
  // Setup in your app
38
49
  await session.setup(app, (user) => ({ ...user, displayName: user.email }));
39
50
 
@@ -66,10 +77,11 @@ flexRouter.mount(app, '');
66
77
  ```javascript
67
78
  import { JwtManager } from '@igxjs/node-components';
68
79
 
69
- const jwt = new JwtManager({ expirationTime: '1h' });
80
+ // Constructor uses UPPERCASE naming with JWT_ prefix
81
+ const jwt = new JwtManager({ SESSION_AGE: 64800000 });
70
82
  const SECRET = process.env.JWT_SECRET;
71
83
 
72
- // Create token
84
+ // Create token (encrypt method uses camelCase for per-call options)
73
85
  const token = await jwt.encrypt({ userId: '123', email: 'user@example.com' }, SECRET);
74
86
 
75
87
  // Verify token
@@ -105,9 +117,91 @@ app.use(httpErrorHandler);
105
117
 
106
118
  [📖 Full HTTP Handlers Documentation](./docs/http-handlers.md)
107
119
 
120
+ ## SessionManager Authentication Modes
121
+
122
+ The `SessionManager` supports two authentication modes:
123
+
124
+ ### SESSION Mode (Default)
125
+
126
+ Uses traditional server-side session cookies. When a user authenticates via SSO, their session is stored in Redis or memory storage. The client sends the session cookie with each request to prove authentication.
127
+
128
+ **Configuration:**
129
+ - `SESSION_MODE`: `SessionMode.SESSION` (default) - Uses session-based authentication
130
+ - `SESSION_AGE`: Session timeout in milliseconds (default: 64800000)
131
+ - `REDIS_URL`: Redis connection string for session storage
132
+
133
+ **Auth Methods:**
134
+ - `session.authenticate()` - Protect routes with SSO session verification
135
+ - `session.verifySession(isDebugging, redirectUrl)` - Explicit session verification method
136
+ - `session.logout(redirect?, all?)` - Logout current session (or logout all for token mode)
137
+
138
+ ### TOKEN Mode
139
+
140
+ Uses JWT bearer tokens instead of session cookies. When a user authenticates via SSO, a JWT token is generated and stored in Redis. The client includes the token in the Authorization header (`Bearer {token}`) with each request.
141
+
142
+ **Configuration:**
143
+ - `SESSION_MODE`: `SessionMode.TOKEN` - Uses token-based authentication
144
+ - `SSO_SUCCESS_URL`: Redirect URL after successful SSO login
145
+ - `SSO_FAILURE_URL`: Redirect URL after failed SSO login
146
+ - `JWT_ALGORITHM`: JWT algorithm (default: `'dir'`)
147
+ - `JWT_ENCRYPTION`: Encryption algorithm (default: `'A256GCM'`)
148
+ - `JWT_CLOCK_TOLERANCE`: Clock skew tolerance in seconds (default: 30)
149
+
150
+ **Auth Methods:**
151
+ - `session.verifyToken(isDebugging, redirectUrl)` - Protect routes with token verification
152
+ - `session.callback(initUser)` - SSO callback handler for token generation
153
+ - `session.refresh(initUser)` - Refresh user authentication based on auth mode
154
+ - `session.logout(redirect?, all?)` - Logout current or all tokens
155
+
156
+ **Token Storage (Client-Side):**
157
+
158
+ When using token-based authentication, the SSO callback returns an HTML page that stores the token in `localStorage` and redirects the user:
159
+
160
+ ```javascript
161
+ // The token is automatically stored in localStorage by the callback HTML page
162
+ // Default keys (customizable via SESSION_KEY and SESSION_EXPIRY_KEY config):
163
+ localStorage.getItem('session_token'); // JWT token
164
+ localStorage.getItem('session_expires_at'); // Expiry timestamp
165
+
166
+ // Making authenticated requests from the client:
167
+ const token = localStorage.getItem('session_token');
168
+ fetch('/api/protected', {
169
+ headers: { 'Authorization': `Bearer ${token}` }
170
+ });
171
+ ```
172
+
173
+ **Note:** The actual localStorage keys used are determined by the `SESSION_KEY` and `SESSION_EXPIRY_KEY` configuration options (defaults shown above).
174
+
175
+ ## SessionManager Configuration Options
176
+
177
+ | Option | Type | Default | Description |
178
+ |--------|------|---------|-------------|
179
+ | `SSO_ENDPOINT_URL` | string | - | Identity provider endpoint URL |
180
+ | `SSO_CLIENT_ID` | string | - | SSO client ID |
181
+ | `SSO_CLIENT_SECRET` | string | - | SSO client secret |
182
+ | `SSO_SUCCESS_URL` | string | - | Redirect URL after successful login (token mode) |
183
+ | `SSO_FAILURE_URL` | string | - | Redirect URL after failed login (token mode) |
184
+ | `SESSION_MODE` | string | `SessionMode.SESSION` | Authentication mode: `SessionMode.SESSION` or `SessionMode.TOKEN` |
185
+ | `SESSION_AGE` | number | 64800000 | Session timeout in milliseconds |
186
+ | `SESSION_COOKIE_PATH` | string | `'/'` | Session cookie path |
187
+ | `SESSION_SECRET` | string | - | Session/JWT secret key |
188
+ | `SESSION_PREFIX` | string | `'ibmid:'` | Redis session/key prefix |
189
+ | `REDIS_URL` | string | - | Redis connection URL (optional) |
190
+ | `REDIS_CERT_PATH` | string | - | Path to Redis TLS certificate |
191
+ | `JWT_ALGORITHM` | string | `'dir'` | JWT signing algorithm |
192
+ | `JWT_ENCRYPTION` | string | `'A256GCM'` | JWE encryption algorithm |
193
+ | `JWT_CLOCK_TOLERANCE` | number | 30 | Clock skew tolerance in seconds |
194
+ | `JWT_SECRET_HASH_ALGORITHM` | string | `'SHA-256'` | Algorithm for hashing secrets |
195
+ | `JWT_ISSUER` | string | - | JWT issuer identifier |
196
+ | `JWT_AUDIENCE` | string | - | JWT audience identifier |
197
+ | `JWT_SUBJECT` | string | - | JWT subject identifier |
198
+
108
199
  ## Features
109
200
 
110
201
  - ✅ **SSO Integration** - Full SSO support with Redis or memory storage
202
+ - ✅ **Dual Authentication Modes** - SESSION (cookies) or TOKEN (Bearer tokens)
203
+ - ✅ **Token Refresh** - Automatic token refresh via SSO endpoints
204
+ - ✅ **Session Refresh Locks** - Prevent concurrent token/session refresh attacks
111
205
  - ✅ **JWT Security** - Encrypted JWT tokens using JWE (jose library)
112
206
  - ✅ **Flexible Routing** - Easy mounting with context paths and middleware
113
207
  - ✅ **Redis Support** - TLS/SSL and automatic reconnection
@@ -148,4 +242,4 @@ import type {
148
242
 
149
243
  ## License
150
244
 
151
- [Apache 2.0](LICENSE)
245
+ [Apache 2.0](LICENSE)
@@ -0,0 +1,111 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Sign in IBM Garage</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover,minimum-scale=1,maximum-scale=1,user-scalable=no">
6
+ <meta name="robots" content="noindex, nofollow" />
7
+ <style>
8
+ :root {
9
+ --foreground-color-primary: #444;
10
+ --font-size: 0.8rem;
11
+ --background-color-primary: #f1f1f199;
12
+ --gap-global: 0.4rem;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --background-color-primary: black;
17
+ --foreground-color-primary: #bbb;
18
+ }
19
+ }
20
+ html, body {
21
+ padding: 0px;
22
+ margin: 0px auto;
23
+ height: 100%;
24
+ width: 100%;
25
+ }
26
+ body {
27
+ background-image: radial-gradient(circle at 20% 80%, rgba(255, 140, 140, 0.5), transparent 42%),
28
+ radial-gradient(circle at 80% 20%, rgba(140, 200, 255, 0.55), transparent 42%),
29
+ radial-gradient(circle at 40% 40%, rgba(255, 230, 140, 0.45), transparent 32%),
30
+ linear-gradient(135deg, rgba(160, 120, 240, 0.95), rgba(120, 160, 240, 0.95));
31
+ background-size: cover;
32
+ background-repeat: no-repeat;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ font-family: Verdana, Helvetica, Arial, sans-serif;
37
+ font-size: var(--font-size);
38
+ color: var(--foreground-color-primary);
39
+ flex-flow: column;
40
+ position: relative;
41
+ z-index: 10;
42
+ overflow: hidden;
43
+ }
44
+ body::before {
45
+ background: var(--background-color-primary);
46
+ content: "";
47
+ position: absolute;
48
+ top: 0;
49
+ left: 0;
50
+ right: 0;
51
+ bottom: 0;
52
+ z-index: -1;
53
+ backdrop-filter: blur(10px);
54
+ -webkit-backdrop-filter: blur(10px);
55
+ }
56
+ .wrapper {
57
+ width: 100%;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ box-sizing: border-box;
62
+ row-gap: var(--gap-global);
63
+ flex-direction: column;
64
+ padding: var(--gap-global);
65
+ }
66
+ .wrapper #failure {
67
+ display: none;
68
+ }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <hr />
73
+ <div class="wrapper">
74
+ <div id="success">Redirecting... <a href="{{SSO_SUCCESS_URL}}" target="_blank" style="color: blue; text-decoration: underline;">(click here if not redirected)</a></div>
75
+ <div id="failure">
76
+ <p>Authentication failed. Please try again.</p>
77
+ <a href="{{SSO_FAILURE_URL}}">Return to login</a>
78
+ </div>
79
+ </div>
80
+ <script>
81
+ (function() {
82
+ let success = true;
83
+
84
+ // Check localStorage support first
85
+ if (typeof localStorage !== 'object' || typeof localStorage.setItem !== 'function') {
86
+ console.warn('localStorage not supported, falling back to session cookie');
87
+ success = false;
88
+ }
89
+
90
+ try {
91
+ // Try localStorage first
92
+ if (localStorage.setItem) {
93
+ localStorage.setItem('{{SESSION_DATA_KEY}}', '{{SESSION_DATA_VALUE}}');
94
+ localStorage.setItem('{{SESSION_EXPIRY_KEY}}', '{{SESSION_EXPIRY_VALUE}}');
95
+ }
96
+
97
+ // Fall back to simple navigation
98
+ location.href = '{{SSO_SUCCESS_URL}}';
99
+ } catch (e) {
100
+ console.error('Redirect failed:', e);
101
+ success = false;
102
+ }
103
+
104
+ if (!success) {
105
+ document.getElementById('success').style.display = 'none';
106
+ document.getElementById('failure').style.display = 'block';
107
+ }
108
+ })();
109
+ </script>
110
+ </body>
111
+ </html>
@@ -1,20 +1,11 @@
1
1
  import { STATUS_CODES } from 'node:http';
2
+ import { Logger } from './logger.js';
2
3
 
3
- export const httpMessages = {
4
- OK: 'OK',
5
- CREATED: 'Created',
6
- NO_CONTENT: 'No Content',
7
- BAD_REQUEST: 'Bad Request',
8
- UNAUTHORIZED: 'Unauthorized',
9
- FORBIDDEN: 'Forbidden',
10
- NOT_FOUND: 'Not Found',
11
- NOT_ACCEPTABLE: 'Not Acceptable',
12
- CONFLICT: 'Conflict',
13
- LOCKED: 'Locked',
14
- SYSTEM_FAILURE: 'System Error',
15
- NOT_IMPLEMENTED: 'Not Implemented',
16
- };
4
+ const logger = Logger.getInstance('httpError');
17
5
 
6
+ /**
7
+ * HTTP status codes
8
+ */
18
9
  export const httpCodes = {
19
10
  OK: 200,
20
11
  CREATED: 201,
@@ -30,23 +21,45 @@ export const httpCodes = {
30
21
  NOT_IMPLEMENTED: 501,
31
22
  };
32
23
 
24
+ /**
25
+ * HTTP status messages
26
+ */
27
+ export const httpMessages = {
28
+ OK: 'OK',
29
+ CREATED: 'Created',
30
+ NO_CONTENT: 'No Content',
31
+ BAD_REQUEST: 'Bad Request',
32
+ UNAUTHORIZED: 'Unauthorized',
33
+ FORBIDDEN: 'Forbidden',
34
+ NOT_FOUND: 'Not Found',
35
+ NOT_ACCEPTABLE: 'Not Acceptable',
36
+ CONFLICT: 'Conflict',
37
+ LOCKED: 'Locked',
38
+ SYSTEM_FAILURE: 'Internal Server Error',
39
+ NOT_IMPLEMENTED: 'Not Implemented',
40
+ };
41
+
42
+ /**
43
+ * Custom Error class
44
+ */
33
45
  export class CustomError extends Error {
34
- /** @type {number} */
35
- code;
36
- /** @type {object} */
37
- data;
38
- /** @type {object} */
39
- error;
40
46
  /**
41
- * Construct a custom error
42
- * @param {number} code Error code
43
- * @param {string} message Message
47
+ * @param {number} code HTTP status code
48
+ * @param {string} message Error message
49
+ * @param {Error} [error] Original error object
50
+ * @param {object} [data] Additional error data
44
51
  */
45
- constructor(code, message, error = {}, data = {}) {
52
+ constructor(code, message, error, data) {
46
53
  super(message);
54
+ this.name = 'CustomError';
47
55
  this.code = code;
48
56
  this.error = error;
49
57
  this.data = data;
58
+
59
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
60
+ if (Error.captureStackTrace) {
61
+ Error.captureStackTrace(this, CustomError);
62
+ }
50
63
  }
51
64
  }
52
65
 
@@ -85,21 +98,21 @@ export const httpErrorHandler = (err, req, res, next) => {
85
98
  res.status(responseBody.status).json(responseBody);
86
99
 
87
100
  // Log error details
88
- console.error('### ERROR ###');
89
- console.error(`${req.method} ${req.path}`);
101
+ logger.error('### ERROR ###');
102
+ logger.error(`${req.method} ${req.path}`);
90
103
 
91
104
  // Log based on error type
92
105
  if ([httpCodes.UNAUTHORIZED, httpCodes.FORBIDDEN, httpCodes.NOT_FOUND].includes(err.code)) {
93
- console.error('>>> Auth Error:', err.message);
106
+ logger.error('>>> Auth Error:', err.message);
94
107
  } else {
95
- console.error('>>> Name:', err.name);
96
- console.error('>>> Message:', err.message);
108
+ logger.error('>>> Name:', err.name);
109
+ logger.error('>>> Message:', err.message);
97
110
  if (err.stack) {
98
- console.error('>>> Stack:', err.stack);
111
+ logger.error('>>> Stack:', err.stack);
99
112
  }
100
113
  }
101
114
 
102
- console.error('### /ERROR ###');
115
+ logger.error('### /ERROR ###');
103
116
  };
104
117
 
105
118
  /**
@@ -177,7 +190,7 @@ export const httpHelper = {
177
190
  * @returns {CustomError} Returns CustomError instance
178
191
  */
179
192
  handleAxiosError(error, defaultMessage = 'An error occurred') {
180
- console.warn(`### TRY ERROR: ${defaultMessage} ###`);
193
+ logger.warn(`### TRY ERROR: ${defaultMessage} ###`);
181
194
  // Extract error details
182
195
  const errorCode = _getErrorCode(error);
183
196
  const errorMessage = _getErrorMessage(error, defaultMessage);
@@ -196,4 +209,4 @@ export const httpHelper = {
196
209
  */
197
210
  export const httpError = (code, message, error, data) => {
198
211
  return new CustomError(code, message, error, data);
199
- };
212
+ };
package/components/jwt.js CHANGED
@@ -13,7 +13,7 @@ export class JwtManager {
13
13
  /** @type {string} Encryption method */
14
14
  encryption;
15
15
 
16
- /** @type {string} Token expiration time */
16
+ /** @type {number} Token expiration time */
17
17
  expirationTime;
18
18
 
19
19
  /** @type {number} Clock tolerance in seconds */
@@ -37,7 +37,7 @@ export class JwtManager {
37
37
  * @typedef {Object} JwtManagerOptions JwtManager configuration options
38
38
  * @property {string} [JWT_ALGORITHM='dir'] JWE algorithm (default: 'dir')
39
39
  * @property {string} [JWT_ENCRYPTION='A256GCM'] Encryption method (default: 'A256GCM')
40
- * @property {string} [JWT_EXPIRATION_TIME='10m'] Token expiration time (default: '10m')
40
+ * @property {number} [SESSION_AGE=64800000] Token expiration time in milliseconds (default: 64800000 = 18 hours)
41
41
  * @property {string} [JWT_SECRET_HASH_ALGORITHM='SHA-256'] Hash algorithm (default: 'SHA-256')
42
42
  * @property {string?} [JWT_ISSUER] Optional JWT issuer claim
43
43
  * @property {string?} [JWT_AUDIENCE] Optional JWT audience claim
@@ -48,7 +48,8 @@ export class JwtManager {
48
48
  constructor(options = {}) {
49
49
  this.algorithm = options.JWT_ALGORITHM || 'dir';
50
50
  this.encryption = options.JWT_ENCRYPTION || 'A256GCM';
51
- this.expirationTime = options.JWT_EXPIRATION_TIME || '10m';
51
+ // SESSION_AGE is in milliseconds, convert to seconds for expirationTime
52
+ this.expirationTime = Math.floor((options.SESSION_AGE || 64800000) / 1000);
52
53
  this.secretHashAlgorithm = options.JWT_SECRET_HASH_ALGORITHM || 'SHA-256';
53
54
  this.issuer = options.JWT_ISSUER;
54
55
  this.audience = options.JWT_AUDIENCE;
@@ -62,7 +63,7 @@ export class JwtManager {
62
63
  * @typedef {Object} JwtEncryptOptions Encryption method options
63
64
  * @property {string} [algorithm='dir'] JWE algorithm (overrides instance JWT_ALGORITHM)
64
65
  * @property {string} [encryption='A256GCM'] Encryption method (overrides instance JWT_ENCRYPTION)
65
- * @property {string} [expirationTime='10m'] Token expiration time (overrides instance JWT_EXPIRATION_TIME)
66
+ * @property {number} [expirationTime] Token expiration time in seconds (overrides instance expirationTime derived from SESSION_AGE)
66
67
  * @property {string} [secretHashAlgorithm='SHA-256'] Hash algorithm for secret derivation (overrides instance JWT_SECRET_HASH_ALGORITHM)
67
68
  * @property {string?} [issuer] Optional JWT issuer claim (overrides instance JWT_ISSUER)
68
69
  * @property {string?} [audience] Optional JWT audience claim (overrides instance JWT_AUDIENCE)
@@ -93,10 +94,11 @@ export class JwtManager {
93
94
  const jwt = new EncryptJWT(data)
94
95
  .setProtectedHeader({
95
96
  alg: algorithm,
96
- enc: encryption
97
+ enc: encryption,
98
+ typ: 'JWT',
97
99
  })
98
100
  .setIssuedAt()
99
- .setExpirationTime(expirationTime);
101
+ .setExpirationTime(`${expirationTime}s`); // Pass as string with 's' suffix for seconds
100
102
 
101
103
  // Add optional claims if provided
102
104
  if (issuer) jwt.setIssuer(issuer);
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Logger utility for node-components
3
+ * Provides configurable logging with enable/disable functionality
4
+ * Uses singleton pattern to manage logger instances per component
5
+ *
6
+ * @example
7
+ * import { Logger } from './logger.js';
8
+ *
9
+ * // Recommended: Get logger instance (singleton pattern)
10
+ * const logger = Logger.getInstance('ComponentName');
11
+ *
12
+ * // With explicit enable/disable
13
+ * const logger = Logger.getInstance('ComponentName', true); // Always enabled
14
+ * const logger = Logger.getInstance('ComponentName', false); // Always disabled
15
+ *
16
+ * // Backward compatibility: Constructor still works
17
+ * const logger = new Logger('ComponentName');
18
+ *
19
+ * // Use logger
20
+ * logger.info('Operation completed');
21
+ * logger.error('Error occurred', error);
22
+ */
23
+ export class Logger {
24
+ /** @type {Map<string, Logger>} */
25
+ static #instances = new Map();
26
+
27
+ /** @type {boolean} - Global flag to enable/disable colors */
28
+ static #colorsEnabled = true;
29
+
30
+ /** ANSI color codes for different log levels */
31
+ static #colors = {
32
+ reset: '\x1b[0m',
33
+ dim: '\x1b[2m', // debug - dim/gray
34
+ cyan: '\x1b[36m', // info - cyan
35
+ yellow: '\x1b[33m', // warn - yellow
36
+ red: '\x1b[31m', // error - red
37
+ };
38
+
39
+ /** @type {boolean} */
40
+ #enabled;
41
+ /** @type {string} */
42
+ #prefix;
43
+ /** @type {boolean} */
44
+ #useColors;
45
+
46
+ /**
47
+ * Get or create a Logger instance (singleton pattern)
48
+ * @param {string} componentName Component name for log prefix
49
+ * @param {boolean} [enableLogging] Enable/disable logging. Defaults to NODE_ENV !== 'production'
50
+ * @returns {Logger} Logger instance
51
+ */
52
+ static getInstance(componentName, enableLogging) {
53
+ const key = `${componentName}:${enableLogging ?? 'default'}`;
54
+
55
+ if (!Logger.#instances.has(key)) {
56
+ Logger.#instances.set(key, new Logger(componentName, enableLogging));
57
+ }
58
+
59
+ return Logger.#instances.get(key);
60
+ }
61
+
62
+ /**
63
+ * Clear all logger instances (useful for testing)
64
+ */
65
+ static clearInstances() {
66
+ Logger.#instances.clear();
67
+ }
68
+
69
+ /**
70
+ * Disable colors globally for all logger instances
71
+ */
72
+ static disableColors() {
73
+ Logger.#colorsEnabled = false;
74
+ }
75
+
76
+ /**
77
+ * Enable colors globally for all logger instances
78
+ */
79
+ static enableColors() {
80
+ Logger.#colorsEnabled = true;
81
+ }
82
+
83
+ /**
84
+ * Create a new Logger instance
85
+ * @param {string} componentName Component name for log prefix
86
+ * @param {boolean} [enableLogging] Enable/disable logging. Defaults to NODE_ENV !== 'production'
87
+ */
88
+ constructor(componentName, enableLogging) {
89
+ // If enableLogging is explicitly set (true/false), use it
90
+ // Otherwise, default to enabled in development, disabled in production
91
+ this.#enabled = enableLogging ?? (process.env.NODE_ENV !== 'production');
92
+ this.#prefix = `[${componentName}]`;
93
+
94
+ // Determine if colors should be used:
95
+ // - Colors are globally enabled
96
+ // - Output is a terminal (TTY)
97
+ // - NO_COLOR environment variable is not set
98
+ this.#useColors = Logger.#colorsEnabled &&
99
+ process.stdout.isTTY &&
100
+ !process.env.NO_COLOR;
101
+
102
+ // Assign methods based on enabled flag to eliminate runtime checks
103
+ // This improves performance by avoiding conditional checks on every log call
104
+ if (this.#enabled) {
105
+ const colors = Logger.#colors;
106
+
107
+ if (this.#useColors) {
108
+ // Logging enabled with colors: colorize the prefix
109
+ this.debug = (...args) => console.debug(colors.dim + this.#prefix + colors.reset, ...args);
110
+ this.info = (...args) => console.info(colors.cyan + this.#prefix + colors.reset, ...args);
111
+ this.warn = (...args) => console.warn(colors.yellow + this.#prefix + colors.reset, ...args);
112
+ this.error = (...args) => console.error(colors.red + this.#prefix + colors.reset, ...args);
113
+ this.log = (...args) => console.log(this.#prefix, ...args);
114
+ } else {
115
+ // Logging enabled without colors: plain text
116
+ this.debug = (...args) => console.debug(this.#prefix, ...args);
117
+ this.info = (...args) => console.info(this.#prefix, ...args);
118
+ this.warn = (...args) => console.warn(this.#prefix, ...args);
119
+ this.error = (...args) => console.error(this.#prefix, ...args);
120
+ this.log = (...args) => console.log(this.#prefix, ...args);
121
+ }
122
+ } else {
123
+ // Logging disabled: assign no-op functions that do nothing
124
+ this.debug = () => {};
125
+ this.info = () => {};
126
+ this.warn = () => {};
127
+ this.error = () => {};
128
+ this.log = () => {};
129
+ }
130
+ }
131
+ }
@@ -1,8 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
 
3
3
  import { createClient } from '@redis/client';
4
+ import { Logger } from './logger.js';
4
5
 
5
6
  export class RedisManager {
7
+ /** @type {Logger} */
8
+ #logger = Logger.getInstance('RedisManager');
9
+
6
10
  /** @type {import('@redis/client').RedisClientType} */
7
11
  #client = null;
8
12
  /**
@@ -22,21 +26,23 @@ export class RedisManager {
22
26
  options.socket.ca = caCert;
23
27
  }
24
28
  this.#client = createClient(options);
29
+ this.#logger.info('### REDIS CONNECTING ###');
25
30
  this.#client.on('ready', () => {
26
- console.info('### REDIS READY ###');
31
+ this.#logger.info('### REDIS READY ###');
27
32
  });
28
33
  this.#client.on('reconnecting', (_res) => {
29
- console.warn('### REDIS RECONNECTING ###');
34
+ this.#logger.warn('### REDIS RECONNECTING ###');
30
35
  });
31
36
  this.#client.on('error', (error) => {
32
- console.error(`### REDIS ERROR: ${error.message} ###`);
37
+ this.#logger.error(`### REDIS ERROR: ${error.message} ###`);
33
38
  });
34
39
  await this.#client.connect();
40
+ this.#logger.info('### REDIS CONNECTED SUCCESSFULLY ###');
35
41
  return true;
36
42
  }
37
43
  catch (error) {
38
- console.error('### REDIS CONNECT ERROR ###');
39
- console.error(error);
44
+ this.#logger.error('### REDIS CONNECT ERROR ###', error);
45
+ return false;
40
46
  }
41
47
  }
42
48
  return false;
@@ -60,8 +66,8 @@ export class RedisManager {
60
66
  const pongMessage = await this.#client.ping();
61
67
  return 'PONG' === pongMessage;
62
68
  } catch (error) {
63
- console.error(`### REDIS PING ERROR ###`);
64
- console.error(error);
69
+ this.#logger.error(`### REDIS PING ERROR: ${error.message} ###`);
70
+ return false;
65
71
  }
66
72
  return false;
67
73
  }
@@ -70,7 +76,9 @@ export class RedisManager {
70
76
  * Disconnect with Redis
71
77
  * @returns {Promise<void>} Returns nothing
72
78
  */
73
- disConnect() {
74
- return this.#client.quit();
79
+ async disconnect() {
80
+ this.#logger.info('### REDIS DISCONNECTING ###');
81
+ await this.#client.quit();
82
+ this.#logger.info('### REDIS DISCONNECTED ###');
75
83
  }
76
- }
84
+ }