@codisolutions23/node-utils 3.0.1 → 3.1.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.
@@ -0,0 +1,11 @@
1
+ ### Minor Changes
2
+
3
+ - **Cache System Improvements**: Enhanced cache utility with clearer function names (`getCache`, `setCache`, `delCache`, `delCacheGroup`) and configurable TTL settings via `CACHE_SHORT_TTL` and `CACHE_LONG_TTL` environment variables
4
+
5
+ - **Enhanced Error Handling**: Replaced generic Error with NotFoundError for missing environment variables, providing better error specificity and handling
6
+
7
+ - **Reduced Auth Logging**: Optimized authentication middleware logging to reduce production noise while maintaining debug capabilities in development
8
+
9
+ ### Patch Changes
10
+
11
+ - **Dependencies**: Updated AWS SDK packages (@aws-sdk, @smithy) to latest versions (3.908.0, 4.x) for improved security and compatibility
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @codisolutions23/node-utils
2
2
 
3
+ ## 3.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - c740901: **Patch Changes and Improvements**
8
+
9
+ - **Dependencies**: Upgraded @aws-sdk and @smithy packages from v3.883.0 to v3.890.0, bringing latest features, bug fixes, and security improvements
10
+ - **Template Resolution**: Enhanced getTemplatePath to check multiple locations (src/public, public, provided directory) with fallback to src/public for improved flexibility
11
+ - **Handlebars Helpers**: Added formatCurrency for Philippine peso formatting and formatDate for flexible date formatting
12
+ - **Authentication Middleware**: Refactored for better modularity, added admin and role-based access control, improved logging and error handling with token revocation checks
13
+
3
14
  ## 3.0.1
4
15
 
5
16
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -6,14 +6,26 @@ import Redis from 'ioredis';
6
6
  import * as winston from 'winston';
7
7
  import { RedisClientType } from 'redis';
8
8
 
9
+ interface DecodedToken extends JwtPayload {
10
+ user?: string;
11
+ id?: string;
12
+ jti?: string;
13
+ role?: string;
14
+ }
9
15
  interface AuthenticatedRequest extends Request {
10
- user?: JwtPayload & {
11
- user?: string;
12
- id?: string;
16
+ user?: DecodedToken & {
17
+ id: string;
13
18
  };
14
19
  token?: string;
15
20
  }
21
+ interface AuthOptions {
22
+ secretKey?: string;
23
+ isTokenRevoked?: (jti: string) => Promise<boolean>;
24
+ requiredRole?: string;
25
+ }
16
26
  declare function authenticate(secretKey?: string, isTokenRevoked?: (jti: string) => Promise<boolean>): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
27
+ declare function requireAdmin(secretKey?: string, isTokenRevoked?: (jti: string) => Promise<boolean>): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
28
+ declare function requireRole(role: string, options?: Omit<AuthOptions, "requiredRole">): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
17
29
 
18
30
  declare class HttpError extends Error {
19
31
  readonly statusCode: number;
@@ -60,10 +72,10 @@ declare class useAtlas {
60
72
  }
61
73
 
62
74
  declare function useCache(): {
63
- get: <T = unknown>(cacheKey: string) => Promise<T | null>;
64
- set: <T = unknown>(cacheKey: string, data: T, ttl?: number, group?: string) => Promise<void>;
65
- remove: (cacheKey: string) => Promise<void>;
66
- clearGroup: (group: string) => Promise<void>;
75
+ getCache: <T = unknown>(cacheKey: string) => Promise<T | null>;
76
+ setCache: <T = unknown>(cacheKey: string, data: T, ttl?: number, group?: string) => Promise<void>;
77
+ delCache: (cacheKey: string) => Promise<void>;
78
+ delCacheGroup: (group: string) => Promise<void>;
67
79
  };
68
80
 
69
81
  declare function buildCacheKey(prefix: string, values: Record<string, any>): string;
@@ -154,4 +166,4 @@ declare class useS3 {
154
166
 
155
167
  declare function hashToken(token: string): string;
156
168
 
157
- export { type AuthenticatedRequest, BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, NotFoundError, UnauthorizedError, UnprocessableEntityError, authenticate, buildCacheKey, comparePasswords, errorHandler, getTemplatePath, hashPassword, hashToken, initRedisClient, logger, paginate, renderHandlebarsTemplate, signJwtToken, toObjectId, useAtlas, useCache, useMailer, useRedis, useRedisClient, useS3 };
169
+ export { type AuthenticatedRequest, BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, NotFoundError, UnauthorizedError, UnprocessableEntityError, authenticate, buildCacheKey, comparePasswords, errorHandler, getTemplatePath, hashPassword, hashToken, initRedisClient, logger, paginate, renderHandlebarsTemplate, requireAdmin, requireRole, signJwtToken, toObjectId, useAtlas, useCache, useMailer, useRedis, useRedisClient, useS3 };
package/dist/index.d.ts CHANGED
@@ -6,14 +6,26 @@ import Redis from 'ioredis';
6
6
  import * as winston from 'winston';
7
7
  import { RedisClientType } from 'redis';
8
8
 
9
+ interface DecodedToken extends JwtPayload {
10
+ user?: string;
11
+ id?: string;
12
+ jti?: string;
13
+ role?: string;
14
+ }
9
15
  interface AuthenticatedRequest extends Request {
10
- user?: JwtPayload & {
11
- user?: string;
12
- id?: string;
16
+ user?: DecodedToken & {
17
+ id: string;
13
18
  };
14
19
  token?: string;
15
20
  }
21
+ interface AuthOptions {
22
+ secretKey?: string;
23
+ isTokenRevoked?: (jti: string) => Promise<boolean>;
24
+ requiredRole?: string;
25
+ }
16
26
  declare function authenticate(secretKey?: string, isTokenRevoked?: (jti: string) => Promise<boolean>): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
27
+ declare function requireAdmin(secretKey?: string, isTokenRevoked?: (jti: string) => Promise<boolean>): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
28
+ declare function requireRole(role: string, options?: Omit<AuthOptions, "requiredRole">): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
17
29
 
18
30
  declare class HttpError extends Error {
19
31
  readonly statusCode: number;
@@ -60,10 +72,10 @@ declare class useAtlas {
60
72
  }
61
73
 
62
74
  declare function useCache(): {
63
- get: <T = unknown>(cacheKey: string) => Promise<T | null>;
64
- set: <T = unknown>(cacheKey: string, data: T, ttl?: number, group?: string) => Promise<void>;
65
- remove: (cacheKey: string) => Promise<void>;
66
- clearGroup: (group: string) => Promise<void>;
75
+ getCache: <T = unknown>(cacheKey: string) => Promise<T | null>;
76
+ setCache: <T = unknown>(cacheKey: string, data: T, ttl?: number, group?: string) => Promise<void>;
77
+ delCache: (cacheKey: string) => Promise<void>;
78
+ delCacheGroup: (group: string) => Promise<void>;
67
79
  };
68
80
 
69
81
  declare function buildCacheKey(prefix: string, values: Record<string, any>): string;
@@ -154,4 +166,4 @@ declare class useS3 {
154
166
 
155
167
  declare function hashToken(token: string): string;
156
168
 
157
- export { type AuthenticatedRequest, BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, NotFoundError, UnauthorizedError, UnprocessableEntityError, authenticate, buildCacheKey, comparePasswords, errorHandler, getTemplatePath, hashPassword, hashToken, initRedisClient, logger, paginate, renderHandlebarsTemplate, signJwtToken, toObjectId, useAtlas, useCache, useMailer, useRedis, useRedisClient, useS3 };
169
+ export { type AuthenticatedRequest, BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, NotFoundError, UnauthorizedError, UnprocessableEntityError, authenticate, buildCacheKey, comparePasswords, errorHandler, getTemplatePath, hashPassword, hashToken, initRedisClient, logger, paginate, renderHandlebarsTemplate, requireAdmin, requireRole, signJwtToken, toObjectId, useAtlas, useCache, useMailer, useRedis, useRedisClient, useS3 };
package/dist/index.js CHANGED
@@ -86,6 +86,8 @@ __export(index_exports, {
86
86
  logger: () => logger,
87
87
  paginate: () => paginate,
88
88
  renderHandlebarsTemplate: () => renderHandlebarsTemplate,
89
+ requireAdmin: () => requireAdmin,
90
+ requireRole: () => requireRole,
89
91
  signJwtToken: () => signJwtToken,
90
92
  toObjectId: () => toObjectId,
91
93
  useAtlas: () => useAtlas,
@@ -164,40 +166,143 @@ var InternalServerError = class extends HttpError {
164
166
  };
165
167
 
166
168
  // src/middleware/auth.middleware.ts
167
- function authenticate(secretKey = process.env.ACCESS_TOKEN_SECRET || "", isTokenRevoked) {
168
- return (req, res, next) => __async(null, null, function* () {
169
- const authHeader = req.headers.authorization;
170
- const token = authHeader == null ? void 0 : authHeader.split(" ")[1];
169
+ function extractToken(authHeader) {
170
+ if (!authHeader) {
171
+ return null;
172
+ }
173
+ if (!authHeader.startsWith("Bearer ")) {
174
+ logger.warn("Invalid Authorization header format", {
175
+ format: "Expected 'Bearer <token>'"
176
+ });
177
+ return null;
178
+ }
179
+ return authHeader.split(" ")[1] || null;
180
+ }
181
+ function authenticateToken(req, options) {
182
+ return __async(this, null, function* () {
183
+ const {
184
+ secretKey = process.env.ACCESS_TOKEN_SECRET || "",
185
+ isTokenRevoked,
186
+ requiredRole
187
+ } = options;
188
+ const requestId = req.headers["x-request-id"] || "unknown";
189
+ const isDev = process.env.NODE_ENV === "development";
190
+ if (isDev) {
191
+ logger.debug("Starting authentication process", {
192
+ requestId,
193
+ path: req.path,
194
+ requiredRole
195
+ });
196
+ }
197
+ const token = extractToken(req.headers.authorization);
171
198
  if (!token) {
172
- logger.error("No access token provided in the request.");
173
- return next(
174
- new UnauthorizedError("Access token is required to proceed.")
175
- );
199
+ logger.error("Authentication failed: No access token provided", {
200
+ requestId,
201
+ path: req.path
202
+ });
203
+ throw new UnauthorizedError("Access token is required to proceed.");
176
204
  }
205
+ let decoded;
177
206
  try {
178
- const decoded = import_jsonwebtoken.default.verify(token, secretKey);
179
- if (isTokenRevoked && decoded.jti && (yield isTokenRevoked(decoded.jti))) {
180
- logger.warn("Access token is revoked (blacklisted).", {
181
- jti: decoded.jti
207
+ decoded = import_jsonwebtoken.default.verify(token, secretKey);
208
+ if (isDev) {
209
+ logger.debug("JWT token verified", {
210
+ requestId,
211
+ userId: decoded.user || decoded.id,
212
+ role: decoded.role
213
+ });
214
+ }
215
+ } catch (error) {
216
+ logger.error("JWT verification failed", {
217
+ requestId,
218
+ error: error instanceof Error ? error.message : "Invalid token",
219
+ path: req.path
220
+ });
221
+ throw new UnauthorizedError(
222
+ "Your session has expired or the token is invalid. Please log in again."
223
+ );
224
+ }
225
+ if (isTokenRevoked && decoded.jti) {
226
+ const isRevoked = yield isTokenRevoked(decoded.jti);
227
+ if (isRevoked) {
228
+ logger.warn("Authentication failed: Token is revoked", {
229
+ requestId,
230
+ jti: decoded.jti,
231
+ userId: decoded.user || decoded.id
182
232
  });
183
- return next(
184
- new UnauthorizedError(
185
- "Your session has expired or the token is invalid. Please log in again."
186
- )
233
+ throw new UnauthorizedError(
234
+ "Your session has expired or the token is invalid. Please log in again."
187
235
  );
188
236
  }
189
- req.user = __spreadProps(__spreadValues({}, decoded), {
190
- id: decoded.user || decoded.id
237
+ }
238
+ if (requiredRole && decoded.role !== requiredRole) {
239
+ logger.warn("Authorization failed: Insufficient permissions", {
240
+ requestId,
241
+ userId: decoded.user || decoded.id,
242
+ userRole: decoded.role,
243
+ requiredRole,
244
+ path: req.path
245
+ });
246
+ throw new ForbiddenError(
247
+ "Insufficient permissions to access this resource."
248
+ );
249
+ }
250
+ req.user = __spreadProps(__spreadValues({}, decoded), {
251
+ id: decoded.user || decoded.id || ""
252
+ });
253
+ req.token = token;
254
+ if (isDev) {
255
+ logger.debug("Authentication completed successfully", {
256
+ requestId,
257
+ userId: req.user.id,
258
+ role: req.user.role
259
+ });
260
+ }
261
+ });
262
+ }
263
+ function authenticate(secretKey = process.env.ACCESS_TOKEN_SECRET || "", isTokenRevoked) {
264
+ return (req, res, next) => __async(null, null, function* () {
265
+ try {
266
+ yield authenticateToken(req, { secretKey, isTokenRevoked });
267
+ next();
268
+ } catch (error) {
269
+ logger.error("Authenticate middleware failed", {
270
+ error: error instanceof Error ? error.message : "Unknown error"
271
+ });
272
+ next(error);
273
+ }
274
+ });
275
+ }
276
+ function requireAdmin(secretKey = process.env.ACCESS_TOKEN_SECRET || "", isTokenRevoked) {
277
+ return (req, res, next) => __async(null, null, function* () {
278
+ try {
279
+ yield authenticateToken(req, {
280
+ secretKey,
281
+ isTokenRevoked,
282
+ requiredRole: "admin"
191
283
  });
192
- req.token = token;
193
284
  next();
194
285
  } catch (error) {
195
- logger.error("Failed to verify access token.", error);
196
- return next(
197
- new UnauthorizedError(
198
- "Your session has expired or the token is invalid. Please log in again."
199
- )
200
- );
286
+ logger.error("RequireAdmin middleware failed", {
287
+ error: error instanceof Error ? error.message : "Unknown error"
288
+ });
289
+ next(error);
290
+ }
291
+ });
292
+ }
293
+ function requireRole(role, options) {
294
+ return (req, res, next) => __async(null, null, function* () {
295
+ try {
296
+ yield authenticateToken(req, __spreadProps(__spreadValues({}, options), {
297
+ requiredRole: role
298
+ }));
299
+ next();
300
+ } catch (error) {
301
+ logger.error("RequireRole middleware failed", {
302
+ requiredRole: role,
303
+ error: error instanceof Error ? error.message : "Unknown error"
304
+ });
305
+ next(error);
201
306
  }
202
307
  });
203
308
  }
@@ -294,6 +399,34 @@ var useAtlas = class {
294
399
  useAtlas.mongoClient = null;
295
400
  useAtlas.mongoDb = null;
296
401
 
402
+ // src/config.ts
403
+ var dotenv = __toESM(require("dotenv"));
404
+ dotenv.config();
405
+ function getEnv(key, fallback) {
406
+ const value = process.env[key];
407
+ if (value !== void 0) return value;
408
+ if (fallback !== void 0) return fallback;
409
+ throw new NotFoundError(`Missing required environment variable: ${key}`);
410
+ }
411
+ function getEnvNumber(key, fallback) {
412
+ const value = process.env[key];
413
+ if (value !== void 0) return Number(value);
414
+ if (fallback !== void 0) return fallback;
415
+ throw new NotFoundError(`Missing required environment variable: ${key}`);
416
+ }
417
+ function getEnvBoolean(key, fallback) {
418
+ const value = process.env[key];
419
+ if (value !== void 0) return value === "true";
420
+ if (fallback !== void 0) return fallback;
421
+ throw new NotFoundError(`Missing required environment variable: ${key}`);
422
+ }
423
+ var REDIS_HOST = getEnv("REDIS_HOST");
424
+ var REDIS_PORT = getEnvNumber("REDIS_PORT", 6379);
425
+ var REDIS_PASSWORD = getEnv("REDIS_PASSWORD");
426
+ var REDIS_TLS = getEnvBoolean("REDIS_TLS", true);
427
+ var CACHE_SHORT_TTL = getEnvNumber("CACHE_SHORT_TTL", 300);
428
+ var CACHE_LONG_TTL = getEnvNumber("CACHE_LONG_TTL", 3600);
429
+
297
430
  // src/utils/ioredis.ts
298
431
  var import_ioredis = __toESM(require("ioredis"));
299
432
  var redisClient = null;
@@ -352,12 +485,11 @@ function useRedis() {
352
485
  }
353
486
 
354
487
  // src/utils/cache.ts
355
- var DEFAULT_TTL = 300;
356
488
  function useCache() {
357
489
  function getRedisClient() {
358
490
  return useRedis().getClient();
359
491
  }
360
- function get(cacheKey) {
492
+ function getCache(cacheKey) {
361
493
  return __async(this, null, function* () {
362
494
  try {
363
495
  const redisClient3 = getRedisClient();
@@ -371,13 +503,16 @@ function useCache() {
371
503
  }
372
504
  });
373
505
  }
374
- function set(_0, _1) {
375
- return __async(this, arguments, function* (cacheKey, data, ttl = DEFAULT_TTL, group) {
506
+ function setCache(_0, _1) {
507
+ return __async(this, arguments, function* (cacheKey, data, ttl = CACHE_SHORT_TTL, group) {
376
508
  try {
377
509
  const redisClient3 = getRedisClient();
378
510
  yield redisClient3.set(cacheKey, JSON.stringify(data), "EX", ttl);
379
511
  logger.info(`[Cache][Set] Stored key "${cacheKey}" with TTL ${ttl}s.`);
380
- if (group) yield redisClient3.sadd(`cache:group:${group}`, cacheKey);
512
+ if (group) {
513
+ yield redisClient3.sadd(`cache:group:${group}`, cacheKey);
514
+ yield redisClient3.expire(`cache:group:${group}`, CACHE_LONG_TTL);
515
+ }
381
516
  } catch (error) {
382
517
  logger.error(
383
518
  `[Cache][Set] Failed to store key "${cacheKey}": ${error instanceof Error ? error.message : error}`
@@ -385,7 +520,7 @@ function useCache() {
385
520
  }
386
521
  });
387
522
  }
388
- function remove(cacheKey) {
523
+ function delCache(cacheKey) {
389
524
  return __async(this, null, function* () {
390
525
  try {
391
526
  const redisClient3 = getRedisClient();
@@ -398,7 +533,7 @@ function useCache() {
398
533
  }
399
534
  });
400
535
  }
401
- function clearGroup(group) {
536
+ function delCacheGroup(group) {
402
537
  return __async(this, null, function* () {
403
538
  try {
404
539
  const redisClient3 = getRedisClient();
@@ -414,10 +549,10 @@ function useCache() {
414
549
  });
415
550
  }
416
551
  return {
417
- get,
418
- set,
419
- remove,
420
- clearGroup
552
+ getCache,
553
+ setCache,
554
+ delCache,
555
+ delCacheGroup
421
556
  };
422
557
  }
423
558
 
@@ -434,13 +569,58 @@ var import_path = __toESM(require("path"));
434
569
  function getTemplatePath(directory, filePath) {
435
570
  const ext = ".hbs";
436
571
  const file = filePath.endsWith(ext) ? filePath : `${filePath}${ext}`;
437
- return import_path.default.resolve(directory, file);
572
+ const possiblePaths = [
573
+ import_path.default.join(process.cwd(), "src", "public", directory, file),
574
+ import_path.default.join(process.cwd(), "public", directory, file),
575
+ import_path.default.join(process.cwd(), directory, file)
576
+ ];
577
+ const fs2 = require("fs");
578
+ for (const templatePath of possiblePaths) {
579
+ if (fs2.existsSync(templatePath)) {
580
+ return templatePath;
581
+ }
582
+ }
583
+ return possiblePaths[0];
438
584
  }
439
585
 
440
586
  // src/utils/handlebars-compiler.ts
441
587
  var import_handlebars = __toESM(require("handlebars"));
442
588
  var import_fs = require("fs");
443
589
  var handlebarsTemplateCache = /* @__PURE__ */ new Map();
590
+ import_handlebars.default.registerHelper("formatCurrency", function(amount) {
591
+ if (typeof amount !== "number") return "0.00";
592
+ return new Intl.NumberFormat("en-PH", {
593
+ style: "decimal",
594
+ minimumFractionDigits: 2,
595
+ maximumFractionDigits: 2
596
+ }).format(amount);
597
+ });
598
+ import_handlebars.default.registerHelper(
599
+ "formatDate",
600
+ function(date, format2) {
601
+ if (!date) return "N/A";
602
+ const dateObj = typeof date === "string" ? new Date(date) : date;
603
+ if (isNaN(dateObj.getTime())) return "N/A";
604
+ const options = {};
605
+ if (format2.includes("MMMM")) options.month = "long";
606
+ else if (format2.includes("MMM")) options.month = "short";
607
+ else if (format2.includes("MM")) options.month = "2-digit";
608
+ if (format2.includes("D,")) options.day = "numeric";
609
+ else if (format2.includes("DD")) options.day = "2-digit";
610
+ if (format2.includes("YYYY")) options.year = "numeric";
611
+ else if (format2.includes("YY")) options.year = "2-digit";
612
+ if (format2.includes("h:mm A")) {
613
+ options.hour = "numeric";
614
+ options.minute = "2-digit";
615
+ options.hour12 = true;
616
+ } else if (format2.includes("HH:mm")) {
617
+ options.hour = "2-digit";
618
+ options.minute = "2-digit";
619
+ options.hour12 = false;
620
+ }
621
+ return new Intl.DateTimeFormat("en-US", options).format(dateObj);
622
+ }
623
+ );
444
624
  function renderHandlebarsTemplate(_0) {
445
625
  return __async(this, arguments, function* ({
446
626
  context = {},
@@ -617,34 +797,6 @@ function hashPassword(_0) {
617
797
 
618
798
  // src/utils/redis.ts
619
799
  var import_redis = require("redis");
620
-
621
- // src/config.ts
622
- var dotenv = __toESM(require("dotenv"));
623
- dotenv.config();
624
- function getEnv(key, fallback) {
625
- const value = process.env[key];
626
- if (value !== void 0) return value;
627
- if (fallback !== void 0) return fallback;
628
- throw new Error(`Missing required environment variable: ${key}`);
629
- }
630
- function getEnvNumber(key, fallback) {
631
- const value = process.env[key];
632
- if (value !== void 0) return Number(value);
633
- if (fallback !== void 0) return fallback;
634
- throw new Error(`Missing required environment variable: ${key}`);
635
- }
636
- function getEnvBoolean(key, fallback) {
637
- const value = process.env[key];
638
- if (value !== void 0) return value === "true";
639
- if (fallback !== void 0) return fallback;
640
- throw new Error(`Missing required environment variable: ${key}`);
641
- }
642
- var REDIS_HOST = getEnv("REDIS_HOST");
643
- var REDIS_PORT = getEnvNumber("REDIS_PORT", 6379);
644
- var REDIS_PASSWORD = getEnv("REDIS_PASSWORD");
645
- var REDIS_TLS = getEnvBoolean("REDIS_TLS", true);
646
-
647
- // src/utils/redis.ts
648
800
  var redisClient2 = null;
649
801
  function initRedisClient() {
650
802
  return __async(this, null, function* () {
@@ -767,6 +919,8 @@ function hashToken(token) {
767
919
  logger,
768
920
  paginate,
769
921
  renderHandlebarsTemplate,
922
+ requireAdmin,
923
+ requireRole,
770
924
  signJwtToken,
771
925
  toObjectId,
772
926
  useAtlas,