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",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsConfigPath": "tsconfig.build.json",
|
"tsConfigPath": "tsconfig.build.json",
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": false,
|
||||||
"watchAssets": true,
|
"watchAssets": true,
|
||||||
"assets": ["**/*.prisma"]
|
"assets": ["**/*.prisma"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"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:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
@ -21,7 +22,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"type-check": "tsc --project tsconfig.json --noEmit",
|
"type-check": "tsc --project tsconfig.json --noEmit",
|
||||||
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch",
|
"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:migrate": "prisma migrate dev",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export class DistributedTransactionService {
|
|||||||
description,
|
description,
|
||||||
timeout = 120000, // 2 minutes default for distributed operations
|
timeout = 120000, // 2 minutes default for distributed operations
|
||||||
maxRetries = 1, // Less retries for distributed operations
|
maxRetries = 1, // Less retries for distributed operations
|
||||||
continueOnNonCriticalFailure = false
|
continueOnNonCriticalFailure = false,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const transactionId = this.generateTransactionId();
|
const transactionId = this.generateTransactionId();
|
||||||
@ -100,7 +100,7 @@ export class DistributedTransactionService {
|
|||||||
this.logger.log(`Starting distributed transaction [${transactionId}]`, {
|
this.logger.log(`Starting distributed transaction [${transactionId}]`, {
|
||||||
description,
|
description,
|
||||||
stepsCount: steps.length,
|
stepsCount: steps.length,
|
||||||
timeout
|
timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stepResults: Record<string, any> = {};
|
const stepResults: Record<string, any> = {};
|
||||||
@ -113,7 +113,7 @@ export class DistributedTransactionService {
|
|||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
|
this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
|
||||||
description: step.description,
|
description: step.description,
|
||||||
critical: step.critical
|
critical: step.critical,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -125,9 +125,8 @@ export class DistributedTransactionService {
|
|||||||
executedSteps.push(step.id);
|
executedSteps.push(step.id);
|
||||||
|
|
||||||
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
|
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
|
||||||
duration: stepDuration
|
duration: stepDuration,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (stepError) {
|
} catch (stepError) {
|
||||||
lastError = stepError as Error;
|
lastError = stepError as Error;
|
||||||
failedSteps.push(step.id);
|
failedSteps.push(step.id);
|
||||||
@ -135,7 +134,7 @@ export class DistributedTransactionService {
|
|||||||
this.logger.error(`Step failed: ${step.id} [${transactionId}]`, {
|
this.logger.error(`Step failed: ${step.id} [${transactionId}]`, {
|
||||||
error: getErrorMessage(stepError),
|
error: getErrorMessage(stepError),
|
||||||
critical: step.critical,
|
critical: step.critical,
|
||||||
retryable: step.retryable
|
retryable: step.retryable,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If it's a critical step, stop the entire transaction
|
// If it's a critical step, stop the entire transaction
|
||||||
@ -149,7 +148,9 @@ export class DistributedTransactionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, log and continue
|
// 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}]`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +160,7 @@ export class DistributedTransactionService {
|
|||||||
description,
|
description,
|
||||||
duration,
|
duration,
|
||||||
stepsExecuted: executedSteps.length,
|
stepsExecuted: executedSteps.length,
|
||||||
failedSteps: failedSteps.length
|
failedSteps: failedSteps.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -169,9 +170,8 @@ export class DistributedTransactionService {
|
|||||||
stepsExecuted: executedSteps.length,
|
stepsExecuted: executedSteps.length,
|
||||||
stepsRolledBack: 0,
|
stepsRolledBack: 0,
|
||||||
stepResults,
|
stepResults,
|
||||||
failedSteps
|
failedSteps,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ export class DistributedTransactionService {
|
|||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
duration,
|
duration,
|
||||||
stepsExecuted: executedSteps.length,
|
stepsExecuted: executedSteps.length,
|
||||||
failedSteps: failedSteps.length
|
failedSteps: failedSteps.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute rollbacks for completed steps
|
// Execute rollbacks for completed steps
|
||||||
@ -198,7 +198,7 @@ export class DistributedTransactionService {
|
|||||||
stepsExecuted: executedSteps.length,
|
stepsExecuted: executedSteps.length,
|
||||||
stepsRolledBack: rollbacksExecuted,
|
stepsRolledBack: rollbacksExecuted,
|
||||||
stepResults,
|
stepResults,
|
||||||
failedSteps
|
failedSteps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +226,7 @@ export class DistributedTransactionService {
|
|||||||
this.logger.log(`Starting hybrid transaction [${transactionId}]`, {
|
this.logger.log(`Starting hybrid transaction [${transactionId}]`, {
|
||||||
description: options.description,
|
description: options.description,
|
||||||
databaseFirst,
|
databaseFirst,
|
||||||
externalStepsCount: externalSteps.length
|
externalStepsCount: externalSteps.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -240,12 +240,12 @@ export class DistributedTransactionService {
|
|||||||
databaseOperation,
|
databaseOperation,
|
||||||
{
|
{
|
||||||
description: `${options.description} - Database Operations`,
|
description: `${options.description} - Database Operations`,
|
||||||
timeout: options.timeout
|
timeout: options.timeout,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dbTransactionResult.success) {
|
if (!dbTransactionResult.success) {
|
||||||
throw new Error(dbTransactionResult.error || 'Database transaction failed');
|
throw new Error(dbTransactionResult.error || "Database transaction failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseResult = dbTransactionResult.data!;
|
databaseResult = dbTransactionResult.data!;
|
||||||
@ -254,27 +254,29 @@ export class DistributedTransactionService {
|
|||||||
this.logger.debug(`Executing external operations [${transactionId}]`);
|
this.logger.debug(`Executing external operations [${transactionId}]`);
|
||||||
externalResult = await this.executeDistributedTransaction(externalSteps, {
|
externalResult = await this.executeDistributedTransaction(externalSteps, {
|
||||||
...distributedOptions,
|
...distributedOptions,
|
||||||
description: distributedOptions.description || 'External operations'
|
description: distributedOptions.description || "External operations",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
|
if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
|
||||||
// Note: Database transaction already committed, so we can't rollback automatically
|
// 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 is a limitation of this approach - consider using saga pattern for true rollback
|
||||||
this.logger.error(`External operations failed but database already committed [${transactionId}]`, {
|
this.logger.error(
|
||||||
externalError: externalResult.error
|
`External operations failed but database already committed [${transactionId}]`,
|
||||||
});
|
{
|
||||||
|
externalError: externalResult.error,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Execute external operations first
|
// Execute external operations first
|
||||||
this.logger.debug(`Executing external operations [${transactionId}]`);
|
this.logger.debug(`Executing external operations [${transactionId}]`);
|
||||||
externalResult = await this.executeDistributedTransaction(externalSteps, {
|
externalResult = await this.executeDistributedTransaction(externalSteps, {
|
||||||
...distributedOptions,
|
...distributedOptions,
|
||||||
description: distributedOptions.description || 'External operations'
|
description: distributedOptions.description || "External operations",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!externalResult.success) {
|
if (!externalResult.success) {
|
||||||
throw new Error(externalResult.error || 'External operations failed');
|
throw new Error(externalResult.error || "External operations failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute database operations
|
// Execute database operations
|
||||||
@ -283,7 +285,7 @@ export class DistributedTransactionService {
|
|||||||
databaseOperation,
|
databaseOperation,
|
||||||
{
|
{
|
||||||
description: `${options.description} - Database Operations`,
|
description: `${options.description} - Database Operations`,
|
||||||
timeout: options.timeout
|
timeout: options.timeout,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -295,7 +297,7 @@ export class DistributedTransactionService {
|
|||||||
externalResult.stepResults,
|
externalResult.stepResults,
|
||||||
transactionId
|
transactionId
|
||||||
);
|
);
|
||||||
throw new Error(dbTransactionResult.error || 'Database transaction failed');
|
throw new Error(dbTransactionResult.error || "Database transaction failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseResult = dbTransactionResult.data!;
|
databaseResult = dbTransactionResult.data!;
|
||||||
@ -305,7 +307,7 @@ export class DistributedTransactionService {
|
|||||||
|
|
||||||
this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, {
|
this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, {
|
||||||
description: options.description,
|
description: options.description,
|
||||||
duration
|
duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -315,16 +317,15 @@ export class DistributedTransactionService {
|
|||||||
stepsExecuted: externalResult?.stepsExecuted || 0,
|
stepsExecuted: externalResult?.stepsExecuted || 0,
|
||||||
stepsRolledBack: 0,
|
stepsRolledBack: 0,
|
||||||
stepResults: externalResult?.stepResults || {},
|
stepResults: externalResult?.stepResults || {},
|
||||||
failedSteps: externalResult?.failedSteps || []
|
failedSteps: externalResult?.failedSteps || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
this.logger.error(`Hybrid transaction failed [${transactionId}]`, {
|
this.logger.error(`Hybrid transaction failed [${transactionId}]`, {
|
||||||
description: options.description,
|
description: options.description,
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
duration
|
duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -334,7 +335,7 @@ export class DistributedTransactionService {
|
|||||||
stepsExecuted: 0,
|
stepsExecuted: 0,
|
||||||
stepsRolledBack: 0,
|
stepsRolledBack: 0,
|
||||||
stepResults: {},
|
stepResults: {},
|
||||||
failedSteps: []
|
failedSteps: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -346,7 +347,7 @@ export class DistributedTransactionService {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
|
reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,7 +374,7 @@ export class DistributedTransactionService {
|
|||||||
this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`);
|
this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`);
|
||||||
} catch (rollbackError) {
|
} catch (rollbackError) {
|
||||||
this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, {
|
this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, {
|
||||||
error: getErrorMessage(rollbackError)
|
error: getErrorMessage(rollbackError),
|
||||||
});
|
});
|
||||||
// Continue with other rollbacks even if one fails
|
// Continue with other rollbacks even if one fails
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export interface TransactionOptions {
|
|||||||
* Custom isolation level for the transaction
|
* Custom isolation level for the transaction
|
||||||
* Default: ReadCommitted
|
* Default: ReadCommitted
|
||||||
*/
|
*/
|
||||||
isolationLevel?: 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable';
|
isolationLevel?: "ReadUncommitted" | "ReadCommitted" | "RepeatableRead" | "Serializable";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Description of the transaction for logging
|
* Description of the transaction for logging
|
||||||
@ -102,9 +102,9 @@ export class TransactionService {
|
|||||||
const {
|
const {
|
||||||
timeout = this.defaultTimeout,
|
timeout = this.defaultTimeout,
|
||||||
maxRetries = this.defaultMaxRetries,
|
maxRetries = this.defaultMaxRetries,
|
||||||
isolationLevel = 'ReadCommitted',
|
isolationLevel = "ReadCommitted",
|
||||||
description = 'Database transaction',
|
description = "Database transaction",
|
||||||
autoRollback = true
|
autoRollback = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const transactionId = this.generateTransactionId();
|
const transactionId = this.generateTransactionId();
|
||||||
@ -114,14 +114,14 @@ export class TransactionService {
|
|||||||
id: transactionId,
|
id: transactionId,
|
||||||
startTime,
|
startTime,
|
||||||
operations: [],
|
operations: [],
|
||||||
rollbackActions: []
|
rollbackActions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.log(`Starting transaction [${transactionId}]`, {
|
this.logger.log(`Starting transaction [${transactionId}]`, {
|
||||||
description,
|
description,
|
||||||
timeout,
|
timeout,
|
||||||
isolationLevel,
|
isolationLevel,
|
||||||
maxRetries
|
maxRetries,
|
||||||
});
|
});
|
||||||
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
@ -137,13 +137,13 @@ export class TransactionService {
|
|||||||
id: transactionId,
|
id: transactionId,
|
||||||
startTime,
|
startTime,
|
||||||
operations: [],
|
operations: [],
|
||||||
rollbackActions: []
|
rollbackActions: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
this.executeTransactionAttempt(operation, context, isolationLevel),
|
this.executeTransactionAttempt(operation, context, isolationLevel),
|
||||||
this.createTimeoutPromise<T>(timeout, transactionId)
|
this.createTimeoutPromise<T>(timeout, transactionId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const duration = Date.now() - startTime.getTime();
|
const duration = Date.now() - startTime.getTime();
|
||||||
@ -152,7 +152,7 @@ export class TransactionService {
|
|||||||
description,
|
description,
|
||||||
duration,
|
duration,
|
||||||
attempt,
|
attempt,
|
||||||
operationsCount: context.operations.length
|
operationsCount: context.operations.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -160,9 +160,8 @@ export class TransactionService {
|
|||||||
data: result,
|
data: result,
|
||||||
duration,
|
duration,
|
||||||
operationsCount: context.operations.length,
|
operationsCount: context.operations.length,
|
||||||
rollbacksExecuted: 0
|
rollbacksExecuted: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error as Error;
|
lastError = error as Error;
|
||||||
const duration = Date.now() - startTime.getTime();
|
const duration = Date.now() - startTime.getTime();
|
||||||
@ -172,7 +171,7 @@ export class TransactionService {
|
|||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
duration,
|
duration,
|
||||||
operationsCount: context.operations.length,
|
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
|
// Execute rollbacks if this is the final attempt or not a retryable error
|
||||||
@ -184,7 +183,7 @@ export class TransactionService {
|
|||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
duration,
|
duration,
|
||||||
operationsCount: context.operations.length,
|
operationsCount: context.operations.length,
|
||||||
rollbacksExecuted
|
rollbacksExecuted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,10 +196,10 @@ export class TransactionService {
|
|||||||
const duration = Date.now() - startTime.getTime();
|
const duration = Date.now() - startTime.getTime();
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: lastError ? getErrorMessage(lastError) : 'Unknown transaction error',
|
error: lastError ? getErrorMessage(lastError) : "Unknown transaction error",
|
||||||
duration,
|
duration,
|
||||||
operationsCount: context.operations.length,
|
operationsCount: context.operations.length,
|
||||||
rollbacksExecuted: 0
|
rollbacksExecuted: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,15 +208,15 @@ export class TransactionService {
|
|||||||
*/
|
*/
|
||||||
async executeSimpleTransaction<T>(
|
async executeSimpleTransaction<T>(
|
||||||
operation: (tx: any) => Promise<T>,
|
operation: (tx: any) => Promise<T>,
|
||||||
options: Omit<TransactionOptions, 'autoRollback'> = {}
|
options: Omit<TransactionOptions, "autoRollback"> = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const result = await this.executeTransaction(
|
const result = await this.executeTransaction(async (tx, _context) => operation(tx), {
|
||||||
async (tx, _context) => operation(tx),
|
...options,
|
||||||
{ ...options, autoRollback: false }
|
autoRollback: false,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'Transaction failed');
|
throw new Error(result.error || "Transaction failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data!;
|
return result.data!;
|
||||||
@ -229,7 +228,7 @@ export class TransactionService {
|
|||||||
isolationLevel: string
|
isolationLevel: string
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return await this.prisma.$transaction(
|
return await this.prisma.$transaction(
|
||||||
async (tx) => {
|
async tx => {
|
||||||
// Enhance context with helper methods
|
// Enhance context with helper methods
|
||||||
const enhancedContext = this.enhanceContext(context);
|
const enhancedContext = this.enhanceContext(context);
|
||||||
|
|
||||||
@ -238,7 +237,7 @@ export class TransactionService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
isolationLevel: isolationLevel as any,
|
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>) => {
|
addRollback: (rollbackFn: () => Promise<void>) => {
|
||||||
context.rollbackActions.push(rollbackFn);
|
context.rollbackActions.push(rollbackFn);
|
||||||
}
|
},
|
||||||
} as TransactionContext & {
|
} as TransactionContext & {
|
||||||
addOperation: (description: string) => void;
|
addOperation: (description: string) => void;
|
||||||
addRollback: (rollbackFn: () => Promise<void>) => void;
|
addRollback: (rollbackFn: () => Promise<void>) => void;
|
||||||
@ -266,7 +265,9 @@ export class TransactionService {
|
|||||||
return 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;
|
let rollbacksExecuted = 0;
|
||||||
|
|
||||||
@ -278,13 +279,15 @@ export class TransactionService {
|
|||||||
this.logger.debug(`Rollback ${i + 1} completed [${context.id}]`);
|
this.logger.debug(`Rollback ${i + 1} completed [${context.id}]`);
|
||||||
} catch (rollbackError) {
|
} catch (rollbackError) {
|
||||||
this.logger.error(`Rollback ${i + 1} failed [${context.id}]`, {
|
this.logger.error(`Rollback ${i + 1} failed [${context.id}]`, {
|
||||||
error: getErrorMessage(rollbackError)
|
error: getErrorMessage(rollbackError),
|
||||||
});
|
});
|
||||||
// Continue with other rollbacks even if one fails
|
// 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;
|
return rollbacksExecuted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,11 +296,11 @@ export class TransactionService {
|
|||||||
|
|
||||||
// Retry on serialization failures, deadlocks, and temporary connection issues
|
// Retry on serialization failures, deadlocks, and temporary connection issues
|
||||||
return (
|
return (
|
||||||
errorMessage.includes('serialization failure') ||
|
errorMessage.includes("serialization failure") ||
|
||||||
errorMessage.includes('deadlock') ||
|
errorMessage.includes("deadlock") ||
|
||||||
errorMessage.includes('connection') ||
|
errorMessage.includes("connection") ||
|
||||||
errorMessage.includes('timeout') ||
|
errorMessage.includes("timeout") ||
|
||||||
errorMessage.includes('lock wait timeout')
|
errorMessage.includes("lock wait timeout")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,7 +329,7 @@ export class TransactionService {
|
|||||||
activeTransactions: 0, // Would need to track active transactions
|
activeTransactions: 0, // Would need to track active transactions
|
||||||
totalTransactions: 0, // Would need to track total count
|
totalTransactions: 0, // Would need to track total count
|
||||||
successRate: 0, // Would need to track success/failure rates
|
successRate: 0, // Would need to track success/failure rates
|
||||||
averageDuration: 0 // Would need to track durations
|
averageDuration: 0, // Would need to track durations
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,11 +14,11 @@ export class QueueHealthController {
|
|||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get queue health status",
|
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({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: "Queue health status retrieved successfully"
|
description: "Queue health status retrieved successfully",
|
||||||
})
|
})
|
||||||
getQueueHealth() {
|
getQueueHealth() {
|
||||||
return {
|
return {
|
||||||
@ -38,11 +38,11 @@ export class QueueHealthController {
|
|||||||
@Get("whmcs")
|
@Get("whmcs")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get WHMCS queue metrics",
|
summary: "Get WHMCS queue metrics",
|
||||||
description: "Returns detailed metrics for the WHMCS request queue"
|
description: "Returns detailed metrics for the WHMCS request queue",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: "WHMCS queue metrics retrieved successfully"
|
description: "WHMCS queue metrics retrieved successfully",
|
||||||
})
|
})
|
||||||
getWhmcsQueueMetrics() {
|
getWhmcsQueueMetrics() {
|
||||||
return {
|
return {
|
||||||
@ -55,11 +55,12 @@ export class QueueHealthController {
|
|||||||
@Get("salesforce")
|
@Get("salesforce")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get Salesforce queue metrics",
|
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({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: "Salesforce queue metrics retrieved successfully"
|
description: "Salesforce queue metrics retrieved successfully",
|
||||||
})
|
})
|
||||||
getSalesforceQueueMetrics() {
|
getSalesforceQueueMetrics() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { getClientSafeErrorMessage } from "../utils/error.util";
|
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { SecureErrorMapperService } from "../security/services/secure-error-mapper.service";
|
import { SecureErrorMapperService } from "../security/services/secure-error-mapper.service";
|
||||||
@ -48,7 +47,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
|||||||
const errorResponse = exceptionResponse as { message?: string; error?: string };
|
const errorResponse = exceptionResponse as { message?: string; error?: string };
|
||||||
originalError = errorResponse.message || exception.message;
|
originalError = errorResponse.message || exception.message;
|
||||||
} else {
|
} else {
|
||||||
originalError = typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
|
originalError =
|
||||||
|
typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
@ -62,7 +62,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
|||||||
// Log the error securely (this handles sensitive data filtering)
|
// Log the error securely (this handles sensitive data filtering)
|
||||||
this.secureErrorMapper.logSecureError(originalError, errorContext, {
|
this.secureErrorMapper.logSecureError(originalError, errorContext, {
|
||||||
httpStatus: status,
|
httpStatus: status,
|
||||||
exceptionType: exception instanceof Error ? exception.constructor.name : 'Unknown'
|
exceptionType: exception instanceof Error ? exception.constructor.name : "Unknown",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create secure error response
|
// Create secure error response
|
||||||
|
|||||||
@ -110,10 +110,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
|||||||
|
|
||||||
// Wait for pending requests to complete (with timeout)
|
// Wait for pending requests to complete (with timeout)
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
|
||||||
this.standardQueue.onIdle(),
|
|
||||||
this.longRunningQueue.onIdle(),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn("Some Salesforce requests may not have completed during shutdown", {
|
this.logger.warn("Some Salesforce requests may not have completed during shutdown", {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
@ -219,10 +216,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
|||||||
/**
|
/**
|
||||||
* Execute high-priority Salesforce request (jumps queue)
|
* Execute high-priority Salesforce request (jumps queue)
|
||||||
*/
|
*/
|
||||||
async executeHighPriority<T>(
|
async executeHighPriority<T>(requestFn: () => Promise<T>, isLongRunning = false): Promise<T> {
|
||||||
requestFn: () => Promise<T>,
|
|
||||||
isLongRunning = false
|
|
||||||
): Promise<T> {
|
|
||||||
return this.execute(requestFn, { priority: 10, isLongRunning });
|
return this.execute(requestFn, { priority: 10, isLongRunning });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,9 +248,8 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
|||||||
} {
|
} {
|
||||||
this.updateQueueMetrics();
|
this.updateQueueMetrics();
|
||||||
|
|
||||||
const errorRate = this.metrics.totalRequests > 0
|
const errorRate =
|
||||||
? this.metrics.failedRequests / this.metrics.totalRequests
|
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Estimate daily limit (conservative: 150,000 for ~50 users)
|
// Estimate daily limit (conservative: 150,000 for ~50 users)
|
||||||
const estimatedDailyLimit = 150000;
|
const estimatedDailyLimit = 150000;
|
||||||
@ -265,17 +258,9 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
|||||||
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
|
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
|
||||||
|
|
||||||
// Adjusted thresholds for higher throughput (15 concurrent, 10 RPS)
|
// Adjusted thresholds for higher throughput (15 concurrent, 10 RPS)
|
||||||
if (
|
if (this.metrics.queueSize > 200 || errorRate > 0.1 || dailyUsagePercent > 0.9) {
|
||||||
this.metrics.queueSize > 200 ||
|
|
||||||
errorRate > 0.1 ||
|
|
||||||
dailyUsagePercent > 0.9
|
|
||||||
) {
|
|
||||||
status = "unhealthy";
|
status = "unhealthy";
|
||||||
} else if (
|
} else if (this.metrics.queueSize > 80 || errorRate > 0.05 || dailyUsagePercent > 0.7) {
|
||||||
this.metrics.queueSize > 80 ||
|
|
||||||
errorRate > 0.05 ||
|
|
||||||
dailyUsagePercent > 0.7
|
|
||||||
) {
|
|
||||||
status = "degraded";
|
status = "degraded";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,10 +305,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
|||||||
this.standardQueue.clear();
|
this.standardQueue.clear();
|
||||||
this.longRunningQueue.clear();
|
this.longRunningQueue.clear();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
|
||||||
this.standardQueue.onIdle(),
|
|
||||||
this.longRunningQueue.onIdle(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeWithRetry<T>(
|
private async executeWithRetry<T>(
|
||||||
|
|||||||
@ -100,10 +100,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
|||||||
/**
|
/**
|
||||||
* Execute a WHMCS API request through the queue
|
* Execute a WHMCS API request through the queue
|
||||||
*/
|
*/
|
||||||
async execute<T>(
|
async execute<T>(requestFn: () => Promise<T>, options: WhmcsRequestOptions = {}): Promise<T> {
|
||||||
requestFn: () => Promise<T>,
|
|
||||||
options: WhmcsRequestOptions = {}
|
|
||||||
): Promise<T> {
|
|
||||||
await this.initializeQueue();
|
await this.initializeQueue();
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const requestId = this.generateRequestId();
|
const requestId = this.generateRequestId();
|
||||||
@ -200,9 +197,8 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
|||||||
} {
|
} {
|
||||||
this.updateQueueMetrics();
|
this.updateQueueMetrics();
|
||||||
|
|
||||||
const errorRate = this.metrics.totalRequests > 0
|
const errorRate =
|
||||||
? this.metrics.failedRequests / this.metrics.totalRequests
|
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
|
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
|
||||||
|
|
||||||
|
|||||||
@ -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 { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from "@nestjs/swagger";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
@ -8,30 +8,30 @@ interface AuthenticatedRequest extends Request {
|
|||||||
user?: { id: string; sessionId?: string };
|
user?: { id: string; sessionId?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiTags('Security')
|
@ApiTags("Security")
|
||||||
@Controller('security/csrf')
|
@Controller("security/csrf")
|
||||||
export class CsrfController {
|
export class CsrfController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly csrfService: CsrfService,
|
private readonly csrfService: CsrfService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('token')
|
@Get("token")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get CSRF token',
|
summary: "Get CSRF token",
|
||||||
description: 'Generates and returns a new CSRF token for the current session'
|
description: "Generates and returns a new CSRF token for the current session",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'CSRF token generated successfully',
|
description: "CSRF token generated successfully",
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
success: { type: 'boolean', example: true },
|
success: { type: "boolean", example: true },
|
||||||
token: { type: 'string', example: 'abc123...' },
|
token: { type: "string", example: "abc123..." },
|
||||||
expiresAt: { type: 'string', format: 'date-time' }
|
expiresAt: { type: "string", format: "date-time" },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
|
getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
|
||||||
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
|
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
|
||||||
@ -41,49 +41,49 @@ export class CsrfController {
|
|||||||
const tokenData = this.csrfService.generateToken(sessionId, userId);
|
const tokenData = this.csrfService.generateToken(sessionId, userId);
|
||||||
|
|
||||||
// Set CSRF secret in secure cookie
|
// Set CSRF secret in secure cookie
|
||||||
res.cookie('csrf-secret', tokenData.secret, {
|
res.cookie("csrf-secret", tokenData.secret, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === "production",
|
||||||
sameSite: 'strict',
|
sameSite: "strict",
|
||||||
maxAge: 3600000, // 1 hour
|
maxAge: 3600000, // 1 hour
|
||||||
path: '/',
|
path: "/",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.debug("CSRF token requested", {
|
this.logger.debug("CSRF token requested", {
|
||||||
userId,
|
userId,
|
||||||
sessionId,
|
sessionId,
|
||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get("user-agent"),
|
||||||
ip: req.ip
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
token: tokenData.token,
|
token: tokenData.token,
|
||||||
expiresAt: tokenData.expiresAt.toISOString()
|
expiresAt: tokenData.expiresAt.toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('refresh')
|
@Post("refresh")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Refresh CSRF token',
|
summary: "Refresh CSRF token",
|
||||||
description: 'Invalidates current token and generates a new one for authenticated users'
|
description: "Invalidates current token and generates a new one for authenticated users",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'CSRF token refreshed successfully',
|
description: "CSRF token refreshed successfully",
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
success: { type: 'boolean', example: true },
|
success: { type: "boolean", example: true },
|
||||||
token: { type: 'string', example: 'xyz789...' },
|
token: { type: "string", example: "xyz789..." },
|
||||||
expiresAt: { type: 'string', format: 'date-time' }
|
expiresAt: { type: "string", format: "date-time" },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
refreshCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
|
refreshCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
|
||||||
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
|
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
|
// Invalidate existing tokens for this user
|
||||||
this.csrfService.invalidateUserTokens(userId);
|
this.csrfService.invalidateUserTokens(userId);
|
||||||
@ -92,76 +92,75 @@ export class CsrfController {
|
|||||||
const tokenData = this.csrfService.generateToken(sessionId, userId);
|
const tokenData = this.csrfService.generateToken(sessionId, userId);
|
||||||
|
|
||||||
// Set CSRF secret in secure cookie
|
// Set CSRF secret in secure cookie
|
||||||
res.cookie('csrf-secret', tokenData.secret, {
|
res.cookie("csrf-secret", tokenData.secret, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === "production",
|
||||||
sameSite: 'strict',
|
sameSite: "strict",
|
||||||
maxAge: 3600000, // 1 hour
|
maxAge: 3600000, // 1 hour
|
||||||
path: '/',
|
path: "/",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.debug("CSRF token refreshed", {
|
this.logger.debug("CSRF token refreshed", {
|
||||||
userId,
|
userId,
|
||||||
sessionId,
|
sessionId,
|
||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get("user-agent"),
|
||||||
ip: req.ip
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
token: tokenData.token,
|
token: tokenData.token,
|
||||||
expiresAt: tokenData.expiresAt.toISOString()
|
expiresAt: tokenData.expiresAt.toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('stats')
|
@Get("stats")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get CSRF token statistics',
|
summary: "Get CSRF token statistics",
|
||||||
description: 'Returns statistics about CSRF tokens (admin/monitoring endpoint)'
|
description: "Returns statistics about CSRF tokens (admin/monitoring endpoint)",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'CSRF token statistics',
|
description: "CSRF token statistics",
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
success: { type: 'boolean', example: true },
|
success: { type: "boolean", example: true },
|
||||||
stats: {
|
stats: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
totalTokens: { type: 'number', example: 150 },
|
totalTokens: { type: "number", example: 150 },
|
||||||
activeTokens: { type: 'number', example: 120 },
|
activeTokens: { type: "number", example: 120 },
|
||||||
expiredTokens: { type: 'number', example: 30 },
|
expiredTokens: { type: "number", example: 30 },
|
||||||
cacheSize: { type: 'number', example: 150 },
|
cacheSize: { type: "number", example: 150 },
|
||||||
maxCacheSize: { type: 'number', example: 10000 }
|
maxCacheSize: { type: "number", example: 10000 },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
getCsrfStats(@Req() req: AuthenticatedRequest) {
|
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)
|
// Only allow admin users to see stats (you might want to add role checking)
|
||||||
this.logger.debug("CSRF stats requested", {
|
this.logger.debug("CSRF stats requested", {
|
||||||
userId,
|
userId,
|
||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get("user-agent"),
|
||||||
ip: req.ip
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stats = this.csrfService.getTokenStats();
|
const stats = this.csrfService.getTokenStats();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
stats
|
stats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractSessionId(req: AuthenticatedRequest): string | null {
|
private extractSessionId(req: AuthenticatedRequest): string | null {
|
||||||
return req.cookies?.['session-id'] ||
|
return (
|
||||||
req.cookies?.['connect.sid'] ||
|
req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || (req as any).sessionID || null
|
||||||
(req as any).sessionID ||
|
);
|
||||||
null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,16 +29,16 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
|
|
||||||
// Paths that don't require CSRF protection
|
// Paths that don't require CSRF protection
|
||||||
this.exemptPaths = new Set([
|
this.exemptPaths = new Set([
|
||||||
'/api/auth/login',
|
"/api/auth/login",
|
||||||
'/api/auth/signup',
|
"/api/auth/signup",
|
||||||
'/api/auth/refresh',
|
"/api/auth/refresh",
|
||||||
'/api/health',
|
"/api/health",
|
||||||
'/docs',
|
"/docs",
|
||||||
'/api/webhooks', // Webhooks typically don't use CSRF
|
"/api/webhooks", // Webhooks typically don't use CSRF
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Methods that don't require CSRF protection (safe methods)
|
// 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 {
|
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
|
// Check for API endpoints that might be exempt
|
||||||
if (req.path.startsWith('/api/webhooks/')) {
|
if (req.path.startsWith("/api/webhooks/")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
|
|
||||||
private requiresCsrfProtection(req: CsrfRequest): boolean {
|
private requiresCsrfProtection(req: CsrfRequest): boolean {
|
||||||
// State-changing methods require CSRF protection
|
// 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 {
|
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", {
|
this.logger.warn("CSRF validation failed - missing token", {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.path,
|
path: req.path,
|
||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get("user-agent"),
|
||||||
ip: req.ip
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
throw new ForbiddenException("CSRF token required");
|
throw new ForbiddenException("CSRF token required");
|
||||||
}
|
}
|
||||||
@ -100,23 +100,28 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
this.logger.warn("CSRF validation failed - missing secret cookie", {
|
this.logger.warn("CSRF validation failed - missing secret cookie", {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.path,
|
path: req.path,
|
||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get("user-agent"),
|
||||||
ip: req.ip
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
throw new ForbiddenException("CSRF secret required");
|
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) {
|
if (!validationResult.isValid) {
|
||||||
this.logger.warn("CSRF validation failed", {
|
this.logger.warn("CSRF validation failed", {
|
||||||
reason: validationResult.reason,
|
reason: validationResult.reason,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.path,
|
path: req.path,
|
||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get("user-agent"),
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
userId,
|
userId,
|
||||||
sessionId
|
sessionId,
|
||||||
});
|
});
|
||||||
throw new ForbiddenException(`CSRF validation failed: ${validationResult.reason}`);
|
throw new ForbiddenException(`CSRF validation failed: ${validationResult.reason}`);
|
||||||
}
|
}
|
||||||
@ -128,7 +133,7 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.path,
|
path: req.path,
|
||||||
userId,
|
userId,
|
||||||
sessionId
|
sessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
next();
|
next();
|
||||||
@ -151,13 +156,13 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
this.setCsrfSecretCookie(res, tokenData.secret);
|
this.setCsrfSecretCookie(res, tokenData.secret);
|
||||||
|
|
||||||
// Set CSRF token in response header for client to use
|
// 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", {
|
this.logger.debug("CSRF token generated and set", {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.path,
|
path: req.path,
|
||||||
userId,
|
userId,
|
||||||
sessionId
|
sessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
next();
|
next();
|
||||||
@ -167,28 +172,28 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
// Check multiple possible locations for the CSRF token
|
// Check multiple possible locations for the CSRF token
|
||||||
|
|
||||||
// 1. X-CSRF-Token header (most common)
|
// 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;
|
if (token) return token;
|
||||||
|
|
||||||
// 2. X-Requested-With header (alternative)
|
// 2. X-Requested-With header (alternative)
|
||||||
token = req.get('X-Requested-With');
|
token = req.get("X-Requested-With");
|
||||||
if (token && token !== 'XMLHttpRequest') return token;
|
if (token && token !== "XMLHttpRequest") return token;
|
||||||
|
|
||||||
// 3. Authorization header (if using Bearer token pattern)
|
// 3. Authorization header (if using Bearer token pattern)
|
||||||
const authHeader = req.get('Authorization');
|
const authHeader = req.get("Authorization");
|
||||||
if (authHeader && authHeader.startsWith('CSRF ')) {
|
if (authHeader && authHeader.startsWith("CSRF ")) {
|
||||||
return authHeader.substring(5);
|
return authHeader.substring(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Request body (for form submissions)
|
// 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;
|
token = req.body._csrf || req.body.csrfToken;
|
||||||
if (token) return token;
|
if (token) return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Query parameter (least secure, only for GET requests)
|
// 5. Query parameter (least secure, only for GET requests)
|
||||||
if (req.method === 'GET') {
|
if (req.method === "GET") {
|
||||||
token = req.query._csrf as string || req.query.csrfToken as string;
|
token = (req.query._csrf as string) || (req.query.csrfToken as string);
|
||||||
if (token) return token;
|
if (token) return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,26 +201,23 @@ export class CsrfMiddleware implements NestMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extractSecretFromCookie(req: CsrfRequest): string | null {
|
private extractSecretFromCookie(req: CsrfRequest): string | null {
|
||||||
return req.cookies?.['csrf-secret'] || null;
|
return req.cookies?.["csrf-secret"] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractSessionId(req: CsrfRequest): string | null {
|
private extractSessionId(req: CsrfRequest): string | null {
|
||||||
// Try to extract session ID from various sources
|
// Try to extract session ID from various sources
|
||||||
return req.cookies?.['session-id'] ||
|
return req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || req.sessionID || null;
|
||||||
req.cookies?.['connect.sid'] ||
|
|
||||||
req.sessionID ||
|
|
||||||
null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCsrfSecretCookie(res: Response, secret: string): void {
|
private setCsrfSecretCookie(res: Response, secret: string): void {
|
||||||
const cookieOptions = {
|
const cookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: this.isProduction,
|
secure: this.isProduction,
|
||||||
sameSite: 'strict' as const,
|
sameSite: "strict" as const,
|
||||||
maxAge: 3600000, // 1 hour
|
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 {
|
export class SecurityModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
// Apply CSRF middleware to all routes except those handled by the middleware itself
|
// Apply CSRF middleware to all routes except those handled by the middleware itself
|
||||||
consumer
|
consumer.apply(CsrfMiddleware).forRoutes("*");
|
||||||
.apply(CsrfMiddleware)
|
|
||||||
.forRoutes('*');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,9 @@ export class CsrfService {
|
|||||||
this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey();
|
this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey();
|
||||||
|
|
||||||
if (!this.configService.get("CSRF_SECRET_KEY")) {
|
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
|
// Clean up expired tokens periodically
|
||||||
@ -56,7 +58,7 @@ export class CsrfService {
|
|||||||
secret,
|
secret,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
sessionId,
|
sessionId,
|
||||||
userId
|
userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in cache for validation
|
// Store in cache for validation
|
||||||
@ -71,7 +73,7 @@ export class CsrfService {
|
|||||||
tokenHash: this.hashToken(token),
|
tokenHash: this.hashToken(token),
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
expiresAt: expiresAt.toISOString()
|
expiresAt: expiresAt.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return tokenData;
|
return tokenData;
|
||||||
@ -89,7 +91,7 @@ export class CsrfService {
|
|||||||
if (!token || !secret) {
|
if (!token || !secret) {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
reason: "Missing token or secret"
|
reason: "Missing token or secret",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +100,7 @@ export class CsrfService {
|
|||||||
if (!cachedTokenData) {
|
if (!cachedTokenData) {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
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);
|
this.tokenCache.delete(token);
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
reason: "Token expired"
|
reason: "Token expired",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,11 +118,11 @@ export class CsrfService {
|
|||||||
this.logger.warn("CSRF token validation failed - secret mismatch", {
|
this.logger.warn("CSRF token validation failed - secret mismatch", {
|
||||||
tokenHash: this.hashToken(token),
|
tokenHash: this.hashToken(token),
|
||||||
sessionId,
|
sessionId,
|
||||||
userId
|
userId,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
reason: "Invalid secret"
|
reason: "Invalid secret",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,11 +131,11 @@ export class CsrfService {
|
|||||||
this.logger.warn("CSRF token validation failed - session mismatch", {
|
this.logger.warn("CSRF token validation failed - session mismatch", {
|
||||||
tokenHash: this.hashToken(token),
|
tokenHash: this.hashToken(token),
|
||||||
expectedSession: cachedTokenData.sessionId,
|
expectedSession: cachedTokenData.sessionId,
|
||||||
providedSession: sessionId
|
providedSession: sessionId,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
reason: "Session mismatch"
|
reason: "Session mismatch",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,11 +144,11 @@ export class CsrfService {
|
|||||||
this.logger.warn("CSRF token validation failed - user mismatch", {
|
this.logger.warn("CSRF token validation failed - user mismatch", {
|
||||||
tokenHash: this.hashToken(token),
|
tokenHash: this.hashToken(token),
|
||||||
expectedUser: cachedTokenData.userId,
|
expectedUser: cachedTokenData.userId,
|
||||||
providedUser: userId
|
providedUser: userId,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
reason: "User mismatch"
|
reason: "User mismatch",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,23 +164,23 @@ export class CsrfService {
|
|||||||
this.logger.warn("CSRF token validation failed - token mismatch", {
|
this.logger.warn("CSRF token validation failed - token mismatch", {
|
||||||
tokenHash: this.hashToken(token),
|
tokenHash: this.hashToken(token),
|
||||||
sessionId,
|
sessionId,
|
||||||
userId
|
userId,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
reason: "Invalid token"
|
reason: "Invalid token",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug("CSRF token validated successfully", {
|
this.logger.debug("CSRF token validated successfully", {
|
||||||
tokenHash: this.hashToken(token),
|
tokenHash: this.hashToken(token),
|
||||||
sessionId,
|
sessionId,
|
||||||
userId
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
tokenData: cachedTokenData
|
tokenData: cachedTokenData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +190,7 @@ export class CsrfService {
|
|||||||
invalidateToken(token: string): void {
|
invalidateToken(token: string): void {
|
||||||
this.tokenCache.delete(token);
|
this.tokenCache.delete(token);
|
||||||
this.logger.debug("CSRF token invalidated", {
|
this.logger.debug("CSRF token invalidated", {
|
||||||
tokenHash: this.hashToken(token)
|
tokenHash: this.hashToken(token),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +208,7 @@ export class CsrfService {
|
|||||||
|
|
||||||
this.logger.debug("CSRF tokens invalidated for session", {
|
this.logger.debug("CSRF tokens invalidated for session", {
|
||||||
sessionId,
|
sessionId,
|
||||||
invalidatedCount
|
invalidatedCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +226,7 @@ export class CsrfService {
|
|||||||
|
|
||||||
this.logger.debug("CSRF tokens invalidated for user", {
|
this.logger.debug("CSRF tokens invalidated for user", {
|
||||||
userId,
|
userId,
|
||||||
invalidatedCount
|
invalidatedCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,30 +251,32 @@ export class CsrfService {
|
|||||||
activeTokens,
|
activeTokens,
|
||||||
expiredTokens,
|
expiredTokens,
|
||||||
cacheSize: this.tokenCache.size,
|
cacheSize: this.tokenCache.size,
|
||||||
maxCacheSize: this.maxCacheSize
|
maxCacheSize: this.maxCacheSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSecret(): string {
|
private generateSecret(): string {
|
||||||
return crypto.randomBytes(32).toString('base64url');
|
return crypto.randomBytes(32).toString("base64url");
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateTokenFromSecret(secret: string, sessionId?: string, userId?: string): string {
|
private generateTokenFromSecret(secret: string, sessionId?: string, userId?: string): string {
|
||||||
const data = [secret, sessionId || '', userId || ''].join('|');
|
const data = [secret, sessionId || "", userId || ""].join("|");
|
||||||
const hmac = crypto.createHmac('sha256', this.secretKey);
|
const hmac = crypto.createHmac("sha256", this.secretKey);
|
||||||
hmac.update(data);
|
hmac.update(data);
|
||||||
return hmac.digest('base64url');
|
return hmac.digest("base64url");
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSecretKey(): string {
|
private generateSecretKey(): string {
|
||||||
const key = crypto.randomBytes(64).toString('base64url');
|
const key = crypto.randomBytes(64).toString("base64url");
|
||||||
this.logger.warn("Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production");
|
this.logger.warn(
|
||||||
|
"Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production"
|
||||||
|
);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
private hashToken(token: string): string {
|
private hashToken(token: string): string {
|
||||||
// Create a hash of the token for logging (never log the actual token)
|
// 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 {
|
private constantTimeEquals(a: string, b: string): boolean {
|
||||||
@ -302,7 +306,7 @@ export class CsrfService {
|
|||||||
if (cleanedCount > 0) {
|
if (cleanedCount > 0) {
|
||||||
this.logger.debug("Cleaned up expired CSRF tokens", {
|
this.logger.debug("Cleaned up expired CSRF tokens", {
|
||||||
cleanedCount,
|
cleanedCount,
|
||||||
remainingTokens: this.tokenCache.size
|
remainingTokens: this.tokenCache.size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,13 +14,13 @@ export interface ErrorContext {
|
|||||||
export interface SecureErrorMapping {
|
export interface SecureErrorMapping {
|
||||||
code: string;
|
code: string;
|
||||||
publicMessage: string;
|
publicMessage: string;
|
||||||
logLevel: 'error' | 'warn' | 'info' | 'debug';
|
logLevel: "error" | "warn" | "info" | "debug";
|
||||||
shouldAlert?: boolean; // Whether to send alerts to monitoring
|
shouldAlert?: boolean; // Whether to send alerts to monitoring
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorClassification {
|
export interface ErrorClassification {
|
||||||
category: 'authentication' | 'authorization' | 'validation' | 'business' | 'system' | 'external';
|
category: "authentication" | "authorization" | "validation" | "business" | "system" | "external";
|
||||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
severity: "low" | "medium" | "high" | "critical";
|
||||||
mapping: SecureErrorMapping;
|
mapping: SecureErrorMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,10 +46,7 @@ export class SecureErrorMapperService {
|
|||||||
/**
|
/**
|
||||||
* Map an error to a secure public message
|
* Map an error to a secure public message
|
||||||
*/
|
*/
|
||||||
mapError(
|
mapError(error: unknown, context?: ErrorContext): ErrorClassification {
|
||||||
error: unknown,
|
|
||||||
context?: ErrorContext
|
|
||||||
): ErrorClassification {
|
|
||||||
const errorMessage = this.extractErrorMessage(error);
|
const errorMessage = this.extractErrorMessage(error);
|
||||||
const errorCode = this.extractErrorCode(error);
|
const errorCode = this.extractErrorCode(error);
|
||||||
|
|
||||||
@ -104,27 +101,27 @@ export class SecureErrorMapperService {
|
|||||||
publicMessage: classification.mapping.publicMessage,
|
publicMessage: classification.mapping.publicMessage,
|
||||||
originalMessage: this.sanitizeForLogging(originalMessage),
|
originalMessage: this.sanitizeForLogging(originalMessage),
|
||||||
context,
|
context,
|
||||||
...additionalData
|
...additionalData,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log based on severity and log level
|
// Log based on severity and log level
|
||||||
switch (classification.mapping.logLevel) {
|
switch (classification.mapping.logLevel) {
|
||||||
case 'error':
|
case "error":
|
||||||
this.logger.error(`Security Error: ${classification.mapping.code}`, logData);
|
this.logger.error(`Security Error: ${classification.mapping.code}`, logData);
|
||||||
break;
|
break;
|
||||||
case 'warn':
|
case "warn":
|
||||||
this.logger.warn(`Security Warning: ${classification.mapping.code}`, logData);
|
this.logger.warn(`Security Warning: ${classification.mapping.code}`, logData);
|
||||||
break;
|
break;
|
||||||
case 'info':
|
case "info":
|
||||||
this.logger.log(`Security Info: ${classification.mapping.code}`, logData);
|
this.logger.log(`Security Info: ${classification.mapping.code}`, logData);
|
||||||
break;
|
break;
|
||||||
case 'debug':
|
case "debug":
|
||||||
this.logger.debug(`Security Debug: ${classification.mapping.code}`, logData);
|
this.logger.debug(`Security Debug: ${classification.mapping.code}`, logData);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send alerts for critical errors
|
// Send alerts for critical errors
|
||||||
if (classification.mapping.shouldAlert && classification.severity === 'critical') {
|
if (classification.mapping.shouldAlert && classification.severity === "critical") {
|
||||||
this.sendSecurityAlert(classification, context, logData);
|
this.sendSecurityAlert(classification, context, logData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,101 +129,149 @@ export class SecureErrorMapperService {
|
|||||||
private initializeErrorMappings(): Map<string, SecureErrorMapping> {
|
private initializeErrorMappings(): Map<string, SecureErrorMapping> {
|
||||||
return new Map([
|
return new Map([
|
||||||
// Authentication Errors
|
// Authentication Errors
|
||||||
['INVALID_CREDENTIALS', {
|
[
|
||||||
code: 'AUTH_001',
|
"INVALID_CREDENTIALS",
|
||||||
publicMessage: 'Invalid email or password',
|
{
|
||||||
logLevel: 'warn'
|
code: "AUTH_001",
|
||||||
}],
|
publicMessage: "Invalid email or password",
|
||||||
['ACCOUNT_LOCKED', {
|
logLevel: "warn",
|
||||||
code: 'AUTH_002',
|
},
|
||||||
publicMessage: 'Account temporarily locked. Please try again later',
|
],
|
||||||
logLevel: 'warn'
|
[
|
||||||
}],
|
"ACCOUNT_LOCKED",
|
||||||
['TOKEN_EXPIRED', {
|
{
|
||||||
code: 'AUTH_003',
|
code: "AUTH_002",
|
||||||
publicMessage: 'Session expired. Please log in again',
|
publicMessage: "Account temporarily locked. Please try again later",
|
||||||
logLevel: 'info'
|
logLevel: "warn",
|
||||||
}],
|
},
|
||||||
['TOKEN_INVALID', {
|
],
|
||||||
code: 'AUTH_004',
|
[
|
||||||
publicMessage: 'Invalid session. Please log in again',
|
"TOKEN_EXPIRED",
|
||||||
logLevel: 'warn'
|
{
|
||||||
}],
|
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
|
// Authorization Errors
|
||||||
['INSUFFICIENT_PERMISSIONS', {
|
[
|
||||||
code: 'AUTHZ_001',
|
"INSUFFICIENT_PERMISSIONS",
|
||||||
publicMessage: 'You do not have permission to perform this action',
|
{
|
||||||
logLevel: 'warn'
|
code: "AUTHZ_001",
|
||||||
}],
|
publicMessage: "You do not have permission to perform this action",
|
||||||
['RESOURCE_NOT_FOUND', {
|
logLevel: "warn",
|
||||||
code: 'AUTHZ_002',
|
},
|
||||||
publicMessage: 'The requested resource was not found',
|
],
|
||||||
logLevel: 'info'
|
[
|
||||||
}],
|
"RESOURCE_NOT_FOUND",
|
||||||
|
{
|
||||||
|
code: "AUTHZ_002",
|
||||||
|
publicMessage: "The requested resource was not found",
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Validation Errors
|
// Validation Errors
|
||||||
['VALIDATION_FAILED', {
|
[
|
||||||
code: 'VAL_001',
|
"VALIDATION_FAILED",
|
||||||
publicMessage: 'The provided data is invalid',
|
{
|
||||||
logLevel: 'info'
|
code: "VAL_001",
|
||||||
}],
|
publicMessage: "The provided data is invalid",
|
||||||
['REQUIRED_FIELD_MISSING', {
|
logLevel: "info",
|
||||||
code: 'VAL_002',
|
},
|
||||||
publicMessage: 'Required information is missing',
|
],
|
||||||
logLevel: 'info'
|
[
|
||||||
}],
|
"REQUIRED_FIELD_MISSING",
|
||||||
|
{
|
||||||
|
code: "VAL_002",
|
||||||
|
publicMessage: "Required information is missing",
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Business Logic Errors
|
// Business Logic Errors
|
||||||
['ORDER_ALREADY_PROCESSED', {
|
[
|
||||||
code: 'BIZ_001',
|
"ORDER_ALREADY_PROCESSED",
|
||||||
publicMessage: 'This order has already been processed',
|
{
|
||||||
logLevel: 'info'
|
code: "BIZ_001",
|
||||||
}],
|
publicMessage: "This order has already been processed",
|
||||||
['INSUFFICIENT_BALANCE', {
|
logLevel: "info",
|
||||||
code: 'BIZ_002',
|
},
|
||||||
publicMessage: 'Insufficient account balance',
|
],
|
||||||
logLevel: 'info'
|
[
|
||||||
}],
|
"INSUFFICIENT_BALANCE",
|
||||||
['SERVICE_UNAVAILABLE', {
|
{
|
||||||
code: 'BIZ_003',
|
code: "BIZ_002",
|
||||||
publicMessage: 'Service is temporarily unavailable',
|
publicMessage: "Insufficient account balance",
|
||||||
logLevel: 'warn'
|
logLevel: "info",
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SERVICE_UNAVAILABLE",
|
||||||
|
{
|
||||||
|
code: "BIZ_003",
|
||||||
|
publicMessage: "Service is temporarily unavailable",
|
||||||
|
logLevel: "warn",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// System Errors (High Security)
|
// System Errors (High Security)
|
||||||
['DATABASE_ERROR', {
|
[
|
||||||
code: 'SYS_001',
|
"DATABASE_ERROR",
|
||||||
publicMessage: 'A system error occurred. Please try again later',
|
{
|
||||||
logLevel: 'error',
|
code: "SYS_001",
|
||||||
shouldAlert: true
|
publicMessage: "A system error occurred. Please try again later",
|
||||||
}],
|
logLevel: "error",
|
||||||
['EXTERNAL_SERVICE_ERROR', {
|
shouldAlert: true,
|
||||||
code: 'SYS_002',
|
},
|
||||||
publicMessage: 'External service temporarily unavailable',
|
],
|
||||||
logLevel: 'error'
|
[
|
||||||
}],
|
"EXTERNAL_SERVICE_ERROR",
|
||||||
['CONFIGURATION_ERROR', {
|
{
|
||||||
code: 'SYS_003',
|
code: "SYS_002",
|
||||||
publicMessage: 'System configuration error',
|
publicMessage: "External service temporarily unavailable",
|
||||||
logLevel: 'error',
|
logLevel: "error",
|
||||||
shouldAlert: true
|
},
|
||||||
}],
|
],
|
||||||
|
[
|
||||||
|
"CONFIGURATION_ERROR",
|
||||||
|
{
|
||||||
|
code: "SYS_003",
|
||||||
|
publicMessage: "System configuration error",
|
||||||
|
logLevel: "error",
|
||||||
|
shouldAlert: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Rate Limiting
|
// Rate Limiting
|
||||||
['RATE_LIMIT_EXCEEDED', {
|
[
|
||||||
code: 'RATE_001',
|
"RATE_LIMIT_EXCEEDED",
|
||||||
publicMessage: 'Too many requests. Please try again later',
|
{
|
||||||
logLevel: 'warn'
|
code: "RATE_001",
|
||||||
}],
|
publicMessage: "Too many requests. Please try again later",
|
||||||
|
logLevel: "warn",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Generic Fallbacks
|
// Generic Fallbacks
|
||||||
['UNKNOWN_ERROR', {
|
[
|
||||||
code: 'GEN_001',
|
"UNKNOWN_ERROR",
|
||||||
publicMessage: 'An unexpected error occurred',
|
{
|
||||||
logLevel: 'error',
|
code: "GEN_001",
|
||||||
shouldAlert: true
|
publicMessage: "An unexpected error occurred",
|
||||||
}]
|
logLevel: "error",
|
||||||
|
shouldAlert: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,82 +281,82 @@ export class SecureErrorMapperService {
|
|||||||
{
|
{
|
||||||
pattern: /database|connection|sql|prisma|postgres/i,
|
pattern: /database|connection|sql|prisma|postgres/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'SYS_001',
|
code: "SYS_001",
|
||||||
publicMessage: 'A system error occurred. Please try again later',
|
publicMessage: "A system error occurred. Please try again later",
|
||||||
logLevel: 'error',
|
logLevel: "error",
|
||||||
shouldAlert: true
|
shouldAlert: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Authentication patterns
|
// Authentication patterns
|
||||||
{
|
{
|
||||||
pattern: /password|credential|token|secret|key|auth/i,
|
pattern: /password|credential|token|secret|key|auth/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'AUTH_001',
|
code: "AUTH_001",
|
||||||
publicMessage: 'Authentication failed',
|
publicMessage: "Authentication failed",
|
||||||
logLevel: 'warn'
|
logLevel: "warn",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// File system patterns
|
// File system patterns
|
||||||
{
|
{
|
||||||
pattern: /file|path|directory|permission denied|enoent|eacces/i,
|
pattern: /file|path|directory|permission denied|enoent|eacces/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'SYS_002',
|
code: "SYS_002",
|
||||||
publicMessage: 'System resource error',
|
publicMessage: "System resource error",
|
||||||
logLevel: 'error',
|
logLevel: "error",
|
||||||
shouldAlert: true
|
shouldAlert: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Network/External service patterns
|
// Network/External service patterns
|
||||||
{
|
{
|
||||||
pattern: /network|timeout|connection refused|econnrefused|whmcs|salesforce/i,
|
pattern: /network|timeout|connection refused|econnrefused|whmcs|salesforce/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'SYS_002',
|
code: "SYS_002",
|
||||||
publicMessage: 'External service temporarily unavailable',
|
publicMessage: "External service temporarily unavailable",
|
||||||
logLevel: 'error'
|
logLevel: "error",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Stack trace patterns
|
// Stack trace patterns
|
||||||
{
|
{
|
||||||
pattern: /\s+at\s+|\.js:\d+|\.ts:\d+|stack trace/i,
|
pattern: /\s+at\s+|\.js:\d+|\.ts:\d+|stack trace/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'SYS_001',
|
code: "SYS_001",
|
||||||
publicMessage: 'A system error occurred. Please try again later',
|
publicMessage: "A system error occurred. Please try again later",
|
||||||
logLevel: 'error',
|
logLevel: "error",
|
||||||
shouldAlert: true
|
shouldAlert: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Memory/Resource patterns
|
// Memory/Resource patterns
|
||||||
{
|
{
|
||||||
pattern: /memory|heap|out of memory|resource|limit exceeded/i,
|
pattern: /memory|heap|out of memory|resource|limit exceeded/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'SYS_003',
|
code: "SYS_003",
|
||||||
publicMessage: 'System resources temporarily unavailable',
|
publicMessage: "System resources temporarily unavailable",
|
||||||
logLevel: 'error',
|
logLevel: "error",
|
||||||
shouldAlert: true
|
shouldAlert: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Validation patterns
|
// Validation patterns
|
||||||
{
|
{
|
||||||
pattern: /invalid|required|missing|validation|format/i,
|
pattern: /invalid|required|missing|validation|format/i,
|
||||||
mapping: {
|
mapping: {
|
||||||
code: 'VAL_001',
|
code: "VAL_001",
|
||||||
publicMessage: 'The provided data is invalid',
|
publicMessage: "The provided data is invalid",
|
||||||
logLevel: 'info'
|
logLevel: "info",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private createClassification(
|
private createClassification(
|
||||||
originalMessage: string,
|
originalMessage: string,
|
||||||
mapping: SecureErrorMapping,
|
mapping: SecureErrorMapping,
|
||||||
context?: ErrorContext
|
_context?: ErrorContext
|
||||||
): ErrorClassification {
|
): ErrorClassification {
|
||||||
// Determine category and severity based on error code
|
// Determine category and severity based on error code
|
||||||
const category = this.determineCategory(mapping.code);
|
const category = this.determineCategory(mapping.code);
|
||||||
@ -320,50 +365,50 @@ export class SecureErrorMapperService {
|
|||||||
return {
|
return {
|
||||||
category,
|
category,
|
||||||
severity,
|
severity,
|
||||||
mapping
|
mapping,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private determineCategory(code: string): ErrorClassification['category'] {
|
private determineCategory(code: string): ErrorClassification["category"] {
|
||||||
if (code.startsWith('AUTH_')) return 'authentication';
|
if (code.startsWith("AUTH_")) return "authentication";
|
||||||
if (code.startsWith('AUTHZ_')) return 'authorization';
|
if (code.startsWith("AUTHZ_")) return "authorization";
|
||||||
if (code.startsWith('VAL_')) return 'validation';
|
if (code.startsWith("VAL_")) return "validation";
|
||||||
if (code.startsWith('BIZ_')) return 'business';
|
if (code.startsWith("BIZ_")) return "business";
|
||||||
if (code.startsWith('SYS_')) return 'system';
|
if (code.startsWith("SYS_")) return "system";
|
||||||
return 'system';
|
return "system";
|
||||||
}
|
}
|
||||||
|
|
||||||
private determineSeverity(code: string, message: string): ErrorClassification['severity'] {
|
private determineSeverity(code: string, message: string): ErrorClassification["severity"] {
|
||||||
// Critical system errors
|
// 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
|
// 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
|
// Medium for external service issues
|
||||||
if (code === 'SYS_002') return 'medium';
|
if (code === "SYS_002") return "medium";
|
||||||
|
|
||||||
// Low for validation and business logic
|
// Low for validation and business logic
|
||||||
if (code.startsWith('VAL_') || code.startsWith('BIZ_')) return 'low';
|
if (code.startsWith("VAL_") || code.startsWith("BIZ_")) return "low";
|
||||||
|
|
||||||
return 'medium';
|
return "medium";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultMapping(message: string): SecureErrorMapping {
|
private getDefaultMapping(message: string): SecureErrorMapping {
|
||||||
// Analyze message for sensitivity
|
// Analyze message for sensitivity
|
||||||
if (this.containsSensitiveInfo(message)) {
|
if (this.containsSensitiveInfo(message)) {
|
||||||
return {
|
return {
|
||||||
code: 'SYS_001',
|
code: "SYS_001",
|
||||||
publicMessage: 'A system error occurred. Please try again later',
|
publicMessage: "A system error occurred. Please try again later",
|
||||||
logLevel: 'error',
|
logLevel: "error",
|
||||||
shouldAlert: true
|
shouldAlert: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 'GEN_001',
|
code: "GEN_001",
|
||||||
publicMessage: 'An unexpected error occurred',
|
publicMessage: "An unexpected error occurred",
|
||||||
logLevel: 'error'
|
logLevel: "error",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,9 +419,9 @@ export class SecureErrorMapperService {
|
|||||||
/file|path|directory/i,
|
/file|path|directory/i,
|
||||||
/\s+at\s+.*\.js:\d+/i, // Stack traces
|
/\s+at\s+.*\.js:\d+/i, // Stack traces
|
||||||
/[a-zA-Z]:[\\\/]/, // Windows paths
|
/[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
|
/\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));
|
return sensitivePatterns.some(pattern => pattern.test(message));
|
||||||
@ -386,22 +431,22 @@ export class SecureErrorMapperService {
|
|||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return error.message;
|
return error.message;
|
||||||
}
|
}
|
||||||
if (typeof error === 'string') {
|
if (typeof error === "string") {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
if (typeof error === 'object' && error !== null) {
|
if (typeof error === "object" && error !== null) {
|
||||||
const obj = error as Record<string, unknown>;
|
const obj = error as Record<string, unknown>;
|
||||||
if (typeof obj.message === 'string') {
|
if (typeof obj.message === "string") {
|
||||||
return obj.message;
|
return obj.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'Unknown error';
|
return "Unknown error";
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractErrorCode(error: unknown): string | null {
|
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>;
|
const obj = error as Record<string, unknown>;
|
||||||
if (typeof obj.code === 'string') {
|
if (typeof obj.code === "string") {
|
||||||
return obj.code;
|
return obj.code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -409,29 +454,31 @@ export class SecureErrorMapperService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeForLogging(message: string): string {
|
private sanitizeForLogging(message: string): string {
|
||||||
return message
|
return (
|
||||||
|
message
|
||||||
// Remove file paths
|
// Remove file paths
|
||||||
.replace(/\/[a-zA-Z0-9._\-/]+\.(js|ts|py|php)/g, '[file]')
|
.replace(/[/][a-zA-Z0-9._\-/]+\.(js|ts|py|php)/g, "[file]")
|
||||||
// Remove stack traces
|
// Remove stack traces
|
||||||
.replace(/\s+at\s+.*/g, '')
|
.replace(/\s+at\s+.*/g, "")
|
||||||
// Remove absolute paths
|
// Remove absolute paths
|
||||||
.replace(/[a-zA-Z]:[\\\/][^:]+/g, '[path]')
|
.replace(/[a-zA-Z]:[\\\/][^:]+/g, "[path]")
|
||||||
// Remove IP addresses
|
// Remove IP addresses
|
||||||
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[ip]')
|
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[ip]")
|
||||||
// Remove URLs with credentials
|
// Remove URLs with credentials
|
||||||
.replace(/https?:\/\/[^:]+:[^@]+@[^\s]+/g, '[url]')
|
.replace(/https?:\/\/[^:]+:[^@]+@[^\s]+/g, "[url]")
|
||||||
// Remove potential secrets
|
// Remove potential secrets
|
||||||
.replace(/\b[A-Za-z0-9]{32,}\b/g, '[token]')
|
.replace(/\b[A-Za-z0-9]{32,}\b/g, "[token]")
|
||||||
.trim();
|
.trim()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeForDevelopment(message: string): string {
|
private sanitizeForDevelopment(message: string): string {
|
||||||
// In development, show more but still remove the most sensitive parts
|
// In development, show more but still remove the most sensitive parts
|
||||||
return message
|
return message
|
||||||
.replace(/password[=:]\s*[^\s]+/gi, 'password=[HIDDEN]')
|
.replace(/password[=:]\s*[^\s]+/gi, "password=[HIDDEN]")
|
||||||
.replace(/secret[=:]\s*[^\s]+/gi, 'secret=[HIDDEN]')
|
.replace(/secret[=:]\s*[^\s]+/gi, "secret=[HIDDEN]")
|
||||||
.replace(/token[=:]\s*[^\s]+/gi, 'token=[HIDDEN]')
|
.replace(/token[=:]\s*[^\s]+/gi, "token=[HIDDEN]")
|
||||||
.replace(/key[=:]\s*[^\s]+/gi, 'key=[HIDDEN]');
|
.replace(/key[=:]\s*[^\s]+/gi, "key=[HIDDEN]");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendSecurityAlert(
|
private sendSecurityAlert(
|
||||||
@ -441,14 +488,14 @@ export class SecureErrorMapperService {
|
|||||||
): void {
|
): void {
|
||||||
// In a real implementation, this would send alerts to monitoring systems
|
// In a real implementation, this would send alerts to monitoring systems
|
||||||
// like Slack, PagerDuty, or custom alerting systems
|
// like Slack, PagerDuty, or custom alerting systems
|
||||||
this.logger.error('SECURITY ALERT TRIGGERED', {
|
this.logger.error("SECURITY ALERT TRIGGERED", {
|
||||||
alertType: 'CRITICAL_ERROR',
|
alertType: "CRITICAL_ERROR",
|
||||||
errorCode: classification.mapping.code,
|
errorCode: classification.mapping.code,
|
||||||
category: classification.category,
|
category: classification.category,
|
||||||
severity: classification.severity,
|
severity: classification.severity,
|
||||||
context,
|
context,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
...logData
|
...logData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -192,15 +192,16 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
|||||||
const errorData = data as SalesforcePubSubError;
|
const errorData = data as SalesforcePubSubError;
|
||||||
const details = errorData.details || "";
|
const details = errorData.details || "";
|
||||||
const metadata = errorData.metadata || {};
|
const metadata = errorData.metadata || {};
|
||||||
const errorCodes = Array.isArray(metadata["error-code"])
|
const errorCodes = Array.isArray(metadata["error-code"]) ? metadata["error-code"] : [];
|
||||||
? metadata["error-code"]
|
|
||||||
: [];
|
|
||||||
const hasCorruptionCode = errorCodes.some(code =>
|
const hasCorruptionCode = errorCodes.some(code =>
|
||||||
String(code).includes("replayid.corrupted")
|
String(code).includes("replayid.corrupted")
|
||||||
);
|
);
|
||||||
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
|
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
|
||||||
|
|
||||||
if ((hasCorruptionCode || mentionsReplayValidation) && !this.replayCorruptionRecovered) {
|
if (
|
||||||
|
(hasCorruptionCode || mentionsReplayValidation) &&
|
||||||
|
!this.replayCorruptionRecovered
|
||||||
|
) {
|
||||||
this.replayCorruptionRecovered = true;
|
this.replayCorruptionRecovered = true;
|
||||||
const key = sfReplayKey(this.channel);
|
const key = sfReplayKey(this.channel);
|
||||||
await this.cache.del(key);
|
await this.cache.del(key);
|
||||||
@ -291,7 +292,7 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
|||||||
this.replayCorruptionRecovered = false;
|
this.replayCorruptionRecovered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.client!;
|
return this.client;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -326,11 +327,7 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
|||||||
Number(storedReplay)
|
Number(storedReplay)
|
||||||
);
|
);
|
||||||
} else if (replayMode === "ALL") {
|
} else if (replayMode === "ALL") {
|
||||||
await client.subscribeFromEarliestEvent(
|
await client.subscribeFromEarliestEvent(this.channel, this.subscribeCallback, numRequested);
|
||||||
this.channel,
|
|
||||||
this.subscribeCallback,
|
|
||||||
numRequested
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await client.subscribe(this.channel, this.subscribeCallback, numRequested);
|
await client.subscribe(this.channel, this.subscribeCallback, numRequested);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,8 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
|||||||
options: WhmcsRequestOptions = {}
|
options: WhmcsRequestOptions = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// Wrap the actual request in the queue to prevent race conditions
|
// Wrap the actual request in the queue to prevent race conditions
|
||||||
return this.requestQueue.execute(async () => {
|
return this.requestQueue.execute(
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
const config = this.configService.getConfig();
|
const config = this.configService.getConfig();
|
||||||
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
|
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
|
||||||
@ -87,12 +88,14 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
|||||||
// Handle general request errors
|
// Handle general request errors
|
||||||
this.errorHandler.handleRequestError(error, action, params);
|
this.errorHandler.handleRequestError(error, action, params);
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
priority: this.getRequestPriority(action),
|
priority: this.getRequestPriority(action),
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
retryAttempts: options.retryAttempts,
|
retryAttempts: options.retryAttempts,
|
||||||
retryDelay: options.retryDelay,
|
retryDelay: options.retryDelay,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -286,15 +289,11 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
|||||||
"GetClientDetails",
|
"GetClientDetails",
|
||||||
"GetInvoice",
|
"GetInvoice",
|
||||||
"CapturePayment",
|
"CapturePayment",
|
||||||
"CreateSsoToken"
|
"CreateSsoToken",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Medium priority actions (important but can wait)
|
// Medium priority actions (important but can wait)
|
||||||
const mediumPriorityActions = [
|
const mediumPriorityActions = ["GetInvoices", "GetClientsProducts", "GetPayMethods"];
|
||||||
"GetInvoices",
|
|
||||||
"GetClientsProducts",
|
|
||||||
"GetPayMethods"
|
|
||||||
];
|
|
||||||
|
|
||||||
if (highPriorityActions.includes(action)) {
|
if (highPriorityActions.includes(action)) {
|
||||||
return 8; // High priority
|
return 8; // High priority
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export class WhmcsInvoiceService {
|
|||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}`
|
`Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}`
|
||||||
);
|
);
|
||||||
return result as InvoiceList;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to fetch invoices for client ${clientId}`, {
|
this.logger.error(`Failed to fetch invoices for client ${clientId}`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -136,7 +136,7 @@ export class WhmcsInvoiceService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const result: InvoiceList = {
|
const result: InvoiceList = {
|
||||||
invoices: invoicesWithItems as Invoice[],
|
invoices: invoicesWithItems,
|
||||||
pagination: invoiceList.pagination,
|
pagination: invoiceList.pagination,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ export class WhmcsInvoiceService {
|
|||||||
try {
|
try {
|
||||||
const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice);
|
const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice);
|
||||||
const parsed = invoiceSchema.parse(transformed);
|
const parsed = invoiceSchema.parse(transformed);
|
||||||
invoices.push(parsed as Invoice);
|
invoices.push(parsed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
|
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
|
||||||
error: getErrorMessage(error),
|
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 type { Request, Response } from "express";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
@ -99,10 +109,7 @@ export class AuthController {
|
|||||||
@ApiResponse({ status: 409, description: "Customer already has account" })
|
@ApiResponse({ status: 409, description: "Customer already has account" })
|
||||||
@ApiResponse({ status: 400, description: "Customer number not found" })
|
@ApiResponse({ status: 400, description: "Customer number not found" })
|
||||||
@ApiResponse({ status: 429, description: "Too many validation attempts" })
|
@ApiResponse({ status: 429, description: "Too many validation attempts" })
|
||||||
async validateSignup(
|
async validateSignup(@Body() validateData: ValidateSignupRequestInput, @Req() req: Request) {
|
||||||
@Body() validateData: ValidateSignupRequestInput,
|
|
||||||
@Req() req: Request
|
|
||||||
) {
|
|
||||||
return this.authService.validateSignup(validateData, req);
|
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> {
|
override async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context
|
const request = context
|
||||||
.switchToHttp()
|
.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}`;
|
const route = `${request.method} ${request.route?.path ?? request.url}`;
|
||||||
|
|
||||||
// Check if the route is marked as public
|
// Check if the route is marked as public
|
||||||
|
|||||||
@ -110,7 +110,10 @@ function coerceNumber(value: unknown): number | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function baseProduct(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap): CatalogProductBase {
|
function baseProduct(
|
||||||
|
product: SalesforceCatalogProductRecord,
|
||||||
|
fieldMap: SalesforceFieldMap
|
||||||
|
): CatalogProductBase {
|
||||||
const sku = getStringField(product, "sku", fieldMap) ?? "";
|
const sku = getStringField(product, "sku", fieldMap) ?? "";
|
||||||
const base: CatalogProductBase = {
|
const base: CatalogProductBase = {
|
||||||
id: product.Id,
|
id: product.Id,
|
||||||
@ -139,12 +142,18 @@ function getBoolean(
|
|||||||
return typeof value === "boolean" ? value : undefined;
|
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);
|
const raw = getProductField(product, "bundledAddon", fieldMap);
|
||||||
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBundledAddon(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap) {
|
function resolveBundledAddon(
|
||||||
|
product: SalesforceCatalogProductRecord,
|
||||||
|
fieldMap: SalesforceFieldMap
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
bundledAddonId: resolveBundledAddonId(product, fieldMap),
|
bundledAddonId: resolveBundledAddonId(product, fieldMap),
|
||||||
isBundledAddon: Boolean(getBoolean(product, "isBundledAddon", fieldMap)),
|
isBundledAddon: Boolean(getBoolean(product, "isBundledAddon", fieldMap)),
|
||||||
|
|||||||
@ -109,7 +109,11 @@ export class OrderBuilder {
|
|||||||
assignIfString(orderFields, orderField("accessMode", fieldMap), config.accessMode);
|
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 || {};
|
const config = body.configurations || {};
|
||||||
assignIfString(orderFields, orderField("simType", fieldMap), config.simType);
|
assignIfString(orderFields, orderField("simType", fieldMap), config.simType);
|
||||||
assignIfString(orderFields, orderField("eid", fieldMap), config.eid);
|
assignIfString(orderFields, orderField("eid", fieldMap), config.eid);
|
||||||
@ -119,7 +123,11 @@ export class OrderBuilder {
|
|||||||
assignIfString(orderFields, mnpField("reservationNumber", fieldMap), config.mnpNumber);
|
assignIfString(orderFields, mnpField("reservationNumber", fieldMap), config.mnpNumber);
|
||||||
assignIfString(orderFields, mnpField("expiryDate", fieldMap), config.mnpExpiry);
|
assignIfString(orderFields, mnpField("expiryDate", fieldMap), config.mnpExpiry);
|
||||||
assignIfString(orderFields, mnpField("phoneNumber", fieldMap), config.mnpPhone);
|
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("portingLastName", fieldMap), config.portingLastName);
|
||||||
assignIfString(orderFields, mnpField("portingFirstName", fieldMap), config.portingFirstName);
|
assignIfString(orderFields, mnpField("portingFirstName", fieldMap), config.portingFirstName);
|
||||||
assignIfString(
|
assignIfString(
|
||||||
@ -133,7 +141,11 @@ export class OrderBuilder {
|
|||||||
config.portingFirstNameKatakana
|
config.portingFirstNameKatakana
|
||||||
);
|
);
|
||||||
assignIfString(orderFields, mnpField("portingGender", fieldMap), config.portingGender);
|
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) {
|
} catch (error) {
|
||||||
this.logger.error("Fulfillment validation failed", {
|
this.logger.error("Fulfillment validation failed", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
error: getErrorMessage(error)
|
error: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -118,16 +118,18 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("Failed to get order details", {
|
this.logger.error("Failed to get order details", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
error: getErrorMessage(error)
|
error: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Execute the main fulfillment workflow as a distributed transaction
|
// Step 3: Execute the main fulfillment workflow as a distributed transaction
|
||||||
const fulfillmentResult = await this.distributedTransactionService.executeDistributedTransaction([
|
const fulfillmentResult =
|
||||||
|
await this.distributedTransactionService.executeDistributedTransaction(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
id: 'sf_status_update',
|
id: "sf_status_update",
|
||||||
description: 'Update Salesforce order status to Activating',
|
description: "Update Salesforce order status to Activating",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
const fields = this.fieldMapService.getFieldMap();
|
||||||
return await this.salesforceService.updateOrder({
|
return await this.salesforceService.updateOrder({
|
||||||
@ -142,24 +144,22 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
[fields.order.activationStatus]: "Failed",
|
[fields.order.activationStatus]: "Failed",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
critical: true
|
critical: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mapping',
|
id: "mapping",
|
||||||
description: 'Map OrderItems to WHMCS format',
|
description: "Map OrderItems to WHMCS format",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
if (!context.orderDetails) {
|
if (!context.orderDetails) {
|
||||||
throw new Error("Order details are required for mapping");
|
throw new Error("Order details are required for mapping");
|
||||||
}
|
}
|
||||||
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(
|
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(context.orderDetails.items);
|
||||||
context.orderDetails.items
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
critical: true
|
critical: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'whmcs_create',
|
id: "whmcs_create",
|
||||||
description: 'Create order in WHMCS',
|
description: "Create order in WHMCS",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const mappingResult = fulfillmentResult.stepResults?.mapping;
|
const mappingResult = fulfillmentResult.stepResults?.mapping;
|
||||||
if (!mappingResult) {
|
if (!mappingResult) {
|
||||||
@ -187,47 +187,50 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
if (createResult?.orderId) {
|
if (createResult?.orderId) {
|
||||||
// Note: WHMCS doesn't have an automated cancel API
|
// Note: WHMCS doesn't have an automated cancel API
|
||||||
// Manual intervention required for order cleanup
|
// Manual intervention required for order cleanup
|
||||||
this.logger.error("WHMCS order created but fulfillment failed - manual cleanup required", {
|
this.logger.error(
|
||||||
|
"WHMCS order created but fulfillment failed - manual cleanup required",
|
||||||
|
{
|
||||||
orderId: createResult.orderId,
|
orderId: createResult.orderId,
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
action: "MANUAL_CLEANUP_REQUIRED"
|
action: "MANUAL_CLEANUP_REQUIRED",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
critical: true
|
critical: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'whmcs_accept',
|
id: "whmcs_accept",
|
||||||
description: 'Accept/provision order in WHMCS',
|
description: "Accept/provision order in WHMCS",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||||
if (!createResult?.orderId) {
|
if (!createResult?.orderId) {
|
||||||
throw new Error("WHMCS order ID missing before acceptance step");
|
throw new Error("WHMCS order ID missing before acceptance step");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.whmcsOrderService.acceptOrder(
|
return await this.whmcsOrderService.acceptOrder(createResult.orderId, sfOrderId);
|
||||||
createResult.orderId,
|
|
||||||
sfOrderId
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
rollback: async () => {
|
rollback: async () => {
|
||||||
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
|
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||||
if (acceptResult?.orderId) {
|
if (acceptResult?.orderId) {
|
||||||
// Note: WHMCS doesn't have an automated cancel API for accepted orders
|
// Note: WHMCS doesn't have an automated cancel API for accepted orders
|
||||||
// Manual intervention required for service termination
|
// Manual intervention required for service termination
|
||||||
this.logger.error("WHMCS order accepted but fulfillment failed - manual cleanup required", {
|
this.logger.error(
|
||||||
|
"WHMCS order accepted but fulfillment failed - manual cleanup required",
|
||||||
|
{
|
||||||
orderId: acceptResult.orderId,
|
orderId: acceptResult.orderId,
|
||||||
serviceIds: acceptResult.serviceIds,
|
serviceIds: acceptResult.serviceIds,
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
action: "MANUAL_SERVICE_TERMINATION_REQUIRED"
|
action: "MANUAL_SERVICE_TERMINATION_REQUIRED",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
critical: true
|
critical: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sim_fulfillment',
|
id: "sim_fulfillment",
|
||||||
description: 'SIM-specific fulfillment (if applicable)',
|
description: "SIM-specific fulfillment (if applicable)",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
if (context.orderDetails?.orderType === "SIM") {
|
if (context.orderDetails?.orderType === "SIM") {
|
||||||
const configurations = this.extractConfigurations(payload.configurations);
|
const configurations = this.extractConfigurations(payload.configurations);
|
||||||
@ -239,11 +242,11 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
}
|
}
|
||||||
return { skipped: true };
|
return { skipped: true };
|
||||||
},
|
},
|
||||||
critical: false // SIM fulfillment failure shouldn't rollback the entire order
|
critical: false, // SIM fulfillment failure shouldn't rollback the entire order
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sf_success_update',
|
id: "sf_success_update",
|
||||||
description: 'Update Salesforce with success',
|
description: "Update Salesforce with success",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
const fields = this.fieldMapService.getFieldMap();
|
||||||
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
|
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||||
@ -262,20 +265,22 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
[fields.order.activationStatus]: "Failed",
|
[fields.order.activationStatus]: "Failed",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
critical: true
|
critical: true,
|
||||||
}
|
},
|
||||||
], {
|
],
|
||||||
|
{
|
||||||
description: `Order fulfillment for ${sfOrderId}`,
|
description: `Order fulfillment for ${sfOrderId}`,
|
||||||
timeout: 300000, // 5 minutes
|
timeout: 300000, // 5 minutes
|
||||||
continueOnNonCriticalFailure: true
|
continueOnNonCriticalFailure: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!fulfillmentResult.success) {
|
if (!fulfillmentResult.success) {
|
||||||
this.logger.error("Fulfillment transaction failed", {
|
this.logger.error("Fulfillment transaction failed", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
error: fulfillmentResult.error,
|
error: fulfillmentResult.error,
|
||||||
stepsExecuted: fulfillmentResult.stepsExecuted,
|
stepsExecuted: fulfillmentResult.stepsExecuted,
|
||||||
stepsRolledBack: fulfillmentResult.stepsRolledBack
|
stepsRolledBack: fulfillmentResult.stepsRolledBack,
|
||||||
});
|
});
|
||||||
throw new Error(fulfillmentResult.error || "Fulfillment transaction failed");
|
throw new Error(fulfillmentResult.error || "Fulfillment transaction failed");
|
||||||
}
|
}
|
||||||
@ -287,7 +292,7 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
this.logger.log("Transactional fulfillment completed successfully", {
|
this.logger.log("Transactional fulfillment completed successfully", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
stepsExecuted: fulfillmentResult.stepsExecuted,
|
stepsExecuted: fulfillmentResult.stepsExecuted,
|
||||||
duration: fulfillmentResult.duration
|
duration: fulfillmentResult.duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|||||||
@ -116,7 +116,9 @@ export class OrderPricebookService {
|
|||||||
internetOfferingType: product
|
internetOfferingType: product
|
||||||
? getStringField(product, "internetOfferingType", fields)
|
? getStringField(product, "internetOfferingType", fields)
|
||||||
: undefined,
|
: undefined,
|
||||||
internetPlanTier: product ? getStringField(product, "internetPlanTier", fields) : undefined,
|
internetPlanTier: product
|
||||||
|
? getStringField(product, "internetPlanTier", fields)
|
||||||
|
: undefined,
|
||||||
vpnRegion: product ? getStringField(product, "vpnRegion", 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({
|
export const refreshTokenRequestSchema = z.object({
|
||||||
refreshToken: z
|
refreshToken: z.string().min(1, "Refresh token is required").optional(),
|
||||||
.string()
|
|
||||||
.min(1, "Refresh token is required")
|
|
||||||
.optional(),
|
|
||||||
deviceId: z.string().optional(),
|
deviceId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,5 +7,4 @@
|
|||||||
export { z } from "zod";
|
export { z } from "zod";
|
||||||
|
|
||||||
// Framework-specific exports
|
// Framework-specific exports
|
||||||
export * from "./nestjs";
|
|
||||||
export * from "./react";
|
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
|
specifier: ^5.0.8
|
||||||
version: 5.0.8(@types/react@19.1.12)(react@19.1.1)
|
version: 5.0.8(@types/react@19.1.12)(react@19.1.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@next/bundle-analyzer':
|
||||||
|
specifier: ^15.5.0
|
||||||
|
version: 15.5.4
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.12
|
specifier: ^4.1.12
|
||||||
version: 4.1.13
|
version: 4.1.13
|
||||||
@ -319,6 +322,9 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.2
|
specifier: ^5.9.2
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
|
webpack-bundle-analyzer:
|
||||||
|
specifier: ^4.10.2
|
||||||
|
version: 4.10.2
|
||||||
|
|
||||||
packages/domain:
|
packages/domain:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -576,6 +582,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@discoveryjs/json-ext@0.5.7':
|
||||||
|
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
'@emnapi/core@1.5.0':
|
'@emnapi/core@1.5.0':
|
||||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
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
|
'@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
|
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':
|
'@next/env@15.5.0':
|
||||||
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
|
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
|
||||||
|
|
||||||
@ -1474,6 +1487,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
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':
|
'@prisma/client@6.16.0':
|
||||||
resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
|
resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
@ -2476,6 +2492,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
commander@7.2.0:
|
||||||
|
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
comment-json@4.2.5:
|
comment-json@4.2.5:
|
||||||
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
|
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -2593,6 +2613,9 @@ packages:
|
|||||||
dateformat@4.6.3:
|
dateformat@4.6.3:
|
||||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||||
|
|
||||||
|
debounce@1.2.1:
|
||||||
|
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||||
|
|
||||||
debug@3.2.7:
|
debug@3.2.7:
|
||||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2693,6 +2716,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
duplexer@0.1.2:
|
||||||
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
|
|
||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
@ -3212,6 +3238,10 @@ packages:
|
|||||||
graphemer@1.4.0:
|
graphemer@1.4.0:
|
||||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
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:
|
handlebars@4.7.8:
|
||||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||||
engines: {node: '>=0.4.7'}
|
engines: {node: '>=0.4.7'}
|
||||||
@ -3423,6 +3453,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
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:
|
is-promise@4.0.0:
|
||||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
|
||||||
@ -4000,6 +4034,10 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
mrmime@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@ -4194,6 +4232,10 @@ packages:
|
|||||||
openapi-typescript-helpers@0.0.15:
|
openapi-typescript-helpers@0.0.15:
|
||||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||||
|
|
||||||
|
opener@1.5.2:
|
||||||
|
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -4676,6 +4718,10 @@ packages:
|
|||||||
simple-swizzle@0.2.2:
|
simple-swizzle@0.2.2:
|
||||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
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:
|
slash@3.0.0:
|
||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -4926,6 +4972,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
totalist@3.0.1:
|
||||||
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tough-cookie@6.0.0:
|
tough-cookie@6.0.0:
|
||||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@ -5159,6 +5209,16 @@ packages:
|
|||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
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:
|
webpack-node-externals@3.0.0:
|
||||||
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -5238,6 +5298,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
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:
|
xml2js@0.6.2:
|
||||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@ -5543,6 +5615,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
|
'@discoveryjs/json-ext@0.5.7': {}
|
||||||
|
|
||||||
'@emnapi/core@1.5.0':
|
'@emnapi/core@1.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.1.0
|
'@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)
|
'@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
|
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/env@15.5.0': {}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@15.5.0':
|
'@next/eslint-plugin-next@15.5.0':
|
||||||
@ -6426,6 +6507,8 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
'@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)':
|
'@prisma/client@6.16.0(prisma@6.16.0(typescript@5.9.2))(typescript@5.9.2)':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
prisma: 6.16.0(typescript@5.9.2)
|
prisma: 6.16.0(typescript@5.9.2)
|
||||||
@ -7521,6 +7604,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
|
commander@7.2.0: {}
|
||||||
|
|
||||||
comment-json@4.2.5:
|
comment-json@4.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-timsort: 1.0.3
|
array-timsort: 1.0.3
|
||||||
@ -7631,6 +7716,8 @@ snapshots:
|
|||||||
|
|
||||||
dateformat@4.6.3: {}
|
dateformat@4.6.3: {}
|
||||||
|
|
||||||
|
debounce@1.2.1: {}
|
||||||
|
|
||||||
debug@3.2.7:
|
debug@3.2.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@ -7702,6 +7789,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
duplexer@0.1.2: {}
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
ecdsa-sig-formatter@1.0.11:
|
||||||
@ -8441,6 +8530,10 @@ snapshots:
|
|||||||
|
|
||||||
graphemer@1.4.0: {}
|
graphemer@1.4.0: {}
|
||||||
|
|
||||||
|
gzip-size@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
duplexer: 0.1.2
|
||||||
|
|
||||||
handlebars@4.7.8:
|
handlebars@4.7.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
@ -8660,6 +8753,8 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
is-plain-object@5.0.0: {}
|
||||||
|
|
||||||
is-promise@4.0.0: {}
|
is-promise@4.0.0: {}
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
@ -9390,6 +9485,8 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@3.0.1: {}
|
mkdirp@3.0.1: {}
|
||||||
|
|
||||||
|
mrmime@2.0.1: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
msgpackr-extract@3.0.3:
|
msgpackr-extract@3.0.3:
|
||||||
@ -9584,6 +9681,8 @@ snapshots:
|
|||||||
|
|
||||||
openapi-typescript-helpers@0.0.15: {}
|
openapi-typescript-helpers@0.0.15: {}
|
||||||
|
|
||||||
|
opener@1.5.2: {}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@ -10163,6 +10262,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.3.2
|
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: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
sonic-boom@4.2.0:
|
sonic-boom@4.2.0:
|
||||||
@ -10421,6 +10526,8 @@ snapshots:
|
|||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
totalist@3.0.1: {}
|
||||||
|
|
||||||
tough-cookie@6.0.0:
|
tough-cookie@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tldts: 7.0.13
|
tldts: 7.0.13
|
||||||
@ -10672,6 +10779,43 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
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-node-externals@3.0.0: {}
|
||||||
|
|
||||||
webpack-sources@3.3.3: {}
|
webpack-sources@3.3.3: {}
|
||||||
@ -10797,6 +10941,8 @@ snapshots:
|
|||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
ws@7.5.10: {}
|
||||||
|
|
||||||
xml2js@0.6.2:
|
xml2js@0.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.4.1
|
sax: 1.4.1
|
||||||
|
|||||||
@ -328,9 +328,21 @@ start_apps() {
|
|||||||
log "🔨 Building shared package..."
|
log "🔨 Building shared package..."
|
||||||
pnpm --filter @customer-portal/domain build
|
pnpm --filter @customer-portal/domain build
|
||||||
|
|
||||||
# Build BFF before watch (ensures dist exists)
|
# Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
|
||||||
log "🔨 Building BFF for initial setup..."
|
log "🔨 Building BFF for initial setup (ts emit)..."
|
||||||
(cd "$PROJECT_ROOT/apps/bff" && pnpm tsc -p tsconfig.build.json)
|
(
|
||||||
|
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 next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
|
||||||
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
|
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user