@valentine-efagene/qshelter-common 2.0.96 → 2.0.99

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 @@
1
+ export {};
@@ -60,4 +60,5 @@ export * from './User';
60
60
  export * from './UserRole';
61
61
  export * from './UserSuspension';
62
62
  export * from './Wallet';
63
+ export * from './WorkflowBlocker';
63
64
  export * from './WorkflowEvent';
@@ -60,4 +60,5 @@ export * from './User';
60
60
  export * from './UserRole';
61
61
  export * from './UserSuspension';
62
62
  export * from './Wallet';
63
+ export * from './WorkflowBlocker';
63
64
  export * from './WorkflowEvent';
@@ -61,4 +61,5 @@ export type * from './models/EventHandlerExecution.js';
61
61
  export type * from './models/DomainEvent.js';
62
62
  export type * from './models/PropertyTransferRequest.js';
63
63
  export type * from './models/ApprovalRequest.js';
64
+ export type * from './models/WorkflowBlocker.js';
64
65
  export type * from './commonInputTypes.js';
@@ -59,11 +59,21 @@ export declare class ConfigService {
59
59
  */
60
60
  getParameter(name: string): Promise<string>;
61
61
  /**
62
- * Get JWT secrets from Secrets Manager
62
+ * Get JWT access secret from Secrets Manager
63
+ * Used for signing and verifying access tokens
64
+ */
65
+ getJwtAccessSecret(stage?: string): Promise<JwtSecrets>;
66
+ /**
67
+ * Get JWT refresh token secret from Secrets Manager
68
+ * Used for signing and verifying refresh tokens
69
+ */
70
+ getJwtRefreshSecret(stage?: string): Promise<JwtSecrets>;
71
+ /**
72
+ * @deprecated Use getJwtAccessSecret instead
63
73
  */
64
74
  getJwtSecret(stage?: string): Promise<JwtSecrets>;
65
75
  /**
66
- * Get refresh token secret from Secrets Manager
76
+ * @deprecated Use getJwtRefreshSecret instead
67
77
  */
68
78
  getRefreshTokenSecret(stage?: string): Promise<JwtSecrets>;
69
79
  /**
@@ -90,16 +90,30 @@ export class ConfigService {
90
90
  }
91
91
  }
92
92
  /**
93
- * Get JWT secrets from Secrets Manager
93
+ * Get JWT access secret from Secrets Manager
94
+ * Used for signing and verifying access tokens
95
+ */
96
+ async getJwtAccessSecret(stage = process.env.NODE_ENV || 'dev') {
97
+ return this.getSecret(`qshelter/${stage}/jwt-access-secret`);
98
+ }
99
+ /**
100
+ * Get JWT refresh token secret from Secrets Manager
101
+ * Used for signing and verifying refresh tokens
102
+ */
103
+ async getJwtRefreshSecret(stage = process.env.NODE_ENV || 'dev') {
104
+ return this.getSecret(`qshelter/${stage}/jwt-refresh-secret`);
105
+ }
106
+ /**
107
+ * @deprecated Use getJwtAccessSecret instead
94
108
  */
95
109
  async getJwtSecret(stage = process.env.NODE_ENV || 'dev') {
96
- return this.getSecret(`qshelter/${stage}/jwt-secret`);
110
+ return this.getJwtAccessSecret(stage);
97
111
  }
98
112
  /**
99
- * Get refresh token secret from Secrets Manager
113
+ * @deprecated Use getJwtRefreshSecret instead
100
114
  */
101
115
  async getRefreshTokenSecret(stage = process.env.NODE_ENV || 'dev') {
102
- return this.getSecret(`qshelter/${stage}/refresh-token-secret`);
116
+ return this.getJwtRefreshSecret(stage);
103
117
  }
104
118
  /**
105
119
  * Get database credentials - combines secret and infrastructure config
@@ -1,4 +1,5 @@
1
1
  export * from './types/response';
2
+ export * from './types/action-status';
2
3
  export * from './utils/errors';
3
4
  export * from './config';
4
5
  export { PrismaClient, Prisma } from '../generated/client/client';
package/dist/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './types/response';
2
+ export * from './types/action-status';
2
3
  export * from './utils/errors';
3
4
  export * from './config';
4
5
  export { PrismaClient, Prisma } from '../generated/client/client';
@@ -20,11 +20,15 @@ export interface AuthContext {
20
20
  roles?: string[];
21
21
  }
22
22
  /**
23
- * Extracts auth context from API Gateway authorizer.
23
+ * Extracts auth context from API Gateway authorizer or JWT token.
24
24
  *
25
25
  * Priority:
26
- * 1. Production: requestContext.authorizer (set by API Gateway)
27
- * 2. Test/Dev: x-authorizer-* headers (set by test harness)
26
+ * 1. Production: requestContext.authorizer (set by API Gateway Lambda Authorizer)
27
+ * 2. Fallback: Decode JWT from Authorization header (LocalStack/dev/tests)
28
+ *
29
+ * In production, the Lambda Authorizer validates the JWT and injects context.
30
+ * In LocalStack (no authorizer), we decode the JWT directly since it contains
31
+ * all the same information: sub (userId), tenantId, email, roles.
28
32
  *
29
33
  * @param req Express request object
30
34
  * @returns AuthContext or null if not authenticated
@@ -54,18 +58,71 @@ export declare function requireAuth(req: Request, res: Response, next: NextFunct
54
58
  */
55
59
  export declare function getAuthContext(req: Request): AuthContext;
56
60
  /**
57
- * Test helper to generate authorizer headers.
58
- * Use this in tests to simulate API Gateway authorizer context.
61
+ * Test helper to generate authorization header with JWT.
62
+ *
63
+ * Since auth context is now extracted directly from the JWT,
64
+ * tests only need to pass the Authorization header with a valid token.
59
65
  *
60
66
  * @example
61
67
  * ```typescript
62
68
  * const response = await request(app)
63
69
  * .post('/users')
64
- * .set(authHeaders(userId, tenantId))
70
+ * .set('Authorization', `Bearer ${token}`)
65
71
  * .send({ name: 'John' });
66
72
  * ```
73
+ *
74
+ * @deprecated Use Authorization header directly with JWT token.
75
+ * This helper is kept for backward compatibility.
67
76
  */
68
77
  export declare function authHeaders(userId: string, tenantId: string, extras?: {
69
78
  email?: string;
70
79
  roles?: string[];
80
+ token?: string;
71
81
  }): Record<string, string>;
82
+ /**
83
+ * Standard role names used across the platform.
84
+ */
85
+ export declare const ROLES: {
86
+ readonly SUPER_ADMIN: "SUPER_ADMIN";
87
+ readonly TENANT_ADMIN: "TENANT_ADMIN";
88
+ readonly LOAN_OFFICER: "LOAN_OFFICER";
89
+ readonly CUSTOMER: "CUSTOMER";
90
+ readonly VIEWER: "VIEWER";
91
+ };
92
+ export type RoleName = (typeof ROLES)[keyof typeof ROLES];
93
+ /**
94
+ * Roles that have admin privileges (can manage resources).
95
+ */
96
+ export declare const ADMIN_ROLES: RoleName[];
97
+ /**
98
+ * Check if user has any of the specified roles.
99
+ */
100
+ export declare function hasAnyRole(userRoles: string[] | undefined, requiredRoles: string[]): boolean;
101
+ /**
102
+ * Check if user has admin privileges.
103
+ */
104
+ export declare function isAdmin(userRoles: string[] | undefined): boolean;
105
+ /**
106
+ * Middleware factory that requires user to have specific role(s).
107
+ * Uses roles from API Gateway authorizer context.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * // Require any admin role
112
+ * router.post('/payment-plans', requireRole(ADMIN_ROLES), createPaymentPlan);
113
+ *
114
+ * // Require specific role
115
+ * router.delete('/users/:id', requireRole(['SUPER_ADMIN']), deleteUser);
116
+ * ```
117
+ */
118
+ export declare function requireRole(allowedRoles: string[]): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
119
+ /**
120
+ * Middleware that requires admin privileges.
121
+ * Shorthand for requireRole(ADMIN_ROLES).
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * router.post('/payment-methods', requireAdmin, createPaymentMethod);
126
+ * ```
127
+ */
128
+ export declare function requireAdmin(req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>> | undefined;
@@ -1,9 +1,30 @@
1
1
  /**
2
- * Extracts auth context from API Gateway authorizer.
2
+ * Safely decode JWT payload without verification.
3
+ * Used to extract claims like roles when authorizer context isn't available.
4
+ * Note: This is NOT validation - we trust the token was already validated upstream.
5
+ */
6
+ function decodeJwtPayload(token) {
7
+ try {
8
+ const parts = token.split('.');
9
+ if (parts.length !== 3)
10
+ return null;
11
+ const payload = Buffer.from(parts[1], 'base64').toString('utf-8');
12
+ return JSON.parse(payload);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /**
19
+ * Extracts auth context from API Gateway authorizer or JWT token.
3
20
  *
4
21
  * Priority:
5
- * 1. Production: requestContext.authorizer (set by API Gateway)
6
- * 2. Test/Dev: x-authorizer-* headers (set by test harness)
22
+ * 1. Production: requestContext.authorizer (set by API Gateway Lambda Authorizer)
23
+ * 2. Fallback: Decode JWT from Authorization header (LocalStack/dev/tests)
24
+ *
25
+ * In production, the Lambda Authorizer validates the JWT and injects context.
26
+ * In LocalStack (no authorizer), we decode the JWT directly since it contains
27
+ * all the same information: sub (userId), tenantId, email, roles.
7
28
  *
8
29
  * @param req Express request object
9
30
  * @returns AuthContext or null if not authenticated
@@ -20,15 +41,30 @@ export function extractAuthContext(req) {
20
41
  roles: authorizer.roles ? JSON.parse(authorizer.roles) : [],
21
42
  };
22
43
  }
23
- // Test/Development: Simulated authorizer headers
24
- // These headers should only be set by test harness or local dev proxy
25
- const userId = req.headers['x-authorizer-user-id'];
26
- const tenantId = req.headers['x-authorizer-tenant-id'];
27
- if (userId && tenantId) {
44
+ // Fallback: Decode JWT directly (LocalStack, local dev, tests)
45
+ // The JWT already contains: sub (userId), tenantId, email, roles
46
+ const authHeader = req.headers['authorization'];
47
+ if (authHeader?.startsWith('Bearer ')) {
48
+ const token = authHeader.substring(7);
49
+ const payload = decodeJwtPayload(token);
50
+ if (payload?.sub && payload?.tenantId) {
51
+ return {
52
+ userId: payload.sub,
53
+ tenantId: payload.tenantId,
54
+ email: payload.email,
55
+ roles: Array.isArray(payload.roles) ? payload.roles : [],
56
+ };
57
+ }
58
+ }
59
+ // Legacy fallback: Mock headers for unit tests without real JWTs
60
+ // These should only be used in unit tests, not E2E or production
61
+ const mockUserId = req.headers['x-authorizer-user-id'];
62
+ const mockTenantId = req.headers['x-authorizer-tenant-id'];
63
+ if (mockUserId && mockTenantId) {
28
64
  const rolesHeader = req.headers['x-authorizer-roles'];
29
65
  return {
30
- userId,
31
- tenantId,
66
+ userId: mockUserId,
67
+ tenantId: mockTenantId,
32
68
  email: req.headers['x-authorizer-email'],
33
69
  roles: rolesHeader ? JSON.parse(rolesHeader) : [],
34
70
  };
@@ -79,18 +115,29 @@ export function getAuthContext(req) {
79
115
  return auth;
80
116
  }
81
117
  /**
82
- * Test helper to generate authorizer headers.
83
- * Use this in tests to simulate API Gateway authorizer context.
118
+ * Test helper to generate authorization header with JWT.
119
+ *
120
+ * Since auth context is now extracted directly from the JWT,
121
+ * tests only need to pass the Authorization header with a valid token.
84
122
  *
85
123
  * @example
86
124
  * ```typescript
87
125
  * const response = await request(app)
88
126
  * .post('/users')
89
- * .set(authHeaders(userId, tenantId))
127
+ * .set('Authorization', `Bearer ${token}`)
90
128
  * .send({ name: 'John' });
91
129
  * ```
130
+ *
131
+ * @deprecated Use Authorization header directly with JWT token.
132
+ * This helper is kept for backward compatibility.
92
133
  */
93
134
  export function authHeaders(userId, tenantId, extras) {
135
+ // If a token is provided, just use that (preferred)
136
+ if (extras?.token) {
137
+ return { 'Authorization': `Bearer ${extras.token}` };
138
+ }
139
+ // Legacy: Build mock headers for tests without real JWT
140
+ // This is only for unit tests that don't have access to real tokens
94
141
  return {
95
142
  'x-authorizer-user-id': userId,
96
143
  'x-authorizer-tenant-id': tenantId,
@@ -98,3 +145,75 @@ export function authHeaders(userId, tenantId, extras) {
98
145
  ...(extras?.roles && { 'x-authorizer-roles': JSON.stringify(extras.roles) }),
99
146
  };
100
147
  }
148
+ /**
149
+ * Standard role names used across the platform.
150
+ */
151
+ export const ROLES = {
152
+ SUPER_ADMIN: 'SUPER_ADMIN',
153
+ TENANT_ADMIN: 'TENANT_ADMIN',
154
+ LOAN_OFFICER: 'LOAN_OFFICER',
155
+ CUSTOMER: 'CUSTOMER',
156
+ VIEWER: 'VIEWER',
157
+ };
158
+ /**
159
+ * Roles that have admin privileges (can manage resources).
160
+ */
161
+ export const ADMIN_ROLES = [ROLES.SUPER_ADMIN, ROLES.TENANT_ADMIN, ROLES.LOAN_OFFICER];
162
+ /**
163
+ * Check if user has any of the specified roles.
164
+ */
165
+ export function hasAnyRole(userRoles, requiredRoles) {
166
+ if (!userRoles || userRoles.length === 0)
167
+ return false;
168
+ return requiredRoles.some(role => userRoles.includes(role));
169
+ }
170
+ /**
171
+ * Check if user has admin privileges.
172
+ */
173
+ export function isAdmin(userRoles) {
174
+ return hasAnyRole(userRoles, ADMIN_ROLES);
175
+ }
176
+ /**
177
+ * Middleware factory that requires user to have specific role(s).
178
+ * Uses roles from API Gateway authorizer context.
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * // Require any admin role
183
+ * router.post('/payment-plans', requireRole(ADMIN_ROLES), createPaymentPlan);
184
+ *
185
+ * // Require specific role
186
+ * router.delete('/users/:id', requireRole(['SUPER_ADMIN']), deleteUser);
187
+ * ```
188
+ */
189
+ export function requireRole(allowedRoles) {
190
+ return function (req, res, next) {
191
+ const auth = extractAuthContext(req);
192
+ if (!auth) {
193
+ return res.status(401).json({
194
+ success: false,
195
+ error: 'Unauthorized - authentication required'
196
+ });
197
+ }
198
+ if (!hasAnyRole(auth.roles, allowedRoles)) {
199
+ return res.status(403).json({
200
+ success: false,
201
+ error: 'Forbidden - insufficient permissions',
202
+ requiredRoles: allowedRoles,
203
+ });
204
+ }
205
+ next();
206
+ };
207
+ }
208
+ /**
209
+ * Middleware that requires admin privileges.
210
+ * Shorthand for requireRole(ADMIN_ROLES).
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * router.post('/payment-methods', requireAdmin, createPaymentMethod);
215
+ * ```
216
+ */
217
+ export function requireAdmin(req, res, next) {
218
+ return requireRole(ADMIN_ROLES)(req, res, next);
219
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Action Status Types - Back-end driven UI indicators
3
+ *
4
+ * This module provides types for indicating who needs to act next
5
+ * at various levels of the application (application, phase, step).
6
+ *
7
+ * The frontend uses this to show:
8
+ * - "Awaiting your action" (CUSTOMER)
9
+ * - "Under review" (ADMIN)
10
+ * - "Processing..." (SYSTEM)
11
+ * - "Completed" (NONE)
12
+ * - "Awaiting payment" (CUSTOMER for payment phases)
13
+ */
14
+ /**
15
+ * The actor who needs to take the next action
16
+ */
17
+ export declare enum NextActor {
18
+ /** Customer must take action (upload, sign, pay) */
19
+ CUSTOMER = "CUSTOMER",
20
+ /** Admin must take action (review, approve, reject) */
21
+ ADMIN = "ADMIN",
22
+ /** System is processing (auto-generation, webhook, etc.) */
23
+ SYSTEM = "SYSTEM",
24
+ /** No action required - completed or waiting for external event */
25
+ NONE = "NONE"
26
+ }
27
+ /**
28
+ * High-level action categories for easier UI grouping
29
+ */
30
+ export declare enum ActionCategory {
31
+ /** Document upload/reupload needed */
32
+ UPLOAD = "UPLOAD",
33
+ /** Signature required */
34
+ SIGNATURE = "SIGNATURE",
35
+ /** Review/approval needed */
36
+ REVIEW = "REVIEW",
37
+ /** Payment required */
38
+ PAYMENT = "PAYMENT",
39
+ /** Waiting for external process */
40
+ PROCESSING = "PROCESSING",
41
+ /** Phase/step/application completed */
42
+ COMPLETED = "COMPLETED",
43
+ /** Waiting for previous phase/step */
44
+ WAITING = "WAITING"
45
+ }
46
+ /**
47
+ * Detailed action status for a step, phase, or application
48
+ */
49
+ export interface ActionStatus {
50
+ /** Who needs to act next */
51
+ nextActor: NextActor;
52
+ /** Category of action required */
53
+ actionCategory: ActionCategory;
54
+ /** Human-readable description of what's needed */
55
+ actionRequired: string;
56
+ /** Optional: Additional context (e.g., "2 of 3 documents uploaded") */
57
+ progress?: string;
58
+ /** Optional: When this action is due (for time-sensitive actions) */
59
+ dueDate?: Date | string | null;
60
+ /** Optional: Whether this is blocking the overall workflow */
61
+ isBlocking?: boolean;
62
+ }
63
+ /**
64
+ * Step-level action status with step details
65
+ */
66
+ export interface StepActionStatus extends ActionStatus {
67
+ stepId: string;
68
+ stepName: string;
69
+ stepType: string;
70
+ stepOrder: number;
71
+ }
72
+ /**
73
+ * Phase-level action status with aggregated step info
74
+ */
75
+ export interface PhaseActionStatus extends ActionStatus {
76
+ phaseId: string;
77
+ phaseName: string;
78
+ phaseType: string;
79
+ phaseCategory: string;
80
+ /** Current step requiring attention (if documentation phase) */
81
+ currentStep?: StepActionStatus | null;
82
+ /** Summary of step progress (e.g., "3 of 5 steps completed") */
83
+ stepsProgress?: string;
84
+ /** For payment phases: payment progress summary */
85
+ paymentProgress?: string;
86
+ }
87
+ /**
88
+ * Application-level action status with phase info
89
+ */
90
+ export interface ApplicationActionStatus extends ActionStatus {
91
+ applicationId: string;
92
+ applicationNumber: string;
93
+ /** Current phase requiring attention */
94
+ currentPhase?: PhaseActionStatus | null;
95
+ /** Summary of phase progress */
96
+ phasesProgress?: string;
97
+ }
98
+ /**
99
+ * Compute action status for a documentation step
100
+ */
101
+ export declare function computeStepActionStatus(step: {
102
+ id: string;
103
+ name: string;
104
+ stepType: string;
105
+ order: number;
106
+ status: string;
107
+ actionReason?: string | null;
108
+ dueDate?: Date | string | null;
109
+ }, pendingDocuments?: number, totalDocuments?: number): StepActionStatus;
110
+ /**
111
+ * Compute action status for a phase based on its category and current state
112
+ */
113
+ export declare function computePhaseActionStatus(phase: {
114
+ id: string;
115
+ name: string;
116
+ phaseType: string;
117
+ phaseCategory: string;
118
+ status: string;
119
+ dueDate?: Date | string | null;
120
+ documentationPhase?: {
121
+ currentStep?: any | null;
122
+ steps?: any[];
123
+ completedStepsCount?: number;
124
+ totalStepsCount?: number;
125
+ approvedDocumentsCount?: number;
126
+ requiredDocumentsCount?: number;
127
+ } | null;
128
+ paymentPhase?: {
129
+ totalAmount?: number;
130
+ paidAmount?: number;
131
+ installments?: any[];
132
+ } | null;
133
+ questionnairePhase?: {
134
+ completedFieldsCount?: number;
135
+ totalFieldsCount?: number;
136
+ } | null;
137
+ }): PhaseActionStatus;