@heavybit/pendoadmin-shared-lib 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.
Files changed (132) hide show
  1. package/dist/common/index.d.ts +6 -0
  2. package/dist/common/index.d.ts.map +1 -0
  3. package/dist/common/index.js +6 -0
  4. package/dist/common/index.js.map +1 -0
  5. package/dist/common/jwt.utils.d.ts +7 -0
  6. package/dist/common/jwt.utils.d.ts.map +1 -0
  7. package/dist/common/jwt.utils.js +31 -0
  8. package/dist/common/jwt.utils.js.map +1 -0
  9. package/dist/common/password.utils.d.ts +4 -0
  10. package/dist/common/password.utils.d.ts.map +1 -0
  11. package/dist/common/password.utils.js +17 -0
  12. package/dist/common/password.utils.js.map +1 -0
  13. package/dist/common/phone.utils.d.ts +4 -0
  14. package/dist/common/phone.utils.d.ts.map +1 -0
  15. package/dist/common/phone.utils.js +36 -0
  16. package/dist/common/phone.utils.js.map +1 -0
  17. package/dist/common/response.utils.d.ts +6 -0
  18. package/dist/common/response.utils.d.ts.map +1 -0
  19. package/dist/common/response.utils.js +33 -0
  20. package/dist/common/response.utils.js.map +1 -0
  21. package/dist/common/uuid.utils.d.ts +5 -0
  22. package/dist/common/uuid.utils.d.ts.map +1 -0
  23. package/dist/common/uuid.utils.js +10 -0
  24. package/dist/common/uuid.utils.js.map +1 -0
  25. package/dist/config/index.d.ts +95 -0
  26. package/dist/config/index.d.ts.map +1 -0
  27. package/dist/config/index.js +71 -0
  28. package/dist/config/index.js.map +1 -0
  29. package/dist/database/index.d.ts +25 -0
  30. package/dist/database/index.d.ts.map +1 -0
  31. package/dist/database/index.js +62 -0
  32. package/dist/database/index.js.map +1 -0
  33. package/dist/express/auth.middleware.d.ts +20 -0
  34. package/dist/express/auth.middleware.d.ts.map +1 -0
  35. package/dist/express/auth.middleware.js +83 -0
  36. package/dist/express/auth.middleware.js.map +1 -0
  37. package/dist/express/correlation.middleware.d.ts +6 -0
  38. package/dist/express/correlation.middleware.d.ts.map +1 -0
  39. package/dist/express/correlation.middleware.js +15 -0
  40. package/dist/express/correlation.middleware.js.map +1 -0
  41. package/dist/express/error-handler.d.ts +8 -0
  42. package/dist/express/error-handler.d.ts.map +1 -0
  43. package/dist/express/error-handler.js +18 -0
  44. package/dist/express/error-handler.js.map +1 -0
  45. package/dist/express/index.d.ts +6 -0
  46. package/dist/express/index.d.ts.map +1 -0
  47. package/dist/express/index.js +6 -0
  48. package/dist/express/index.js.map +1 -0
  49. package/dist/express/permission.guard.d.ts +14 -0
  50. package/dist/express/permission.guard.d.ts.map +1 -0
  51. package/dist/express/permission.guard.js +66 -0
  52. package/dist/express/permission.guard.js.map +1 -0
  53. package/dist/express/validation.middleware.d.ts +14 -0
  54. package/dist/express/validation.middleware.d.ts.map +1 -0
  55. package/dist/express/validation.middleware.js +80 -0
  56. package/dist/express/validation.middleware.js.map +1 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +17 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/logging/index.d.ts +4 -0
  62. package/dist/logging/index.d.ts.map +1 -0
  63. package/dist/logging/index.js +24 -0
  64. package/dist/logging/index.js.map +1 -0
  65. package/dist/messaging/index.d.ts +58 -0
  66. package/dist/messaging/index.d.ts.map +1 -0
  67. package/dist/messaging/index.js +132 -0
  68. package/dist/messaging/index.js.map +1 -0
  69. package/dist/nestjs/auth.guard.d.ts +12 -0
  70. package/dist/nestjs/auth.guard.d.ts.map +1 -0
  71. package/dist/nestjs/auth.guard.js +75 -0
  72. package/dist/nestjs/auth.guard.js.map +1 -0
  73. package/dist/nestjs/correlation.interceptor.d.ts +9 -0
  74. package/dist/nestjs/correlation.interceptor.d.ts.map +1 -0
  75. package/dist/nestjs/correlation.interceptor.js +29 -0
  76. package/dist/nestjs/correlation.interceptor.js.map +1 -0
  77. package/dist/nestjs/exception.filter.d.ts +8 -0
  78. package/dist/nestjs/exception.filter.d.ts.map +1 -0
  79. package/dist/nestjs/exception.filter.js +54 -0
  80. package/dist/nestjs/exception.filter.js.map +1 -0
  81. package/dist/nestjs/index.d.ts +7 -0
  82. package/dist/nestjs/index.d.ts.map +1 -0
  83. package/dist/nestjs/index.js +7 -0
  84. package/dist/nestjs/index.js.map +1 -0
  85. package/dist/nestjs/permission.decorator.d.ts +22 -0
  86. package/dist/nestjs/permission.decorator.d.ts.map +1 -0
  87. package/dist/nestjs/permission.decorator.js +30 -0
  88. package/dist/nestjs/permission.decorator.js.map +1 -0
  89. package/dist/nestjs/permission.guard.d.ts +12 -0
  90. package/dist/nestjs/permission.guard.d.ts.map +1 -0
  91. package/dist/nestjs/permission.guard.js +66 -0
  92. package/dist/nestjs/permission.guard.js.map +1 -0
  93. package/dist/nestjs/user.decorator.d.ts +12 -0
  94. package/dist/nestjs/user.decorator.d.ts.map +1 -0
  95. package/dist/nestjs/user.decorator.js +16 -0
  96. package/dist/nestjs/user.decorator.js.map +1 -0
  97. package/dist/types/index.d.ts +193 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +17 -0
  100. package/dist/types/index.js.map +1 -0
  101. package/dist/validation/index.d.ts +32 -0
  102. package/dist/validation/index.d.ts.map +1 -0
  103. package/dist/validation/index.js +21 -0
  104. package/dist/validation/index.js.map +1 -0
  105. package/package.json +100 -0
  106. package/src/common/index.ts +5 -0
  107. package/src/common/jwt.utils.ts +43 -0
  108. package/src/common/password.utils.ts +20 -0
  109. package/src/common/phone.utils.ts +36 -0
  110. package/src/common/response.utils.ts +48 -0
  111. package/src/common/uuid.utils.ts +11 -0
  112. package/src/config/index.ts +73 -0
  113. package/src/database/index.ts +77 -0
  114. package/src/express/auth.middleware.ts +93 -0
  115. package/src/express/correlation.middleware.ts +16 -0
  116. package/src/express/error-handler.ts +28 -0
  117. package/src/express/index.ts +5 -0
  118. package/src/express/permission.guard.ts +72 -0
  119. package/src/express/validation.middleware.ts +76 -0
  120. package/src/index.ts +23 -0
  121. package/src/logging/index.ts +31 -0
  122. package/src/messaging/index.ts +161 -0
  123. package/src/nestjs/auth.guard.ts +69 -0
  124. package/src/nestjs/correlation.interceptor.ts +30 -0
  125. package/src/nestjs/exception.filter.ts +53 -0
  126. package/src/nestjs/index.ts +11 -0
  127. package/src/nestjs/permission.decorator.ts +36 -0
  128. package/src/nestjs/permission.guard.ts +76 -0
  129. package/src/nestjs/user.decorator.ts +19 -0
  130. package/src/types/index.ts +239 -0
  131. package/src/validation/index.ts +26 -0
  132. package/tsconfig.json +22 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,oBAAoB;AACpB,iDAAiD;AA+NjD,iDAAiD;AACjD,uBAAuB;AACvB,iDAAiD;AAEjD,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,OAAO,EAAE,WAAW;IACpB,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,cAAc;IAC1B,gBAAgB,EAAE,oBAAoB;IACtC,aAAa,EAAE,iBAAiB;IAChC,cAAc,EAAE,kBAAkB;IAClC,YAAY,EAAE,gBAAgB;CACtB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ export declare const paginationSchema: z.ZodObject<{
3
+ page: z.ZodDefault<z.ZodNumber>;
4
+ limit: z.ZodDefault<z.ZodNumber>;
5
+ search: z.ZodOptional<z.ZodString>;
6
+ sortBy: z.ZodOptional<z.ZodString>;
7
+ sortOrder: z.ZodDefault<z.ZodEnum<["asc", "desc"]>>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ page: number;
10
+ limit: number;
11
+ sortOrder: "asc" | "desc";
12
+ search?: string | undefined;
13
+ sortBy?: string | undefined;
14
+ }, {
15
+ page?: number | undefined;
16
+ limit?: number | undefined;
17
+ search?: string | undefined;
18
+ sortBy?: string | undefined;
19
+ sortOrder?: "asc" | "desc" | undefined;
20
+ }>;
21
+ export declare const emailSchema: z.ZodString;
22
+ export declare const phoneSchema: z.ZodString;
23
+ export declare const uuidSchema: z.ZodString;
24
+ export declare const passwordSchema: z.ZodString;
25
+ export declare const idParamSchema: z.ZodObject<{
26
+ id: z.ZodString;
27
+ }, "strip", z.ZodTypeAny, {
28
+ id: string;
29
+ }, {
30
+ id: string;
31
+ }>;
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;EAM3B,CAAC;AAEH,eAAO,MAAM,WAAW,aAAiE,CAAC;AAE1F,eAAO,MAAM,WAAW,aAAyE,CAAC;AAElG,eAAO,MAAM,UAAU,aAAkC,CAAC;AAE1D,eAAO,MAAM,cAAc,aAK8E,CAAC;AAE1G,eAAO,MAAM,aAAa;;;;;;EAExB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export const paginationSchema = z.object({
3
+ page: z.coerce.number().int().min(1).default(1),
4
+ limit: z.coerce.number().int().min(1).max(100).default(20),
5
+ search: z.string().optional(),
6
+ sortBy: z.string().optional(),
7
+ sortOrder: z.enum(['asc', 'desc']).default('desc'),
8
+ });
9
+ export const emailSchema = z.string().email('Invalid email address').toLowerCase().trim();
10
+ export const phoneSchema = z.string().min(9).max(15).regex(/^\+?[0-9]+$/, 'Invalid phone number');
11
+ export const uuidSchema = z.string().uuid('Invalid UUID');
12
+ export const passwordSchema = z.string()
13
+ .min(8, 'Password must be at least 8 characters')
14
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
15
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
16
+ .regex(/[0-9]/, 'Password must contain at least one digit')
17
+ .regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, 'Password must contain at least one special character');
18
+ export const idParamSchema = z.object({
19
+ id: z.string().min(1),
20
+ });
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/validation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1D,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;CACnD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;AAE1F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,EAAE,sBAAsB,CAAC,CAAC;AAElG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE;KACrC,GAAG,CAAC,CAAC,EAAE,wCAAwC,CAAC;KAChD,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;KACrE,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;KACrE,KAAK,CAAC,OAAO,EAAE,0CAA0C,CAAC;KAC1D,KAAK,CAAC,uCAAuC,EAAE,sDAAsD,CAAC,CAAC;AAE1G,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACtB,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@heavybit/pendoadmin-shared-lib",
3
+ "version": "1.0.0",
4
+ "description": "Shared library for PendoAdmin microservices",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "api",
11
+ "response",
12
+ "error-handling",
13
+ "validation",
14
+ "utilities",
15
+ "typescript",
16
+ "express",
17
+ "middleware",
18
+ "nestjs",
19
+ "logging",
20
+ "configuration",
21
+ "database",
22
+ "messaging",
23
+ "common"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "dev": "tsc --watch",
31
+ "clean": "rm -rf dist"
32
+ },
33
+ "exports": {
34
+ ".": "./dist/index.js",
35
+ "./express": "./dist/express/index.js",
36
+ "./nestjs": "./dist/nestjs/index.js",
37
+ "./common": "./dist/common/index.js",
38
+ "./messaging": "./dist/messaging/index.js",
39
+ "./database": "./dist/database/index.js",
40
+ "./validation": "./dist/validation/index.js",
41
+ "./logging": "./dist/logging/index.js",
42
+ "./config": "./dist/config/index.js",
43
+ "./types": "./dist/types/index.js"
44
+ },
45
+ "typesVersions": {
46
+ "*": {
47
+ "nestjs": ["dist/nestjs/index.d.ts"],
48
+ "express": ["dist/express/index.d.ts"],
49
+ "common": ["dist/common/index.d.ts"],
50
+ "messaging": ["dist/messaging/index.d.ts"],
51
+ "database": ["dist/database/index.d.ts"],
52
+ "validation": ["dist/validation/index.d.ts"],
53
+ "logging": ["dist/logging/index.d.ts"],
54
+ "config": ["dist/config/index.d.ts"],
55
+ "types": ["dist/types/index.d.ts"]
56
+ }
57
+ },
58
+ "dependencies": {
59
+ "amqplib": "^0.10.4",
60
+ "bcrypt": "^5.1.1",
61
+ "google-libphonenumber": "^3.2.38",
62
+ "jsonwebtoken": "^9.0.2",
63
+ "knex": "^3.1.0",
64
+ "mysql2": "^3.11.0",
65
+ "uuid": "^10.0.0",
66
+ "winston": "^3.14.0",
67
+ "zod": "^3.23.0"
68
+ },
69
+ "devDependencies": {
70
+ "@nestjs/common": "^11.1.14",
71
+ "@nestjs/core": "^11.1.14",
72
+ "@types/amqplib": "^0.10.5",
73
+ "@types/bcrypt": "^5.0.2",
74
+ "@types/express": "^5.0.6",
75
+ "@types/google-libphonenumber": "^7.4.30",
76
+ "@types/jsonwebtoken": "^9.0.7",
77
+ "@types/node": "^20.14.0",
78
+ "@types/uuid": "^10.0.0",
79
+ "express": "^5.2.1",
80
+ "reflect-metadata": "^0.2.2",
81
+ "rxjs": "^7.8.2",
82
+ "typescript": "^5.5.0"
83
+ },
84
+ "peerDependencies": {
85
+ "@nestjs/common": ">=10.0.0",
86
+ "@nestjs/core": ">=10.0.0",
87
+ "express": ">=4.0.0"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "@nestjs/common": {
91
+ "optional": true
92
+ },
93
+ "@nestjs/core": {
94
+ "optional": true
95
+ },
96
+ "express": {
97
+ "optional": true
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,5 @@
1
+ export * from './jwt.utils.js';
2
+ export * from './phone.utils.js';
3
+ export * from './password.utils.js';
4
+ export * from './uuid.utils.js';
5
+ export * from './response.utils.js';
@@ -0,0 +1,43 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import type { IJwtPayload } from '../types/index.js';
3
+
4
+ const JWT_ISSUER = 'pendoadmin-auth';
5
+
6
+ export function signAccessToken(
7
+ payload: Omit<IJwtPayload, 'iat' | 'exp' | 'iss'>,
8
+ secret: string,
9
+ expiresIn: string = '1h'
10
+ ): string {
11
+ return jwt.sign(payload, secret, {
12
+ expiresIn: expiresIn as unknown as jwt.SignOptions['expiresIn'],
13
+ issuer: JWT_ISSUER,
14
+ });
15
+ }
16
+
17
+ export function signRefreshToken(
18
+ userId: string,
19
+ secret: string,
20
+ expiresIn: string = '7d'
21
+ ): string {
22
+ return jwt.sign({ sub: userId, type: 'refresh' }, secret, {
23
+ expiresIn: expiresIn as unknown as jwt.SignOptions['expiresIn'],
24
+ issuer: JWT_ISSUER,
25
+ });
26
+ }
27
+
28
+ export function verifyToken(token: string, secret: string): IJwtPayload {
29
+ return jwt.verify(token, secret, { issuer: JWT_ISSUER }) as IJwtPayload;
30
+ }
31
+
32
+ export function decodeToken(token: string): IJwtPayload | null {
33
+ try {
34
+ return jwt.decode(token) as IJwtPayload;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export function extractBearerToken(authHeader?: string): string | null {
41
+ if (!authHeader?.startsWith('Bearer ')) return null;
42
+ return authHeader.slice(7);
43
+ }
@@ -0,0 +1,20 @@
1
+ import bcrypt from 'bcrypt';
2
+
3
+ const SALT_ROUNDS = 12;
4
+
5
+ export async function hashPassword(password: string): Promise<string> {
6
+ return bcrypt.hash(password, SALT_ROUNDS);
7
+ }
8
+
9
+ export async function verifyPassword(password: string, hash: string): Promise<boolean> {
10
+ return bcrypt.compare(password, hash);
11
+ }
12
+
13
+ export function generateRandomPassword(length: number = 16): string {
14
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
15
+ let password = '';
16
+ for (let i = 0; i < length; i++) {
17
+ password += chars.charAt(Math.floor(Math.random() * chars.length));
18
+ }
19
+ return password;
20
+ }
@@ -0,0 +1,36 @@
1
+ import pkg from 'google-libphonenumber';
2
+ const { PhoneNumberUtil, PhoneNumberFormat } = pkg;
3
+
4
+ const phoneUtil = PhoneNumberUtil.getInstance();
5
+
6
+ export function normalizePhoneNumber(phone: string, defaultCountry: string = 'KE'): string {
7
+ try {
8
+ const parsed = phoneUtil.parse(phone, defaultCountry);
9
+ return phoneUtil.format(parsed, PhoneNumberFormat.E164);
10
+ } catch {
11
+ return phone;
12
+ }
13
+ }
14
+
15
+ export function isValidPhoneNumber(phone: string, defaultCountry: string = 'KE'): boolean {
16
+ try {
17
+ const parsed = phoneUtil.parse(phone, defaultCountry);
18
+ return phoneUtil.isValidNumber(parsed);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ export function formatPhoneNumber(phone: string, format: 'e164' | 'international' | 'national' = 'e164', defaultCountry: string = 'KE'): string {
25
+ try {
26
+ const parsed = phoneUtil.parse(phone, defaultCountry);
27
+ const formatMap = {
28
+ e164: PhoneNumberFormat.E164,
29
+ international: PhoneNumberFormat.INTERNATIONAL,
30
+ national: PhoneNumberFormat.NATIONAL,
31
+ };
32
+ return phoneUtil.format(parsed, formatMap[format]);
33
+ } catch {
34
+ return phone;
35
+ }
36
+ }
@@ -0,0 +1,48 @@
1
+ import type { IApiResponse, IPaginationMeta, IPaginatedResult } from '../types/index.js';
2
+
3
+ export function successResponse<T>(data: T, message: string = 'Success'): IApiResponse<T> {
4
+ return { success: true, message, data };
5
+ }
6
+
7
+ export function errorResponse(message: string, errors?: Record<string, string[]>): IApiResponse {
8
+ return { success: false, message, errors };
9
+ }
10
+
11
+ export function paginatedResponse<T>(
12
+ data: T[],
13
+ total: number,
14
+ page: number,
15
+ limit: number,
16
+ message: string = 'Success'
17
+ ): IApiResponse<T[]> {
18
+ const totalPages = Math.ceil(total / limit);
19
+ const meta: IPaginationMeta = {
20
+ page,
21
+ limit,
22
+ total,
23
+ totalPages,
24
+ hasNext: page < totalPages,
25
+ hasPrev: page > 1,
26
+ };
27
+ return { success: true, message, data, meta };
28
+ }
29
+
30
+ export function buildPaginatedResult<T>(
31
+ data: T[],
32
+ total: number,
33
+ page: number,
34
+ limit: number
35
+ ): IPaginatedResult<T> {
36
+ const totalPages = Math.ceil(total / limit);
37
+ return {
38
+ data,
39
+ meta: {
40
+ page,
41
+ limit,
42
+ total,
43
+ totalPages,
44
+ hasNext: page < totalPages,
45
+ hasPrev: page > 1,
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,11 @@
1
+ import { v7 as uuidv7, v4 as uuidv4 } from 'uuid';
2
+
3
+ /** Generate a UUID v7 (time-ordered, recommended for primary keys) */
4
+ export function generateId(): string {
5
+ return uuidv7();
6
+ }
7
+
8
+ /** Generate a UUID v4 (random, for tokens/secrets) */
9
+ export function generateRandomId(): string {
10
+ return uuidv4();
11
+ }
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+
5
+ /**
6
+ * Base environment schema shared across all services.
7
+ */
8
+ export const baseEnvSchema = z.object({
9
+ NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
10
+ PORT: z.coerce.number().int().positive().default(3000),
11
+ DB_HOST: z.string().default('localhost'),
12
+ DB_PORT: z.coerce.number().int().positive().default(3306),
13
+ DB_USER: z.string().default('root'),
14
+ DB_PASSWORD: z.string().default(''),
15
+ DB_NAME: z.string(),
16
+ JWT_SECRET: z.string().min(16),
17
+ JWT_ACCESS_EXPIRY: z.string().default('15m'),
18
+ JWT_REFRESH_EXPIRY: z.string().default('7d'),
19
+ RABBITMQ_URL: z.string().url().default('amqp://localhost:5672'),
20
+ REDIS_URL: z.string().default('redis://localhost:6379'),
21
+ LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
22
+ CORS_ORIGINS: z.string().default('*'),
23
+ });
24
+
25
+ export type BaseEnv = z.infer<typeof baseEnvSchema>;
26
+
27
+ /**
28
+ * Load and validate environment variables using a Zod schema.
29
+ * Reads .env file from cwd if present (manual parse, no dotenv dependency needed).
30
+ */
31
+ export function loadEnv<T extends z.ZodObject<any>>(schema: T): z.infer<T> {
32
+ // Parse .env file from cwd if it exists
33
+ try {
34
+ const envPath = resolve(process.cwd(), '.env');
35
+ if (existsSync(envPath)) {
36
+ const content = readFileSync(envPath, 'utf-8');
37
+ for (const line of content.split('\n')) {
38
+ const trimmed = line.trim();
39
+ if (!trimmed || trimmed.startsWith('#')) continue;
40
+ const eqIdx = trimmed.indexOf('=');
41
+ if (eqIdx === -1) continue;
42
+ const key = trimmed.slice(0, eqIdx).trim();
43
+ const val = trimmed.slice(eqIdx + 1).trim();
44
+ if (!process.env[key]) process.env[key] = val;
45
+ }
46
+ }
47
+ } catch { /* ignore */ }
48
+ const result = schema.safeParse(process.env);
49
+ if (!result.success) {
50
+ const formatted = result.error.issues
51
+ .map((i) => ` ${i.path.join('.')}: ${i.message}`)
52
+ .join('\n');
53
+ console.error(`❌ Environment validation failed:\n${formatted}`);
54
+ process.exit(1);
55
+ }
56
+ return result.data;
57
+ }
58
+
59
+ /**
60
+ * Service registry: central list of service names and default ports.
61
+ */
62
+ export const SERVICE_REGISTRY = {
63
+ 'auth-admin': { port: 3000, prefix: '/api/v1/auth' },
64
+ sms: { port: 3001, prefix: '/api/v1/sms' },
65
+ mpesa: { port: 3002, prefix: '/api/v1/mpesa' },
66
+ ussd: { port: 3003, prefix: '/api/v1/ussd' },
67
+ whatsapp: { port: 3004, prefix: '/api/v1/whatsapp' },
68
+ notification: { port: 3005, prefix: '/api/v1/notifications' },
69
+ analytics: { port: 3006, prefix: '/api/v1/analytics' },
70
+ ai: { port: 3007, prefix: '/api/v1/ai' },
71
+ } as const;
72
+
73
+ export type ServiceName = keyof typeof SERVICE_REGISTRY;
@@ -0,0 +1,77 @@
1
+ import knex, { Knex } from 'knex';
2
+
3
+ export interface DatabaseConfig {
4
+ host: string;
5
+ port: number;
6
+ user: string;
7
+ password: string;
8
+ database: string;
9
+ }
10
+
11
+ /**
12
+ * Create a Knex instance for MySQL 8.0.
13
+ * Used for migrations, seeds, and as a fallback query builder.
14
+ */
15
+ export function createKnexInstance(config: DatabaseConfig): Knex {
16
+ return knex({
17
+ client: 'mysql2',
18
+ connection: {
19
+ host: config.host,
20
+ port: config.port,
21
+ user: config.user,
22
+ password: config.password,
23
+ database: config.database,
24
+ charset: 'utf8mb4',
25
+ timezone: '+00:00',
26
+ },
27
+ pool: {
28
+ min: 2,
29
+ max: 10,
30
+ acquireTimeoutMillis: 30000,
31
+ },
32
+ migrations: {
33
+ tableName: 'knex_migrations',
34
+ },
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Standard Knex config for migrations/seeds.
40
+ */
41
+ export function createKnexConfig(config: DatabaseConfig): Knex.Config {
42
+ return {
43
+ client: 'mysql2',
44
+ connection: {
45
+ host: config.host,
46
+ port: config.port,
47
+ user: config.user,
48
+ password: config.password,
49
+ database: config.database,
50
+ charset: 'utf8mb4',
51
+ },
52
+ migrations: {
53
+ directory: './migrations',
54
+ extension: 'cjs',
55
+ },
56
+ seeds: {
57
+ directory: './seeds',
58
+ extension: 'cjs',
59
+ },
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Helper: build paginated query with Knex.
65
+ */
66
+ export async function paginateQuery<T>(
67
+ query: Knex.QueryBuilder,
68
+ countQuery: Knex.QueryBuilder,
69
+ page: number,
70
+ limit: number
71
+ ): Promise<{ data: T[]; total: number }> {
72
+ const offset = (page - 1) * limit;
73
+ const [countResult] = await countQuery.count('* as total');
74
+ const total = (countResult as any).total as number;
75
+ const data = await query.limit(limit).offset(offset) as T[];
76
+ return { data, total };
77
+ }
@@ -0,0 +1,93 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { verifyToken, extractBearerToken } from '../common/jwt.utils.js';
3
+ import { GATEWAY_HEADERS, type IAuthUser } from '../types/index.js';
4
+
5
+ // Extend Express Request
6
+ declare global {
7
+ namespace Express {
8
+ interface Request {
9
+ user?: IAuthUser;
10
+ }
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Auth middleware for downstream services (behind API gateway).
16
+ * Reads user context from gateway-injected headers.
17
+ * Falls back to JWT validation if headers are missing (direct access).
18
+ */
19
+ export function authenticateFromGateway(jwtSecret?: string) {
20
+ return (req: Request, _res: Response, next: NextFunction): void => {
21
+ // Method 1: Read gateway-injected headers
22
+ const userId = req.headers[GATEWAY_HEADERS.USER_ID] as string;
23
+ if (userId) {
24
+ req.user = {
25
+ id: userId,
26
+ email: req.headers[GATEWAY_HEADERS.USER_EMAIL] as string || '',
27
+ companyId: req.headers[GATEWAY_HEADERS.COMPANY_ID] as string || '',
28
+ roles: ((req.headers[GATEWAY_HEADERS.USER_ROLES] as string) || '').split(',').filter(Boolean),
29
+ permissions: ((req.headers[GATEWAY_HEADERS.USER_PERMISSIONS] as string) || '').split(',').filter(Boolean),
30
+ isSuperAdmin: req.headers[GATEWAY_HEADERS.IS_SUPERADMIN] === 'true',
31
+ };
32
+ return next();
33
+ }
34
+
35
+ // Method 2: Direct JWT validation (for development / direct access)
36
+ if (jwtSecret) {
37
+ const token = extractBearerToken(req.headers.authorization);
38
+ if (token) {
39
+ try {
40
+ const payload = verifyToken(token, jwtSecret);
41
+ req.user = {
42
+ id: payload.sub,
43
+ email: payload.email,
44
+ companyId: payload.companyId,
45
+ roles: payload.roles || [],
46
+ permissions: payload.permissions || [],
47
+ isSuperAdmin: payload.isSuperAdmin || false,
48
+ };
49
+ return next();
50
+ } catch {
51
+ // Fall through to 401
52
+ }
53
+ }
54
+ }
55
+
56
+ _res.status(401).json({ success: false, message: 'Authentication required' });
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Optional auth — attaches user if present, continues if not.
62
+ */
63
+ export function optionalAuthFromGateway(jwtSecret?: string) {
64
+ return (req: Request, _res: Response, next: NextFunction): void => {
65
+ const userId = req.headers[GATEWAY_HEADERS.USER_ID] as string;
66
+ if (userId) {
67
+ req.user = {
68
+ id: userId,
69
+ email: req.headers[GATEWAY_HEADERS.USER_EMAIL] as string || '',
70
+ companyId: req.headers[GATEWAY_HEADERS.COMPANY_ID] as string || '',
71
+ roles: ((req.headers[GATEWAY_HEADERS.USER_ROLES] as string) || '').split(',').filter(Boolean),
72
+ permissions: ((req.headers[GATEWAY_HEADERS.USER_PERMISSIONS] as string) || '').split(',').filter(Boolean),
73
+ isSuperAdmin: req.headers[GATEWAY_HEADERS.IS_SUPERADMIN] === 'true',
74
+ };
75
+ } else if (jwtSecret) {
76
+ const token = extractBearerToken(req.headers.authorization);
77
+ if (token) {
78
+ try {
79
+ const payload = verifyToken(token, jwtSecret);
80
+ req.user = {
81
+ id: payload.sub,
82
+ email: payload.email,
83
+ companyId: payload.companyId,
84
+ roles: payload.roles || [],
85
+ permissions: payload.permissions || [],
86
+ isSuperAdmin: payload.isSuperAdmin || false,
87
+ };
88
+ } catch { /* ignore */ }
89
+ }
90
+ }
91
+ next();
92
+ };
93
+ }
@@ -0,0 +1,16 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { GATEWAY_HEADERS } from '../types/index.js';
4
+
5
+ /**
6
+ * Ensures every request has a correlation ID for distributed tracing.
7
+ */
8
+ export function correlationMiddleware(serviceName: string) {
9
+ return (req: Request, res: Response, next: NextFunction): void => {
10
+ const correlationId = (req.headers[GATEWAY_HEADERS.CORRELATION_ID] as string) || uuidv4();
11
+ req.headers[GATEWAY_HEADERS.CORRELATION_ID] = correlationId;
12
+ req.headers[GATEWAY_HEADERS.SERVICE_NAME] = serviceName;
13
+ res.setHeader(GATEWAY_HEADERS.CORRELATION_ID, correlationId);
14
+ next();
15
+ };
16
+ }
@@ -0,0 +1,28 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { createLogger } from '../logging/index.js';
3
+
4
+ const logger = createLogger('error-handler');
5
+
6
+ export interface AppError extends Error {
7
+ statusCode?: number;
8
+ errors?: Record<string, string[]>;
9
+ }
10
+
11
+ export function globalErrorHandler(err: AppError, _req: Request, res: Response, _next: NextFunction): void {
12
+ const statusCode = err.statusCode || 500;
13
+ const message = statusCode === 500 ? 'Internal server error' : err.message;
14
+
15
+ if (statusCode === 500) {
16
+ logger.error('Unhandled error', { error: err.message, stack: err.stack });
17
+ }
18
+
19
+ res.status(statusCode).json({
20
+ success: false,
21
+ message,
22
+ errors: err.errors,
23
+ });
24
+ }
25
+
26
+ export function notFoundHandler(_req: Request, res: Response): void {
27
+ res.status(404).json({ success: false, message: 'Resource not found' });
28
+ }
@@ -0,0 +1,5 @@
1
+ export * from './auth.middleware.js';
2
+ export * from './permission.guard.js';
3
+ export * from './error-handler.js';
4
+ export * from './correlation.middleware.js';
5
+ export * from './validation.middleware.js';