@igxjs/node-components 1.0.0

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/index.d.ts ADDED
@@ -0,0 +1,289 @@
1
+ import 'express-session';
2
+ import '@types/express';
3
+
4
+ import { AxiosError } from 'axios';
5
+ import { RedisClientType } from '@redis/client';
6
+ import { Application, RequestHandler, Request, Response, NextFunction, Router } from '@types/express';
7
+
8
+ // Session Configuration
9
+ export interface SessionConfig {
10
+ SSO_ENDPOINT_URL?: string;
11
+ SSO_CLIENT_ID?: string;
12
+ SSO_CLIENT_SECRET?: string;
13
+ SSO_SUCCESS_URL?: string;
14
+ SSO_FAILURE_URL?: string;
15
+
16
+ SESSION_AGE?: number;
17
+ SESSION_COOKIE_PATH?: string;
18
+ SESSION_SECRET?: string;
19
+ SESSION_PREFIX?: string;
20
+
21
+ REDIS_URL?: string;
22
+ REDIS_CERT_PATH?: string;
23
+ }
24
+
25
+ export interface SessionUserAttributes {
26
+ /** @type {string} Identity Provider ID */
27
+ idp: string;
28
+ /** @type {string} User ID */
29
+ sub: string;
30
+ /** @type {number} Local timeout timestamp */
31
+ expires_at: number;
32
+ /** @type {number} Remote timeout timestamp */
33
+ expires_rt: number;
34
+ /** @type {string} Access token */
35
+ access_token: string;
36
+ /** @type {string} Refresh token */
37
+ refresh_token: string;
38
+ /** @type {Array<string>} Groups of the user */
39
+ groups: string[];
40
+ }
41
+
42
+ export interface SessionUser {
43
+ /** @type {string} First name */
44
+ first_name: string;
45
+ /** @type {string} Last name */
46
+ last_name: string;
47
+ /** @type {string} Full name */
48
+ name: string;
49
+ /** @type {string} Email address */
50
+ email: string;
51
+ /** @type {SessionUserAttributes} User attributes */
52
+ attributes: SessionUserAttributes;
53
+ /** @type {boolean} User is authorized */
54
+ authorized: boolean;
55
+ }
56
+
57
+ // Session Manager
58
+ export class SessionManager {
59
+ /**
60
+ * Check if the email has a session refresh lock
61
+ * @param email Email address
62
+ * @returns Returns true if the email has a session refresh lock
63
+ */
64
+ hasLock(email: string): boolean;
65
+
66
+ /**
67
+ * Lock the email for session refresh
68
+ * @param email Email address
69
+ */
70
+ lock(email: string): void;
71
+
72
+ /**
73
+ * Clear session refresh locks
74
+ */
75
+ clearLocks(): NodeJS.Timeout;
76
+
77
+ /**
78
+ * Get the Redis Manager
79
+ */
80
+ redisManager(): RedisManager;
81
+
82
+ /**
83
+ * Initialize the session configurations
84
+ * @param app Express application
85
+ * @param config Session configurations
86
+ * @param updateUser Process user object to compute attributes like permissions, avatar URL, etc.
87
+ */
88
+ setup(
89
+ app: Application,
90
+ config: SessionConfig,
91
+ updateUser: (user: SessionUser | undefined) => any
92
+ ): Promise<void>;
93
+
94
+ /**
95
+ * Get session RequestHandler
96
+ * @returns Returns RequestHandler instance of Express
97
+ */
98
+ sessionHandler(): Promise<RequestHandler>;
99
+
100
+ /**
101
+ * Resource protection middleware
102
+ * @param isDebugging Debugging flag (default: false)
103
+ * @param redirectUrl Redirect URL (default: '')
104
+ * @returns Returns express Request Handler
105
+ */
106
+ authenticate(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
107
+
108
+ /**
109
+ * SSO callback for successful login
110
+ * @param initUser Initialize user object function
111
+ * @returns Returns express Request Handler
112
+ */
113
+ callback(initUser: (user: SessionUser) => SessionUser): RequestHandler;
114
+
115
+ /**
116
+ * Get Identity Providers
117
+ * @returns Returns express Request Handler
118
+ */
119
+ identityProviders(): RequestHandler;
120
+
121
+ /**
122
+ * Application logout (NOT SSO)
123
+ * @returns Returns express Request Handler
124
+ */
125
+ logout(): RequestHandler;
126
+
127
+ /**
128
+ * Refresh user session
129
+ * @param initUser Initialize user object function
130
+ * @returns Returns express Request Handler
131
+ */
132
+ refresh(initUser: (user: SessionUser) => SessionUser): RequestHandler;
133
+ }
134
+
135
+ // Custom Error class
136
+ export class CustomError extends Error {
137
+ code: number;
138
+ object;
139
+ error: object;
140
+
141
+ /**
142
+ * Construct a custom error
143
+ * @param code Error code
144
+ * @param message Message
145
+ * @param error Error object (optional)
146
+ * @param data Additional data (optional)
147
+ */
148
+ constructor(code: number, message: string, error?: object, data?: object);
149
+ }
150
+
151
+ // Singleton session instance
152
+ export const session: SessionManager;
153
+
154
+ // FlexRouter class for Express routing
155
+ export class FlexRouter {
156
+ context: string;
157
+ router: Router;
158
+ handlers: RequestHandler[];
159
+
160
+ /**
161
+ * Constructor
162
+ * @param context Context path
163
+ * @param router Router instance
164
+ * @param handlers Request handlers (optional)
165
+ */
166
+ constructor(context: string, router: Router, handlers?: RequestHandler[]);
167
+
168
+ /**
169
+ * Mount router to Express app
170
+ * @param app Express application
171
+ * @param basePath Base path
172
+ */
173
+ mount(app: Application, basePath: string): void;
174
+ }
175
+
176
+ // RedisManager class for Redis connection management
177
+ export class RedisManager {
178
+ /**
179
+ * Connect with Redis
180
+ * @param redisUrl Redis connection URL
181
+ * @param certPath Certificate path for TLS connections
182
+ * @returns Returns true if Redis server is connected
183
+ */
184
+ connect(redisUrl: string, certPath: string): Promise<boolean>;
185
+
186
+ /**
187
+ * Get Redis client
188
+ * @returns Returns Redis client instance
189
+ */
190
+ getClient(): RedisClientType;
191
+
192
+ /**
193
+ * Determine if the Redis server is connected
194
+ * @returns Returns true if Redis server is connected
195
+ */
196
+ isConnected(): Promise<boolean>;
197
+
198
+ /**
199
+ * Disconnect from Redis
200
+ * @returns Returns nothing
201
+ */
202
+ disConnect(): Promise<void>;
203
+ }
204
+
205
+ // HTTP status code keys (exposed for type safety)
206
+ export const httpCodes: {
207
+ OK: number;
208
+ CREATED: number;
209
+ NO_CONTENT: number;
210
+ BAD_REQUEST: number;
211
+ UNAUTHORIZED: number;
212
+ FORBIDDEN: number;
213
+ NOT_FOUND: number;
214
+ NOT_ACCEPTABLE: number;
215
+ CONFLICT: number;
216
+ SYSTEM_FAILURE: number;
217
+ };
218
+
219
+ // HTTP message keys (exposed for type safety)
220
+ export const httpMessages: {
221
+ OK: string;
222
+ CREATED: string;
223
+ NO_CONTENT: string;
224
+ BAD_REQUEST: string;
225
+ UNAUTHORIZED: string;
226
+ FORBIDDEN: string;
227
+ NOT_FOUND: string;
228
+ NOT_ACCEPTABLE: string;
229
+ CONFLICT: string;
230
+ SYSTEM_FAILURE: string;
231
+ };
232
+
233
+ /**
234
+ * HTTP Helper utilities
235
+ */
236
+ export const httpHelper: {
237
+ /**
238
+ * Format a string with placeholders
239
+ * @param str String with {0}, {1}, etc. placeholders
240
+ * @param args Values to replace placeholders
241
+ * @returns Formatted string
242
+ */
243
+ format(str: string, ...args: any[]): string;
244
+
245
+ /**
246
+ * Generate friendly Zod validation error message
247
+ * @param error Zod validation error
248
+ * @returns Formatted error message
249
+ */
250
+ toZodMessage(error: any): string;
251
+
252
+ /**
253
+ * Analyze and convert Axios/HTTP errors to CustomError
254
+ * @param error Error object
255
+ * @param defaultMessage Default error message
256
+ * @returns CustomError instance
257
+ */
258
+ handleAxiosError(error: Error | AxiosError, defaultMessage?: string): CustomError;
259
+ };
260
+
261
+ /**
262
+ * Custom error handler middleware
263
+ * @param err Error object
264
+ * @param req Express Request
265
+ * @param res Express Response
266
+ * @param next Next function
267
+ */
268
+ export function httpErrorHandler(
269
+ err: CustomError | Error | any,
270
+ req: Request,
271
+ res: Response,
272
+ next: NextFunction
273
+ ): void;
274
+
275
+ declare global {
276
+ namespace Express {
277
+ export interface Request {
278
+ user?: SessionUser;
279
+ }
280
+ }
281
+ }
282
+
283
+ // Augment Express Session with custom user property
284
+ declare module 'express-session' {
285
+ interface SessionData {
286
+ [key: string]: any;
287
+ user?: SessionUser;
288
+ }
289
+ }
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { SessionManager } from './components/session.js';
2
+
3
+ export { SessionConfig, SessionManager } from './components/session.js';
4
+ export { httpCodes, httpMessages, httpErrorHandler, CustomError, httpHelper } from './components/http-handlers.js';
5
+ export { RedisManager } from './components/redis.js';
6
+ export { FlexRouter } from './components/router.js';
7
+
8
+ export const session = new SessionManager();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@igxjs/node-components",
3
+ "version": "1.0.0",
4
+ "description": "Node components for igxjs",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "mocha tests/**/*.test.js --timeout 5000"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/igxjs/node-components.git"
13
+ },
14
+ "keywords": [
15
+ "igxjs"
16
+ ],
17
+ "author": "Michael",
18
+ "license": "Apache-2.0",
19
+ "bugs": {
20
+ "url": "https://github.com/igxjs/node-components/issues"
21
+ },
22
+ "homepage": "https://github.com/igxjs/node-components#readme",
23
+ "dependencies": {
24
+ "@redis/client": "^5.11.0",
25
+ "@types/express": "^5.0.6",
26
+ "axios": "^1.13.6",
27
+ "connect-redis": "^9.0.0",
28
+ "express-session": "^1.19.0",
29
+ "jose": "^6.1.3",
30
+ "memorystore": "^1.6.7"
31
+ },
32
+ "devDependencies": {
33
+ "chai": "^6.2.2",
34
+ "express": "^5.2.1",
35
+ "mocha": "^11.0.1",
36
+ "sinon": "^21.0.2",
37
+ "supertest": "^7.0.0"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "types": "./index.d.ts"
43
+ }
@@ -0,0 +1,21 @@
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
+ });
@@ -0,0 +1,50 @@
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
+ });
@@ -0,0 +1,50 @@
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
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, beforeEach, afterEach } from 'mocha';
2
+ import { expect } from 'chai';
3
+ import sinon from 'sinon';
4
+ import { SessionManager, SessionConfig } from '../components/session.js';
5
+ import { CustomError, httpCodes } from '../components/http-handlers.js';
6
+
7
+ describe('SessionManager', () => {
8
+ let sessionManager;
9
+ let clock;
10
+
11
+ beforeEach(() => {
12
+ sessionManager = new SessionManager();
13
+ clock = sinon.useFakeTimers();
14
+ });
15
+
16
+ afterEach(() => {
17
+ clock.restore();
18
+ });
19
+
20
+ describe('SessionConfig', () => {
21
+ it('should create SessionConfig instance with properties', () => {
22
+ const config = new SessionConfig();
23
+ expect(config).to.be.instanceOf(SessionConfig);
24
+ expect(config).to.have.property('SSO_ENDPOINT_URL');
25
+ expect(config).to.have.property('SESSION_SECRET');
26
+ expect(config).to.have.property('REDIS_URL');
27
+ });
28
+ });
29
+
30
+ describe('Lock Management', () => {
31
+ describe('hasLock', () => {
32
+ it('should return false for email without lock', () => {
33
+ const result = sessionManager.hasLock('test@example.com');
34
+ expect(result).to.be.false;
35
+ });
36
+
37
+ it('should return true for email with active lock', () => {
38
+ sessionManager.lock('test@example.com');
39
+ const result = sessionManager.hasLock('test@example.com');
40
+ expect(result).to.be.true;
41
+ });
42
+
43
+ it('should return false for expired lock', () => {
44
+ sessionManager.lock('test@example.com');
45
+ clock.tick(61000);
46
+ const result = sessionManager.hasLock('test@example.com');
47
+ expect(result).to.be.false;
48
+ });
49
+ });
50
+
51
+ describe('lock', () => {
52
+ it('should create a lock for given email', () => {
53
+ sessionManager.lock('test@example.com');
54
+ expect(sessionManager.hasLock('test@example.com')).to.be.true;
55
+ });
56
+
57
+ it('should not create lock for empty email', () => {
58
+ sessionManager.lock('');
59
+ sessionManager.lock(null);
60
+ expect(sessionManager.hasLock('')).to.be.false;
61
+ expect(sessionManager.hasLock(null)).to.be.false;
62
+ });
63
+ });
64
+ });
65
+
66
+ describe('authenticate', () => {
67
+ let req, res, next;
68
+
69
+ beforeEach(() => {
70
+ req = { user: null };
71
+ res = { redirect: sinon.stub() };
72
+ next = sinon.stub();
73
+ });
74
+
75
+ it('should call next() if user is authorized', () => {
76
+ req.user = { authorized: true };
77
+ const middleware = sessionManager.authenticate();
78
+ middleware(req, res, next);
79
+ expect(next.calledOnce).to.be.true;
80
+ expect(next.firstCall.args).to.be.empty;
81
+ });
82
+
83
+ it('should call next with error if user is not authorized', () => {
84
+ req.user = { authorized: false };
85
+ const middleware = sessionManager.authenticate();
86
+ middleware(req, res, next);
87
+ expect(next.calledOnce).to.be.true;
88
+ const error = next.firstCall.args[0];
89
+ expect(error).to.be.instanceOf(CustomError);
90
+ expect(error.code).to.equal(httpCodes.UNAUTHORIZED);
91
+ });
92
+
93
+ it('should redirect if redirectUrl is provided', () => {
94
+ req.user = { authorized: false };
95
+ const middleware = sessionManager.authenticate(false, '/login');
96
+ middleware(req, res, next);
97
+ expect(res.redirect.calledWith('/login')).to.be.true;
98
+ expect(next.called).to.be.false;
99
+ });
100
+
101
+ it('should allow access in debug mode', () => {
102
+ req.user = null;
103
+ const middleware = sessionManager.authenticate(true);
104
+ middleware(req, res, next);
105
+ expect(next.calledOnce).to.be.true;
106
+ expect(next.firstCall.args).to.be.empty;
107
+ });
108
+ });
109
+
110
+ describe('redisManager', () => {
111
+ it('should return null before initialization', () => {
112
+ const manager = sessionManager.redisManager();
113
+ expect(manager).to.be.null;
114
+ });
115
+ });
116
+ });