@igxjs/node-components 1.0.11 → 1.0.13

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
@@ -13,6 +13,7 @@ npm install @igxjs/node-components
13
13
  | Component | Description | Documentation |
14
14
  |-----------|-------------|---------------|
15
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) |
@@ -76,10 +77,11 @@ flexRouter.mount(app, '');
76
77
  ```javascript
77
78
  import { JwtManager } from '@igxjs/node-components';
78
79
 
79
- const jwt = new JwtManager({ expirationTime: '1h' });
80
+ // Constructor uses UPPERCASE naming with JWT_ prefix
81
+ const jwt = new JwtManager({ SESSION_AGE: 64800000 });
80
82
  const SECRET = process.env.JWT_SECRET;
81
83
 
82
- // Create token
84
+ // Create token (encrypt method uses camelCase for per-call options)
83
85
  const token = await jwt.encrypt({ userId: '123', email: 'user@example.com' }, SECRET);
84
86
 
85
87
  // Verify token
@@ -143,7 +145,6 @@ Uses JWT bearer tokens instead of session cookies. When a user authenticates via
143
145
  - `SSO_FAILURE_URL`: Redirect URL after failed SSO login
144
146
  - `JWT_ALGORITHM`: JWT algorithm (default: `'dir'`)
145
147
  - `JWT_ENCRYPTION`: Encryption algorithm (default: `'A256GCM'`)
146
- - `JWT_EXPIRATION_TIME`: Token expiration time (default: `'10m'`)
147
148
  - `JWT_CLOCK_TOLERANCE`: Clock skew tolerance in seconds (default: 30)
148
149
 
149
150
  **Auth Methods:**
@@ -154,23 +155,23 @@ Uses JWT bearer tokens instead of session cookies. When a user authenticates via
154
155
 
155
156
  **Token Storage (Client-Side):**
156
157
 
157
- When using token-based authentication, the client-side HTML page stores the token in `localStorage`:
158
-
159
- ```html
160
- <script>
161
- // Store auth data in localStorage
162
- localStorage.setItem('authToken', ${JSON.stringify(token)});
163
- localStorage.setItem('tokenExpiry', ${Date.now() + sessionAge});
164
- localStorage.setItem('user', ${JSON.stringify({
165
- email: user.email,
166
- name: user.name,
167
- })});
168
-
169
- // Redirect to original destination
170
- window.location.replace(redirectUrl);
171
- </script>
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
+ });
172
171
  ```
173
172
 
173
+ **Note:** The actual localStorage keys used are determined by the `SESSION_KEY` and `SESSION_EXPIRY_KEY` configuration options (defaults shown above).
174
+
174
175
  ## SessionManager Configuration Options
175
176
 
176
177
  | Option | Type | Default | Description |
@@ -189,7 +190,6 @@ When using token-based authentication, the client-side HTML page stores the toke
189
190
  | `REDIS_CERT_PATH` | string | - | Path to Redis TLS certificate |
190
191
  | `JWT_ALGORITHM` | string | `'dir'` | JWT signing algorithm |
191
192
  | `JWT_ENCRYPTION` | string | `'A256GCM'` | JWE encryption algorithm |
192
- | `JWT_EXPIRATION_TIME` | string | `'10m'` | Token expiration duration |
193
193
  | `JWT_CLOCK_TOLERANCE` | number | 30 | Clock skew tolerance in seconds |
194
194
  | `JWT_SECRET_HASH_ALGORITHM` | string | `'SHA-256'` | Algorithm for hashing secrets |
195
195
  | `JWT_ISSUER` | string | - | JWT issuer identifier |
@@ -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
@@ -1,4 +1,4 @@
1
- import crypto from 'node:crypto';
1
+ import { webcrypto as crypto } from 'node:crypto';
2
2
 
3
3
  import { jwtDecrypt, EncryptJWT } from 'jose';
4
4
 
@@ -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|string} [JWT_EXPIRATION_TIME=64800] Token expiration time - number in seconds (e.g., 64800) or string with time suffix (e.g., '18h', '7d', '1080m') (default: 64800 = 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
+ // JWT_EXPIRATION_TIME is in seconds
52
+ this.expirationTime = options.JWT_EXPIRATION_TIME || 64800;
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|string} [expirationTime] Token expiration time - number in seconds or string with time suffix like '1h', '30m', '7d' (overrides instance JWT_EXPIRATION_TIME)
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)
@@ -90,13 +91,17 @@ export class JwtManager {
90
91
  new TextEncoder().encode(secret)
91
92
  );
92
93
 
94
+ // Convert number to string with 's' suffix, pass strings directly
95
+ const expTime = typeof expirationTime === 'number' ? `${expirationTime}s` : expirationTime;
96
+
93
97
  const jwt = new EncryptJWT(data)
94
98
  .setProtectedHeader({
95
99
  alg: algorithm,
96
- enc: encryption
100
+ enc: encryption,
101
+ typ: 'JWT',
97
102
  })
98
103
  .setIssuedAt()
99
- .setExpirationTime(expirationTime);
104
+ .setExpirationTime(expTime); // Accepts: number (as seconds) or string (e.g., '18h', '7d')
100
105
 
101
106
  // Add optional claims if provided
102
107
  if (issuer) jwt.setIssuer(issuer);
@@ -122,7 +127,7 @@ export class JwtManager {
122
127
  * @param {string} token JWT token to decrypt
123
128
  * @param {string} secret Secret key or password for decryption
124
129
  * @param {JwtDecryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
125
- * @returns {Promise<import('jose').JWTDecryptResult<import('jose').EncryptJWT>} Returns decrypted JWT token
130
+ * @returns {Promise<import('jose').JWTDecryptResult>} Returns decrypted JWT token
126
131
  */
127
132
  async decrypt(token, secret, options = {}) {
128
133
  const clockTolerance = options.clockTolerance ?? this.clockTolerance;
@@ -145,4 +150,4 @@ export class JwtManager {
145
150
 
146
151
  return await jwtDecrypt(token, new Uint8Array(secretHash), decryptOptions);
147
152
  }
148
- }
153
+ }
@@ -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,42 +1,67 @@
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
  /**
9
13
  * Connect with Redis
14
+ * @param {string} redisUrl Redis connection URL
15
+ * @param {string} certPath Path to TLS certificate (required when using rediss:// protocol)
10
16
  * @returns {Promise<boolean>} Returns `true` if Redis server is connected
17
+ * @throws {TypeError} If redisUrl is not a string
18
+ * @throws {Error} If TLS is enabled but certPath is invalid
11
19
  */
12
20
  async connect(redisUrl, certPath) {
13
21
  if (redisUrl?.length > 0) {
14
22
  try {
23
+ // Validate redisUrl is a string
24
+ if (typeof redisUrl !== 'string') {
25
+ throw new TypeError(`Invalid redisUrl: expected string but received ${typeof redisUrl}`);
26
+ }
27
+
15
28
  /** @type {import('@redis/client').RedisClientOptions} */
16
29
  const options = {
17
30
  url: redisUrl,
18
31
  socket: { tls: redisUrl.includes('rediss:'), ca: null },
19
32
  };
20
- if(options.socket.tls) {
33
+
34
+ if (options.socket.tls) {
35
+ // Validate certPath when TLS is enabled
36
+ if (!certPath || typeof certPath !== 'string') {
37
+ throw new Error('TLS certificate path is required when using rediss:// protocol');
38
+ }
39
+
40
+ if (!fs.existsSync(certPath)) {
41
+ throw new Error(`TLS certificate file not found at path: ${certPath}`);
42
+ }
43
+
21
44
  const caCert = fs.readFileSync(certPath);
22
45
  options.socket.ca = caCert;
23
46
  }
24
47
  this.#client = createClient(options);
48
+ this.#logger.info('### REDIS CONNECTING ###');
25
49
  this.#client.on('ready', () => {
26
- console.info('### REDIS READY ###');
50
+ this.#logger.info('### REDIS READY ###');
27
51
  });
28
52
  this.#client.on('reconnecting', (_res) => {
29
- console.warn('### REDIS RECONNECTING ###');
53
+ this.#logger.warn('### REDIS RECONNECTING ###');
30
54
  });
31
55
  this.#client.on('error', (error) => {
32
- console.error(`### REDIS ERROR: ${error.message} ###`);
56
+ this.#logger.error(`### REDIS ERROR: ${error.message} ###`);
33
57
  });
34
58
  await this.#client.connect();
59
+ this.#logger.info('### REDIS CONNECTED SUCCESSFULLY ###');
35
60
  return true;
36
61
  }
37
62
  catch (error) {
38
- console.error('### REDIS CONNECT ERROR ###');
39
- console.error(error);
63
+ this.#logger.error('### REDIS CONNECT ERROR ###', error);
64
+ return false;
40
65
  }
41
66
  }
42
67
  return false;
@@ -60,8 +85,8 @@ export class RedisManager {
60
85
  const pongMessage = await this.#client.ping();
61
86
  return 'PONG' === pongMessage;
62
87
  } catch (error) {
63
- console.error(`### REDIS PING ERROR ###`);
64
- console.error(error);
88
+ this.#logger.error(`### REDIS PING ERROR: ${error.message} ###`);
89
+ return false;
65
90
  }
66
91
  return false;
67
92
  }
@@ -70,7 +95,9 @@ export class RedisManager {
70
95
  * Disconnect with Redis
71
96
  * @returns {Promise<void>} Returns nothing
72
97
  */
73
- disConnect() {
74
- return this.#client.quit();
98
+ async disconnect() {
99
+ this.#logger.info('### REDIS DISCONNECTING ###');
100
+ await this.#client.quit();
101
+ this.#logger.info('### REDIS DISCONNECTED ###');
75
102
  }
76
- }
103
+ }
@@ -43,11 +43,23 @@ export class FlexRouter {
43
43
  }
44
44
 
45
45
  /**
46
- * Mount router
46
+ * Mount router to Express application
47
47
  * @param {import('@types/express').Express} app Express app
48
48
  * @param {string} basePath Base path
49
+ * @throws {TypeError} If app is not a valid Express instance
50
+ * @throws {TypeError} If basePath is not a string
49
51
  */
50
52
  mount(app, basePath) {
53
+ // Validate app is an Express instance (has 'use' method)
54
+ if (!app || typeof app.use !== 'function') {
55
+ throw new TypeError('Invalid Express app: app must be an Express application instance with a "use" method');
56
+ }
57
+
58
+ // Validate basePath is a string
59
+ if (typeof basePath !== 'string') {
60
+ throw new TypeError(`Invalid basePath: expected string but received ${typeof basePath}`);
61
+ }
62
+
51
63
  const path = basePath.concat(this.context);
52
64
  app.use(path, this.handlers, this.router);
53
65
  }