@igxjs/node-components 1.0.9 → 1.0.11

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.
@@ -1,160 +0,0 @@
1
- # SessionManager
2
-
3
- Provides SSO (Single Sign-On) session management with support for Redis and memory-based session stores.
4
-
5
- ## Configuration Options
6
-
7
- ```javascript
8
- // Example configuration object
9
- // All fields are strings except SESSION_AGE which is a number
10
-
11
- const config = {
12
- // SSO Configuration
13
- SSO_ENDPOINT_URL: 'https://sso.example.com',
14
- SSO_CLIENT_ID: 'your-client-id',
15
- SSO_CLIENT_SECRET: 'your-client-secret',
16
- SSO_SUCCESS_URL: '/dashboard',
17
- SSO_FAILURE_URL: '/login',
18
-
19
- // Session Configuration
20
- SESSION_AGE: 64800000, // 18 hours in milliseconds
21
- SESSION_COOKIE_PATH: '/',
22
- SESSION_SECRET: 'your-session-secret',
23
- SESSION_PREFIX: 'ibmid:', // Default value when not provided
24
-
25
- // Redis Configuration (optional - uses memory store if not provided)
26
- REDIS_URL: 'redis://localhost:6379',
27
- REDIS_CERT_PATH: '/path/to/cert.pem' // For TLS connections
28
- };
29
- ```
30
-
31
- ## ⚠️ Important: Singleton Pattern
32
-
33
- **SessionManager should be instantiated once and exported as a singleton.**
34
-
35
- **Why?**
36
- - SessionManager manages Redis connections (if configured)
37
- - Multiple instances can lead to connection pool exhaustion
38
- - Session state consistency requires a single source of truth
39
- - Middleware functions need to reference the same instance
40
-
41
- **✅ Recommended:** Create a separate module that exports a single SessionManager instance
42
- **❌ Avoid:** Creating new SessionManager instances in multiple files
43
-
44
- ## Recommended File Structure
45
-
46
- ```
47
- your-project/
48
- ├── src/
49
- │ ├── config/
50
- │ │ └── session-manager.js ← Create SessionManager singleton here
51
- │ ├── routes/
52
- │ │ └── auth.js ← Import session from config
53
- │ └── app.js ← Import and setup session
54
- └── package.json
55
- ```
56
-
57
- ## Usage Example
58
-
59
- ### Step 1: Create SessionManager Singleton
60
-
61
- Create a dedicated file for your SessionManager instance:
62
-
63
- ```javascript
64
- // config/session-manager.js
65
- import { SessionManager } from '@igxjs/node-components';
66
-
67
- // Create and export a single SessionManager instance
68
- export const session = new SessionManager({
69
- SSO_ENDPOINT_URL: process.env.SSO_ENDPOINT_URL,
70
- SSO_CLIENT_ID: process.env.SSO_CLIENT_ID,
71
- SSO_CLIENT_SECRET: process.env.SSO_CLIENT_SECRET,
72
- SSO_SUCCESS_URL: '/dashboard',
73
- SSO_FAILURE_URL: '/login',
74
- SESSION_AGE: 64800000, // 18 hours in milliseconds
75
- SESSION_COOKIE_PATH: '/',
76
- SESSION_SECRET: process.env.SESSION_SECRET,
77
- SESSION_PREFIX: 'ibmid:',
78
- REDIS_URL: process.env.REDIS_URL,
79
- REDIS_CERT_PATH: process.env.REDIS_CERT_PATH
80
- });
81
- ```
82
-
83
- ### Step 2: Setup and Use in Your Application
84
-
85
- Import and use the singleton instance throughout your application:
86
-
87
- ```javascript
88
- // app.js or index.js
89
- import express from 'express';
90
- import { session } from './config/session-manager.js';
91
-
92
- const app = express();
93
-
94
- // Initialize session middleware (call once during app startup)
95
- await session.setup(app, (user) => {
96
- // Process user object - compute permissions, avatar URL, etc.
97
- return {
98
- ...user,
99
- displayName: user.email?.split('@')[0],
100
- hasAdminAccess: user.authorized && user.email?.endsWith('@admin.com')
101
- };
102
- });
103
-
104
- // Use session instance in your routes
105
- app.get('/protected', session.authenticate(), (req, res) => {
106
- res.json({ user: req.user });
107
- });
108
-
109
- app.get('/auth/callback', session.callback((user) => {
110
- return {
111
- ...user,
112
- loginTime: new Date()
113
- };
114
- }));
115
-
116
- app.get('/auth/providers', session.identityProviders());
117
- app.post('/auth/refresh', session.refresh((user) => {
118
- return { ...user, refreshedAt: new Date() };
119
- }));
120
- app.get('/auth/logout', session.logout());
121
-
122
- app.listen(3000, () => {
123
- console.log('Server running on port 3000');
124
- });
125
- ```
126
-
127
- ### Step 3: Import in Other Files
128
-
129
- ```javascript
130
- // routes/auth.js
131
- import { Router } from 'express';
132
- import { session } from '../config/session-manager.js';
133
-
134
- const router = Router();
135
-
136
- // Reuse the same session instance
137
- router.get('/profile', session.authenticate(), (req, res) => {
138
- res.json({ profile: req.user });
139
- });
140
-
141
- export default router;
142
- ```
143
-
144
- ## API Methods
145
-
146
- - **`setup(app, updateUser)`** - Initialize session configurations
147
- - **`authenticate(isDebugging?, redirectUrl?)`** - Resource protection middleware
148
- - **`callback(initUser)`** - SSO callback handler for successful login
149
- - **`identityProviders()`** - Get available identity providers
150
- - **`logout()`** - Application logout handler (not SSO logout)
151
- - **`refresh(initUser)`** - Refresh user session with new token
152
- - **`redisManager()`** - Get the RedisManager instance (returns RedisManager or null)
153
- - **`hasLock(email)`** - Check if email has a session refresh lock
154
- - **`lock(email)`** - Lock email for session refresh (prevents concurrent refreshes)
155
- - **`clearLocks()`** - Clear expired session refresh locks
156
-
157
- ## Related Documentation
158
-
159
- - [RedisManager](./redis-manager.md) - Used internally by SessionManager for Redis storage
160
- - [Back to main documentation](../README.md)
@@ -1,21 +0,0 @@
1
- import { describe, it } from 'mocha';
2
- import { expect } from 'chai';
3
- import { httpCodes, httpMessages, CustomError } from '../components/http-handlers.js';
4
-
5
- describe('HTTP Handlers', () => {
6
- describe('httpCodes', () => {
7
- it('should have correct HTTP status codes', () => {
8
- expect(httpCodes.OK).to.equal(200);
9
- expect(httpCodes.BAD_REQUEST).to.equal(400);
10
- expect(httpCodes.NOT_FOUND).to.equal(404);
11
- });
12
- });
13
-
14
- describe('CustomError', () => {
15
- it('should create a CustomError', () => {
16
- const error = new CustomError(404, 'Not found');
17
- expect(error.code).to.equal(404);
18
- expect(error.message).to.equal('Not found');
19
- });
20
- });
21
- });
package/tests/jwt.test.js DELETED
@@ -1,345 +0,0 @@
1
- import { describe, it, beforeEach } from 'mocha';
2
- import { expect } from 'chai';
3
- import { JwtManager } from '../components/jwt.js';
4
-
5
- describe('JwtManager', () => {
6
- let jwtManager;
7
- const testSecret = 'test-secret-key-12345';
8
- const testData = {
9
- userId: '123',
10
- email: 'test@example.com',
11
- role: 'user'
12
- };
13
-
14
- beforeEach(() => {
15
- jwtManager = new JwtManager();
16
- });
17
-
18
- describe('Constructor', () => {
19
- it('should create JwtManager instance with default options', () => {
20
- const manager = new JwtManager();
21
- expect(manager).to.be.instanceOf(JwtManager);
22
- expect(manager.algorithm).to.equal('dir');
23
- expect(manager.encryption).to.equal('A256GCM');
24
- expect(manager.expirationTime).to.equal('10m');
25
- expect(manager.clockTolerance).to.equal(30);
26
- expect(manager.secretHashAlgorithm).to.equal('SHA-256');
27
- });
28
-
29
- it('should create JwtManager instance with custom options', () => {
30
- const options = {
31
- algorithm: 'A128KW',
32
- encryption: 'A128GCM',
33
- expirationTime: '1h',
34
- clockTolerance: 60,
35
- secretHashAlgorithm: 'SHA-512',
36
- issuer: 'test-issuer',
37
- audience: 'test-audience',
38
- subject: 'test-subject'
39
- };
40
- const manager = new JwtManager(options);
41
- expect(manager.algorithm).to.equal('A128KW');
42
- expect(manager.encryption).to.equal('A128GCM');
43
- expect(manager.expirationTime).to.equal('1h');
44
- expect(manager.clockTolerance).to.equal(60);
45
- expect(manager.secretHashAlgorithm).to.equal('SHA-512');
46
- expect(manager.issuer).to.equal('test-issuer');
47
- expect(manager.audience).to.equal('test-audience');
48
- expect(manager.subject).to.equal('test-subject');
49
- });
50
-
51
- it('should handle clockTolerance of 0', () => {
52
- const manager = new JwtManager({ clockTolerance: 0 });
53
- expect(manager.clockTolerance).to.equal(0);
54
- });
55
- });
56
-
57
- describe('encrypt', () => {
58
- it('should encrypt data and return a JWT token', async () => {
59
- const token = await jwtManager.encrypt(testData, testSecret);
60
- expect(token).to.be.a('string');
61
- expect(token.split('.').length).to.equal(5); // JWE has 5 parts
62
- });
63
-
64
- it('should encrypt with custom encryption method', async () => {
65
- const token = await jwtManager.encrypt(testData, testSecret, {
66
- algorithm: 'dir',
67
- encryption: 'A256GCM'
68
- });
69
- expect(token).to.be.a('string');
70
- });
71
-
72
- it('should encrypt with custom expiration time', async () => {
73
- const token = await jwtManager.encrypt(testData, testSecret, {
74
- expirationTime: '1h'
75
- });
76
- expect(token).to.be.a('string');
77
- });
78
-
79
- it('should encrypt with issuer claim', async () => {
80
- const token = await jwtManager.encrypt(testData, testSecret, {
81
- issuer: 'test-issuer'
82
- });
83
- expect(token).to.be.a('string');
84
- });
85
-
86
- it('should encrypt with audience claim', async () => {
87
- const token = await jwtManager.encrypt(testData, testSecret, {
88
- audience: 'test-audience'
89
- });
90
- expect(token).to.be.a('string');
91
- });
92
-
93
- it('should encrypt with subject claim', async () => {
94
- const token = await jwtManager.encrypt(testData, testSecret, {
95
- subject: 'test-subject'
96
- });
97
- expect(token).to.be.a('string');
98
- });
99
-
100
- it('should encrypt with all optional claims', async () => {
101
- const token = await jwtManager.encrypt(testData, testSecret, {
102
- issuer: 'test-issuer',
103
- audience: 'test-audience',
104
- subject: 'test-subject'
105
- });
106
- expect(token).to.be.a('string');
107
- });
108
-
109
- it('should use default claims from constructor if not overridden', async () => {
110
- const manager = new JwtManager({
111
- issuer: 'default-issuer',
112
- audience: 'default-audience'
113
- });
114
- const token = await manager.encrypt(testData, testSecret);
115
- expect(token).to.be.a('string');
116
- });
117
- });
118
-
119
- describe('decrypt', () => {
120
- it('should decrypt a valid JWT token', async () => {
121
- const token = await jwtManager.encrypt(testData, testSecret);
122
- const result = await jwtManager.decrypt(token, testSecret);
123
-
124
- expect(result).to.have.property('payload');
125
- expect(result).to.have.property('protectedHeader');
126
- expect(result.payload).to.include(testData);
127
- });
128
-
129
- it('should decrypt token with correct payload data', async () => {
130
- const token = await jwtManager.encrypt(testData, testSecret);
131
- const result = await jwtManager.decrypt(token, testSecret);
132
-
133
- expect(result.payload.userId).to.equal(testData.userId);
134
- expect(result.payload.email).to.equal(testData.email);
135
- expect(result.payload.role).to.equal(testData.role);
136
- });
137
-
138
- it('should include standard JWT claims in decrypted payload', async () => {
139
- const token = await jwtManager.encrypt(testData, testSecret);
140
- const result = await jwtManager.decrypt(token, testSecret);
141
-
142
- expect(result.payload).to.have.property('iat');
143
- expect(result.payload).to.have.property('exp');
144
- });
145
-
146
- it('should fail to decrypt with wrong secret', async () => {
147
- const token = await jwtManager.encrypt(testData, testSecret);
148
-
149
- try {
150
- await jwtManager.decrypt(token, 'wrong-secret');
151
- expect.fail('Should have thrown an error');
152
- } catch (error) {
153
- expect(error).to.exist;
154
- }
155
- });
156
-
157
- it('should decrypt token with custom clock tolerance', async () => {
158
- const token = await jwtManager.encrypt(testData, testSecret, {
159
- expirationTime: '1s'
160
- });
161
-
162
- // Use higher clock tolerance to allow slightly expired tokens
163
- const result = await jwtManager.decrypt(token, testSecret, {
164
- clockTolerance: 120
165
- });
166
-
167
- expect(result.payload).to.include(testData);
168
- });
169
-
170
- it('should validate issuer claim when provided', async () => {
171
- const issuer = 'test-issuer';
172
- const token = await jwtManager.encrypt(testData, testSecret, { issuer });
173
-
174
- const result = await jwtManager.decrypt(token, testSecret, { issuer });
175
- expect(result.payload.iss).to.equal(issuer);
176
- });
177
-
178
- it('should fail when issuer claim does not match', async () => {
179
- const token = await jwtManager.encrypt(testData, testSecret, {
180
- issuer: 'correct-issuer'
181
- });
182
-
183
- try {
184
- await jwtManager.decrypt(token, testSecret, {
185
- issuer: 'wrong-issuer'
186
- });
187
- expect.fail('Should have thrown an error');
188
- } catch (error) {
189
- expect(error).to.exist;
190
- }
191
- });
192
-
193
- it('should validate audience claim when provided', async () => {
194
- const audience = 'test-audience';
195
- const token = await jwtManager.encrypt(testData, testSecret, { audience });
196
-
197
- const result = await jwtManager.decrypt(token, testSecret, { audience });
198
- expect(result.payload.aud).to.equal(audience);
199
- });
200
-
201
- it('should fail when audience claim does not match', async () => {
202
- const token = await jwtManager.encrypt(testData, testSecret, {
203
- audience: 'correct-audience'
204
- });
205
-
206
- try {
207
- await jwtManager.decrypt(token, testSecret, {
208
- audience: 'wrong-audience'
209
- });
210
- expect.fail('Should have thrown an error');
211
- } catch (error) {
212
- expect(error).to.exist;
213
- }
214
- });
215
-
216
- it('should validate subject claim when provided', async () => {
217
- const subject = 'test-subject';
218
- const token = await jwtManager.encrypt(testData, testSecret, { subject });
219
-
220
- const result = await jwtManager.decrypt(token, testSecret, { subject });
221
- expect(result.payload.sub).to.equal(subject);
222
- });
223
-
224
- it('should fail when subject claim does not match', async () => {
225
- const token = await jwtManager.encrypt(testData, testSecret, {
226
- subject: 'correct-subject'
227
- });
228
-
229
- try {
230
- await jwtManager.decrypt(token, testSecret, {
231
- subject: 'wrong-subject'
232
- });
233
- expect.fail('Should have thrown an error');
234
- } catch (error) {
235
- expect(error).to.exist;
236
- }
237
- });
238
-
239
- it('should use default claims from constructor for validation', async () => {
240
- const manager = new JwtManager({
241
- issuer: 'default-issuer',
242
- audience: 'default-audience',
243
- subject: 'default-subject'
244
- });
245
-
246
- const token = await manager.encrypt(testData, testSecret);
247
- const result = await manager.decrypt(token, testSecret);
248
-
249
- expect(result.payload.iss).to.equal('default-issuer');
250
- expect(result.payload.aud).to.equal('default-audience');
251
- expect(result.payload.sub).to.equal('default-subject');
252
- });
253
- });
254
-
255
- describe('encrypt/decrypt round-trip', () => {
256
- it('should successfully encrypt and decrypt data', async () => {
257
- const originalData = {
258
- id: 'user-123',
259
- name: 'John Doe',
260
- permissions: ['read', 'write']
261
- };
262
-
263
- const token = await jwtManager.encrypt(originalData, testSecret);
264
- const result = await jwtManager.decrypt(token, testSecret);
265
-
266
- expect(result.payload.id).to.equal(originalData.id);
267
- expect(result.payload.name).to.equal(originalData.name);
268
- expect(result.payload.permissions).to.deep.equal(originalData.permissions);
269
- });
270
-
271
- it('should handle SHA-256 hash algorithm', async () => {
272
- const manager = new JwtManager({
273
- secretHashAlgorithm: 'SHA-256',
274
- encryption: 'A256GCM' // SHA-256 produces 256 bits, matches A256GCM
275
- });
276
-
277
- const token = await manager.encrypt(testData, testSecret);
278
- const result = await manager.decrypt(token, testSecret);
279
-
280
- expect(result.payload).to.include(testData);
281
- });
282
-
283
- it('should handle empty payload', async () => {
284
- const emptyData = {};
285
- const token = await jwtManager.encrypt(emptyData, testSecret);
286
- const result = await jwtManager.decrypt(token, testSecret);
287
-
288
- expect(result.payload).to.have.property('iat');
289
- expect(result.payload).to.have.property('exp');
290
- });
291
-
292
- it('should preserve different data types', async () => {
293
- const complexData = {
294
- string: 'test',
295
- number: 42,
296
- boolean: true,
297
- array: [1, 2, 3],
298
- object: { nested: 'value' },
299
- null: null
300
- };
301
-
302
- const token = await jwtManager.encrypt(complexData, testSecret);
303
- const result = await jwtManager.decrypt(token, testSecret);
304
-
305
- expect(result.payload.string).to.equal(complexData.string);
306
- expect(result.payload.number).to.equal(complexData.number);
307
- expect(result.payload.boolean).to.equal(complexData.boolean);
308
- expect(result.payload.array).to.deep.equal(complexData.array);
309
- expect(result.payload.object).to.deep.equal(complexData.object);
310
- expect(result.payload.null).to.equal(complexData.null);
311
- });
312
- });
313
-
314
- describe('Error Handling', () => {
315
- it('should fail with invalid token format', async () => {
316
- try {
317
- await jwtManager.decrypt('invalid.token.format', testSecret);
318
- expect.fail('Should have thrown an error');
319
- } catch (error) {
320
- expect(error).to.exist;
321
- }
322
- });
323
-
324
- it('should fail with empty token', async () => {
325
- try {
326
- await jwtManager.decrypt('', testSecret);
327
- expect.fail('Should have thrown an error');
328
- } catch (error) {
329
- expect(error).to.exist;
330
- }
331
- });
332
-
333
- it('should fail with corrupted token', async () => {
334
- const token = await jwtManager.encrypt(testData, testSecret);
335
- const corruptedToken = token.slice(0, -10) + 'corrupted';
336
-
337
- try {
338
- await jwtManager.decrypt(corruptedToken, testSecret);
339
- expect.fail('Should have thrown an error');
340
- } catch (error) {
341
- expect(error).to.exist;
342
- }
343
- });
344
- });
345
- });
@@ -1,50 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'mocha';
2
- import { expect } from 'chai';
3
- import sinon from 'sinon';
4
- import { RedisManager } from '../components/redis.js';
5
-
6
- describe('RedisManager', () => {
7
- let redisManager;
8
- let consoleStubs;
9
-
10
- beforeEach(() => {
11
- redisManager = new RedisManager();
12
- consoleStubs = {
13
- info: sinon.stub(console, 'info'),
14
- warn: sinon.stub(console, 'warn'),
15
- error: sinon.stub(console, 'error')
16
- };
17
- });
18
-
19
- afterEach(() => {
20
- consoleStubs.info.restore();
21
- consoleStubs.warn.restore();
22
- consoleStubs.error.restore();
23
- });
24
-
25
- describe('connect', () => {
26
- it('should return false if redisUrl is empty', async () => {
27
- const result = await redisManager.connect('', null);
28
- expect(result).to.be.false;
29
- });
30
-
31
- it('should return false if redisUrl is null', async () => {
32
- const result = await redisManager.connect(null, null);
33
- expect(result).to.be.false;
34
- });
35
- });
36
-
37
- describe('getClient', () => {
38
- it('should return null when not connected', () => {
39
- const client = redisManager.getClient();
40
- expect(client).to.be.null;
41
- });
42
- });
43
-
44
- describe('isConnected', () => {
45
- it('should return false when client is null', async () => {
46
- const result = await redisManager.isConnected();
47
- expect(result).to.be.false;
48
- });
49
- });
50
- });
@@ -1,50 +0,0 @@
1
- import { describe, it, beforeEach } from 'mocha';
2
- import { expect } from 'chai';
3
- import sinon from 'sinon';
4
- import express from 'express';
5
- import { FlexRouter } from '../components/router.js';
6
-
7
- describe('FlexRouter', () => {
8
- let app, router, middleware1, middleware2;
9
-
10
- beforeEach(() => {
11
- app = express();
12
- router = express.Router();
13
- middleware1 = sinon.stub().callsFake((req, res, next) => next());
14
- middleware2 = sinon.stub().callsFake((req, res, next) => next());
15
- sinon.spy(app, 'use');
16
- });
17
-
18
- describe('constructor', () => {
19
- it('should create FlexRouter with context and router', () => {
20
- const flexRouter = new FlexRouter('/api', router);
21
- expect(flexRouter.context).to.equal('/api');
22
- expect(flexRouter.router).to.equal(router);
23
- expect(flexRouter.handlers).to.deep.equal([]);
24
- });
25
-
26
- it('should create FlexRouter with handlers', () => {
27
- const handlers = [middleware1, middleware2];
28
- const flexRouter = new FlexRouter('/api', router, handlers);
29
- expect(flexRouter.handlers).to.deep.equal(handlers);
30
- });
31
- });
32
-
33
- describe('mount', () => {
34
- it('should mount router to app with correct path', () => {
35
- const flexRouter = new FlexRouter('/users', router);
36
- flexRouter.mount(app, '/api/v1');
37
- expect(app.use.calledOnce).to.be.true;
38
- expect(app.use.firstCall.args[0]).to.equal('/api/v1/users');
39
- });
40
-
41
- it('should mount router with handlers', () => {
42
- const handlers = [middleware1, middleware2];
43
- const flexRouter = new FlexRouter('/protected', router, handlers);
44
- flexRouter.mount(app, '/api');
45
- expect(app.use.calledOnce).to.be.true;
46
- expect(app.use.firstCall.args[0]).to.equal('/api/protected');
47
- expect(app.use.firstCall.args[1]).to.deep.equal(handlers);
48
- });
49
- });
50
- });