@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 +100 -6
- package/components/assets/template.html +111 -0
- package/components/http-handlers.js +46 -33
- package/components/jwt.js +8 -6
- package/components/logger.js +131 -0
- package/components/redis.js +18 -10
- package/components/session.js +580 -110
- package/index.d.ts +127 -18
- package/index.js +2 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
106
|
+
logger.error('>>> Auth Error:', err.message);
|
|
94
107
|
} else {
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
logger.error('>>> Name:', err.name);
|
|
109
|
+
logger.error('>>> Message:', err.message);
|
|
97
110
|
if (err.stack) {
|
|
98
|
-
|
|
111
|
+
logger.error('>>> Stack:', err.stack);
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
102
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|
package/components/redis.js
CHANGED
|
@@ -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
|
-
|
|
31
|
+
this.#logger.info('### REDIS READY ###');
|
|
27
32
|
});
|
|
28
33
|
this.#client.on('reconnecting', (_res) => {
|
|
29
|
-
|
|
34
|
+
this.#logger.warn('### REDIS RECONNECTING ###');
|
|
30
35
|
});
|
|
31
36
|
this.#client.on('error', (error) => {
|
|
32
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
74
|
-
|
|
79
|
+
async disconnect() {
|
|
80
|
+
this.#logger.info('### REDIS DISCONNECTING ###');
|
|
81
|
+
await this.#client.quit();
|
|
82
|
+
this.#logger.info('### REDIS DISCONNECTED ###');
|
|
75
83
|
}
|
|
76
|
-
}
|
|
84
|
+
}
|