Refactor BFF services and update dependencies. Remove deprecated type generation scripts and streamline post-install processes in package.json. Enhance WHMCS integration by improving error handling and response validation. Update README to reflect changes in type generation and service structure. Adjust various services for better maintainability and clarity in data handling.
This commit is contained in:
parent
cdec21e012
commit
0c3aa9ff4b
@ -177,8 +177,8 @@ const response = await apiClient.GET<DashboardSummary>("/api/me/summary");
|
||||
const summary = getDataOrThrow(response);
|
||||
```
|
||||
|
||||
Because the schemas and types live in the shared domain package there is no code
|
||||
generation step—`pnpm types:gen` is now a no-op placeholder.
|
||||
Because the schemas and types live in the shared domain package there is no separate
|
||||
code generation step.
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
|
||||
@ -50,7 +50,6 @@
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.7.0",
|
||||
"rate-limiter-flexible": "^4.0.0",
|
||||
"jsforce": "^3.10.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nestjs-pino": "^4.4.0",
|
||||
@ -62,6 +61,7 @@
|
||||
"pino": "^9.9.0",
|
||||
"pino-http": "^10.5.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"rate-limiter-flexible": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"salesforce-pubsub-api-client": "^5.5.0",
|
||||
|
||||
@ -310,7 +310,7 @@ export class DistributedTransactionService {
|
||||
|
||||
if (!dbTransactionResult.success) {
|
||||
// Rollback external operations
|
||||
const executedExternalSteps = Object.keys(externalResult.stepResults) as string[];
|
||||
const executedExternalSteps = Object.keys(externalResult.stepResults);
|
||||
await this.executeRollbacks(externalSteps, executedExternalSteps, transactionId);
|
||||
throw new Error(dbTransactionResult.error || "Database transaction failed");
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import type { WhmcsApiResponse, WhmcsErrorResponse } from "../../types/whmcs-api.types";
|
||||
import type { WhmcsApiResponse } from "../../types/whmcs-api.types";
|
||||
import type {
|
||||
WhmcsApiConfig,
|
||||
WhmcsRequestOptions,
|
||||
@ -244,7 +244,7 @@ export class WhmcsHttpClientService {
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): WhmcsApiResponse<T> {
|
||||
let parsedResponse: any;
|
||||
let parsedResponse: unknown;
|
||||
|
||||
try {
|
||||
parsedResponse = JSON.parse(responseText);
|
||||
@ -258,7 +258,7 @@ export class WhmcsHttpClientService {
|
||||
}
|
||||
|
||||
// Validate basic response structure
|
||||
if (!parsedResponse || typeof parsedResponse !== 'object') {
|
||||
if (!this.isWhmcsResponse(parsedResponse)) {
|
||||
this.logger.error(`WHMCS API returned invalid response structure [${action}]`, {
|
||||
responseType: typeof parsedResponse,
|
||||
responseText: responseText.substring(0, 500),
|
||||
@ -269,8 +269,11 @@ export class WhmcsHttpClientService {
|
||||
|
||||
// Handle error responses according to WHMCS API documentation
|
||||
if (parsedResponse.result === "error") {
|
||||
const errorMessage = parsedResponse.message || parsedResponse.error || "Unknown WHMCS API error";
|
||||
const errorCode = parsedResponse.errorcode || "unknown";
|
||||
const errorMessage = this.toDisplayString(
|
||||
parsedResponse.message ?? parsedResponse.error,
|
||||
"Unknown WHMCS API error"
|
||||
);
|
||||
const errorCode = this.toDisplayString(parsedResponse.errorcode, "unknown");
|
||||
|
||||
this.logger.error(`WHMCS API returned error [${action}]`, {
|
||||
errorMessage,
|
||||
@ -284,10 +287,44 @@ export class WhmcsHttpClientService {
|
||||
// For successful responses, WHMCS API returns data directly at the root level
|
||||
// The response structure is: { "result": "success", ...actualData }
|
||||
// We return the parsed response directly as T since it contains the actual data
|
||||
const { result, message, ...rest } = parsedResponse;
|
||||
return {
|
||||
result: "success",
|
||||
data: parsedResponse as T
|
||||
} as WhmcsApiResponse<T>;
|
||||
result,
|
||||
message: typeof message === "string" ? message : undefined,
|
||||
data: rest as T,
|
||||
} satisfies WhmcsApiResponse<T>;
|
||||
}
|
||||
|
||||
private isWhmcsResponse(value: unknown): value is {
|
||||
result: "success" | "error";
|
||||
message?: unknown;
|
||||
error?: unknown;
|
||||
errorcode?: unknown;
|
||||
} & Record<string, unknown> {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const rawResult = record.result;
|
||||
return rawResult === "success" || rawResult === "error";
|
||||
}
|
||||
|
||||
private toDisplayString(value: unknown, fallback: string): string {
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -36,8 +36,9 @@ export class WhmcsSubscriptionService {
|
||||
|
||||
// Apply status filter if needed
|
||||
if (filters.status) {
|
||||
const statusFilter = filters.status.toLowerCase();
|
||||
const filtered = cached.subscriptions.filter(
|
||||
(sub: Subscription) => sub.status.toLowerCase() === filters.status!.toLowerCase()
|
||||
(sub: Subscription) => sub.status.toLowerCase() === statusFilter
|
||||
);
|
||||
return {
|
||||
subscriptions: filtered,
|
||||
@ -58,22 +59,21 @@ export class WhmcsSubscriptionService {
|
||||
const response = await this.connectionService.getClientsProducts(params);
|
||||
|
||||
// Debug logging to understand the response structure
|
||||
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
|
||||
hasResponse: !!response,
|
||||
responseKeys: response ? Object.keys(response) : [],
|
||||
hasProducts: !!(response as any)?.products,
|
||||
productsKeys: (response as any)?.products ? Object.keys((response as any).products) : [],
|
||||
hasProductArray: Array.isArray((response as any)?.products?.product),
|
||||
productCount: Array.isArray((response as any)?.products?.product) ? (response as any).products.product.length : 0,
|
||||
});
|
||||
|
||||
const productData = response.products?.product;
|
||||
const products = Array.isArray(productData)
|
||||
? productData
|
||||
: productData
|
||||
? [productData]
|
||||
const productContainer = response.products?.product;
|
||||
const products = Array.isArray(productContainer)
|
||||
? productContainer
|
||||
: productContainer
|
||||
? [productContainer]
|
||||
: [];
|
||||
|
||||
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
|
||||
hasResponse: Boolean(response),
|
||||
responseKeys: response ? Object.keys(response) : [],
|
||||
hasProducts: Boolean(response.products),
|
||||
productType: productContainer ? typeof productContainer : "undefined",
|
||||
productCount: products.length,
|
||||
});
|
||||
|
||||
if (products.length === 0) {
|
||||
this.logger.warn(`No products found for client ${clientId}`, {
|
||||
responseStructure: response ? Object.keys(response) : "null response",
|
||||
@ -109,8 +109,9 @@ export class WhmcsSubscriptionService {
|
||||
|
||||
// Apply status filter if needed
|
||||
if (filters.status) {
|
||||
const statusFilter = filters.status.toLowerCase();
|
||||
const filtered = result.subscriptions.filter(
|
||||
(sub: Subscription) => sub.status.toLowerCase() === filters.status!.toLowerCase()
|
||||
(sub: Subscription) => sub.status.toLowerCase() === statusFilter
|
||||
);
|
||||
return {
|
||||
subscriptions: filtered,
|
||||
|
||||
@ -1 +1 @@
|
||||
|
||||
export {};
|
||||
|
||||
@ -118,7 +118,7 @@ export interface WhmcsInvoiceResponse extends WhmcsInvoice {
|
||||
// Product/Service Types
|
||||
export interface WhmcsProductsResponse {
|
||||
products: {
|
||||
product: WhmcsProduct[];
|
||||
product: WhmcsProduct | WhmcsProduct[];
|
||||
};
|
||||
totalresults?: number;
|
||||
numreturned?: number;
|
||||
@ -383,14 +383,7 @@ export interface WhmcsCreateInvoiceResponse {
|
||||
// UpdateInvoice API Types
|
||||
export interface WhmcsUpdateInvoiceParams {
|
||||
invoiceid: number;
|
||||
status?:
|
||||
| "Draft"
|
||||
| "Paid"
|
||||
| "Unpaid"
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Overdue";
|
||||
status?: "Draft" | "Paid" | "Unpaid" | "Cancelled" | "Refunded" | "Collections" | "Overdue";
|
||||
duedate?: string; // YYYY-MM-DD format
|
||||
notes?: string;
|
||||
[key: string]: unknown;
|
||||
|
||||
@ -6,8 +6,9 @@ import type { Request } from "express";
|
||||
import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible";
|
||||
import { createHash } from "crypto";
|
||||
import type { Redis } from "ioredis";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
|
||||
interface RateLimitOutcome {
|
||||
export interface RateLimitOutcome {
|
||||
key: string;
|
||||
remainingPoints: number;
|
||||
consumedPoints: number;
|
||||
@ -15,10 +16,6 @@ interface RateLimitOutcome {
|
||||
needsCaptcha: boolean;
|
||||
}
|
||||
|
||||
interface EnsureResult extends RateLimitOutcome {
|
||||
headerValue?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthRateLimitService {
|
||||
private readonly loginLimiter: RateLimiterRedis;
|
||||
@ -44,8 +41,14 @@ export class AuthRateLimitService {
|
||||
const signupLimit = this.configService.get<number>("SIGNUP_RATE_LIMIT_LIMIT", 5);
|
||||
const signupTtlMs = this.configService.get<number>("SIGNUP_RATE_LIMIT_TTL", 900000);
|
||||
|
||||
const passwordResetLimit = this.configService.get<number>("PASSWORD_RESET_RATE_LIMIT_LIMIT", 5);
|
||||
const passwordResetTtlMs = this.configService.get<number>("PASSWORD_RESET_RATE_LIMIT_TTL", 900000);
|
||||
const passwordResetLimit = this.configService.get<number>(
|
||||
"PASSWORD_RESET_RATE_LIMIT_LIMIT",
|
||||
5
|
||||
);
|
||||
const passwordResetTtlMs = this.configService.get<number>(
|
||||
"PASSWORD_RESET_RATE_LIMIT_TTL",
|
||||
900000
|
||||
);
|
||||
|
||||
const refreshLimit = this.configService.get<number>("AUTH_REFRESH_RATE_LIMIT_LIMIT", 10);
|
||||
const refreshTtlMs = this.configService.get<number>("AUTH_REFRESH_RATE_LIMIT_TTL", 300000);
|
||||
@ -142,11 +145,14 @@ export class AuthRateLimitService {
|
||||
keyPrefix: prefix,
|
||||
points: limit,
|
||||
duration,
|
||||
inmemoryBlockOnConsumed: limit + 1,
|
||||
inMemoryBlockOnConsumed: limit + 1,
|
||||
insuranceLimiter: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error, `Failed to initialize rate limiter: ${prefix}`);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(
|
||||
{ prefix, error: getErrorMessage(error) },
|
||||
"Failed to initialize rate limiter"
|
||||
);
|
||||
throw new InternalServerErrorException("Rate limiter initialization failed");
|
||||
}
|
||||
}
|
||||
@ -167,9 +173,9 @@ export class AuthRateLimitService {
|
||||
msBeforeNext: res.msBeforeNext,
|
||||
needsCaptcha: false,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RateLimiterRes) {
|
||||
const retryAfterMs = error.msBeforeNext || 0;
|
||||
const retryAfterMs = error?.msBeforeNext ?? 0;
|
||||
const message = this.buildThrottleMessage(context, retryAfterMs);
|
||||
|
||||
this.logger.warn({ key, context, retryAfterMs }, "Auth rate limit reached");
|
||||
@ -177,7 +183,7 @@ export class AuthRateLimitService {
|
||||
throw new ThrottlerException(message);
|
||||
}
|
||||
|
||||
this.logger.error(error, "Rate limiter failure");
|
||||
this.logger.error({ key, context, error: getErrorMessage(error) }, "Rate limiter failure");
|
||||
throw new ThrottlerException("Authentication temporarily unavailable");
|
||||
}
|
||||
}
|
||||
@ -189,8 +195,11 @@ export class AuthRateLimitService {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await limiter.delete(key);
|
||||
} catch (error) {
|
||||
this.logger.warn({ key, context, error }, "Failed to reset rate limiter key");
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(
|
||||
{ key, context, error: getErrorMessage(error) },
|
||||
"Failed to reset rate limiter key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,9 +210,7 @@ export class AuthRateLimitService {
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
const timeMessage = remainingSeconds
|
||||
? `${minutes}m ${remainingSeconds}s`
|
||||
: `${minutes}m`;
|
||||
const timeMessage = remainingSeconds ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
||||
return `Too many ${context} attempts. Try again in ${timeMessage}.`;
|
||||
}
|
||||
|
||||
@ -211,4 +218,3 @@ export class AuthRateLimitService {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Redis } from "ioredis";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
|
||||
export interface MigrationStats {
|
||||
totalKeysScanned: number;
|
||||
@ -14,6 +15,15 @@ export interface MigrationStats {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface ParsedFamily {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface ParsedToken {
|
||||
userId: string;
|
||||
familyId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TokenMigrationService {
|
||||
private readonly REFRESH_TOKEN_FAMILY_PREFIX = "refresh_family:";
|
||||
@ -119,12 +129,8 @@ export class TokenMigrationService {
|
||||
|
||||
stats.familiesFound++;
|
||||
|
||||
try {
|
||||
const family = JSON.parse(familyData);
|
||||
|
||||
// Validate family structure
|
||||
if (!family.userId || typeof family.userId !== "string") {
|
||||
this.logger.warn("Invalid family structure, skipping", { familyKey });
|
||||
const family = this.parseFamilyData(familyKey, familyData, stats);
|
||||
if (!family) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -160,13 +166,6 @@ export class TokenMigrationService {
|
||||
userId: family.userId,
|
||||
dryRun,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to parse family data", {
|
||||
familyKey,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
stats.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -212,12 +211,8 @@ export class TokenMigrationService {
|
||||
|
||||
stats.tokensFound++;
|
||||
|
||||
try {
|
||||
const token = JSON.parse(tokenData);
|
||||
|
||||
// Validate token structure
|
||||
if (!token.familyId || !token.userId || typeof token.userId !== "string") {
|
||||
this.logger.warn("Invalid token structure, skipping", { tokenKey });
|
||||
const token = this.parseTokenData(tokenKey, tokenData, stats);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -272,13 +267,6 @@ export class TokenMigrationService {
|
||||
userId: token.userId,
|
||||
dryRun,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to parse token data", {
|
||||
tokenKey,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
stats.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,8 +290,10 @@ export class TokenMigrationService {
|
||||
const tokenData = await this.redis.get(key);
|
||||
if (!tokenData) continue;
|
||||
|
||||
const token = JSON.parse(tokenData);
|
||||
if (!token.familyId) continue;
|
||||
const token = this.parseTokenData(key, tokenData, stats);
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the corresponding family exists
|
||||
const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${token.familyId}`;
|
||||
@ -325,7 +315,7 @@ export class TokenMigrationService {
|
||||
stats.errors++;
|
||||
this.logger.error("Failed to cleanup token", {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -401,4 +391,71 @@ export class TokenMigrationService {
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private parseFamilyData(
|
||||
familyKey: string,
|
||||
raw: string,
|
||||
tracker: { errors: number }
|
||||
): ParsedFamily | null {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
tracker.errors++;
|
||||
this.logger.error("Failed to parse family data", {
|
||||
familyKey,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
this.logger.warn("Invalid family structure, skipping", { familyKey });
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = parsed as Record<string, unknown>;
|
||||
const userId = record.userId;
|
||||
|
||||
if (typeof userId !== "string" || userId.length === 0) {
|
||||
this.logger.warn("Invalid family structure, skipping", { familyKey });
|
||||
return null;
|
||||
}
|
||||
|
||||
return { userId };
|
||||
}
|
||||
|
||||
private parseTokenData(
|
||||
tokenKey: string,
|
||||
raw: string,
|
||||
tracker: { errors: number }
|
||||
): ParsedToken | null {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
tracker.errors++;
|
||||
this.logger.error("Failed to parse token data", {
|
||||
tokenKey,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
this.logger.warn("Invalid token structure, skipping", { tokenKey });
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = parsed as Record<string, unknown>;
|
||||
const userId = record.userId;
|
||||
const familyId = record.familyId;
|
||||
|
||||
if (typeof userId !== "string" || typeof familyId !== "string") {
|
||||
this.logger.warn("Invalid token structure, skipping", { tokenKey });
|
||||
return null;
|
||||
}
|
||||
|
||||
return { userId, familyId };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
import { Injectable, ExecutionContext } from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
import { AuthRateLimitService } from "../../../infra/rate-limiting/auth-rate-limit.service";
|
||||
import {
|
||||
AuthRateLimitService,
|
||||
type RateLimitOutcome,
|
||||
} from "../../../infra/rate-limiting/auth-rate-limit.service";
|
||||
|
||||
type RequestWithRateLimit = Request & { __authRateLimit?: RateLimitOutcome };
|
||||
|
||||
@Injectable()
|
||||
export class FailedLoginThrottleGuard {
|
||||
constructor(private readonly authRateLimitService: AuthRateLimitService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const request = context.switchToHttp().getRequest<RequestWithRateLimit>();
|
||||
|
||||
const outcome = await this.authRateLimitService.consumeLoginAttempt(request);
|
||||
(request as any).__authRateLimit = outcome;
|
||||
request.__authRateLimit = outcome;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -13,8 +13,14 @@ import type { Request } from "express";
|
||||
|
||||
import { TokenBlacklistService } from "../../../infra/token/token-blacklist.service";
|
||||
import { IS_PUBLIC_KEY } from "../../../decorators/public.decorator";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
|
||||
type RequestWithCookies = Request & { cookies?: Record<string, string | undefined> };
|
||||
type RequestWithRoute = RequestWithCookies & {
|
||||
method: string;
|
||||
url: string;
|
||||
route?: { path?: string };
|
||||
};
|
||||
|
||||
const headerExtractor = ExtractJwt.fromAuthHeaderAsBearerToken();
|
||||
const extractTokenFromRequest = (request: RequestWithCookies): string | undefined => {
|
||||
@ -38,11 +44,7 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
|
||||
}
|
||||
|
||||
override async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<
|
||||
RequestWithCookies & { method: string; url: string; route?: { path?: string } }
|
||||
>();
|
||||
const request = context.switchToHttp().getRequest<RequestWithRoute>();
|
||||
const route = `${request.method} ${request.route?.path ?? request.url}`;
|
||||
|
||||
// Check if the route is marked as public
|
||||
@ -78,7 +80,7 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
|
||||
this.logger.debug(`Authenticated access to: ${route}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(`Authentication error for route ${route}:`, error);
|
||||
this.logger.error(`Authentication error for route ${route}: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import { Logger } from "nestjs-pino";
|
||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
||||
import { OrderPricebookService } from "./order-pricebook.service";
|
||||
import type { PrismaService } from "@bff/infra/database/prisma.service";
|
||||
import { z } from "zod";
|
||||
import { createOrderRequestSchema } from "@customer-portal/domain";
|
||||
|
||||
/**
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
type CreateOrderRequest,
|
||||
type OrderBusinessValidation,
|
||||
} from "@customer-portal/domain";
|
||||
import type { WhmcsProduct } from "@bff/integrations/whmcs/types/whmcs-api.types";
|
||||
import { OrderPricebookService } from "./order-pricebook.service";
|
||||
|
||||
/**
|
||||
@ -107,11 +108,12 @@ export class OrderValidator {
|
||||
async validatePaymentMethod(userId: string, whmcsClientId: number): Promise<void> {
|
||||
try {
|
||||
const pay = await this.whmcs.getPaymentMethods({ clientid: whmcsClientId });
|
||||
if (!Array.isArray(pay?.paymethods) || pay.paymethods.length === 0) {
|
||||
const paymentMethods = Array.isArray(pay.paymethods) ? pay.paymethods : [];
|
||||
if (paymentMethods.length === 0) {
|
||||
this.logger.warn({ userId }, "No WHMCS payment method on file");
|
||||
throw new BadRequestException("A payment method is required before ordering");
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
const err = getErrorMessage(e);
|
||||
this.logger.error({ err }, "Payment method verification failed");
|
||||
throw new BadRequestException("Unable to verify payment method. Please try again later.");
|
||||
@ -124,14 +126,19 @@ export class OrderValidator {
|
||||
async validateInternetDuplication(userId: string, whmcsClientId: number): Promise<void> {
|
||||
try {
|
||||
const products = await this.whmcs.getClientsProducts({ clientid: whmcsClientId });
|
||||
const existing = products?.products?.product || [];
|
||||
const hasInternet = existing.some(product =>
|
||||
(product.groupname || "").toLowerCase().includes("internet")
|
||||
const productContainer = products.products?.product;
|
||||
const existing = Array.isArray(productContainer)
|
||||
? productContainer
|
||||
: productContainer
|
||||
? [productContainer]
|
||||
: [];
|
||||
const hasInternet = existing.some((product: WhmcsProduct) =>
|
||||
(product.groupname || product.translated_groupname || "").toLowerCase().includes("internet")
|
||||
);
|
||||
if (hasInternet) {
|
||||
throw new BadRequestException("An Internet service already exists for this account");
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
const err = getErrorMessage(e);
|
||||
this.logger.error({ err }, "Internet duplicate check failed");
|
||||
throw new BadRequestException(
|
||||
|
||||
@ -514,7 +514,12 @@ export class SubscriptionsService {
|
||||
const productsResponse: WhmcsProductsResponse = await this.whmcsService.getClientsProducts({
|
||||
clientid: mapping.whmcsClientId,
|
||||
});
|
||||
const services = productsResponse.products?.product ?? [];
|
||||
const productContainer = productsResponse.products?.product;
|
||||
const services = Array.isArray(productContainer)
|
||||
? productContainer
|
||||
: productContainer
|
||||
? [productContainer]
|
||||
: [];
|
||||
|
||||
return services.some((service: WhmcsProduct) => {
|
||||
const group = typeof service.groupname === "string" ? service.groupname.toLowerCase() : "";
|
||||
|
||||
@ -31,6 +31,6 @@
|
||||
"module": "CommonJS"
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*"],
|
||||
"include": ["src/**/*", "scripts/**/*", "src/types/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@ -54,9 +54,7 @@
|
||||
"update:safe": "pnpm update --recursive && pnpm audit && pnpm type-check",
|
||||
"dev:watch": "pnpm --parallel --filter @customer-portal/domain --filter @customer-portal/portal --filter @customer-portal/bff run dev",
|
||||
"plesk:images": "bash ./scripts/plesk/build-images.sh",
|
||||
"types:gen": "./scripts/generate-frontend-types.sh",
|
||||
"codegen": "pnpm types:gen",
|
||||
"postinstall": "pnpm codegen || true"
|
||||
"postinstall": "husky install || true"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@ -83,9 +83,6 @@ importers:
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^11.1.6
|
||||
version: 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/core@11.1.6)
|
||||
'@nestjs/swagger':
|
||||
specifier: ^11.2.0
|
||||
version: 11.2.0(@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/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
||||
'@nestjs/throttler':
|
||||
specifier: ^6.4.0
|
||||
version: 6.4.0(@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/core@11.1.6)(reflect-metadata@0.2.2)
|
||||
@ -152,6 +149,9 @@ importers:
|
||||
pino-pretty:
|
||||
specifier: ^13.1.1
|
||||
version: 13.1.1
|
||||
rate-limiter-flexible:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@ -285,9 +285,6 @@ importers:
|
||||
next:
|
||||
specifier: 15.5.0
|
||||
version: 15.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
openapi-fetch:
|
||||
specifier: ^0.13.5
|
||||
version: 0.13.8
|
||||
react:
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
@ -4238,12 +4235,6 @@ packages:
|
||||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
openapi-fetch@0.13.8:
|
||||
resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
opener@1.5.2:
|
||||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||
hasBin: true
|
||||
@ -4491,6 +4482,9 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
rate-limiter-flexible@4.0.1:
|
||||
resolution: {integrity: sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==}
|
||||
|
||||
raw-body@3.0.1:
|
||||
resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -6285,7 +6279,8 @@ snapshots:
|
||||
|
||||
'@lukeed/csprng@1.1.0': {}
|
||||
|
||||
'@microsoft/tsdoc@0.15.1': {}
|
||||
'@microsoft/tsdoc@0.15.1':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
|
||||
optional: true
|
||||
@ -6403,6 +6398,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
class-transformer: 0.5.1
|
||||
class-validator: 0.14.2
|
||||
optional: true
|
||||
|
||||
'@nestjs/passport@11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)':
|
||||
dependencies:
|
||||
@ -6457,6 +6453,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
class-transformer: 0.5.1
|
||||
class-validator: 0.14.2
|
||||
optional: true
|
||||
|
||||
'@nestjs/testing@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/core@11.1.6)(@nestjs/platform-express@11.1.6)':
|
||||
dependencies:
|
||||
@ -6602,7 +6599,8 @@ snapshots:
|
||||
|
||||
'@rushstack/eslint-patch@1.12.0': {}
|
||||
|
||||
'@scarf/scarf@1.4.0': {}
|
||||
'@scarf/scarf@1.4.0':
|
||||
optional: true
|
||||
|
||||
'@sendgrid/client@8.1.5':
|
||||
dependencies:
|
||||
@ -9706,12 +9704,6 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openapi-fetch@0.13.8:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
opener@1.5.2: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
@ -9983,6 +9975,8 @@ snapshots:
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
rate-limiter-flexible@4.0.1: {}
|
||||
|
||||
raw-body@3.0.1:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -10480,6 +10474,7 @@ snapshots:
|
||||
swagger-ui-dist@5.21.0:
|
||||
dependencies:
|
||||
'@scarf/scarf': 1.4.0
|
||||
optional: true
|
||||
|
||||
symbol-observable@4.0.0: {}
|
||||
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Zod-based frontend type support
|
||||
# OpenAPI generation has been removed; shared schemas live in @customer-portal/domain.
|
||||
|
||||
set -e
|
||||
|
||||
echo "ℹ️ Skipping OpenAPI generation: frontend consumes shared Zod schemas from @customer-portal/domain."
|
||||
Loading…
x
Reference in New Issue
Block a user