Update pnpm-lock.yaml to add '@next/bundle-analyzer' and 'webpack-bundle-analyzer' dependencies. Modify nest-cli.json to prevent deletion of output directory during build. Enhance package.json scripts for development and clean commands. Refactor distributed-transaction.service.ts and transaction.service.ts for improved error handling and logging consistency. Update queue-health.controller.ts and csrf.controller.ts for better API documentation. Clean up whitespace and formatting across various files for improved readability.
This commit is contained in:
parent
4b877fb3e0
commit
ac61dd1e17
5313
apps/bff/build-trace/trace.json
Normal file
5313
apps/bff/build-trace/trace.json
Normal file
File diff suppressed because it is too large
Load Diff
85
apps/bff/build-trace/types.json
Normal file
85
apps/bff/build-trace/types.json
Normal file
@ -0,0 +1,85 @@
|
||||
[{"id":1,"intrinsicName":"any","recursionId":0,"flags":["Any"]},
|
||||
{"id":2,"intrinsicName":"any","recursionId":1,"flags":["Any"]},
|
||||
{"id":3,"intrinsicName":"any","recursionId":2,"flags":["Any"]},
|
||||
{"id":4,"intrinsicName":"any","recursionId":3,"flags":["Any"]},
|
||||
{"id":5,"intrinsicName":"error","recursionId":4,"flags":["Any"]},
|
||||
{"id":6,"intrinsicName":"unresolved","recursionId":5,"flags":["Any"]},
|
||||
{"id":7,"intrinsicName":"any","recursionId":6,"flags":["Any"]},
|
||||
{"id":8,"intrinsicName":"intrinsic","recursionId":7,"flags":["Any"]},
|
||||
{"id":9,"intrinsicName":"unknown","recursionId":8,"flags":["Unknown"]},
|
||||
{"id":10,"intrinsicName":"undefined","recursionId":9,"flags":["Undefined"]},
|
||||
{"id":11,"intrinsicName":"undefined","recursionId":10,"flags":["Undefined"]},
|
||||
{"id":12,"intrinsicName":"undefined","recursionId":11,"flags":["Undefined"]},
|
||||
{"id":13,"intrinsicName":"null","recursionId":12,"flags":["Null"]},
|
||||
{"id":14,"intrinsicName":"string","recursionId":13,"flags":["String"]},
|
||||
{"id":15,"intrinsicName":"number","recursionId":14,"flags":["Number"]},
|
||||
{"id":16,"intrinsicName":"bigint","recursionId":15,"flags":["BigInt"]},
|
||||
{"id":17,"intrinsicName":"false","recursionId":16,"flags":["BooleanLiteral"],"display":"false"},
|
||||
{"id":18,"intrinsicName":"false","recursionId":17,"flags":["BooleanLiteral"],"display":"false"},
|
||||
{"id":19,"intrinsicName":"true","recursionId":18,"flags":["BooleanLiteral"],"display":"true"},
|
||||
{"id":20,"intrinsicName":"true","recursionId":19,"flags":["BooleanLiteral"],"display":"true"},
|
||||
{"id":21,"intrinsicName":"boolean","recursionId":20,"unionTypes":[18,20],"flags":["Boolean","BooleanLike","PossiblyFalsy","Union"]},
|
||||
{"id":22,"intrinsicName":"symbol","recursionId":21,"flags":["ESSymbol"]},
|
||||
{"id":23,"intrinsicName":"void","recursionId":22,"flags":["Void"]},
|
||||
{"id":24,"intrinsicName":"never","recursionId":23,"flags":["Never"]},
|
||||
{"id":25,"intrinsicName":"never","recursionId":24,"flags":["Never"]},
|
||||
{"id":26,"intrinsicName":"never","recursionId":25,"flags":["Never"]},
|
||||
{"id":27,"intrinsicName":"never","recursionId":26,"flags":["Never"]},
|
||||
{"id":28,"intrinsicName":"object","recursionId":27,"flags":["NonPrimitive"]},
|
||||
{"id":29,"recursionId":28,"unionTypes":[14,15],"flags":["Union"]},
|
||||
{"id":30,"recursionId":29,"unionTypes":[14,15,22],"flags":["Union"]},
|
||||
{"id":31,"recursionId":30,"unionTypes":[15,16],"flags":["Union"]},
|
||||
{"id":32,"recursionId":31,"unionTypes":[10,13,14,15,16,18,20],"flags":["Union"]},
|
||||
{"id":33,"recursionId":32,"flags":["TemplateLiteral"]},
|
||||
{"id":34,"intrinsicName":"never","recursionId":33,"flags":["Never"]},
|
||||
{"id":35,"recursionId":34,"flags":["Object"],"display":"{}"},
|
||||
{"id":36,"recursionId":35,"flags":["Object"],"display":"{}"},
|
||||
{"id":37,"recursionId":36,"flags":["Object"],"display":"{}"},
|
||||
{"id":38,"symbolName":"__type","recursionId":37,"flags":["Object"],"display":"{}"},
|
||||
{"id":39,"recursionId":38,"flags":["Object"],"display":"{}"},
|
||||
{"id":40,"recursionId":39,"unionTypes":[10,13,39],"flags":["Union"]},
|
||||
{"id":41,"recursionId":40,"flags":["Object"],"display":"{}"},
|
||||
{"id":42,"recursionId":41,"flags":["Object"],"display":"{}"},
|
||||
{"id":43,"recursionId":42,"flags":["Object"],"display":"{}"},
|
||||
{"id":44,"recursionId":43,"flags":["Object"],"display":"{}"},
|
||||
{"id":45,"recursionId":44,"flags":["Object"],"display":"{}"},
|
||||
{"id":46,"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":47,"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":48,"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":49,"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":50,"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":51,"recursionId":45,"flags":["StringLiteral"],"display":"\"\""},
|
||||
{"id":52,"recursionId":46,"flags":["NumberLiteral"],"display":"0"},
|
||||
{"id":53,"recursionId":47,"flags":["BigIntLiteral"],"display":"0n"},
|
||||
{"id":54,"recursionId":48,"flags":["StringLiteral"],"display":"\"string\""},
|
||||
{"id":55,"recursionId":49,"flags":["StringLiteral"],"display":"\"number\""},
|
||||
{"id":56,"recursionId":50,"flags":["StringLiteral"],"display":"\"bigint\""},
|
||||
{"id":57,"recursionId":51,"flags":["StringLiteral"],"display":"\"boolean\""},
|
||||
{"id":58,"recursionId":52,"flags":["StringLiteral"],"display":"\"symbol\""},
|
||||
{"id":59,"recursionId":53,"flags":["StringLiteral"],"display":"\"undefined\""},
|
||||
{"id":60,"recursionId":54,"flags":["StringLiteral"],"display":"\"object\""},
|
||||
{"id":61,"recursionId":55,"flags":["StringLiteral"],"display":"\"function\""},
|
||||
{"id":62,"recursionId":56,"unionTypes":[54,55,56,57,58,59,60,61],"flags":["Union"]},
|
||||
{"id":63,"symbolName":"IArguments","recursionId":57,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":402,"character":2},"end":{"line":408,"character":2}},"flags":["Object"]},
|
||||
{"id":64,"symbolName":"globalThis","recursionId":58,"flags":["Object"],"display":"typeof globalThis"},
|
||||
{"id":65,"symbolName":"Array","recursionId":59,"instantiatedType":65,"typeArguments":[66],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["Object"]},
|
||||
{"id":66,"symbolName":"T","recursionId":60,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1325,"character":17},"end":{"line":1325,"character":18}},"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":67,"symbolName":"Array","recursionId":59,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":68,"symbolName":"Object","recursionId":61,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":121,"character":2},"end":{"line":153,"character":2}},"flags":["Object"]},
|
||||
{"id":69,"symbolName":"Function","recursionId":62,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":270,"character":39},"end":{"line":307,"character":2}},"flags":["Object"]},
|
||||
{"id":70,"symbolName":"CallableFunction","recursionId":63,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":329,"character":136},"end":{"line":366,"character":2}},"flags":["Object"]},
|
||||
{"id":71,"symbolName":"NewableFunction","recursionId":64,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":366,"character":2},"end":{"line":402,"character":2}},"flags":["Object"]},
|
||||
{"id":72,"symbolName":"String","recursionId":65,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":408,"character":2},"end":{"line":532,"character":2}},"flags":["Object"]},
|
||||
{"id":73,"symbolName":"Number","recursionId":66,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":557,"character":41},"end":{"line":586,"character":2}},"flags":["Object"]},
|
||||
{"id":74,"symbolName":"Boolean","recursionId":67,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":544,"character":39},"end":{"line":549,"character":2}},"flags":["Object"]},
|
||||
{"id":75,"symbolName":"RegExp","recursionId":68,"instantiatedType":75,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":991,"character":2},"end":{"line":1023,"character":2}},"flags":["Object"]},
|
||||
{"id":76,"symbolName":"RegExp","recursionId":68,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":991,"character":2},"end":{"line":1023,"character":2}},"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":77,"symbolName":"Array","recursionId":59,"instantiatedType":65,"typeArguments":[1],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["Object"]},
|
||||
{"id":78,"symbolName":"Array","recursionId":59,"instantiatedType":65,"typeArguments":[2],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["Object"]},
|
||||
{"id":79,"symbolName":"ReadonlyArray","recursionId":69,"instantiatedType":79,"typeArguments":[80],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1185,"character":24},"end":{"line":1316,"character":2}},"flags":["Object"]},
|
||||
{"id":80,"symbolName":"T","recursionId":70,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1191,"character":25},"end":{"line":1191,"character":26}},"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":81,"symbolName":"ReadonlyArray","recursionId":69,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1185,"character":24},"end":{"line":1316,"character":2}},"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":82,"symbolName":"ReadonlyArray","recursionId":69,"instantiatedType":79,"typeArguments":[1],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1185,"character":24},"end":{"line":1316,"character":2}},"flags":["Object"]},
|
||||
{"id":83,"symbolName":"ThisType","recursionId":71,"instantiatedType":83,"typeArguments":[84],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1680,"character":29},"end":{"line":1685,"character":25}},"flags":["Object"]},
|
||||
{"id":84,"symbolName":"T","recursionId":72,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1685,"character":20},"end":{"line":1685,"character":21}},"flags":["TypeParameter","IncludesMissingType"]},
|
||||
{"id":85,"symbolName":"ThisType","recursionId":71,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1680,"character":29},"end":{"line":1685,"character":25}},"flags":["TypeParameter","IncludesMissingType"]}]
|
||||
@ -4,7 +4,7 @@
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "tsconfig.build.json",
|
||||
"deleteOutDir": true,
|
||||
"deleteOutDir": false,
|
||||
"watchAssets": true,
|
||||
"assets": ["**/*.prisma"]
|
||||
}
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"dev": "nest start --watch --preserveWatchOutput",
|
||||
"predev": "tsc -b --force tsconfig.build.json",
|
||||
"dev": "nest start -p tsconfig.build.json --watch --preserveWatchOutput",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint .",
|
||||
@ -21,7 +22,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"type-check": "tsc --project tsconfig.json --noEmit",
|
||||
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"clean": "rm -rf dist tsconfig.build.tsbuildinfo",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:generate": "prisma generate",
|
||||
"db:studio": "prisma studio",
|
||||
|
||||
@ -43,7 +43,7 @@ export class DistributedTransactionService {
|
||||
|
||||
/**
|
||||
* Execute a distributed transaction with multiple steps across different systems
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await this.distributedTransactionService.executeDistributedTransaction([
|
||||
@ -91,16 +91,16 @@ export class DistributedTransactionService {
|
||||
description,
|
||||
timeout = 120000, // 2 minutes default for distributed operations
|
||||
maxRetries = 1, // Less retries for distributed operations
|
||||
continueOnNonCriticalFailure = false
|
||||
continueOnNonCriticalFailure = false,
|
||||
} = options;
|
||||
|
||||
const transactionId = this.generateTransactionId();
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
this.logger.log(`Starting distributed transaction [${transactionId}]`, {
|
||||
description,
|
||||
stepsCount: steps.length,
|
||||
timeout
|
||||
timeout,
|
||||
});
|
||||
|
||||
const stepResults: Record<string, any> = {};
|
||||
@ -113,29 +113,28 @@ export class DistributedTransactionService {
|
||||
for (const step of steps) {
|
||||
this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
|
||||
description: step.description,
|
||||
critical: step.critical
|
||||
critical: step.critical,
|
||||
});
|
||||
|
||||
try {
|
||||
const stepStartTime = Date.now();
|
||||
const result = await this.executeStepWithTimeout(step, timeout);
|
||||
const stepDuration = Date.now() - stepStartTime;
|
||||
|
||||
|
||||
stepResults[step.id] = result;
|
||||
executedSteps.push(step.id);
|
||||
|
||||
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
|
||||
duration: stepDuration
|
||||
});
|
||||
|
||||
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
|
||||
duration: stepDuration,
|
||||
});
|
||||
} catch (stepError) {
|
||||
lastError = stepError as Error;
|
||||
failedSteps.push(step.id);
|
||||
|
||||
|
||||
this.logger.error(`Step failed: ${step.id} [${transactionId}]`, {
|
||||
error: getErrorMessage(stepError),
|
||||
critical: step.critical,
|
||||
retryable: step.retryable
|
||||
retryable: step.retryable,
|
||||
});
|
||||
|
||||
// If it's a critical step, stop the entire transaction
|
||||
@ -149,17 +148,19 @@ export class DistributedTransactionService {
|
||||
}
|
||||
|
||||
// Otherwise, log and continue
|
||||
this.logger.warn(`Continuing despite non-critical step failure: ${step.id} [${transactionId}]`);
|
||||
this.logger.warn(
|
||||
`Continuing despite non-critical step failure: ${step.id} [${transactionId}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
this.logger.log(`Distributed transaction completed successfully [${transactionId}]`, {
|
||||
description,
|
||||
duration,
|
||||
stepsExecuted: executedSteps.length,
|
||||
failedSteps: failedSteps.length
|
||||
failedSteps: failedSteps.length,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -169,25 +170,24 @@ export class DistributedTransactionService {
|
||||
stepsExecuted: executedSteps.length,
|
||||
stepsRolledBack: 0,
|
||||
stepResults,
|
||||
failedSteps
|
||||
failedSteps,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
this.logger.error(`Distributed transaction failed [${transactionId}]`, {
|
||||
description,
|
||||
error: getErrorMessage(error),
|
||||
duration,
|
||||
stepsExecuted: executedSteps.length,
|
||||
failedSteps: failedSteps.length
|
||||
failedSteps: failedSteps.length,
|
||||
});
|
||||
|
||||
// Execute rollbacks for completed steps
|
||||
const rollbacksExecuted = await this.executeRollbacks(
|
||||
steps,
|
||||
executedSteps,
|
||||
stepResults,
|
||||
steps,
|
||||
executedSteps,
|
||||
stepResults,
|
||||
transactionId
|
||||
);
|
||||
|
||||
@ -198,7 +198,7 @@ export class DistributedTransactionService {
|
||||
stepsExecuted: executedSteps.length,
|
||||
stepsRolledBack: rollbacksExecuted,
|
||||
stepResults,
|
||||
failedSteps
|
||||
failedSteps,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -209,7 +209,7 @@ export class DistributedTransactionService {
|
||||
async executeHybridTransaction<T>(
|
||||
databaseOperation: (tx: any, context: TransactionContext) => Promise<T>,
|
||||
externalSteps: DistributedStep[],
|
||||
options: DistributedTransactionOptions & {
|
||||
options: DistributedTransactionOptions & {
|
||||
databaseFirst?: boolean;
|
||||
rollbackDatabaseOnExternalFailure?: boolean;
|
||||
}
|
||||
@ -222,11 +222,11 @@ export class DistributedTransactionService {
|
||||
|
||||
const transactionId = this.generateTransactionId();
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
this.logger.log(`Starting hybrid transaction [${transactionId}]`, {
|
||||
description: options.description,
|
||||
databaseFirst,
|
||||
externalStepsCount: externalSteps.length
|
||||
externalStepsCount: externalSteps.length,
|
||||
});
|
||||
|
||||
try {
|
||||
@ -240,12 +240,12 @@ export class DistributedTransactionService {
|
||||
databaseOperation,
|
||||
{
|
||||
description: `${options.description} - Database Operations`,
|
||||
timeout: options.timeout
|
||||
timeout: options.timeout,
|
||||
}
|
||||
);
|
||||
|
||||
if (!dbTransactionResult.success) {
|
||||
throw new Error(dbTransactionResult.error || 'Database transaction failed');
|
||||
throw new Error(dbTransactionResult.error || "Database transaction failed");
|
||||
}
|
||||
|
||||
databaseResult = dbTransactionResult.data!;
|
||||
@ -254,27 +254,29 @@ export class DistributedTransactionService {
|
||||
this.logger.debug(`Executing external operations [${transactionId}]`);
|
||||
externalResult = await this.executeDistributedTransaction(externalSteps, {
|
||||
...distributedOptions,
|
||||
description: distributedOptions.description || 'External operations'
|
||||
description: distributedOptions.description || "External operations",
|
||||
});
|
||||
|
||||
if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
|
||||
// Note: Database transaction already committed, so we can't rollback automatically
|
||||
// This is a limitation of this approach - consider using saga pattern for true rollback
|
||||
this.logger.error(`External operations failed but database already committed [${transactionId}]`, {
|
||||
externalError: externalResult.error
|
||||
});
|
||||
this.logger.error(
|
||||
`External operations failed but database already committed [${transactionId}]`,
|
||||
{
|
||||
externalError: externalResult.error,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Execute external operations first
|
||||
this.logger.debug(`Executing external operations [${transactionId}]`);
|
||||
externalResult = await this.executeDistributedTransaction(externalSteps, {
|
||||
...distributedOptions,
|
||||
description: distributedOptions.description || 'External operations'
|
||||
description: distributedOptions.description || "External operations",
|
||||
});
|
||||
|
||||
if (!externalResult.success) {
|
||||
throw new Error(externalResult.error || 'External operations failed');
|
||||
throw new Error(externalResult.error || "External operations failed");
|
||||
}
|
||||
|
||||
// Execute database operations
|
||||
@ -283,7 +285,7 @@ export class DistributedTransactionService {
|
||||
databaseOperation,
|
||||
{
|
||||
description: `${options.description} - Database Operations`,
|
||||
timeout: options.timeout
|
||||
timeout: options.timeout,
|
||||
}
|
||||
);
|
||||
|
||||
@ -295,17 +297,17 @@ export class DistributedTransactionService {
|
||||
externalResult.stepResults,
|
||||
transactionId
|
||||
);
|
||||
throw new Error(dbTransactionResult.error || 'Database transaction failed');
|
||||
throw new Error(dbTransactionResult.error || "Database transaction failed");
|
||||
}
|
||||
|
||||
databaseResult = dbTransactionResult.data!;
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, {
|
||||
description: options.description,
|
||||
duration
|
||||
duration,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -315,16 +317,15 @@ export class DistributedTransactionService {
|
||||
stepsExecuted: externalResult?.stepsExecuted || 0,
|
||||
stepsRolledBack: 0,
|
||||
stepResults: externalResult?.stepResults || {},
|
||||
failedSteps: externalResult?.failedSteps || []
|
||||
failedSteps: externalResult?.failedSteps || [],
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
this.logger.error(`Hybrid transaction failed [${transactionId}]`, {
|
||||
description: options.description,
|
||||
error: getErrorMessage(error),
|
||||
duration
|
||||
duration,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -334,7 +335,7 @@ export class DistributedTransactionService {
|
||||
stepsExecuted: 0,
|
||||
stepsRolledBack: 0,
|
||||
stepResults: {},
|
||||
failedSteps: []
|
||||
failedSteps: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -346,7 +347,7 @@ export class DistributedTransactionService {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
})
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -357,14 +358,14 @@ export class DistributedTransactionService {
|
||||
transactionId: string
|
||||
): Promise<number> {
|
||||
this.logger.warn(`Executing rollbacks for ${executedSteps.length} steps [${transactionId}]`);
|
||||
|
||||
|
||||
let rollbacksExecuted = 0;
|
||||
|
||||
|
||||
// Execute rollbacks in reverse order (LIFO)
|
||||
for (let i = executedSteps.length - 1; i >= 0; i--) {
|
||||
const stepId = executedSteps[i];
|
||||
const step = steps.find(s => s.id === stepId);
|
||||
|
||||
|
||||
if (step?.rollback) {
|
||||
try {
|
||||
this.logger.debug(`Executing rollback for step: ${stepId} [${transactionId}]`);
|
||||
@ -373,7 +374,7 @@ export class DistributedTransactionService {
|
||||
this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`);
|
||||
} catch (rollbackError) {
|
||||
this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, {
|
||||
error: getErrorMessage(rollbackError)
|
||||
error: getErrorMessage(rollbackError),
|
||||
});
|
||||
// Continue with other rollbacks even if one fails
|
||||
}
|
||||
|
||||
@ -16,24 +16,24 @@ export interface TransactionOptions {
|
||||
* Default: 30 seconds
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
|
||||
/**
|
||||
* Maximum number of retry attempts on serialization failures
|
||||
* Default: 3
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
||||
/**
|
||||
* Custom isolation level for the transaction
|
||||
* Default: ReadCommitted
|
||||
*/
|
||||
isolationLevel?: 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable';
|
||||
|
||||
isolationLevel?: "ReadUncommitted" | "ReadCommitted" | "RepeatableRead" | "Serializable";
|
||||
|
||||
/**
|
||||
* Description of the transaction for logging
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
|
||||
/**
|
||||
* Whether to automatically rollback external operations on database rollback
|
||||
* Default: true
|
||||
@ -58,7 +58,7 @@ export interface TransactionResult<T> {
|
||||
export class TransactionService {
|
||||
private readonly defaultTimeout = 30000; // 30 seconds
|
||||
private readonly defaultMaxRetries = 3;
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
@ -66,26 +66,26 @@ export class TransactionService {
|
||||
|
||||
/**
|
||||
* Execute operations within a database transaction with rollback support
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await this.transactionService.executeTransaction(
|
||||
* async (tx, context) => {
|
||||
* // Database operations
|
||||
* const user = await tx.user.create({ data: userData });
|
||||
*
|
||||
*
|
||||
* // External operations with rollback
|
||||
* const whmcsClient = await this.whmcsService.createClient(user.email);
|
||||
* context.addRollback(async () => {
|
||||
* await this.whmcsService.deleteClient(whmcsClient.id);
|
||||
* });
|
||||
*
|
||||
* // Salesforce operations with rollback
|
||||
*
|
||||
* // Salesforce operations with rollback
|
||||
* const sfAccount = await this.salesforceService.createAccount(user);
|
||||
* context.addRollback(async () => {
|
||||
* await this.salesforceService.deleteAccount(sfAccount.Id);
|
||||
* });
|
||||
*
|
||||
*
|
||||
* return { user, whmcsClient, sfAccount };
|
||||
* },
|
||||
* {
|
||||
@ -102,26 +102,26 @@ export class TransactionService {
|
||||
const {
|
||||
timeout = this.defaultTimeout,
|
||||
maxRetries = this.defaultMaxRetries,
|
||||
isolationLevel = 'ReadCommitted',
|
||||
description = 'Database transaction',
|
||||
autoRollback = true
|
||||
isolationLevel = "ReadCommitted",
|
||||
description = "Database transaction",
|
||||
autoRollback = true,
|
||||
} = options;
|
||||
|
||||
const transactionId = this.generateTransactionId();
|
||||
const startTime = new Date();
|
||||
|
||||
|
||||
let context: TransactionContext = {
|
||||
id: transactionId,
|
||||
startTime,
|
||||
operations: [],
|
||||
rollbackActions: []
|
||||
rollbackActions: [],
|
||||
};
|
||||
|
||||
this.logger.log(`Starting transaction [${transactionId}]`, {
|
||||
description,
|
||||
timeout,
|
||||
isolationLevel,
|
||||
maxRetries
|
||||
maxRetries,
|
||||
});
|
||||
|
||||
let attempt = 0;
|
||||
@ -129,7 +129,7 @@ export class TransactionService {
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
attempt++;
|
||||
|
||||
|
||||
try {
|
||||
// Reset context for retry attempts
|
||||
if (attempt > 1) {
|
||||
@ -137,22 +137,22 @@ export class TransactionService {
|
||||
id: transactionId,
|
||||
startTime,
|
||||
operations: [],
|
||||
rollbackActions: []
|
||||
rollbackActions: [],
|
||||
};
|
||||
}
|
||||
|
||||
const result = await Promise.race([
|
||||
this.executeTransactionAttempt(operation, context, isolationLevel),
|
||||
this.createTimeoutPromise<T>(timeout, transactionId)
|
||||
this.createTimeoutPromise<T>(timeout, transactionId),
|
||||
]);
|
||||
|
||||
const duration = Date.now() - startTime.getTime();
|
||||
|
||||
|
||||
this.logger.log(`Transaction completed successfully [${transactionId}]`, {
|
||||
description,
|
||||
duration,
|
||||
attempt,
|
||||
operationsCount: context.operations.length
|
||||
operationsCount: context.operations.length,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -160,31 +160,30 @@ export class TransactionService {
|
||||
data: result,
|
||||
duration,
|
||||
operationsCount: context.operations.length,
|
||||
rollbacksExecuted: 0
|
||||
rollbacksExecuted: 0,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
const duration = Date.now() - startTime.getTime();
|
||||
|
||||
|
||||
this.logger.error(`Transaction attempt ${attempt} failed [${transactionId}]`, {
|
||||
description,
|
||||
error: getErrorMessage(error),
|
||||
duration,
|
||||
operationsCount: context.operations.length,
|
||||
rollbackActionsCount: context.rollbackActions.length
|
||||
rollbackActionsCount: context.rollbackActions.length,
|
||||
});
|
||||
|
||||
// Execute rollbacks if this is the final attempt or not a retryable error
|
||||
if (attempt === maxRetries || !this.isRetryableError(error)) {
|
||||
const rollbacksExecuted = await this.executeRollbacks(context, autoRollback);
|
||||
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
duration,
|
||||
operationsCount: context.operations.length,
|
||||
rollbacksExecuted
|
||||
rollbacksExecuted,
|
||||
};
|
||||
}
|
||||
|
||||
@ -197,10 +196,10 @@ export class TransactionService {
|
||||
const duration = Date.now() - startTime.getTime();
|
||||
return {
|
||||
success: false,
|
||||
error: lastError ? getErrorMessage(lastError) : 'Unknown transaction error',
|
||||
error: lastError ? getErrorMessage(lastError) : "Unknown transaction error",
|
||||
duration,
|
||||
operationsCount: context.operations.length,
|
||||
rollbacksExecuted: 0
|
||||
rollbacksExecuted: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -209,15 +208,15 @@ export class TransactionService {
|
||||
*/
|
||||
async executeSimpleTransaction<T>(
|
||||
operation: (tx: any) => Promise<T>,
|
||||
options: Omit<TransactionOptions, 'autoRollback'> = {}
|
||||
options: Omit<TransactionOptions, "autoRollback"> = {}
|
||||
): Promise<T> {
|
||||
const result = await this.executeTransaction(
|
||||
async (tx, _context) => operation(tx),
|
||||
{ ...options, autoRollback: false }
|
||||
);
|
||||
const result = await this.executeTransaction(async (tx, _context) => operation(tx), {
|
||||
...options,
|
||||
autoRollback: false,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Transaction failed');
|
||||
throw new Error(result.error || "Transaction failed");
|
||||
}
|
||||
|
||||
return result.data!;
|
||||
@ -229,16 +228,16 @@ export class TransactionService {
|
||||
isolationLevel: string
|
||||
): Promise<T> {
|
||||
return await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
async tx => {
|
||||
// Enhance context with helper methods
|
||||
const enhancedContext = this.enhanceContext(context);
|
||||
|
||||
|
||||
// Execute the operation
|
||||
return await operation(tx, enhancedContext);
|
||||
},
|
||||
{
|
||||
isolationLevel: isolationLevel as any,
|
||||
timeout: 30000 // Prisma transaction timeout
|
||||
timeout: 30000, // Prisma transaction timeout
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -251,7 +250,7 @@ export class TransactionService {
|
||||
},
|
||||
addRollback: (rollbackFn: () => Promise<void>) => {
|
||||
context.rollbackActions.push(rollbackFn);
|
||||
}
|
||||
},
|
||||
} as TransactionContext & {
|
||||
addOperation: (description: string) => void;
|
||||
addRollback: (rollbackFn: () => Promise<void>) => void;
|
||||
@ -259,17 +258,19 @@ export class TransactionService {
|
||||
}
|
||||
|
||||
private async executeRollbacks(
|
||||
context: TransactionContext,
|
||||
context: TransactionContext,
|
||||
autoRollback: boolean
|
||||
): Promise<number> {
|
||||
if (!autoRollback || context.rollbackActions.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.logger.warn(`Executing ${context.rollbackActions.length} rollback actions [${context.id}]`);
|
||||
|
||||
this.logger.warn(
|
||||
`Executing ${context.rollbackActions.length} rollback actions [${context.id}]`
|
||||
);
|
||||
|
||||
let rollbacksExecuted = 0;
|
||||
|
||||
|
||||
// Execute rollbacks in reverse order (LIFO)
|
||||
for (let i = context.rollbackActions.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
@ -278,26 +279,28 @@ export class TransactionService {
|
||||
this.logger.debug(`Rollback ${i + 1} completed [${context.id}]`);
|
||||
} catch (rollbackError) {
|
||||
this.logger.error(`Rollback ${i + 1} failed [${context.id}]`, {
|
||||
error: getErrorMessage(rollbackError)
|
||||
error: getErrorMessage(rollbackError),
|
||||
});
|
||||
// Continue with other rollbacks even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Completed ${rollbacksExecuted}/${context.rollbackActions.length} rollbacks [${context.id}]`);
|
||||
this.logger.log(
|
||||
`Completed ${rollbacksExecuted}/${context.rollbackActions.length} rollbacks [${context.id}]`
|
||||
);
|
||||
return rollbacksExecuted;
|
||||
}
|
||||
|
||||
private isRetryableError(error: unknown): boolean {
|
||||
const errorMessage = getErrorMessage(error).toLowerCase();
|
||||
|
||||
|
||||
// Retry on serialization failures, deadlocks, and temporary connection issues
|
||||
return (
|
||||
errorMessage.includes('serialization failure') ||
|
||||
errorMessage.includes('deadlock') ||
|
||||
errorMessage.includes('connection') ||
|
||||
errorMessage.includes('timeout') ||
|
||||
errorMessage.includes('lock wait timeout')
|
||||
errorMessage.includes("serialization failure") ||
|
||||
errorMessage.includes("deadlock") ||
|
||||
errorMessage.includes("connection") ||
|
||||
errorMessage.includes("timeout") ||
|
||||
errorMessage.includes("lock wait timeout")
|
||||
);
|
||||
}
|
||||
|
||||
@ -326,7 +329,7 @@ export class TransactionService {
|
||||
activeTransactions: 0, // Would need to track active transactions
|
||||
totalTransactions: 0, // Would need to track total count
|
||||
successRate: 0, // Would need to track success/failure rates
|
||||
averageDuration: 0 // Would need to track durations
|
||||
averageDuration: 0, // Would need to track durations
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,13 @@ export class QueueHealthController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
@ApiOperation({
|
||||
summary: "Get queue health status",
|
||||
description: "Returns health status and metrics for WHMCS and Salesforce request queues"
|
||||
description: "Returns health status and metrics for WHMCS and Salesforce request queues",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Queue health status retrieved successfully"
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Queue health status retrieved successfully",
|
||||
})
|
||||
getQueueHealth() {
|
||||
return {
|
||||
@ -36,13 +36,13 @@ export class QueueHealthController {
|
||||
}
|
||||
|
||||
@Get("whmcs")
|
||||
@ApiOperation({
|
||||
@ApiOperation({
|
||||
summary: "Get WHMCS queue metrics",
|
||||
description: "Returns detailed metrics for the WHMCS request queue"
|
||||
description: "Returns detailed metrics for the WHMCS request queue",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "WHMCS queue metrics retrieved successfully"
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "WHMCS queue metrics retrieved successfully",
|
||||
})
|
||||
getWhmcsQueueMetrics() {
|
||||
return {
|
||||
@ -53,13 +53,14 @@ export class QueueHealthController {
|
||||
}
|
||||
|
||||
@Get("salesforce")
|
||||
@ApiOperation({
|
||||
@ApiOperation({
|
||||
summary: "Get Salesforce queue metrics",
|
||||
description: "Returns detailed metrics for the Salesforce request queue including daily API usage"
|
||||
description:
|
||||
"Returns detailed metrics for the Salesforce request queue including daily API usage",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Salesforce queue metrics retrieved successfully"
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Salesforce queue metrics retrieved successfully",
|
||||
})
|
||||
getSalesforceQueueMetrics() {
|
||||
return {
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
Inject,
|
||||
} from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
import { getClientSafeErrorMessage } from "../utils/error.util";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { SecureErrorMapperService } from "../security/services/secure-error-mapper.service";
|
||||
@ -41,14 +40,15 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
// Determine HTTP status
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
|
||||
|
||||
// Extract the actual error from HttpException response
|
||||
const exceptionResponse = exception.getResponse();
|
||||
if (typeof exceptionResponse === "object" && exceptionResponse !== null) {
|
||||
const errorResponse = exceptionResponse as { message?: string; error?: string };
|
||||
originalError = errorResponse.message || exception.message;
|
||||
} else {
|
||||
originalError = typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
|
||||
originalError =
|
||||
typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
|
||||
}
|
||||
} else {
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
@ -58,11 +58,11 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
// Use secure error mapper to get safe public message and log securely
|
||||
const errorClassification = this.secureErrorMapper.mapError(originalError, errorContext);
|
||||
const publicMessage = this.secureErrorMapper.getPublicMessage(originalError, errorContext);
|
||||
|
||||
|
||||
// Log the error securely (this handles sensitive data filtering)
|
||||
this.secureErrorMapper.logSecureError(originalError, errorContext, {
|
||||
httpStatus: status,
|
||||
exceptionType: exception instanceof Error ? exception.constructor.name : 'Unknown'
|
||||
exceptionType: exception instanceof Error ? exception.constructor.name : "Unknown",
|
||||
});
|
||||
|
||||
// Create secure error response
|
||||
|
||||
@ -25,13 +25,13 @@ export interface SalesforceRequestOptions {
|
||||
|
||||
/**
|
||||
* Salesforce Request Queue Service
|
||||
*
|
||||
*
|
||||
* Manages concurrent requests to Salesforce API to prevent:
|
||||
* - Daily API limit exhaustion (100,000 + 1,000 per user)
|
||||
* - Concurrent request limit violations (25 long-running requests)
|
||||
* - Rate limit violations and 429 errors
|
||||
* - Optimal resource utilization
|
||||
*
|
||||
*
|
||||
* Based on Salesforce documentation:
|
||||
* - Daily limit: 100,000 + (1,000 × users) per 24h
|
||||
* - Concurrent limit: 25 long-running requests (>20s)
|
||||
@ -110,10 +110,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
|
||||
// Wait for pending requests to complete (with timeout)
|
||||
try {
|
||||
await Promise.all([
|
||||
this.standardQueue.onIdle(),
|
||||
this.longRunningQueue.onIdle(),
|
||||
]);
|
||||
await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
|
||||
} catch (error) {
|
||||
this.logger.warn("Some Salesforce requests may not have completed during shutdown", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
@ -157,10 +154,10 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
this.recordWaitTime(waitTime);
|
||||
|
||||
const executionStart = Date.now();
|
||||
|
||||
|
||||
try {
|
||||
const response = await this.executeWithRetry(requestFn, options);
|
||||
|
||||
|
||||
const executionTime = Date.now() - executionStart;
|
||||
this.recordExecutionTime(executionTime);
|
||||
this.metrics.completedRequests++;
|
||||
@ -219,10 +216,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
/**
|
||||
* Execute high-priority Salesforce request (jumps queue)
|
||||
*/
|
||||
async executeHighPriority<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
isLongRunning = false
|
||||
): Promise<T> {
|
||||
async executeHighPriority<T>(requestFn: () => Promise<T>, isLongRunning = false): Promise<T> {
|
||||
return this.execute(requestFn, { priority: 10, isLongRunning });
|
||||
}
|
||||
|
||||
@ -253,29 +247,20 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
averageWaitTime: number;
|
||||
} {
|
||||
this.updateQueueMetrics();
|
||||
|
||||
const errorRate = this.metrics.totalRequests > 0
|
||||
? this.metrics.failedRequests / this.metrics.totalRequests
|
||||
: 0;
|
||||
|
||||
const errorRate =
|
||||
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
|
||||
|
||||
// Estimate daily limit (conservative: 150,000 for ~50 users)
|
||||
const estimatedDailyLimit = 150000;
|
||||
const dailyUsagePercent = this.metrics.dailyApiUsage / estimatedDailyLimit;
|
||||
|
||||
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
|
||||
|
||||
|
||||
// Adjusted thresholds for higher throughput (15 concurrent, 10 RPS)
|
||||
if (
|
||||
this.metrics.queueSize > 200 ||
|
||||
errorRate > 0.1 ||
|
||||
dailyUsagePercent > 0.9
|
||||
) {
|
||||
if (this.metrics.queueSize > 200 || errorRate > 0.1 || dailyUsagePercent > 0.9) {
|
||||
status = "unhealthy";
|
||||
} else if (
|
||||
this.metrics.queueSize > 80 ||
|
||||
errorRate > 0.05 ||
|
||||
dailyUsagePercent > 0.7
|
||||
) {
|
||||
} else if (this.metrics.queueSize > 80 || errorRate > 0.05 || dailyUsagePercent > 0.7) {
|
||||
status = "degraded";
|
||||
}
|
||||
|
||||
@ -319,11 +304,8 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
|
||||
this.standardQueue.clear();
|
||||
this.longRunningQueue.clear();
|
||||
|
||||
await Promise.all([
|
||||
this.standardQueue.onIdle(),
|
||||
this.longRunningQueue.onIdle(),
|
||||
]);
|
||||
|
||||
await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
|
||||
}
|
||||
|
||||
private async executeWithRetry<T>(
|
||||
@ -347,7 +329,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
|
||||
// Special handling for rate limit errors
|
||||
let delay = baseDelay * Math.pow(2, attempt - 1);
|
||||
|
||||
|
||||
if (this.isRateLimitError(error)) {
|
||||
// Longer delay for rate limit errors
|
||||
delay = Math.max(delay, 30000); // At least 30 seconds
|
||||
@ -355,7 +337,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
|
||||
// Add jitter
|
||||
delay += Math.random() * 1000;
|
||||
|
||||
|
||||
this.logger.debug("Salesforce request failed, retrying", {
|
||||
attempt,
|
||||
maxAttempts,
|
||||
@ -386,12 +368,12 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
|
||||
private checkDailyUsage(): void {
|
||||
const now = new Date();
|
||||
|
||||
|
||||
// Reset daily usage if we've passed the reset time
|
||||
if (now >= this.dailyUsageResetTime) {
|
||||
this.metrics.dailyApiUsage = 0;
|
||||
this.dailyUsageResetTime = this.getNextDayReset();
|
||||
|
||||
|
||||
this.logger.log("Daily Salesforce API usage reset", {
|
||||
resetTime: this.dailyUsageResetTime,
|
||||
});
|
||||
@ -436,15 +418,15 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
private updateQueueMetrics(): void {
|
||||
this.metrics.queueSize = this.standardQueue.size + this.longRunningQueue.size;
|
||||
this.metrics.pendingRequests = this.standardQueue.pending + this.longRunningQueue.pending;
|
||||
|
||||
|
||||
// Calculate averages
|
||||
if (this.waitTimes.length > 0) {
|
||||
this.metrics.averageWaitTime =
|
||||
this.metrics.averageWaitTime =
|
||||
this.waitTimes.reduce((sum, time) => sum + time, 0) / this.waitTimes.length;
|
||||
}
|
||||
|
||||
if (this.executionTimes.length > 0) {
|
||||
this.metrics.averageExecutionTime =
|
||||
this.metrics.averageExecutionTime =
|
||||
this.executionTimes.reduce((sum, time) => sum + time, 0) / this.executionTimes.length;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,13 +22,13 @@ export interface WhmcsRequestOptions {
|
||||
|
||||
/**
|
||||
* WHMCS Request Queue Service
|
||||
*
|
||||
*
|
||||
* Manages concurrent requests to WHMCS API to prevent:
|
||||
* - Database connection pool exhaustion
|
||||
* - Server overload from parallel requests
|
||||
* - Rate limit violations (conservative approach)
|
||||
* - Resource contention issues
|
||||
*
|
||||
*
|
||||
* Based on research:
|
||||
* - WHMCS has no official rate limits but performance degrades with high concurrency
|
||||
* - Conservative approach: max 3 concurrent requests
|
||||
@ -56,7 +56,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
private async initializeQueue() {
|
||||
if (!this.queue) {
|
||||
const { default: PQueue } = await import("p-queue");
|
||||
|
||||
|
||||
// Optimized WHMCS queue configuration for better user experience
|
||||
this.queue = new PQueue({
|
||||
concurrency: 15, // Max 15 concurrent WHMCS requests (matches Salesforce)
|
||||
@ -100,10 +100,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
/**
|
||||
* Execute a WHMCS API request through the queue
|
||||
*/
|
||||
async execute<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
options: WhmcsRequestOptions = {}
|
||||
): Promise<T> {
|
||||
async execute<T>(requestFn: () => Promise<T>, options: WhmcsRequestOptions = {}): Promise<T> {
|
||||
await this.initializeQueue();
|
||||
const startTime = Date.now();
|
||||
const requestId = this.generateRequestId();
|
||||
@ -125,10 +122,10 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
this.recordWaitTime(waitTime);
|
||||
|
||||
const executionStart = Date.now();
|
||||
|
||||
|
||||
try {
|
||||
const response = await this.executeWithRetry(requestFn, options);
|
||||
|
||||
|
||||
const executionTime = Date.now() - executionStart;
|
||||
this.recordExecutionTime(executionTime);
|
||||
this.metrics.completedRequests++;
|
||||
@ -199,13 +196,12 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
averageWaitTime: number;
|
||||
} {
|
||||
this.updateQueueMetrics();
|
||||
|
||||
const errorRate = this.metrics.totalRequests > 0
|
||||
? this.metrics.failedRequests / this.metrics.totalRequests
|
||||
: 0;
|
||||
|
||||
const errorRate =
|
||||
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
|
||||
|
||||
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
|
||||
|
||||
|
||||
// Adjusted thresholds for higher throughput (15 concurrent, 5 RPS)
|
||||
if (this.metrics.queueSize > 120 || errorRate > 0.1) {
|
||||
status = "unhealthy";
|
||||
@ -256,7 +252,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
// Exponential backoff with jitter
|
||||
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
|
||||
|
||||
|
||||
this.logger.debug("WHMCS request failed, retrying", {
|
||||
attempt,
|
||||
maxAttempts,
|
||||
@ -295,15 +291,15 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
private updateQueueMetrics(): void {
|
||||
this.metrics.queueSize = this.queue.size;
|
||||
this.metrics.pendingRequests = this.queue.pending;
|
||||
|
||||
|
||||
// Calculate averages
|
||||
if (this.waitTimes.length > 0) {
|
||||
this.metrics.averageWaitTime =
|
||||
this.metrics.averageWaitTime =
|
||||
this.waitTimes.reduce((sum, time) => sum + time, 0) / this.waitTimes.length;
|
||||
}
|
||||
|
||||
if (this.executionTimes.length > 0) {
|
||||
this.metrics.averageExecutionTime =
|
||||
this.metrics.averageExecutionTime =
|
||||
this.executionTimes.reduce((sum, time) => sum + time, 0) / this.executionTimes.length;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Req, Res, UseGuards, Inject } from "@nestjs/common";
|
||||
import { Controller, Get, Post, Req, Res, Inject } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from "@nestjs/swagger";
|
||||
import type { Request, Response } from "express";
|
||||
import { Logger } from "nestjs-pino";
|
||||
@ -8,30 +8,30 @@ interface AuthenticatedRequest extends Request {
|
||||
user?: { id: string; sessionId?: string };
|
||||
}
|
||||
|
||||
@ApiTags('Security')
|
||||
@Controller('security/csrf')
|
||||
@ApiTags("Security")
|
||||
@Controller("security/csrf")
|
||||
export class CsrfController {
|
||||
constructor(
|
||||
private readonly csrfService: CsrfService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
@Get('token')
|
||||
@ApiOperation({
|
||||
summary: 'Get CSRF token',
|
||||
description: 'Generates and returns a new CSRF token for the current session'
|
||||
@Get("token")
|
||||
@ApiOperation({
|
||||
summary: "Get CSRF token",
|
||||
description: "Generates and returns a new CSRF token for the current session",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'CSRF token generated successfully',
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "CSRF token generated successfully",
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
token: { type: 'string', example: 'abc123...' },
|
||||
expiresAt: { type: 'string', format: 'date-time' }
|
||||
}
|
||||
}
|
||||
success: { type: "boolean", example: true },
|
||||
token: { type: "string", example: "abc123..." },
|
||||
expiresAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
},
|
||||
})
|
||||
getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
|
||||
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
|
||||
@ -41,49 +41,49 @@ export class CsrfController {
|
||||
const tokenData = this.csrfService.generateToken(sessionId, userId);
|
||||
|
||||
// Set CSRF secret in secure cookie
|
||||
res.cookie('csrf-secret', tokenData.secret, {
|
||||
res.cookie("csrf-secret", tokenData.secret, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 3600000, // 1 hour
|
||||
path: '/',
|
||||
path: "/",
|
||||
});
|
||||
|
||||
this.logger.debug("CSRF token requested", {
|
||||
userId,
|
||||
sessionId,
|
||||
userAgent: req.get('user-agent'),
|
||||
ip: req.ip
|
||||
userAgent: req.get("user-agent"),
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
token: tokenData.token,
|
||||
expiresAt: tokenData.expiresAt.toISOString()
|
||||
expiresAt: tokenData.expiresAt.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@Post("refresh")
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: 'Refresh CSRF token',
|
||||
description: 'Invalidates current token and generates a new one for authenticated users'
|
||||
@ApiOperation({
|
||||
summary: "Refresh CSRF token",
|
||||
description: "Invalidates current token and generates a new one for authenticated users",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'CSRF token refreshed successfully',
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "CSRF token refreshed successfully",
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
token: { type: 'string', example: 'xyz789...' },
|
||||
expiresAt: { type: 'string', format: 'date-time' }
|
||||
}
|
||||
}
|
||||
success: { type: "boolean", example: true },
|
||||
token: { type: "string", example: "xyz789..." },
|
||||
expiresAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
},
|
||||
})
|
||||
refreshCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
|
||||
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
|
||||
const userId = req.user?.id || 'anonymous'; // Default for unauthenticated users
|
||||
const userId = req.user?.id || "anonymous"; // Default for unauthenticated users
|
||||
|
||||
// Invalidate existing tokens for this user
|
||||
this.csrfService.invalidateUserTokens(userId);
|
||||
@ -92,76 +92,75 @@ export class CsrfController {
|
||||
const tokenData = this.csrfService.generateToken(sessionId, userId);
|
||||
|
||||
// Set CSRF secret in secure cookie
|
||||
res.cookie('csrf-secret', tokenData.secret, {
|
||||
res.cookie("csrf-secret", tokenData.secret, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 3600000, // 1 hour
|
||||
path: '/',
|
||||
path: "/",
|
||||
});
|
||||
|
||||
this.logger.debug("CSRF token refreshed", {
|
||||
userId,
|
||||
sessionId,
|
||||
userAgent: req.get('user-agent'),
|
||||
ip: req.ip
|
||||
userAgent: req.get("user-agent"),
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
token: tokenData.token,
|
||||
expiresAt: tokenData.expiresAt.toISOString()
|
||||
expiresAt: tokenData.expiresAt.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@Get("stats")
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: 'Get CSRF token statistics',
|
||||
description: 'Returns statistics about CSRF tokens (admin/monitoring endpoint)'
|
||||
@ApiOperation({
|
||||
summary: "Get CSRF token statistics",
|
||||
description: "Returns statistics about CSRF tokens (admin/monitoring endpoint)",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'CSRF token statistics',
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "CSRF token statistics",
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
success: { type: "boolean", example: true },
|
||||
stats: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
totalTokens: { type: 'number', example: 150 },
|
||||
activeTokens: { type: 'number', example: 120 },
|
||||
expiredTokens: { type: 'number', example: 30 },
|
||||
cacheSize: { type: 'number', example: 150 },
|
||||
maxCacheSize: { type: 'number', example: 10000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
totalTokens: { type: "number", example: 150 },
|
||||
activeTokens: { type: "number", example: 120 },
|
||||
expiredTokens: { type: "number", example: 30 },
|
||||
cacheSize: { type: "number", example: 150 },
|
||||
maxCacheSize: { type: "number", example: 10000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
getCsrfStats(@Req() req: AuthenticatedRequest) {
|
||||
const userId = req.user?.id || 'anonymous';
|
||||
|
||||
const userId = req.user?.id || "anonymous";
|
||||
|
||||
// Only allow admin users to see stats (you might want to add role checking)
|
||||
this.logger.debug("CSRF stats requested", {
|
||||
userId,
|
||||
userAgent: req.get('user-agent'),
|
||||
ip: req.ip
|
||||
userAgent: req.get("user-agent"),
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
const stats = this.csrfService.getTokenStats();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats
|
||||
stats,
|
||||
};
|
||||
}
|
||||
|
||||
private extractSessionId(req: AuthenticatedRequest): string | null {
|
||||
return req.cookies?.['session-id'] ||
|
||||
req.cookies?.['connect.sid'] ||
|
||||
(req as any).sessionID ||
|
||||
null;
|
||||
return (
|
||||
req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || (req as any).sessionID || null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,19 +26,19 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {
|
||||
this.isProduction = this.configService.get("NODE_ENV") === "production";
|
||||
|
||||
|
||||
// Paths that don't require CSRF protection
|
||||
this.exemptPaths = new Set([
|
||||
'/api/auth/login',
|
||||
'/api/auth/signup',
|
||||
'/api/auth/refresh',
|
||||
'/api/health',
|
||||
'/docs',
|
||||
'/api/webhooks', // Webhooks typically don't use CSRF
|
||||
"/api/auth/login",
|
||||
"/api/auth/signup",
|
||||
"/api/auth/refresh",
|
||||
"/api/health",
|
||||
"/docs",
|
||||
"/api/webhooks", // Webhooks typically don't use CSRF
|
||||
]);
|
||||
|
||||
// Methods that don't require CSRF protection (safe methods)
|
||||
this.exemptMethods = new Set(['GET', 'HEAD', 'OPTIONS']);
|
||||
this.exemptMethods = new Set(["GET", "HEAD", "OPTIONS"]);
|
||||
}
|
||||
|
||||
use(req: CsrfRequest, res: Response, next: NextFunction): void {
|
||||
@ -68,7 +68,7 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
}
|
||||
|
||||
// Check for API endpoints that might be exempt
|
||||
if (req.path.startsWith('/api/webhooks/')) {
|
||||
if (req.path.startsWith("/api/webhooks/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
|
||||
private requiresCsrfProtection(req: CsrfRequest): boolean {
|
||||
// State-changing methods require CSRF protection
|
||||
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
|
||||
return ["POST", "PUT", "PATCH", "DELETE"].includes(req.method);
|
||||
}
|
||||
|
||||
private validateCsrfToken(req: CsrfRequest, res: Response, next: NextFunction): void {
|
||||
@ -90,8 +90,8 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
this.logger.warn("CSRF validation failed - missing token", {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
userAgent: req.get('user-agent'),
|
||||
ip: req.ip
|
||||
userAgent: req.get("user-agent"),
|
||||
ip: req.ip,
|
||||
});
|
||||
throw new ForbiddenException("CSRF token required");
|
||||
}
|
||||
@ -100,23 +100,28 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
this.logger.warn("CSRF validation failed - missing secret cookie", {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
userAgent: req.get('user-agent'),
|
||||
ip: req.ip
|
||||
userAgent: req.get("user-agent"),
|
||||
ip: req.ip,
|
||||
});
|
||||
throw new ForbiddenException("CSRF secret required");
|
||||
}
|
||||
|
||||
const validationResult = this.csrfService.validateToken(token, secret, sessionId || undefined, userId);
|
||||
const validationResult = this.csrfService.validateToken(
|
||||
token,
|
||||
secret,
|
||||
sessionId || undefined,
|
||||
userId
|
||||
);
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
this.logger.warn("CSRF validation failed", {
|
||||
reason: validationResult.reason,
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
userAgent: req.get('user-agent'),
|
||||
userAgent: req.get("user-agent"),
|
||||
ip: req.ip,
|
||||
userId,
|
||||
sessionId
|
||||
sessionId,
|
||||
});
|
||||
throw new ForbiddenException(`CSRF validation failed: ${validationResult.reason}`);
|
||||
}
|
||||
@ -128,7 +133,7 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
userId,
|
||||
sessionId
|
||||
sessionId,
|
||||
});
|
||||
|
||||
next();
|
||||
@ -151,13 +156,13 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
this.setCsrfSecretCookie(res, tokenData.secret);
|
||||
|
||||
// Set CSRF token in response header for client to use
|
||||
res.setHeader('X-CSRF-Token', tokenData.token);
|
||||
res.setHeader("X-CSRF-Token", tokenData.token);
|
||||
|
||||
this.logger.debug("CSRF token generated and set", {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
userId,
|
||||
sessionId
|
||||
sessionId,
|
||||
});
|
||||
|
||||
next();
|
||||
@ -165,30 +170,30 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
|
||||
private extractTokenFromRequest(req: CsrfRequest): string | null {
|
||||
// Check multiple possible locations for the CSRF token
|
||||
|
||||
|
||||
// 1. X-CSRF-Token header (most common)
|
||||
let token = req.get('X-CSRF-Token');
|
||||
let token = req.get("X-CSRF-Token");
|
||||
if (token) return token;
|
||||
|
||||
// 2. X-Requested-With header (alternative)
|
||||
token = req.get('X-Requested-With');
|
||||
if (token && token !== 'XMLHttpRequest') return token;
|
||||
token = req.get("X-Requested-With");
|
||||
if (token && token !== "XMLHttpRequest") return token;
|
||||
|
||||
// 3. Authorization header (if using Bearer token pattern)
|
||||
const authHeader = req.get('Authorization');
|
||||
if (authHeader && authHeader.startsWith('CSRF ')) {
|
||||
const authHeader = req.get("Authorization");
|
||||
if (authHeader && authHeader.startsWith("CSRF ")) {
|
||||
return authHeader.substring(5);
|
||||
}
|
||||
|
||||
// 4. Request body (for form submissions)
|
||||
if (req.body && typeof req.body === 'object') {
|
||||
if (req.body && typeof req.body === "object") {
|
||||
token = req.body._csrf || req.body.csrfToken;
|
||||
if (token) return token;
|
||||
}
|
||||
|
||||
// 5. Query parameter (least secure, only for GET requests)
|
||||
if (req.method === 'GET') {
|
||||
token = req.query._csrf as string || req.query.csrfToken as string;
|
||||
if (req.method === "GET") {
|
||||
token = (req.query._csrf as string) || (req.query.csrfToken as string);
|
||||
if (token) return token;
|
||||
}
|
||||
|
||||
@ -196,26 +201,23 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
}
|
||||
|
||||
private extractSecretFromCookie(req: CsrfRequest): string | null {
|
||||
return req.cookies?.['csrf-secret'] || null;
|
||||
return req.cookies?.["csrf-secret"] || null;
|
||||
}
|
||||
|
||||
private extractSessionId(req: CsrfRequest): string | null {
|
||||
// Try to extract session ID from various sources
|
||||
return req.cookies?.['session-id'] ||
|
||||
req.cookies?.['connect.sid'] ||
|
||||
req.sessionID ||
|
||||
null;
|
||||
return req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || req.sessionID || null;
|
||||
}
|
||||
|
||||
private setCsrfSecretCookie(res: Response, secret: string): void {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: this.isProduction,
|
||||
sameSite: 'strict' as const,
|
||||
sameSite: "strict" as const,
|
||||
maxAge: 3600000, // 1 hour
|
||||
path: '/',
|
||||
path: "/",
|
||||
};
|
||||
|
||||
res.cookie('csrf-secret', secret, cookieOptions);
|
||||
res.cookie("csrf-secret", secret, cookieOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,8 +14,6 @@ import { CsrfController } from "./controllers/csrf.controller";
|
||||
export class SecurityModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// Apply CSRF middleware to all routes except those handled by the middleware itself
|
||||
consumer
|
||||
.apply(CsrfMiddleware)
|
||||
.forRoutes('*');
|
||||
consumer.apply(CsrfMiddleware).forRoutes("*");
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,9 +34,11 @@ export class CsrfService {
|
||||
) {
|
||||
this.tokenExpiry = Number(this.configService.get("CSRF_TOKEN_EXPIRY", "3600000")); // 1 hour default
|
||||
this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey();
|
||||
|
||||
|
||||
if (!this.configService.get("CSRF_SECRET_KEY")) {
|
||||
this.logger.warn("CSRF_SECRET_KEY not configured, using generated key (not suitable for production)");
|
||||
this.logger.warn(
|
||||
"CSRF_SECRET_KEY not configured, using generated key (not suitable for production)"
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up expired tokens periodically
|
||||
@ -56,12 +58,12 @@ export class CsrfService {
|
||||
secret,
|
||||
expiresAt,
|
||||
sessionId,
|
||||
userId
|
||||
userId,
|
||||
};
|
||||
|
||||
// Store in cache for validation
|
||||
this.tokenCache.set(token, tokenData);
|
||||
|
||||
|
||||
// Prevent memory leaks
|
||||
if (this.tokenCache.size > this.maxCacheSize) {
|
||||
this.cleanupExpiredTokens();
|
||||
@ -71,7 +73,7 @@ export class CsrfService {
|
||||
tokenHash: this.hashToken(token),
|
||||
sessionId,
|
||||
userId,
|
||||
expiresAt: expiresAt.toISOString()
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
});
|
||||
|
||||
return tokenData;
|
||||
@ -81,15 +83,15 @@ export class CsrfService {
|
||||
* Validate a CSRF token against the provided secret
|
||||
*/
|
||||
validateToken(
|
||||
token: string,
|
||||
secret: string,
|
||||
sessionId?: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
sessionId?: string,
|
||||
userId?: string
|
||||
): CsrfValidationResult {
|
||||
if (!token || !secret) {
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "Missing token or secret"
|
||||
reason: "Missing token or secret",
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,7 +100,7 @@ export class CsrfService {
|
||||
if (!cachedTokenData) {
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "Token not found or expired"
|
||||
reason: "Token not found or expired",
|
||||
};
|
||||
}
|
||||
|
||||
@ -107,7 +109,7 @@ export class CsrfService {
|
||||
this.tokenCache.delete(token);
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "Token expired"
|
||||
reason: "Token expired",
|
||||
};
|
||||
}
|
||||
|
||||
@ -116,11 +118,11 @@ export class CsrfService {
|
||||
this.logger.warn("CSRF token validation failed - secret mismatch", {
|
||||
tokenHash: this.hashToken(token),
|
||||
sessionId,
|
||||
userId
|
||||
userId,
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "Invalid secret"
|
||||
reason: "Invalid secret",
|
||||
};
|
||||
}
|
||||
|
||||
@ -129,11 +131,11 @@ export class CsrfService {
|
||||
this.logger.warn("CSRF token validation failed - session mismatch", {
|
||||
tokenHash: this.hashToken(token),
|
||||
expectedSession: cachedTokenData.sessionId,
|
||||
providedSession: sessionId
|
||||
providedSession: sessionId,
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "Session mismatch"
|
||||
reason: "Session mismatch",
|
||||
};
|
||||
}
|
||||
|
||||
@ -142,18 +144,18 @@ export class CsrfService {
|
||||
this.logger.warn("CSRF token validation failed - user mismatch", {
|
||||
tokenHash: this.hashToken(token),
|
||||
expectedUser: cachedTokenData.userId,
|
||||
providedUser: userId
|
||||
providedUser: userId,
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "User mismatch"
|
||||
reason: "User mismatch",
|
||||
};
|
||||
}
|
||||
|
||||
// Regenerate expected token to prevent timing attacks
|
||||
const expectedToken = this.generateTokenFromSecret(
|
||||
cachedTokenData.secret,
|
||||
cachedTokenData.sessionId,
|
||||
cachedTokenData.secret,
|
||||
cachedTokenData.sessionId,
|
||||
cachedTokenData.userId
|
||||
);
|
||||
|
||||
@ -162,23 +164,23 @@ export class CsrfService {
|
||||
this.logger.warn("CSRF token validation failed - token mismatch", {
|
||||
tokenHash: this.hashToken(token),
|
||||
sessionId,
|
||||
userId
|
||||
userId,
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
reason: "Invalid token"
|
||||
reason: "Invalid token",
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.debug("CSRF token validated successfully", {
|
||||
tokenHash: this.hashToken(token),
|
||||
sessionId,
|
||||
userId
|
||||
userId,
|
||||
});
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
tokenData: cachedTokenData
|
||||
tokenData: cachedTokenData,
|
||||
};
|
||||
}
|
||||
|
||||
@ -188,7 +190,7 @@ export class CsrfService {
|
||||
invalidateToken(token: string): void {
|
||||
this.tokenCache.delete(token);
|
||||
this.logger.debug("CSRF token invalidated", {
|
||||
tokenHash: this.hashToken(token)
|
||||
tokenHash: this.hashToken(token),
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,10 +205,10 @@ export class CsrfService {
|
||||
invalidatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug("CSRF tokens invalidated for session", {
|
||||
sessionId,
|
||||
invalidatedCount
|
||||
invalidatedCount,
|
||||
});
|
||||
}
|
||||
|
||||
@ -221,10 +223,10 @@ export class CsrfService {
|
||||
invalidatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug("CSRF tokens invalidated for user", {
|
||||
userId,
|
||||
invalidatedCount
|
||||
invalidatedCount,
|
||||
});
|
||||
}
|
||||
|
||||
@ -249,30 +251,32 @@ export class CsrfService {
|
||||
activeTokens,
|
||||
expiredTokens,
|
||||
cacheSize: this.tokenCache.size,
|
||||
maxCacheSize: this.maxCacheSize
|
||||
maxCacheSize: this.maxCacheSize,
|
||||
};
|
||||
}
|
||||
|
||||
private generateSecret(): string {
|
||||
return crypto.randomBytes(32).toString('base64url');
|
||||
return crypto.randomBytes(32).toString("base64url");
|
||||
}
|
||||
|
||||
private generateTokenFromSecret(secret: string, sessionId?: string, userId?: string): string {
|
||||
const data = [secret, sessionId || '', userId || ''].join('|');
|
||||
const hmac = crypto.createHmac('sha256', this.secretKey);
|
||||
const data = [secret, sessionId || "", userId || ""].join("|");
|
||||
const hmac = crypto.createHmac("sha256", this.secretKey);
|
||||
hmac.update(data);
|
||||
return hmac.digest('base64url');
|
||||
return hmac.digest("base64url");
|
||||
}
|
||||
|
||||
private generateSecretKey(): string {
|
||||
const key = crypto.randomBytes(64).toString('base64url');
|
||||
this.logger.warn("Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production");
|
||||
const key = crypto.randomBytes(64).toString("base64url");
|
||||
this.logger.warn(
|
||||
"Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production"
|
||||
);
|
||||
return key;
|
||||
}
|
||||
|
||||
private hashToken(token: string): string {
|
||||
// Create a hash of the token for logging (never log the actual token)
|
||||
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
|
||||
return crypto.createHash("sha256").update(token).digest("hex").substring(0, 16);
|
||||
}
|
||||
|
||||
private constantTimeEquals(a: string, b: string): boolean {
|
||||
@ -302,7 +306,7 @@ export class CsrfService {
|
||||
if (cleanedCount > 0) {
|
||||
this.logger.debug("Cleaned up expired CSRF tokens", {
|
||||
cleanedCount,
|
||||
remainingTokens: this.tokenCache.size
|
||||
remainingTokens: this.tokenCache.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,13 +14,13 @@ export interface ErrorContext {
|
||||
export interface SecureErrorMapping {
|
||||
code: string;
|
||||
publicMessage: string;
|
||||
logLevel: 'error' | 'warn' | 'info' | 'debug';
|
||||
logLevel: "error" | "warn" | "info" | "debug";
|
||||
shouldAlert?: boolean; // Whether to send alerts to monitoring
|
||||
}
|
||||
|
||||
export interface ErrorClassification {
|
||||
category: 'authentication' | 'authorization' | 'validation' | 'business' | 'system' | 'external';
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
category: "authentication" | "authorization" | "validation" | "business" | "system" | "external";
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
mapping: SecureErrorMapping;
|
||||
}
|
||||
|
||||
@ -46,13 +46,10 @@ export class SecureErrorMapperService {
|
||||
/**
|
||||
* Map an error to a secure public message
|
||||
*/
|
||||
mapError(
|
||||
error: unknown,
|
||||
context?: ErrorContext
|
||||
): ErrorClassification {
|
||||
mapError(error: unknown, context?: ErrorContext): ErrorClassification {
|
||||
const errorMessage = this.extractErrorMessage(error);
|
||||
const errorCode = this.extractErrorCode(error);
|
||||
|
||||
|
||||
// Try exact code mapping first
|
||||
if (errorCode && this.errorMappings.has(errorCode)) {
|
||||
const mapping = this.errorMappings.get(errorCode)!;
|
||||
@ -76,7 +73,7 @@ export class SecureErrorMapperService {
|
||||
*/
|
||||
getPublicMessage(error: unknown, context?: ErrorContext): string {
|
||||
const classification = this.mapError(error, context);
|
||||
|
||||
|
||||
// In development, show more details
|
||||
if (this.isDevelopment) {
|
||||
const originalMessage = this.extractErrorMessage(error);
|
||||
@ -90,13 +87,13 @@ export class SecureErrorMapperService {
|
||||
* Log error with appropriate security level
|
||||
*/
|
||||
logSecureError(
|
||||
error: unknown,
|
||||
error: unknown,
|
||||
context?: ErrorContext,
|
||||
additionalData?: Record<string, unknown>
|
||||
): void {
|
||||
const classification = this.mapError(error, context);
|
||||
const originalMessage = this.extractErrorMessage(error);
|
||||
|
||||
|
||||
const logData = {
|
||||
errorCode: classification.mapping.code,
|
||||
category: classification.category,
|
||||
@ -104,27 +101,27 @@ export class SecureErrorMapperService {
|
||||
publicMessage: classification.mapping.publicMessage,
|
||||
originalMessage: this.sanitizeForLogging(originalMessage),
|
||||
context,
|
||||
...additionalData
|
||||
...additionalData,
|
||||
};
|
||||
|
||||
// Log based on severity and log level
|
||||
switch (classification.mapping.logLevel) {
|
||||
case 'error':
|
||||
case "error":
|
||||
this.logger.error(`Security Error: ${classification.mapping.code}`, logData);
|
||||
break;
|
||||
case 'warn':
|
||||
case "warn":
|
||||
this.logger.warn(`Security Warning: ${classification.mapping.code}`, logData);
|
||||
break;
|
||||
case 'info':
|
||||
case "info":
|
||||
this.logger.log(`Security Info: ${classification.mapping.code}`, logData);
|
||||
break;
|
||||
case 'debug':
|
||||
case "debug":
|
||||
this.logger.debug(`Security Debug: ${classification.mapping.code}`, logData);
|
||||
break;
|
||||
}
|
||||
|
||||
// Send alerts for critical errors
|
||||
if (classification.mapping.shouldAlert && classification.severity === 'critical') {
|
||||
if (classification.mapping.shouldAlert && classification.severity === "critical") {
|
||||
this.sendSecurityAlert(classification, context, logData);
|
||||
}
|
||||
}
|
||||
@ -132,101 +129,149 @@ export class SecureErrorMapperService {
|
||||
private initializeErrorMappings(): Map<string, SecureErrorMapping> {
|
||||
return new Map([
|
||||
// Authentication Errors
|
||||
['INVALID_CREDENTIALS', {
|
||||
code: 'AUTH_001',
|
||||
publicMessage: 'Invalid email or password',
|
||||
logLevel: 'warn'
|
||||
}],
|
||||
['ACCOUNT_LOCKED', {
|
||||
code: 'AUTH_002',
|
||||
publicMessage: 'Account temporarily locked. Please try again later',
|
||||
logLevel: 'warn'
|
||||
}],
|
||||
['TOKEN_EXPIRED', {
|
||||
code: 'AUTH_003',
|
||||
publicMessage: 'Session expired. Please log in again',
|
||||
logLevel: 'info'
|
||||
}],
|
||||
['TOKEN_INVALID', {
|
||||
code: 'AUTH_004',
|
||||
publicMessage: 'Invalid session. Please log in again',
|
||||
logLevel: 'warn'
|
||||
}],
|
||||
[
|
||||
"INVALID_CREDENTIALS",
|
||||
{
|
||||
code: "AUTH_001",
|
||||
publicMessage: "Invalid email or password",
|
||||
logLevel: "warn",
|
||||
},
|
||||
],
|
||||
[
|
||||
"ACCOUNT_LOCKED",
|
||||
{
|
||||
code: "AUTH_002",
|
||||
publicMessage: "Account temporarily locked. Please try again later",
|
||||
logLevel: "warn",
|
||||
},
|
||||
],
|
||||
[
|
||||
"TOKEN_EXPIRED",
|
||||
{
|
||||
code: "AUTH_003",
|
||||
publicMessage: "Session expired. Please log in again",
|
||||
logLevel: "info",
|
||||
},
|
||||
],
|
||||
[
|
||||
"TOKEN_INVALID",
|
||||
{
|
||||
code: "AUTH_004",
|
||||
publicMessage: "Invalid session. Please log in again",
|
||||
logLevel: "warn",
|
||||
},
|
||||
],
|
||||
|
||||
// Authorization Errors
|
||||
['INSUFFICIENT_PERMISSIONS', {
|
||||
code: 'AUTHZ_001',
|
||||
publicMessage: 'You do not have permission to perform this action',
|
||||
logLevel: 'warn'
|
||||
}],
|
||||
['RESOURCE_NOT_FOUND', {
|
||||
code: 'AUTHZ_002',
|
||||
publicMessage: 'The requested resource was not found',
|
||||
logLevel: 'info'
|
||||
}],
|
||||
// Authorization Errors
|
||||
[
|
||||
"INSUFFICIENT_PERMISSIONS",
|
||||
{
|
||||
code: "AUTHZ_001",
|
||||
publicMessage: "You do not have permission to perform this action",
|
||||
logLevel: "warn",
|
||||
},
|
||||
],
|
||||
[
|
||||
"RESOURCE_NOT_FOUND",
|
||||
{
|
||||
code: "AUTHZ_002",
|
||||
publicMessage: "The requested resource was not found",
|
||||
logLevel: "info",
|
||||
},
|
||||
],
|
||||
|
||||
// Validation Errors
|
||||
['VALIDATION_FAILED', {
|
||||
code: 'VAL_001',
|
||||
publicMessage: 'The provided data is invalid',
|
||||
logLevel: 'info'
|
||||
}],
|
||||
['REQUIRED_FIELD_MISSING', {
|
||||
code: 'VAL_002',
|
||||
publicMessage: 'Required information is missing',
|
||||
logLevel: 'info'
|
||||
}],
|
||||
[
|
||||
"VALIDATION_FAILED",
|
||||
{
|
||||
code: "VAL_001",
|
||||
publicMessage: "The provided data is invalid",
|
||||
logLevel: "info",
|
||||
},
|
||||
],
|
||||
[
|
||||
"REQUIRED_FIELD_MISSING",
|
||||
{
|
||||
code: "VAL_002",
|
||||
publicMessage: "Required information is missing",
|
||||
logLevel: "info",
|
||||
},
|
||||
],
|
||||
|
||||
// Business Logic Errors
|
||||
['ORDER_ALREADY_PROCESSED', {
|
||||
code: 'BIZ_001',
|
||||
publicMessage: 'This order has already been processed',
|
||||
logLevel: 'info'
|
||||
}],
|
||||
['INSUFFICIENT_BALANCE', {
|
||||
code: 'BIZ_002',
|
||||
publicMessage: 'Insufficient account balance',
|
||||
logLevel: 'info'
|
||||
}],
|
||||
['SERVICE_UNAVAILABLE', {
|
||||
code: 'BIZ_003',
|
||||
publicMessage: 'Service is temporarily unavailable',
|
||||
logLevel: 'warn'
|
||||
}],
|
||||
[
|
||||
"ORDER_ALREADY_PROCESSED",
|
||||
{
|
||||
code: "BIZ_001",
|
||||
publicMessage: "This order has already been processed",
|
||||
logLevel: "info",
|
||||
},
|
||||
],
|
||||
[
|
||||
"INSUFFICIENT_BALANCE",
|
||||
{
|
||||
code: "BIZ_002",
|
||||
publicMessage: "Insufficient account balance",
|
||||
logLevel: "info",
|
||||
},
|
||||
],
|
||||
[
|
||||
"SERVICE_UNAVAILABLE",
|
||||
{
|
||||
code: "BIZ_003",
|
||||
publicMessage: "Service is temporarily unavailable",
|
||||
logLevel: "warn",
|
||||
},
|
||||
],
|
||||
|
||||
// System Errors (High Security)
|
||||
['DATABASE_ERROR', {
|
||||
code: 'SYS_001',
|
||||
publicMessage: 'A system error occurred. Please try again later',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}],
|
||||
['EXTERNAL_SERVICE_ERROR', {
|
||||
code: 'SYS_002',
|
||||
publicMessage: 'External service temporarily unavailable',
|
||||
logLevel: 'error'
|
||||
}],
|
||||
['CONFIGURATION_ERROR', {
|
||||
code: 'SYS_003',
|
||||
publicMessage: 'System configuration error',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}],
|
||||
[
|
||||
"DATABASE_ERROR",
|
||||
{
|
||||
code: "SYS_001",
|
||||
publicMessage: "A system error occurred. Please try again later",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
"EXTERNAL_SERVICE_ERROR",
|
||||
{
|
||||
code: "SYS_002",
|
||||
publicMessage: "External service temporarily unavailable",
|
||||
logLevel: "error",
|
||||
},
|
||||
],
|
||||
[
|
||||
"CONFIGURATION_ERROR",
|
||||
{
|
||||
code: "SYS_003",
|
||||
publicMessage: "System configuration error",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Rate Limiting
|
||||
['RATE_LIMIT_EXCEEDED', {
|
||||
code: 'RATE_001',
|
||||
publicMessage: 'Too many requests. Please try again later',
|
||||
logLevel: 'warn'
|
||||
}],
|
||||
[
|
||||
"RATE_LIMIT_EXCEEDED",
|
||||
{
|
||||
code: "RATE_001",
|
||||
publicMessage: "Too many requests. Please try again later",
|
||||
logLevel: "warn",
|
||||
},
|
||||
],
|
||||
|
||||
// Generic Fallbacks
|
||||
['UNKNOWN_ERROR', {
|
||||
code: 'GEN_001',
|
||||
publicMessage: 'An unexpected error occurred',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}]
|
||||
[
|
||||
"UNKNOWN_ERROR",
|
||||
{
|
||||
code: "GEN_001",
|
||||
publicMessage: "An unexpected error occurred",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -236,82 +281,82 @@ export class SecureErrorMapperService {
|
||||
{
|
||||
pattern: /database|connection|sql|prisma|postgres/i,
|
||||
mapping: {
|
||||
code: 'SYS_001',
|
||||
publicMessage: 'A system error occurred. Please try again later',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}
|
||||
code: "SYS_001",
|
||||
publicMessage: "A system error occurred. Please try again later",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Authentication patterns
|
||||
{
|
||||
pattern: /password|credential|token|secret|key|auth/i,
|
||||
mapping: {
|
||||
code: 'AUTH_001',
|
||||
publicMessage: 'Authentication failed',
|
||||
logLevel: 'warn'
|
||||
}
|
||||
code: "AUTH_001",
|
||||
publicMessage: "Authentication failed",
|
||||
logLevel: "warn",
|
||||
},
|
||||
},
|
||||
|
||||
// File system patterns
|
||||
{
|
||||
pattern: /file|path|directory|permission denied|enoent|eacces/i,
|
||||
mapping: {
|
||||
code: 'SYS_002',
|
||||
publicMessage: 'System resource error',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}
|
||||
code: "SYS_002",
|
||||
publicMessage: "System resource error",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Network/External service patterns
|
||||
{
|
||||
pattern: /network|timeout|connection refused|econnrefused|whmcs|salesforce/i,
|
||||
mapping: {
|
||||
code: 'SYS_002',
|
||||
publicMessage: 'External service temporarily unavailable',
|
||||
logLevel: 'error'
|
||||
}
|
||||
code: "SYS_002",
|
||||
publicMessage: "External service temporarily unavailable",
|
||||
logLevel: "error",
|
||||
},
|
||||
},
|
||||
|
||||
// Stack trace patterns
|
||||
{
|
||||
pattern: /\s+at\s+|\.js:\d+|\.ts:\d+|stack trace/i,
|
||||
mapping: {
|
||||
code: 'SYS_001',
|
||||
publicMessage: 'A system error occurred. Please try again later',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}
|
||||
code: "SYS_001",
|
||||
publicMessage: "A system error occurred. Please try again later",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Memory/Resource patterns
|
||||
{
|
||||
pattern: /memory|heap|out of memory|resource|limit exceeded/i,
|
||||
mapping: {
|
||||
code: 'SYS_003',
|
||||
publicMessage: 'System resources temporarily unavailable',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
}
|
||||
code: "SYS_003",
|
||||
publicMessage: "System resources temporarily unavailable",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Validation patterns
|
||||
{
|
||||
pattern: /invalid|required|missing|validation|format/i,
|
||||
mapping: {
|
||||
code: 'VAL_001',
|
||||
publicMessage: 'The provided data is invalid',
|
||||
logLevel: 'info'
|
||||
}
|
||||
}
|
||||
code: "VAL_001",
|
||||
publicMessage: "The provided data is invalid",
|
||||
logLevel: "info",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private createClassification(
|
||||
originalMessage: string,
|
||||
mapping: SecureErrorMapping,
|
||||
context?: ErrorContext
|
||||
_context?: ErrorContext
|
||||
): ErrorClassification {
|
||||
// Determine category and severity based on error code
|
||||
const category = this.determineCategory(mapping.code);
|
||||
@ -320,50 +365,50 @@ export class SecureErrorMapperService {
|
||||
return {
|
||||
category,
|
||||
severity,
|
||||
mapping
|
||||
mapping,
|
||||
};
|
||||
}
|
||||
|
||||
private determineCategory(code: string): ErrorClassification['category'] {
|
||||
if (code.startsWith('AUTH_')) return 'authentication';
|
||||
if (code.startsWith('AUTHZ_')) return 'authorization';
|
||||
if (code.startsWith('VAL_')) return 'validation';
|
||||
if (code.startsWith('BIZ_')) return 'business';
|
||||
if (code.startsWith('SYS_')) return 'system';
|
||||
return 'system';
|
||||
private determineCategory(code: string): ErrorClassification["category"] {
|
||||
if (code.startsWith("AUTH_")) return "authentication";
|
||||
if (code.startsWith("AUTHZ_")) return "authorization";
|
||||
if (code.startsWith("VAL_")) return "validation";
|
||||
if (code.startsWith("BIZ_")) return "business";
|
||||
if (code.startsWith("SYS_")) return "system";
|
||||
return "system";
|
||||
}
|
||||
|
||||
private determineSeverity(code: string, message: string): ErrorClassification['severity'] {
|
||||
private determineSeverity(code: string, message: string): ErrorClassification["severity"] {
|
||||
// Critical system errors
|
||||
if (code === 'SYS_001' || code === 'SYS_003') return 'critical';
|
||||
|
||||
if (code === "SYS_001" || code === "SYS_003") return "critical";
|
||||
|
||||
// High severity for authentication issues
|
||||
if (code.startsWith('AUTH_') && message.toLowerCase().includes('breach')) return 'high';
|
||||
|
||||
if (code.startsWith("AUTH_") && message.toLowerCase().includes("breach")) return "high";
|
||||
|
||||
// Medium for external service issues
|
||||
if (code === 'SYS_002') return 'medium';
|
||||
|
||||
if (code === "SYS_002") return "medium";
|
||||
|
||||
// Low for validation and business logic
|
||||
if (code.startsWith('VAL_') || code.startsWith('BIZ_')) return 'low';
|
||||
|
||||
return 'medium';
|
||||
if (code.startsWith("VAL_") || code.startsWith("BIZ_")) return "low";
|
||||
|
||||
return "medium";
|
||||
}
|
||||
|
||||
private getDefaultMapping(message: string): SecureErrorMapping {
|
||||
// Analyze message for sensitivity
|
||||
if (this.containsSensitiveInfo(message)) {
|
||||
return {
|
||||
code: 'SYS_001',
|
||||
publicMessage: 'A system error occurred. Please try again later',
|
||||
logLevel: 'error',
|
||||
shouldAlert: true
|
||||
code: "SYS_001",
|
||||
publicMessage: "A system error occurred. Please try again later",
|
||||
logLevel: "error",
|
||||
shouldAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 'GEN_001',
|
||||
publicMessage: 'An unexpected error occurred',
|
||||
logLevel: 'error'
|
||||
code: "GEN_001",
|
||||
publicMessage: "An unexpected error occurred",
|
||||
logLevel: "error",
|
||||
};
|
||||
}
|
||||
|
||||
@ -374,9 +419,9 @@ export class SecureErrorMapperService {
|
||||
/file|path|directory/i,
|
||||
/\s+at\s+.*\.js:\d+/i, // Stack traces
|
||||
/[a-zA-Z]:[\\\/]/, // Windows paths
|
||||
/\/[a-zA-Z0-9._\-/]+\.(js|ts|py|php)/i, // Unix paths
|
||||
/[/][a-zA-Z0-9._\-/]+\.(js|ts|py|php)/i, // Unix paths
|
||||
/\b(?:\d{1,3}\.){3}\d{1,3}\b/, // IP addresses
|
||||
/[A-Za-z0-9]{32,}/ // Long tokens/hashes
|
||||
/[A-Za-z0-9]{32,}/, // Long tokens/hashes
|
||||
];
|
||||
|
||||
return sensitivePatterns.some(pattern => pattern.test(message));
|
||||
@ -386,22 +431,22 @@ export class SecureErrorMapperService {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const obj = error as Record<string, unknown>;
|
||||
if (typeof obj.message === 'string') {
|
||||
if (typeof obj.message === "string") {
|
||||
return obj.message;
|
||||
}
|
||||
}
|
||||
return 'Unknown error';
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
private extractErrorCode(error: unknown): string | null {
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const obj = error as Record<string, unknown>;
|
||||
if (typeof obj.code === 'string') {
|
||||
if (typeof obj.code === "string") {
|
||||
return obj.code;
|
||||
}
|
||||
}
|
||||
@ -409,29 +454,31 @@ export class SecureErrorMapperService {
|
||||
}
|
||||
|
||||
private sanitizeForLogging(message: string): string {
|
||||
return message
|
||||
// Remove file paths
|
||||
.replace(/\/[a-zA-Z0-9._\-/]+\.(js|ts|py|php)/g, '[file]')
|
||||
// Remove stack traces
|
||||
.replace(/\s+at\s+.*/g, '')
|
||||
// Remove absolute paths
|
||||
.replace(/[a-zA-Z]:[\\\/][^:]+/g, '[path]')
|
||||
// Remove IP addresses
|
||||
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[ip]')
|
||||
// Remove URLs with credentials
|
||||
.replace(/https?:\/\/[^:]+:[^@]+@[^\s]+/g, '[url]')
|
||||
// Remove potential secrets
|
||||
.replace(/\b[A-Za-z0-9]{32,}\b/g, '[token]')
|
||||
.trim();
|
||||
return (
|
||||
message
|
||||
// Remove file paths
|
||||
.replace(/[/][a-zA-Z0-9._\-/]+\.(js|ts|py|php)/g, "[file]")
|
||||
// Remove stack traces
|
||||
.replace(/\s+at\s+.*/g, "")
|
||||
// Remove absolute paths
|
||||
.replace(/[a-zA-Z]:[\\\/][^:]+/g, "[path]")
|
||||
// Remove IP addresses
|
||||
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[ip]")
|
||||
// Remove URLs with credentials
|
||||
.replace(/https?:\/\/[^:]+:[^@]+@[^\s]+/g, "[url]")
|
||||
// Remove potential secrets
|
||||
.replace(/\b[A-Za-z0-9]{32,}\b/g, "[token]")
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
private sanitizeForDevelopment(message: string): string {
|
||||
// In development, show more but still remove the most sensitive parts
|
||||
return message
|
||||
.replace(/password[=:]\s*[^\s]+/gi, 'password=[HIDDEN]')
|
||||
.replace(/secret[=:]\s*[^\s]+/gi, 'secret=[HIDDEN]')
|
||||
.replace(/token[=:]\s*[^\s]+/gi, 'token=[HIDDEN]')
|
||||
.replace(/key[=:]\s*[^\s]+/gi, 'key=[HIDDEN]');
|
||||
.replace(/password[=:]\s*[^\s]+/gi, "password=[HIDDEN]")
|
||||
.replace(/secret[=:]\s*[^\s]+/gi, "secret=[HIDDEN]")
|
||||
.replace(/token[=:]\s*[^\s]+/gi, "token=[HIDDEN]")
|
||||
.replace(/key[=:]\s*[^\s]+/gi, "key=[HIDDEN]");
|
||||
}
|
||||
|
||||
private sendSecurityAlert(
|
||||
@ -441,14 +488,14 @@ export class SecureErrorMapperService {
|
||||
): void {
|
||||
// In a real implementation, this would send alerts to monitoring systems
|
||||
// like Slack, PagerDuty, or custom alerting systems
|
||||
this.logger.error('SECURITY ALERT TRIGGERED', {
|
||||
alertType: 'CRITICAL_ERROR',
|
||||
this.logger.error("SECURITY ALERT TRIGGERED", {
|
||||
alertType: "CRITICAL_ERROR",
|
||||
errorCode: classification.mapping.code,
|
||||
category: classification.category,
|
||||
severity: classification.severity,
|
||||
context,
|
||||
timestamp: new Date().toISOString(),
|
||||
...logData
|
||||
...logData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,15 +192,16 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
||||
const errorData = data as SalesforcePubSubError;
|
||||
const details = errorData.details || "";
|
||||
const metadata = errorData.metadata || {};
|
||||
const errorCodes = Array.isArray(metadata["error-code"])
|
||||
? metadata["error-code"]
|
||||
: [];
|
||||
const errorCodes = Array.isArray(metadata["error-code"]) ? metadata["error-code"] : [];
|
||||
const hasCorruptionCode = errorCodes.some(code =>
|
||||
String(code).includes("replayid.corrupted")
|
||||
);
|
||||
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
|
||||
|
||||
if ((hasCorruptionCode || mentionsReplayValidation) && !this.replayCorruptionRecovered) {
|
||||
if (
|
||||
(hasCorruptionCode || mentionsReplayValidation) &&
|
||||
!this.replayCorruptionRecovered
|
||||
) {
|
||||
this.replayCorruptionRecovered = true;
|
||||
const key = sfReplayKey(this.channel);
|
||||
await this.cache.del(key);
|
||||
@ -291,7 +292,7 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
||||
this.replayCorruptionRecovered = false;
|
||||
}
|
||||
|
||||
return this.client!;
|
||||
return this.client;
|
||||
})();
|
||||
|
||||
try {
|
||||
@ -326,11 +327,7 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
||||
Number(storedReplay)
|
||||
);
|
||||
} else if (replayMode === "ALL") {
|
||||
await client.subscribeFromEarliestEvent(
|
||||
this.channel,
|
||||
this.subscribeCallback,
|
||||
numRequested
|
||||
);
|
||||
await client.subscribeFromEarliestEvent(this.channel, this.subscribeCallback, numRequested);
|
||||
} else {
|
||||
await client.subscribe(this.channel, this.subscribeCallback, numRequested);
|
||||
}
|
||||
|
||||
@ -363,7 +363,7 @@ export class SalesforceConnection {
|
||||
*/
|
||||
private getQueryPriority(soql: string): number {
|
||||
const lowerSoql = soql.toLowerCase();
|
||||
|
||||
|
||||
// High priority queries (critical for user experience)
|
||||
if (
|
||||
lowerSoql.includes("account") ||
|
||||
@ -372,7 +372,7 @@ export class SalesforceConnection {
|
||||
) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
|
||||
// Medium priority queries
|
||||
if (
|
||||
lowerSoql.includes("order") ||
|
||||
@ -381,7 +381,7 @@ export class SalesforceConnection {
|
||||
) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
|
||||
// Low priority (bulk queries, reports)
|
||||
return 2;
|
||||
}
|
||||
@ -391,7 +391,7 @@ export class SalesforceConnection {
|
||||
*/
|
||||
private isLongRunningQuery(soql: string): boolean {
|
||||
const lowerSoql = soql.toLowerCase();
|
||||
|
||||
|
||||
// Queries likely to take >20 seconds
|
||||
return (
|
||||
lowerSoql.includes("count(") ||
|
||||
|
||||
@ -67,32 +67,35 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
options: WhmcsRequestOptions = {}
|
||||
): Promise<T> {
|
||||
// Wrap the actual request in the queue to prevent race conditions
|
||||
return this.requestQueue.execute(async () => {
|
||||
try {
|
||||
const config = this.configService.getConfig();
|
||||
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
|
||||
return this.requestQueue.execute(
|
||||
async () => {
|
||||
try {
|
||||
const config = this.configService.getConfig();
|
||||
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
|
||||
|
||||
if (response.result === "error") {
|
||||
const errorResponse = response as WhmcsErrorResponse;
|
||||
this.errorHandler.handleApiError(errorResponse, action, params);
|
||||
if (response.result === "error") {
|
||||
const errorResponse = response as WhmcsErrorResponse;
|
||||
this.errorHandler.handleApiError(errorResponse, action, params);
|
||||
}
|
||||
|
||||
return response.data as T;
|
||||
} catch (error) {
|
||||
// If it's already a handled error, re-throw it
|
||||
if (this.isHandledException(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle general request errors
|
||||
this.errorHandler.handleRequestError(error, action, params);
|
||||
}
|
||||
|
||||
return response.data as T;
|
||||
} catch (error) {
|
||||
// If it's already a handled error, re-throw it
|
||||
if (this.isHandledException(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle general request errors
|
||||
this.errorHandler.handleRequestError(error, action, params);
|
||||
},
|
||||
{
|
||||
priority: this.getRequestPriority(action),
|
||||
timeout: options.timeout,
|
||||
retryAttempts: options.retryAttempts,
|
||||
retryDelay: options.retryDelay,
|
||||
}
|
||||
}, {
|
||||
priority: this.getRequestPriority(action),
|
||||
timeout: options.timeout,
|
||||
retryAttempts: options.retryAttempts,
|
||||
retryDelay: options.retryDelay,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -283,18 +286,14 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
// High priority actions (critical for user experience)
|
||||
const highPriorityActions = [
|
||||
"ValidateLogin",
|
||||
"GetClientDetails",
|
||||
"GetClientDetails",
|
||||
"GetInvoice",
|
||||
"CapturePayment",
|
||||
"CreateSsoToken"
|
||||
"CreateSsoToken",
|
||||
];
|
||||
|
||||
// Medium priority actions (important but can wait)
|
||||
const mediumPriorityActions = [
|
||||
"GetInvoices",
|
||||
"GetClientsProducts",
|
||||
"GetPayMethods"
|
||||
];
|
||||
const mediumPriorityActions = ["GetInvoices", "GetClientsProducts", "GetPayMethods"];
|
||||
|
||||
if (highPriorityActions.includes(action)) {
|
||||
return 8; // High priority
|
||||
|
||||
@ -85,7 +85,7 @@ export class WhmcsInvoiceService {
|
||||
this.logger.log(
|
||||
`Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}`
|
||||
);
|
||||
return result as InvoiceList;
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch invoices for client ${clientId}`, {
|
||||
error: getErrorMessage(error),
|
||||
@ -136,7 +136,7 @@ export class WhmcsInvoiceService {
|
||||
);
|
||||
|
||||
const result: InvoiceList = {
|
||||
invoices: invoicesWithItems as Invoice[],
|
||||
invoices: invoicesWithItems,
|
||||
pagination: invoiceList.pagination,
|
||||
};
|
||||
|
||||
@ -230,7 +230,7 @@ export class WhmcsInvoiceService {
|
||||
try {
|
||||
const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice);
|
||||
const parsed = invoiceSchema.parse(transformed);
|
||||
invoices.push(parsed as Invoice);
|
||||
invoices.push(parsed);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
|
||||
error: getErrorMessage(error),
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
import { Controller, Post, Body, UseGuards, Get, Req, HttpCode, UsePipes, Res } from "@nestjs/common";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
Get,
|
||||
Req,
|
||||
HttpCode,
|
||||
UsePipes,
|
||||
Res,
|
||||
} from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
import { AuthService } from "./auth.service";
|
||||
@ -99,10 +109,7 @@ export class AuthController {
|
||||
@ApiResponse({ status: 409, description: "Customer already has account" })
|
||||
@ApiResponse({ status: 400, description: "Customer number not found" })
|
||||
@ApiResponse({ status: 429, description: "Too many validation attempts" })
|
||||
async validateSignup(
|
||||
@Body() validateData: ValidateSignupRequestInput,
|
||||
@Req() req: Request
|
||||
) {
|
||||
async validateSignup(@Body() validateData: ValidateSignupRequestInput, @Req() req: Request) {
|
||||
return this.authService.validateSignup(validateData, req);
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +40,9 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
|
||||
override async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<RequestWithCookies & { method: string; url: string; route?: { path?: string } }>();
|
||||
.getRequest<
|
||||
RequestWithCookies & { method: string; url: string; route?: { path?: string } }
|
||||
>();
|
||||
const route = `${request.method} ${request.route?.path ?? request.url}`;
|
||||
|
||||
// Check if the route is marked as public
|
||||
|
||||
@ -110,7 +110,10 @@ function coerceNumber(value: unknown): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function baseProduct(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap): CatalogProductBase {
|
||||
function baseProduct(
|
||||
product: SalesforceCatalogProductRecord,
|
||||
fieldMap: SalesforceFieldMap
|
||||
): CatalogProductBase {
|
||||
const sku = getStringField(product, "sku", fieldMap) ?? "";
|
||||
const base: CatalogProductBase = {
|
||||
id: product.Id,
|
||||
@ -139,12 +142,18 @@ function getBoolean(
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveBundledAddonId(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap): string | undefined {
|
||||
function resolveBundledAddonId(
|
||||
product: SalesforceCatalogProductRecord,
|
||||
fieldMap: SalesforceFieldMap
|
||||
): string | undefined {
|
||||
const raw = getProductField(product, "bundledAddon", fieldMap);
|
||||
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
||||
}
|
||||
|
||||
function resolveBundledAddon(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap) {
|
||||
function resolveBundledAddon(
|
||||
product: SalesforceCatalogProductRecord,
|
||||
fieldMap: SalesforceFieldMap
|
||||
) {
|
||||
return {
|
||||
bundledAddonId: resolveBundledAddonId(product, fieldMap),
|
||||
isBundledAddon: Boolean(getBoolean(product, "isBundledAddon", fieldMap)),
|
||||
|
||||
@ -109,7 +109,11 @@ export class OrderBuilder {
|
||||
assignIfString(orderFields, orderField("accessMode", fieldMap), config.accessMode);
|
||||
}
|
||||
|
||||
private addSimFields(orderFields: Record<string, unknown>, body: OrderBusinessValidation, fieldMap: SalesforceFieldMap): void {
|
||||
private addSimFields(
|
||||
orderFields: Record<string, unknown>,
|
||||
body: OrderBusinessValidation,
|
||||
fieldMap: SalesforceFieldMap
|
||||
): void {
|
||||
const config = body.configurations || {};
|
||||
assignIfString(orderFields, orderField("simType", fieldMap), config.simType);
|
||||
assignIfString(orderFields, orderField("eid", fieldMap), config.eid);
|
||||
@ -119,7 +123,11 @@ export class OrderBuilder {
|
||||
assignIfString(orderFields, mnpField("reservationNumber", fieldMap), config.mnpNumber);
|
||||
assignIfString(orderFields, mnpField("expiryDate", fieldMap), config.mnpExpiry);
|
||||
assignIfString(orderFields, mnpField("phoneNumber", fieldMap), config.mnpPhone);
|
||||
assignIfString(orderFields, mnpField("mvnoAccountNumber", fieldMap), config.mvnoAccountNumber);
|
||||
assignIfString(
|
||||
orderFields,
|
||||
mnpField("mvnoAccountNumber", fieldMap),
|
||||
config.mvnoAccountNumber
|
||||
);
|
||||
assignIfString(orderFields, mnpField("portingLastName", fieldMap), config.portingLastName);
|
||||
assignIfString(orderFields, mnpField("portingFirstName", fieldMap), config.portingFirstName);
|
||||
assignIfString(
|
||||
@ -133,7 +141,11 @@ export class OrderBuilder {
|
||||
config.portingFirstNameKatakana
|
||||
);
|
||||
assignIfString(orderFields, mnpField("portingGender", fieldMap), config.portingGender);
|
||||
assignIfString(orderFields, mnpField("portingDateOfBirth", fieldMap), config.portingDateOfBirth);
|
||||
assignIfString(
|
||||
orderFields,
|
||||
mnpField("portingDateOfBirth", fieldMap),
|
||||
config.portingDateOfBirth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ export class OrderFulfillmentOrchestrator {
|
||||
} catch (error) {
|
||||
this.logger.error("Fulfillment validation failed", {
|
||||
sfOrderId,
|
||||
error: getErrorMessage(error)
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@ -118,164 +118,169 @@ export class OrderFulfillmentOrchestrator {
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to get order details", {
|
||||
sfOrderId,
|
||||
error: getErrorMessage(error)
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Step 3: Execute the main fulfillment workflow as a distributed transaction
|
||||
const fulfillmentResult = await this.distributedTransactionService.executeDistributedTransaction([
|
||||
{
|
||||
id: 'sf_status_update',
|
||||
description: 'Update Salesforce order status to Activating',
|
||||
execute: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
return await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
[fields.order.activationStatus]: "Activating",
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
[fields.order.activationStatus]: "Failed",
|
||||
});
|
||||
},
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
id: 'mapping',
|
||||
description: 'Map OrderItems to WHMCS format',
|
||||
execute: async () => {
|
||||
if (!context.orderDetails) {
|
||||
throw new Error("Order details are required for mapping");
|
||||
}
|
||||
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(
|
||||
context.orderDetails.items
|
||||
);
|
||||
},
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
id: 'whmcs_create',
|
||||
description: 'Create order in WHMCS',
|
||||
execute: async () => {
|
||||
const mappingResult = fulfillmentResult.stepResults?.mapping;
|
||||
if (!mappingResult) {
|
||||
throw new Error("Mapping result is not available");
|
||||
}
|
||||
const fulfillmentResult =
|
||||
await this.distributedTransactionService.executeDistributedTransaction(
|
||||
[
|
||||
{
|
||||
id: "sf_status_update",
|
||||
description: "Update Salesforce order status to Activating",
|
||||
execute: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
return await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
[fields.order.activationStatus]: "Activating",
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
[fields.order.activationStatus]: "Failed",
|
||||
});
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
{
|
||||
id: "mapping",
|
||||
description: "Map OrderItems to WHMCS format",
|
||||
execute: async () => {
|
||||
if (!context.orderDetails) {
|
||||
throw new Error("Order details are required for mapping");
|
||||
}
|
||||
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(context.orderDetails.items);
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
{
|
||||
id: "whmcs_create",
|
||||
description: "Create order in WHMCS",
|
||||
execute: async () => {
|
||||
const mappingResult = fulfillmentResult.stepResults?.mapping;
|
||||
if (!mappingResult) {
|
||||
throw new Error("Mapping result is not available");
|
||||
}
|
||||
|
||||
const orderNotes = this.orderWhmcsMapper.createOrderNotes(
|
||||
sfOrderId,
|
||||
`Provisioned from Salesforce Order ${sfOrderId}`
|
||||
);
|
||||
const orderNotes = this.orderWhmcsMapper.createOrderNotes(
|
||||
sfOrderId,
|
||||
`Provisioned from Salesforce Order ${sfOrderId}`
|
||||
);
|
||||
|
||||
return await this.whmcsOrderService.addOrder({
|
||||
clientId: context.validation!.clientId,
|
||||
items: mappingResult.whmcsItems,
|
||||
paymentMethod: "stripe",
|
||||
promoCode: "1st Month Free (Monthly Plan)",
|
||||
sfOrderId,
|
||||
notes: orderNotes,
|
||||
noinvoiceemail: true,
|
||||
noemail: true,
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||
if (createResult?.orderId) {
|
||||
// Note: WHMCS doesn't have an automated cancel API
|
||||
// Manual intervention required for order cleanup
|
||||
this.logger.error("WHMCS order created but fulfillment failed - manual cleanup required", {
|
||||
orderId: createResult.orderId,
|
||||
sfOrderId,
|
||||
action: "MANUAL_CLEANUP_REQUIRED"
|
||||
});
|
||||
}
|
||||
},
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
id: 'whmcs_accept',
|
||||
description: 'Accept/provision order in WHMCS',
|
||||
execute: async () => {
|
||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||
if (!createResult?.orderId) {
|
||||
throw new Error("WHMCS order ID missing before acceptance step");
|
||||
}
|
||||
return await this.whmcsOrderService.addOrder({
|
||||
clientId: context.validation!.clientId,
|
||||
items: mappingResult.whmcsItems,
|
||||
paymentMethod: "stripe",
|
||||
promoCode: "1st Month Free (Monthly Plan)",
|
||||
sfOrderId,
|
||||
notes: orderNotes,
|
||||
noinvoiceemail: true,
|
||||
noemail: true,
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||
if (createResult?.orderId) {
|
||||
// Note: WHMCS doesn't have an automated cancel API
|
||||
// Manual intervention required for order cleanup
|
||||
this.logger.error(
|
||||
"WHMCS order created but fulfillment failed - manual cleanup required",
|
||||
{
|
||||
orderId: createResult.orderId,
|
||||
sfOrderId,
|
||||
action: "MANUAL_CLEANUP_REQUIRED",
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
{
|
||||
id: "whmcs_accept",
|
||||
description: "Accept/provision order in WHMCS",
|
||||
execute: async () => {
|
||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||
if (!createResult?.orderId) {
|
||||
throw new Error("WHMCS order ID missing before acceptance step");
|
||||
}
|
||||
|
||||
return await this.whmcsOrderService.acceptOrder(
|
||||
createResult.orderId,
|
||||
sfOrderId
|
||||
);
|
||||
},
|
||||
rollback: async () => {
|
||||
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
if (acceptResult?.orderId) {
|
||||
// Note: WHMCS doesn't have an automated cancel API for accepted orders
|
||||
// Manual intervention required for service termination
|
||||
this.logger.error("WHMCS order accepted but fulfillment failed - manual cleanup required", {
|
||||
orderId: acceptResult.orderId,
|
||||
serviceIds: acceptResult.serviceIds,
|
||||
sfOrderId,
|
||||
action: "MANUAL_SERVICE_TERMINATION_REQUIRED"
|
||||
});
|
||||
}
|
||||
},
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
id: 'sim_fulfillment',
|
||||
description: 'SIM-specific fulfillment (if applicable)',
|
||||
execute: async () => {
|
||||
if (context.orderDetails?.orderType === "SIM") {
|
||||
const configurations = this.extractConfigurations(payload.configurations);
|
||||
await this.simFulfillmentService.fulfillSimOrder({
|
||||
orderDetails: context.orderDetails,
|
||||
configurations,
|
||||
});
|
||||
return { completed: true };
|
||||
}
|
||||
return { skipped: true };
|
||||
},
|
||||
critical: false // SIM fulfillment failure shouldn't rollback the entire order
|
||||
},
|
||||
{
|
||||
id: 'sf_success_update',
|
||||
description: 'Update Salesforce with success',
|
||||
execute: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
|
||||
return await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
Status: "Completed",
|
||||
[fields.order.activationStatus]: "Activated",
|
||||
[fields.order.whmcsOrderId]: whmcsResult?.orderId?.toString(),
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
[fields.order.activationStatus]: "Failed",
|
||||
});
|
||||
},
|
||||
critical: true
|
||||
}
|
||||
], {
|
||||
description: `Order fulfillment for ${sfOrderId}`,
|
||||
timeout: 300000, // 5 minutes
|
||||
continueOnNonCriticalFailure: true
|
||||
});
|
||||
return await this.whmcsOrderService.acceptOrder(createResult.orderId, sfOrderId);
|
||||
},
|
||||
rollback: async () => {
|
||||
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
if (acceptResult?.orderId) {
|
||||
// Note: WHMCS doesn't have an automated cancel API for accepted orders
|
||||
// Manual intervention required for service termination
|
||||
this.logger.error(
|
||||
"WHMCS order accepted but fulfillment failed - manual cleanup required",
|
||||
{
|
||||
orderId: acceptResult.orderId,
|
||||
serviceIds: acceptResult.serviceIds,
|
||||
sfOrderId,
|
||||
action: "MANUAL_SERVICE_TERMINATION_REQUIRED",
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
{
|
||||
id: "sim_fulfillment",
|
||||
description: "SIM-specific fulfillment (if applicable)",
|
||||
execute: async () => {
|
||||
if (context.orderDetails?.orderType === "SIM") {
|
||||
const configurations = this.extractConfigurations(payload.configurations);
|
||||
await this.simFulfillmentService.fulfillSimOrder({
|
||||
orderDetails: context.orderDetails,
|
||||
configurations,
|
||||
});
|
||||
return { completed: true };
|
||||
}
|
||||
return { skipped: true };
|
||||
},
|
||||
critical: false, // SIM fulfillment failure shouldn't rollback the entire order
|
||||
},
|
||||
{
|
||||
id: "sf_success_update",
|
||||
description: "Update Salesforce with success",
|
||||
execute: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
|
||||
return await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
Status: "Completed",
|
||||
[fields.order.activationStatus]: "Activated",
|
||||
[fields.order.whmcsOrderId]: whmcsResult?.orderId?.toString(),
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
[fields.order.activationStatus]: "Failed",
|
||||
});
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
description: `Order fulfillment for ${sfOrderId}`,
|
||||
timeout: 300000, // 5 minutes
|
||||
continueOnNonCriticalFailure: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (!fulfillmentResult.success) {
|
||||
this.logger.error("Fulfillment transaction failed", {
|
||||
sfOrderId,
|
||||
error: fulfillmentResult.error,
|
||||
stepsExecuted: fulfillmentResult.stepsExecuted,
|
||||
stepsRolledBack: fulfillmentResult.stepsRolledBack
|
||||
stepsRolledBack: fulfillmentResult.stepsRolledBack,
|
||||
});
|
||||
throw new Error(fulfillmentResult.error || "Fulfillment transaction failed");
|
||||
}
|
||||
@ -287,7 +292,7 @@ export class OrderFulfillmentOrchestrator {
|
||||
this.logger.log("Transactional fulfillment completed successfully", {
|
||||
sfOrderId,
|
||||
stepsExecuted: fulfillmentResult.stepsExecuted,
|
||||
duration: fulfillmentResult.duration
|
||||
duration: fulfillmentResult.duration,
|
||||
});
|
||||
|
||||
return context;
|
||||
|
||||
@ -116,7 +116,9 @@ export class OrderPricebookService {
|
||||
internetOfferingType: product
|
||||
? getStringField(product, "internetOfferingType", fields)
|
||||
: undefined,
|
||||
internetPlanTier: product ? getStringField(product, "internetPlanTier", fields) : undefined,
|
||||
internetPlanTier: product
|
||||
? getStringField(product, "internetPlanTier", fields)
|
||||
: undefined,
|
||||
vpnRegion: product ? getStringField(product, "vpnRegion", fields) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
0
customer-portal@1.0.0
Normal file
0
customer-portal@1.0.0
Normal file
35
docs/BUNDLE_ANALYSIS.md
Normal file
35
docs/BUNDLE_ANALYSIS.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 📊 Bundle Analysis
|
||||
|
||||
Simple bundle size analysis for the customer portal.
|
||||
|
||||
## 🎯 Quick Commands
|
||||
|
||||
```bash
|
||||
# Analyze frontend bundle
|
||||
pnpm analyze
|
||||
|
||||
# Run bundle analysis script
|
||||
pnpm bundle-analyze
|
||||
```
|
||||
|
||||
## 📈 What to Look For
|
||||
|
||||
- **First Load JS**: < 250KB (good)
|
||||
- **Total Bundle**: < 1MB (good)
|
||||
- **Large Dependencies**: Consider alternatives
|
||||
|
||||
## 🔧 Simple Optimizations
|
||||
|
||||
1. **Dynamic Imports**: Use `lazy()` for heavy components
|
||||
2. **Image Optimization**: Use Next.js `Image` component
|
||||
3. **Tree Shaking**: Import only what you need
|
||||
|
||||
```typescript
|
||||
// Good: Specific imports
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
// Bad: Full library import
|
||||
import * as _ from 'lodash';
|
||||
```
|
||||
|
||||
That's it! Keep it simple.
|
||||
@ -1,383 +0,0 @@
|
||||
# 📊 Bundle Analysis Guide
|
||||
|
||||
Simple guide for analyzing and optimizing bundle sizes.
|
||||
|
||||
## 🎯 Quick Analysis
|
||||
|
||||
### Frontend Bundle Analysis
|
||||
```bash
|
||||
# Analyze bundle size
|
||||
pnpm analyze
|
||||
|
||||
# Or use the script
|
||||
pnpm bundle-analyze
|
||||
```
|
||||
|
||||
### Key Metrics to Monitor
|
||||
- **First Load JS**: Should be < 250KB
|
||||
- **Total Bundle Size**: Should be < 1MB
|
||||
- **Largest Chunks**: Identify optimization targets
|
||||
|
||||
## 🎯 Frontend Optimizations
|
||||
|
||||
### 1. Bundle Analysis & Code Splitting
|
||||
|
||||
```bash
|
||||
# Analyze current bundle size
|
||||
cd apps/portal
|
||||
pnpm run analyze
|
||||
|
||||
# Build with analysis
|
||||
pnpm run build:analyze
|
||||
```
|
||||
|
||||
### 2. Dynamic Imports
|
||||
|
||||
```typescript
|
||||
// Before: Static import
|
||||
import { HeavyComponent } from './HeavyComponent';
|
||||
|
||||
// After: Dynamic import
|
||||
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
||||
|
||||
// Route-level code splitting
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
const Orders = lazy(() => import('./pages/Orders'));
|
||||
```
|
||||
|
||||
### 3. Image Optimization
|
||||
|
||||
```typescript
|
||||
// Use Next.js Image component with optimization
|
||||
import Image from 'next/image';
|
||||
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero"
|
||||
width={800}
|
||||
height={600}
|
||||
priority={false} // Lazy load non-critical images
|
||||
placeholder="blur" // Add blur placeholder
|
||||
/>
|
||||
```
|
||||
|
||||
### 4. Tree Shaking Optimization
|
||||
|
||||
```typescript
|
||||
// Before: Import entire library
|
||||
import * as _ from 'lodash';
|
||||
|
||||
// After: Import specific functions
|
||||
import { debounce, throttle } from 'lodash-es';
|
||||
|
||||
// Or use individual packages
|
||||
import debounce from 'lodash.debounce';
|
||||
```
|
||||
|
||||
### 5. React Query Optimization
|
||||
|
||||
```typescript
|
||||
// Optimize React Query cache
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Reduce memory usage
|
||||
cacheTime: 5 * 60 * 1000, // 5 minutes
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
// Limit concurrent queries
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 🎯 Backend Optimizations
|
||||
|
||||
### 1. Heap Size Optimization
|
||||
|
||||
```json
|
||||
// package.json - Optimized heap sizes
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=\"--max-old-space-size=2048\" nest start --watch",
|
||||
"build": "NODE_OPTIONS=\"--max-old-space-size=3072\" nest build",
|
||||
"type-check": "NODE_OPTIONS=\"--max-old-space-size=4096\" tsc --noEmit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Streaming Responses
|
||||
|
||||
```typescript
|
||||
// For large data responses
|
||||
@Get('large-dataset')
|
||||
async getLargeDataset(@Res() res: Response) {
|
||||
const stream = this.dataService.createDataStream();
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
|
||||
stream.pipe(res);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Memory-Efficient Pagination
|
||||
|
||||
```typescript
|
||||
// Cursor-based pagination instead of offset
|
||||
interface PaginationOptions {
|
||||
cursor?: string;
|
||||
limit: number; // Max 100
|
||||
}
|
||||
|
||||
async findWithCursor(options: PaginationOptions) {
|
||||
return this.prisma.order.findMany({
|
||||
take: Math.min(options.limit, 100),
|
||||
...(options.cursor && {
|
||||
cursor: { id: options.cursor },
|
||||
skip: 1,
|
||||
}),
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Request/Response Caching
|
||||
|
||||
```typescript
|
||||
// Memory-efficient caching
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
private readonly cache = new Map<string, { data: any; expires: number }>();
|
||||
private readonly maxSize = 1000; // Limit cache size
|
||||
|
||||
set(key: string, data: any, ttl: number = 300000) {
|
||||
// Implement LRU eviction
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
expires: Date.now() + ttl,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Database Connection Optimization
|
||||
|
||||
```typescript
|
||||
// Optimize Prisma connection pool
|
||||
const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
},
|
||||
// Optimize connection pool
|
||||
__internal: {
|
||||
engine: {
|
||||
connectionLimit: 10, // Reduce from default 20
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 🎯 Dependency Optimizations
|
||||
|
||||
### 1. Replace Heavy Dependencies
|
||||
|
||||
```bash
|
||||
# Before: moment.js (67KB)
|
||||
npm uninstall moment
|
||||
|
||||
# After: date-fns (13KB with tree shaking)
|
||||
npm install date-fns
|
||||
|
||||
# Before: lodash (71KB)
|
||||
npm uninstall lodash
|
||||
|
||||
# After: Individual functions or native alternatives
|
||||
npm install lodash-es # Better tree shaking
|
||||
```
|
||||
|
||||
### 2. Bundle Analysis Results
|
||||
|
||||
```bash
|
||||
# Run bundle analysis
|
||||
./scripts/memory-optimization.sh
|
||||
|
||||
# Key metrics to monitor:
|
||||
# - First Load JS: < 250KB
|
||||
# - Total Bundle Size: < 1MB
|
||||
# - Largest Chunks: Identify optimization targets
|
||||
```
|
||||
|
||||
### 3. Webpack Optimizations (Already Implemented)
|
||||
|
||||
- **Code Splitting**: Separate vendor, common, and UI chunks
|
||||
- **Tree Shaking**: Remove unused code
|
||||
- **Compression**: Gzip/Brotli compression
|
||||
- **Caching**: Long-term caching for static assets
|
||||
|
||||
## 🎯 Runtime Optimizations
|
||||
|
||||
### 1. Memory Leak Detection
|
||||
|
||||
```typescript
|
||||
// Add memory monitoring
|
||||
@Injectable()
|
||||
export class MemoryMonitorService {
|
||||
@Cron('*/5 * * * *') // Every 5 minutes
|
||||
checkMemoryUsage() {
|
||||
const usage = process.memoryUsage();
|
||||
|
||||
if (usage.heapUsed > 500 * 1024 * 1024) { // 500MB
|
||||
this.logger.warn('High memory usage detected', {
|
||||
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
|
||||
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
|
||||
external: `${Math.round(usage.external / 1024 / 1024)}MB`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Garbage Collection Optimization
|
||||
|
||||
```bash
|
||||
# Enable GC logging in production
|
||||
NODE_OPTIONS="--max-old-space-size=2048 --gc-interval=100" npm start
|
||||
|
||||
# Monitor GC patterns
|
||||
NODE_OPTIONS="--trace-gc --trace-gc-verbose" npm run dev
|
||||
```
|
||||
|
||||
### 3. Worker Threads for CPU-Intensive Tasks
|
||||
|
||||
```typescript
|
||||
// For heavy computations
|
||||
import { Worker, isMainThread, parentPort } from 'worker_threads';
|
||||
|
||||
if (isMainThread) {
|
||||
// Main thread
|
||||
const worker = new Worker(__filename);
|
||||
worker.postMessage({ data: largeDataset });
|
||||
|
||||
worker.on('message', (result) => {
|
||||
// Handle processed result
|
||||
});
|
||||
} else {
|
||||
// Worker thread
|
||||
parentPort?.on('message', ({ data }) => {
|
||||
const result = processLargeDataset(data);
|
||||
parentPort?.postMessage(result);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 📈 Monitoring & Metrics
|
||||
|
||||
### 1. Performance Monitoring
|
||||
|
||||
```typescript
|
||||
// Add performance metrics
|
||||
@Injectable()
|
||||
export class PerformanceService {
|
||||
trackMemoryUsage(operation: string) {
|
||||
const start = process.memoryUsage();
|
||||
|
||||
return {
|
||||
end: () => {
|
||||
const end = process.memoryUsage();
|
||||
const diff = {
|
||||
heapUsed: end.heapUsed - start.heapUsed,
|
||||
heapTotal: end.heapTotal - start.heapTotal,
|
||||
};
|
||||
|
||||
this.logger.debug(`Memory usage for ${operation}`, diff);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Bundle Size Monitoring
|
||||
|
||||
```json
|
||||
// Add to CI/CD pipeline
|
||||
{
|
||||
"scripts": {
|
||||
"build:check-size": "npm run build && bundlesize"
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": ".next/static/js/*.js",
|
||||
"maxSize": "250kb"
|
||||
},
|
||||
{
|
||||
"path": ".next/static/css/*.css",
|
||||
"maxSize": "50kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Implementation Checklist
|
||||
|
||||
### Immediate Actions (Week 1)
|
||||
- [ ] Run bundle analysis: `pnpm run analyze`
|
||||
- [ ] Implement dynamic imports for heavy components
|
||||
- [ ] Optimize image loading with Next.js Image
|
||||
- [ ] Reduce heap allocation in development
|
||||
|
||||
### Short-term (Week 2-3)
|
||||
- [ ] Replace heavy dependencies (moment → date-fns)
|
||||
- [ ] Implement request caching
|
||||
- [ ] Add memory monitoring
|
||||
- [ ] Optimize database connection pool
|
||||
|
||||
### Long-term (Month 1)
|
||||
- [ ] Implement streaming for large responses
|
||||
- [ ] Add worker threads for CPU-intensive tasks
|
||||
- [ ] Set up continuous bundle size monitoring
|
||||
- [ ] Implement advanced caching strategies
|
||||
|
||||
## 🎯 Expected Results
|
||||
|
||||
### Memory Reduction Targets
|
||||
- **Frontend Bundle**: 30-50% reduction
|
||||
- **Backend Heap**: 25-40% reduction
|
||||
- **Build Time**: 20-30% improvement
|
||||
- **Runtime Memory**: 35-50% reduction
|
||||
|
||||
### Performance Improvements
|
||||
- **First Load**: < 2 seconds
|
||||
- **Page Transitions**: < 500ms
|
||||
- **API Response**: < 200ms (95th percentile)
|
||||
- **Memory Stability**: No memory leaks in 24h+ runs
|
||||
|
||||
## 🔧 Tools & Commands
|
||||
|
||||
```bash
|
||||
# Frontend analysis
|
||||
cd apps/portal && pnpm run analyze
|
||||
|
||||
# Backend memory check
|
||||
cd apps/bff && NODE_OPTIONS="--trace-gc" pnpm dev
|
||||
|
||||
# Full optimization analysis
|
||||
./scripts/memory-optimization.sh
|
||||
|
||||
# Dependency audit
|
||||
pnpm audit --recursive
|
||||
|
||||
# Bundle size check
|
||||
pnpm run build && ls -la .next/static/js/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Note**: Always test memory optimizations in a staging environment before deploying to production. Monitor application performance and user experience after implementing changes.
|
||||
@ -79,10 +79,7 @@ export const checkPasswordNeededRequestSchema = z.object({
|
||||
});
|
||||
|
||||
export const refreshTokenRequestSchema = z.object({
|
||||
refreshToken: z
|
||||
.string()
|
||||
.min(1, "Refresh token is required")
|
||||
.optional(),
|
||||
refreshToken: z.string().min(1, "Refresh token is required").optional(),
|
||||
deviceId: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@ -7,5 +7,4 @@
|
||||
export { z } from "zod";
|
||||
|
||||
// Framework-specific exports
|
||||
export * from "./nestjs";
|
||||
export * from "./react";
|
||||
|
||||
6
packages/validation/src/nestjs.ts
Normal file
6
packages/validation/src/nestjs.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* NestJS-specific validation utilities
|
||||
* Import this directly in backend code: import { ... } from "@customer-portal/validation/nestjs"
|
||||
*/
|
||||
|
||||
export * from "./nestjs/index";
|
||||
146
pnpm-lock.yaml
generated
146
pnpm-lock.yaml
generated
@ -301,6 +301,9 @@ importers:
|
||||
specifier: ^5.0.8
|
||||
version: 5.0.8(@types/react@19.1.12)(react@19.1.1)
|
||||
devDependencies:
|
||||
'@next/bundle-analyzer':
|
||||
specifier: ^15.5.0
|
||||
version: 15.5.4
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.13
|
||||
@ -319,6 +322,9 @@ importers:
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
webpack-bundle-analyzer:
|
||||
specifier: ^4.10.2
|
||||
version: 4.10.2
|
||||
|
||||
packages/domain:
|
||||
dependencies:
|
||||
@ -576,6 +582,10 @@ packages:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@discoveryjs/json-ext@0.5.7':
|
||||
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||
|
||||
@ -1384,6 +1394,9 @@ packages:
|
||||
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||
reflect-metadata: ^0.1.13 || ^0.2.0
|
||||
|
||||
'@next/bundle-analyzer@15.5.4':
|
||||
resolution: {integrity: sha512-wMtpIjEHi+B/wC34ZbEcacGIPgQTwTFjjp0+F742s9TxC6QwT0MwB/O0QEgalMe8s3SH/K09DO0gmTvUSJrLRA==}
|
||||
|
||||
'@next/env@15.5.0':
|
||||
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
|
||||
|
||||
@ -1474,6 +1487,9 @@ packages:
|
||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@prisma/client@6.16.0':
|
||||
resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
|
||||
engines: {node: '>=18.18'}
|
||||
@ -2476,6 +2492,10 @@ packages:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commander@7.2.0:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
comment-json@4.2.5:
|
||||
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -2593,6 +2613,9 @@ packages:
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
debounce@1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@ -2693,6 +2716,9 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
duplexer@0.1.2:
|
||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
@ -3212,6 +3238,10 @@ packages:
|
||||
graphemer@1.4.0:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
|
||||
gzip-size@6.0.0:
|
||||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
handlebars@4.7.8:
|
||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
@ -3423,6 +3453,10 @@ packages:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
@ -4000,6 +4034,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@ -4194,6 +4232,10 @@ packages:
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
opener@1.5.2:
|
||||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||
hasBin: true
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -4676,6 +4718,10 @@ packages:
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
sirv@2.0.4:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4926,6 +4972,10 @@ packages:
|
||||
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
totalist@3.0.1:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||
engines: {node: '>=16'}
|
||||
@ -5159,6 +5209,16 @@ packages:
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
webpack-bundle-analyzer@4.10.1:
|
||||
resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
webpack-bundle-analyzer@4.10.2:
|
||||
resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
webpack-node-externals@3.0.0:
|
||||
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
||||
engines: {node: '>=6'}
|
||||
@ -5238,6 +5298,18 @@ packages:
|
||||
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
ws@7.5.10:
|
||||
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xml2js@0.6.2:
|
||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
@ -5543,6 +5615,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@discoveryjs/json-ext@0.5.7': {}
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.1.0
|
||||
@ -6367,6 +6441,13 @@ snapshots:
|
||||
'@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
reflect-metadata: 0.2.2
|
||||
|
||||
'@next/bundle-analyzer@15.5.4':
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.10.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@next/env@15.5.0': {}
|
||||
|
||||
'@next/eslint-plugin-next@15.5.0':
|
||||
@ -6426,6 +6507,8 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.2.9': {}
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@prisma/client@6.16.0(prisma@6.16.0(typescript@5.9.2))(typescript@5.9.2)':
|
||||
optionalDependencies:
|
||||
prisma: 6.16.0(typescript@5.9.2)
|
||||
@ -7521,6 +7604,8 @@ snapshots:
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
comment-json@4.2.5:
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
@ -7631,6 +7716,8 @@ snapshots:
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
debounce@1.2.1: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -7702,6 +7789,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
duplexer@0.1.2: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
@ -8441,6 +8530,10 @@ snapshots:
|
||||
|
||||
graphemer@1.4.0: {}
|
||||
|
||||
gzip-size@6.0.0:
|
||||
dependencies:
|
||||
duplexer: 0.1.2
|
||||
|
||||
handlebars@4.7.8:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
@ -8660,6 +8753,8 @@ snapshots:
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
is-regex@1.2.1:
|
||||
@ -9390,6 +9485,8 @@ snapshots:
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msgpackr-extract@3.0.3:
|
||||
@ -9584,6 +9681,8 @@ snapshots:
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
opener@1.5.2: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@ -10163,6 +10262,12 @@ snapshots:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
|
||||
sirv@2.0.4:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
mrmime: 2.0.1
|
||||
totalist: 3.0.1
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
@ -10421,6 +10526,8 @@ snapshots:
|
||||
'@tokenizer/token': 0.3.0
|
||||
ieee754: 1.2.1
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
dependencies:
|
||||
tldts: 7.0.13
|
||||
@ -10672,6 +10779,43 @@ snapshots:
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-bundle-analyzer@4.10.1:
|
||||
dependencies:
|
||||
'@discoveryjs/json-ext': 0.5.7
|
||||
acorn: 8.15.0
|
||||
acorn-walk: 8.3.4
|
||||
commander: 7.2.0
|
||||
debounce: 1.2.1
|
||||
escape-string-regexp: 4.0.0
|
||||
gzip-size: 6.0.0
|
||||
html-escaper: 2.0.2
|
||||
is-plain-object: 5.0.0
|
||||
opener: 1.5.2
|
||||
picocolors: 1.1.1
|
||||
sirv: 2.0.4
|
||||
ws: 7.5.10
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
webpack-bundle-analyzer@4.10.2:
|
||||
dependencies:
|
||||
'@discoveryjs/json-ext': 0.5.7
|
||||
acorn: 8.15.0
|
||||
acorn-walk: 8.3.4
|
||||
commander: 7.2.0
|
||||
debounce: 1.2.1
|
||||
escape-string-regexp: 4.0.0
|
||||
gzip-size: 6.0.0
|
||||
html-escaper: 2.0.2
|
||||
opener: 1.5.2
|
||||
picocolors: 1.1.1
|
||||
sirv: 2.0.4
|
||||
ws: 7.5.10
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
webpack-node-externals@3.0.0: {}
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
@ -10797,6 +10941,8 @@ snapshots:
|
||||
imurmurhash: 0.1.4
|
||||
signal-exit: 4.1.0
|
||||
|
||||
ws@7.5.10: {}
|
||||
|
||||
xml2js@0.6.2:
|
||||
dependencies:
|
||||
sax: 1.4.1
|
||||
|
||||
@ -328,9 +328,21 @@ start_apps() {
|
||||
log "🔨 Building shared package..."
|
||||
pnpm --filter @customer-portal/domain build
|
||||
|
||||
# Build BFF before watch (ensures dist exists)
|
||||
log "🔨 Building BFF for initial setup..."
|
||||
(cd "$PROJECT_ROOT/apps/bff" && pnpm tsc -p tsconfig.build.json)
|
||||
# Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
|
||||
log "🔨 Building BFF for initial setup (ts emit)..."
|
||||
(
|
||||
cd "$PROJECT_ROOT/apps/bff" \
|
||||
&& pnpm clean \
|
||||
&& rm -f tsconfig.build.tsbuildinfo \
|
||||
&& pnpm build || pnpm exec tsc -b --force tsconfig.build.json
|
||||
)
|
||||
if [ ! -d "$PROJECT_ROOT/apps/bff/dist" ]; then
|
||||
warn "BFF dist not found after build; forcing TypeScript emit..."
|
||||
(cd "$PROJECT_ROOT/apps/bff" && pnpm exec tsc -b --force tsconfig.build.json)
|
||||
fi
|
||||
if [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.js" ] && [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.cjs" ]; then
|
||||
warn "BFF main output not found; will rely on watch to produce it."
|
||||
fi
|
||||
|
||||
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
|
||||
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user