Enhance AuditService request logging by adding IP and connection details. Remove deprecated getSubscriptionStats method from WhmcsService to streamline subscription handling. Update WhmcsInvoiceService imports for better organization. Refactor payment method checks in WhmcsOrderService for clarity and efficiency. Improve error handling in WhmcsPaymentService and WhmcsSubscriptionService. Adjust subscription statistics in SubscriptionsService to reflect completed status instead of suspended and pending. Update frontend components to align with new subscription status structure.
This commit is contained in:
parent
0c3aa9ff4b
commit
eded58ab93
@ -69,7 +69,12 @@ export class AuditService {
|
|||||||
action: AuditAction,
|
action: AuditAction,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
details?: Record<string, unknown> | string | number | boolean | null,
|
details?: Record<string, unknown> | string | number | boolean | null,
|
||||||
request?: { headers?: Record<string, string | string[] | undefined> },
|
request?: {
|
||||||
|
headers?: Record<string, string | string[] | undefined>;
|
||||||
|
ip?: string;
|
||||||
|
connection?: { remoteAddress?: string };
|
||||||
|
socket?: { remoteAddress?: string };
|
||||||
|
},
|
||||||
success: boolean = true,
|
success: boolean = true,
|
||||||
error?: string
|
error?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
||||||
import { Invoice, InvoiceList } from "@customer-portal/domain";
|
import { Invoice, InvoiceList, invoiceListSchema, invoiceSchema } from "@customer-portal/domain";
|
||||||
import {
|
|
||||||
invoiceListSchema,
|
|
||||||
invoiceSchema,
|
|
||||||
} from "@customer-portal/domain/validation/shared/entities";
|
|
||||||
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
|
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
|
||||||
import { InvoiceTransformerService } from "../transformers/services/invoice-transformer.service";
|
import { InvoiceTransformerService } from "../transformers/services/invoice-transformer.service";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||||
|
|||||||
@ -165,34 +165,17 @@ export class WhmcsOrderService {
|
|||||||
*/
|
*/
|
||||||
async hasPaymentMethod(clientId: number): Promise<boolean> {
|
async hasPaymentMethod(clientId: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = (await this.connection.getPaymentMethods({
|
const response = await this.connection.getPaymentMethods({
|
||||||
clientid: clientId,
|
clientid: clientId,
|
||||||
})) as unknown as Record<string, unknown>;
|
});
|
||||||
|
|
||||||
if (response.result !== "success") {
|
const methods = Array.isArray(response.paymethods) ? response.paymethods : [];
|
||||||
this.logger.warn("Failed to check payment methods", {
|
const hasValidMethod = methods.length > 0;
|
||||||
clientId,
|
|
||||||
error: response.message as string,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if client has any payment methods
|
|
||||||
const paymethodsNode = (response.paymethods as { paymethod?: unknown } | undefined)
|
|
||||||
?.paymethod;
|
|
||||||
const totalResults = Number((response as { totalresults?: unknown })?.totalresults ?? 0) || 0;
|
|
||||||
const methodCount = Array.isArray(paymethodsNode)
|
|
||||||
? paymethodsNode.length
|
|
||||||
: paymethodsNode && typeof paymethodsNode === "object"
|
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
const hasValidMethod = methodCount > 0 || totalResults > 0;
|
|
||||||
|
|
||||||
this.logger.log("Payment method check completed", {
|
this.logger.log("Payment method check completed", {
|
||||||
clientId,
|
clientId,
|
||||||
hasPaymentMethod: hasValidMethod,
|
hasPaymentMethod: hasValidMethod,
|
||||||
methodCount,
|
methodCount: methods.length,
|
||||||
totalResults,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return hasValidMethod;
|
return hasValidMethod;
|
||||||
|
|||||||
@ -48,15 +48,6 @@ export class WhmcsPaymentService {
|
|||||||
clientid: clientId,
|
clientid: clientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response || response.result !== "success") {
|
|
||||||
const message = response?.message ?? "GetPayMethods call failed";
|
|
||||||
this.logger.error("WHMCS GetPayMethods returned error", {
|
|
||||||
clientId,
|
|
||||||
response,
|
|
||||||
});
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const paymentMethodsArray: WhmcsPaymentMethod[] = Array.isArray(response.paymethods)
|
const paymentMethodsArray: WhmcsPaymentMethod[] = Array.isArray(response.paymethods)
|
||||||
? response.paymethods
|
? response.paymethods
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@ -58,7 +58,15 @@ export class WhmcsSubscriptionService {
|
|||||||
|
|
||||||
const response = await this.connectionService.getClientsProducts(params);
|
const response = await this.connectionService.getClientsProducts(params);
|
||||||
|
|
||||||
// Debug logging to understand the response structure
|
if (!response || response.result !== "success") {
|
||||||
|
const message = response?.message || "GetClientsProducts call failed";
|
||||||
|
this.logger.error("WHMCS GetClientsProducts returned error", {
|
||||||
|
clientId,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
const productContainer = response.products?.product;
|
const productContainer = response.products?.product;
|
||||||
const products = Array.isArray(productContainer)
|
const products = Array.isArray(productContainer)
|
||||||
? productContainer
|
? productContainer
|
||||||
@ -67,10 +75,9 @@ export class WhmcsSubscriptionService {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
|
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
|
||||||
hasResponse: Boolean(response),
|
totalresults: response.totalresults,
|
||||||
responseKeys: response ? Object.keys(response) : [],
|
startnumber: response.startnumber,
|
||||||
hasProducts: Boolean(response.products),
|
numreturned: response.numreturned,
|
||||||
productType: productContainer ? typeof productContainer : "undefined",
|
|
||||||
productCount: products.length,
|
productCount: products.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -34,8 +34,8 @@ export class SubscriptionTransformerService {
|
|||||||
const subscription: Subscription = {
|
const subscription: Subscription = {
|
||||||
id: Number(whmcsProduct.id),
|
id: Number(whmcsProduct.id),
|
||||||
serviceId: Number(whmcsProduct.id), // In WHMCS, product ID is the service ID
|
serviceId: Number(whmcsProduct.id), // In WHMCS, product ID is the service ID
|
||||||
productName: whmcsProduct.productname || whmcsProduct.name,
|
productName: whmcsProduct.name || "",
|
||||||
domain: whmcsProduct.domain,
|
domain: whmcsProduct.domain || undefined,
|
||||||
status: this.mapSubscriptionStatus(whmcsProduct.status),
|
status: this.mapSubscriptionStatus(whmcsProduct.status),
|
||||||
cycle: billingCycle,
|
cycle: billingCycle,
|
||||||
amount: this.getProductAmount(whmcsProduct),
|
amount: this.getProductAmount(whmcsProduct),
|
||||||
@ -43,7 +43,7 @@ export class SubscriptionTransformerService {
|
|||||||
currencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
currencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
||||||
nextDue: DataUtils.formatDate(whmcsProduct.nextduedate),
|
nextDue: DataUtils.formatDate(whmcsProduct.nextduedate),
|
||||||
registrationDate: DataUtils.formatDate(whmcsProduct.regdate) || new Date().toISOString(),
|
registrationDate: DataUtils.formatDate(whmcsProduct.regdate) || new Date().toISOString(),
|
||||||
customFields: this.extractCustomFields(whmcsProduct.customfields),
|
customFields: this.extractCustomFields(whmcsProduct.customfields?.customfield),
|
||||||
notes: undefined, // WhmcsProduct doesn't have notes field
|
notes: undefined, // WhmcsProduct doesn't have notes field
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export class SubscriptionTransformerService {
|
|||||||
error: message,
|
error: message,
|
||||||
productId: whmcsProduct.id,
|
productId: whmcsProduct.id,
|
||||||
status: whmcsProduct.status,
|
status: whmcsProduct.status,
|
||||||
productName: whmcsProduct.name || whmcsProduct.productname,
|
productName: whmcsProduct.name || whmcsProduct.name,
|
||||||
});
|
});
|
||||||
throw new Error(`Failed to transform subscription: ${message}`);
|
throw new Error(`Failed to transform subscription: ${message}`);
|
||||||
}
|
}
|
||||||
@ -117,40 +117,53 @@ export class SubscriptionTransformerService {
|
|||||||
/**
|
/**
|
||||||
* Normalize field name to camelCase
|
* Normalize field name to camelCase
|
||||||
*/
|
*/
|
||||||
private mapSubscriptionStatus(status: string): Subscription["status"] {
|
private mapSubscriptionStatus(status: string | undefined): Subscription["status"] {
|
||||||
const allowed: Subscription["status"][] = [
|
if (typeof status !== "string") {
|
||||||
"Active",
|
return "Cancelled";
|
||||||
"Suspended",
|
|
||||||
"Terminated",
|
|
||||||
"Cancelled",
|
|
||||||
"Pending",
|
|
||||||
"Completed",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowed.includes(status as Subscription["status"])) {
|
|
||||||
return status as Subscription["status"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported WHMCS subscription status: ${status}`);
|
const normalized = status.trim().toLowerCase();
|
||||||
|
|
||||||
|
const statusMap: Record<string, Subscription["status"]> = {
|
||||||
|
active: "Active",
|
||||||
|
completed: "Completed",
|
||||||
|
cancelled: "Cancelled",
|
||||||
|
canceled: "Cancelled",
|
||||||
|
terminated: "Cancelled",
|
||||||
|
suspended: "Cancelled",
|
||||||
|
pending: "Active",
|
||||||
|
fraud: "Cancelled",
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusMap[normalized] ?? "Cancelled";
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapBillingCycle(cycle: string): Subscription["cycle"] {
|
private mapBillingCycle(cycle: string | undefined): Subscription["cycle"] {
|
||||||
const allowed: Subscription["cycle"][] = [
|
if (typeof cycle !== "string" || cycle.trim().length === 0) {
|
||||||
"Monthly",
|
return "One-time";
|
||||||
"Quarterly",
|
|
||||||
"Semi-Annually",
|
|
||||||
"Annually",
|
|
||||||
"Biennially",
|
|
||||||
"Triennially",
|
|
||||||
"One-time",
|
|
||||||
"Free",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowed.includes(cycle as Subscription["cycle"])) {
|
|
||||||
return cycle as Subscription["cycle"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported WHMCS billing cycle: ${cycle}`);
|
const normalized = cycle.trim().toLowerCase().replace(/[_\s-]+/g, " ");
|
||||||
|
|
||||||
|
const cycleMap: Record<string, Subscription["cycle"]> = {
|
||||||
|
monthly: "Monthly",
|
||||||
|
annually: "Annually",
|
||||||
|
annual: "Annually",
|
||||||
|
yearly: "Annually",
|
||||||
|
quarterly: "Quarterly",
|
||||||
|
"semi annually": "Semi-Annually",
|
||||||
|
"semiannually": "Semi-Annually",
|
||||||
|
"semi-annually": "Semi-Annually",
|
||||||
|
biennially: "Biennially",
|
||||||
|
triennially: "Triennially",
|
||||||
|
"one time": "One-time",
|
||||||
|
onetime: "One-time",
|
||||||
|
"one-time": "One-time",
|
||||||
|
"one time fee": "One-time",
|
||||||
|
free: "Free",
|
||||||
|
};
|
||||||
|
|
||||||
|
return cycleMap[normalized] ?? "One-time";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -53,7 +53,6 @@ export interface WhmcsCustomField {
|
|||||||
value: string;
|
value: string;
|
||||||
// Legacy fields that may appear in some responses
|
// Legacy fields that may appear in some responses
|
||||||
name?: string;
|
name?: string;
|
||||||
translated_name?: string;
|
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,74 +116,72 @@ export interface WhmcsInvoiceResponse extends WhmcsInvoice {
|
|||||||
|
|
||||||
// Product/Service Types
|
// Product/Service Types
|
||||||
export interface WhmcsProductsResponse {
|
export interface WhmcsProductsResponse {
|
||||||
products: {
|
result: "success" | "error";
|
||||||
product: WhmcsProduct | WhmcsProduct[];
|
message?: string;
|
||||||
};
|
clientid?: number | string;
|
||||||
totalresults?: number;
|
serviceid?: number | string | null;
|
||||||
|
pid?: number | string | null;
|
||||||
|
domain?: string | null;
|
||||||
|
totalresults?: number | string;
|
||||||
|
startnumber?: number;
|
||||||
numreturned?: number;
|
numreturned?: number;
|
||||||
|
products?: {
|
||||||
|
product?: WhmcsProduct | WhmcsProduct[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WhmcsProduct {
|
export interface WhmcsProduct {
|
||||||
id: number;
|
id: number | string;
|
||||||
clientid: number;
|
qty?: string;
|
||||||
orderid: number;
|
clientid?: number | string;
|
||||||
pid: number;
|
orderid?: number | string;
|
||||||
regdate: string;
|
ordernumber?: string;
|
||||||
name: string;
|
pid?: number | string;
|
||||||
|
regdate?: string;
|
||||||
|
name?: string;
|
||||||
translated_name?: string;
|
translated_name?: string;
|
||||||
groupname?: string;
|
groupname?: string;
|
||||||
productname?: string;
|
|
||||||
translated_groupname?: string;
|
translated_groupname?: string;
|
||||||
domain: string;
|
domain?: string;
|
||||||
dedicatedip?: string;
|
dedicatedip?: string;
|
||||||
serverid?: number;
|
serverid?: number | string;
|
||||||
servername?: string;
|
servername?: string;
|
||||||
serverip?: string;
|
serverip?: string;
|
||||||
serverhostname?: string;
|
serverhostname?: string;
|
||||||
suspensionreason?: string;
|
suspensionreason?: string;
|
||||||
promoid?: number;
|
firstpaymentamount?: string;
|
||||||
producttype?: string;
|
recurringamount?: string;
|
||||||
modulename?: string;
|
paymentmethod?: string;
|
||||||
billingcycle:
|
paymentmethodname?: string;
|
||||||
| "Monthly"
|
billingcycle?: string;
|
||||||
| "Quarterly"
|
nextduedate?: string;
|
||||||
| "Semi-Annually"
|
status?: string;
|
||||||
| "Annually"
|
|
||||||
| "Biennially"
|
|
||||||
| "Triennially"
|
|
||||||
| "One-time"
|
|
||||||
| "Free";
|
|
||||||
nextduedate: string;
|
|
||||||
status: "Active" | "Suspended" | "Terminated" | "Cancelled" | "Pending" | "Completed";
|
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
subscriptionid?: string;
|
subscriptionid?: string;
|
||||||
promotype?: string;
|
promoid?: string;
|
||||||
promocode?: string;
|
overideautosuspend?: string;
|
||||||
promodescription?: string;
|
|
||||||
promovalue?: string;
|
|
||||||
packageid?: number;
|
|
||||||
packagename?: string;
|
|
||||||
configoptions?: Record<string, unknown>;
|
|
||||||
customfields?: WhmcsCustomField[];
|
|
||||||
firstpaymentamount: string;
|
|
||||||
recurringamount: string;
|
|
||||||
paymentmethod: string;
|
|
||||||
paymentmethodname?: string;
|
|
||||||
currencycode?: string;
|
|
||||||
currencyprefix?: string;
|
|
||||||
currencysuffix?: string;
|
|
||||||
overideautosuspend?: boolean;
|
|
||||||
overidesuspenduntil?: string;
|
overidesuspenduntil?: string;
|
||||||
ns1?: string;
|
ns1?: string;
|
||||||
ns2?: string;
|
ns2?: string;
|
||||||
assignedips?: string;
|
assignedips?: string;
|
||||||
disk?: string;
|
notes?: string;
|
||||||
disklimit?: string;
|
|
||||||
diskusage?: string;
|
diskusage?: string;
|
||||||
bwlimit?: string;
|
disklimit?: string;
|
||||||
bwusage?: string;
|
bwusage?: string;
|
||||||
|
bwlimit?: string;
|
||||||
lastupdate?: string;
|
lastupdate?: string;
|
||||||
|
customfields?: {
|
||||||
|
customfield?: WhmcsCustomField[];
|
||||||
|
};
|
||||||
|
configoptions?: {
|
||||||
|
configoption?: Array<{
|
||||||
|
id?: number | string;
|
||||||
|
option?: string;
|
||||||
|
type?: string;
|
||||||
|
value?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSO Token Types
|
// SSO Token Types
|
||||||
@ -313,8 +310,7 @@ export interface WhmcsPaymentMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WhmcsPayMethodsResponse {
|
export interface WhmcsPayMethodsResponse {
|
||||||
result: "success" | "error";
|
clientid: number | string;
|
||||||
clientid?: number | string;
|
|
||||||
paymethods?: WhmcsPaymentMethod[];
|
paymethods?: WhmcsPaymentMethod[];
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
@ -383,7 +379,7 @@ export interface WhmcsCreateInvoiceResponse {
|
|||||||
// UpdateInvoice API Types
|
// UpdateInvoice API Types
|
||||||
export interface WhmcsUpdateInvoiceParams {
|
export interface WhmcsUpdateInvoiceParams {
|
||||||
invoiceid: number;
|
invoiceid: number;
|
||||||
status?: "Draft" | "Paid" | "Unpaid" | "Cancelled" | "Refunded" | "Collections" | "Overdue";
|
status?: "Draft" | "Paid" | "Unpaid" | "Cancelled" | "Refunded" | "Collections" | "Overdue";
|
||||||
duedate?: string; // YYYY-MM-DD format
|
duedate?: string; // YYYY-MM-DD format
|
||||||
notes?: string;
|
notes?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
|
|||||||
@ -114,42 +114,6 @@ export class WhmcsService {
|
|||||||
return this.subscriptionService.invalidateSubscriptionCache(userId, subscriptionId);
|
return this.subscriptionService.invalidateSubscriptionCache(userId, subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get subscription statistics for a client
|
|
||||||
*/
|
|
||||||
async getSubscriptionStats(
|
|
||||||
clientId: number,
|
|
||||||
userId: string
|
|
||||||
): Promise<{
|
|
||||||
total: number;
|
|
||||||
active: number;
|
|
||||||
suspended: number;
|
|
||||||
cancelled: number;
|
|
||||||
pending: number;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
const subscriptionList = await this.subscriptionService.getSubscriptions(clientId, userId);
|
|
||||||
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
total: subscriptions.length,
|
|
||||||
active: subscriptions.filter((s: Subscription) => s.status === "Active").length,
|
|
||||||
suspended: subscriptions.filter((s: Subscription) => s.status === "Suspended").length,
|
|
||||||
cancelled: subscriptions.filter((s: Subscription) => s.status === "Cancelled").length,
|
|
||||||
pending: subscriptions.filter((s: Subscription) => s.status === "Pending").length,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logger.debug(`Generated subscription stats for client ${clientId}:`, stats);
|
|
||||||
return stats;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get subscription stats for client ${clientId}`, {
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CLIENT OPERATIONS (delegate to ClientService)
|
// CLIENT OPERATIONS (delegate to ClientService)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@ -295,7 +295,7 @@ export class AuthFacade {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(userId: string, token?: string, _request?: Request): Promise<void> {
|
async logout(userId: string, token?: string, request?: Request): Promise<void> {
|
||||||
if (token) {
|
if (token) {
|
||||||
await this.tokenBlacklistService.blacklistToken(token);
|
await this.tokenBlacklistService.blacklistToken(token);
|
||||||
}
|
}
|
||||||
@ -309,7 +309,7 @@ export class AuthFacade {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.auditService.logAuthEvent(AuditAction.LOGOUT, userId, {}, _request, true);
|
await this.auditService.logAuthEvent(AuditAction.LOGOUT, userId, {}, request, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
|
|||||||
@ -41,10 +41,7 @@ export class AuthRateLimitService {
|
|||||||
const signupLimit = this.configService.get<number>("SIGNUP_RATE_LIMIT_LIMIT", 5);
|
const signupLimit = this.configService.get<number>("SIGNUP_RATE_LIMIT_LIMIT", 5);
|
||||||
const signupTtlMs = this.configService.get<number>("SIGNUP_RATE_LIMIT_TTL", 900000);
|
const signupTtlMs = this.configService.get<number>("SIGNUP_RATE_LIMIT_TTL", 900000);
|
||||||
|
|
||||||
const passwordResetLimit = this.configService.get<number>(
|
const passwordResetLimit = this.configService.get<number>("PASSWORD_RESET_RATE_LIMIT_LIMIT", 5);
|
||||||
"PASSWORD_RESET_RATE_LIMIT_LIMIT",
|
|
||||||
5
|
|
||||||
);
|
|
||||||
const passwordResetTtlMs = this.configService.get<number>(
|
const passwordResetTtlMs = this.configService.get<number>(
|
||||||
"PASSWORD_RESET_RATE_LIMIT_TTL",
|
"PASSWORD_RESET_RATE_LIMIT_TTL",
|
||||||
900000
|
900000
|
||||||
@ -53,10 +50,7 @@ export class AuthRateLimitService {
|
|||||||
const refreshLimit = this.configService.get<number>("AUTH_REFRESH_RATE_LIMIT_LIMIT", 10);
|
const refreshLimit = this.configService.get<number>("AUTH_REFRESH_RATE_LIMIT_LIMIT", 10);
|
||||||
const refreshTtlMs = this.configService.get<number>("AUTH_REFRESH_RATE_LIMIT_TTL", 300000);
|
const refreshTtlMs = this.configService.get<number>("AUTH_REFRESH_RATE_LIMIT_TTL", 300000);
|
||||||
|
|
||||||
this.loginCaptchaThreshold = this.configService.get<number>(
|
this.loginCaptchaThreshold = this.configService.get<number>("LOGIN_CAPTCHA_AFTER_ATTEMPTS", 3);
|
||||||
"LOGIN_CAPTCHA_AFTER_ATTEMPTS",
|
|
||||||
3
|
|
||||||
);
|
|
||||||
this.captchaAlwaysOn = this.configService.get("AUTH_CAPTCHA_ALWAYS_ON", "false") === "true";
|
this.captchaAlwaysOn = this.configService.get("AUTH_CAPTCHA_ALWAYS_ON", "false") === "true";
|
||||||
|
|
||||||
this.loginLimiter = this.createLimiter("auth-login", loginLimit, loginTtlMs);
|
this.loginLimiter = this.createLimiter("auth-login", loginLimit, loginTtlMs);
|
||||||
@ -188,11 +182,7 @@ export class AuthRateLimitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteKey(
|
private async deleteKey(limiter: RateLimiterRedis, key: string, context: string): Promise<void> {
|
||||||
limiter: RateLimiterRedis,
|
|
||||||
key: string,
|
|
||||||
context: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
await limiter.delete(key);
|
await limiter.delete(key);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export class TokenMigrationService {
|
|||||||
stats.errors++;
|
stats.errors++;
|
||||||
|
|
||||||
this.logger.error("Token migration failed", {
|
this.logger.error("Token migration failed", {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorMessage(error),
|
||||||
stats,
|
stats,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ export class TokenMigrationService {
|
|||||||
stats.errors++;
|
stats.errors++;
|
||||||
this.logger.error("Failed to migrate family", {
|
this.logger.error("Failed to migrate family", {
|
||||||
key,
|
key,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ export class TokenMigrationService {
|
|||||||
stats.errors++;
|
stats.errors++;
|
||||||
this.logger.error("Failed to migrate token", {
|
this.logger.error("Failed to migrate token", {
|
||||||
key,
|
key,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,16 +48,18 @@ export class WhmcsLinkWorkflowService {
|
|||||||
try {
|
try {
|
||||||
clientDetails = await this.whmcsService.getClientDetailsByEmail(email);
|
clientDetails = await this.whmcsService.getClientDetailsByEmail(email);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("WHMCS client lookup failed", {
|
this.logger.error("WHMCS client lookup failed", {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
email: email // Safe to log email for debugging since it's not sensitive
|
email, // Safe to log email for debugging since it's not sensitive
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide more specific error messages based on the error type
|
// Provide more specific error messages based on the error type
|
||||||
if (error instanceof Error && error.message.includes('not found')) {
|
if (error instanceof Error && error.message.includes("not found")) {
|
||||||
throw new UnauthorizedException("No billing account found with this email address. Please check your email or contact support.");
|
throw new UnauthorizedException(
|
||||||
|
"No billing account found with this email address. Please check your email or contact support."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException("Unable to verify account. Please try again later.");
|
throw new UnauthorizedException("Unable to verify account. Please try again later.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,18 +81,24 @@ export class WhmcsLinkWorkflowService {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof UnauthorizedException) throw error;
|
if (error instanceof UnauthorizedException) throw error;
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
this.logger.error("WHMCS credential validation failed", { error: errorMessage });
|
this.logger.error("WHMCS credential validation failed", { error: errorMessage });
|
||||||
|
|
||||||
// Check if this is a WHMCS authentication error and provide user-friendly message
|
// Check if this is a WHMCS authentication error and provide user-friendly message
|
||||||
if (errorMessage.toLowerCase().includes('email or password invalid') ||
|
const normalizedMessage = errorMessage.toLowerCase();
|
||||||
errorMessage.toLowerCase().includes('invalid email or password') ||
|
const authErrorPhrases = [
|
||||||
errorMessage.toLowerCase().includes('authentication failed') ||
|
"email or password invalid",
|
||||||
errorMessage.toLowerCase().includes('login failed')) {
|
"invalid email or password",
|
||||||
throw new UnauthorizedException("Invalid email or password. Please check your credentials and try again.");
|
"authentication failed",
|
||||||
|
"login failed",
|
||||||
|
];
|
||||||
|
if (authErrorPhrases.some(phrase => normalizedMessage.includes(phrase))) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"Invalid email or password. Please check your credentials and try again."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other errors, provide generic message to avoid exposing system details
|
// For other errors, provide generic message to avoid exposing system details
|
||||||
throw new UnauthorizedException("Unable to verify credentials. Please try again later.");
|
throw new UnauthorizedException("Unable to verify credentials. Please try again later.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,9 @@ import {
|
|||||||
} from "@customer-portal/domain";
|
} from "@customer-portal/domain";
|
||||||
import type { AuthTokens } from "@customer-portal/domain";
|
import type { AuthTokens } from "@customer-portal/domain";
|
||||||
|
|
||||||
type RequestWithCookies = Request & { cookies?: Record<string, string | undefined> };
|
type RequestWithCookies = Request & {
|
||||||
|
cookies: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
const resolveAuthorizationHeader = (req: RequestWithCookies): string | undefined => {
|
const resolveAuthorizationHeader = (req: RequestWithCookies): string | undefined => {
|
||||||
const rawHeader = req.headers?.authorization;
|
const rawHeader = req.headers?.authorization;
|
||||||
@ -197,9 +199,11 @@ export class AuthController {
|
|||||||
@Res({ passthrough: true }) res: Response
|
@Res({ passthrough: true }) res: Response
|
||||||
) {
|
) {
|
||||||
const refreshToken = body.refreshToken ?? req.cookies?.refresh_token;
|
const refreshToken = body.refreshToken ?? req.cookies?.refresh_token;
|
||||||
|
const rawUserAgent = req.headers["user-agent"];
|
||||||
|
const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : undefined;
|
||||||
const result = await this.authFacade.refreshTokens(refreshToken, {
|
const result = await this.authFacade.refreshTokens(refreshToken, {
|
||||||
deviceId: body.deviceId,
|
deviceId: body.deviceId,
|
||||||
userAgent: req.headers["user-agent"],
|
userAgent,
|
||||||
});
|
});
|
||||||
this.setAuthCookies(res, result.tokens);
|
this.setAuthCookies(res, result.tokens);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -15,7 +15,9 @@ import { TokenBlacklistService } from "../../../infra/token/token-blacklist.serv
|
|||||||
import { IS_PUBLIC_KEY } from "../../../decorators/public.decorator";
|
import { IS_PUBLIC_KEY } from "../../../decorators/public.decorator";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||||
|
|
||||||
type RequestWithCookies = Request & { cookies?: Record<string, string | undefined> };
|
type RequestWithCookies = Request & {
|
||||||
|
cookies: Record<string, any>;
|
||||||
|
};
|
||||||
type RequestWithRoute = RequestWithCookies & {
|
type RequestWithRoute = RequestWithCookies & {
|
||||||
method: string;
|
method: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -44,8 +46,8 @@ 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.switchToHttp().getRequest<RequestWithRoute>();
|
const request = this.getRequest(context);
|
||||||
const route = `${request.method} ${request.route?.path ?? request.url}`;
|
const route = `${request.method} ${request.url}`;
|
||||||
|
|
||||||
// Check if the route is marked as public
|
// Check if the route is marked as public
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
@ -84,4 +86,24 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override getRequest(context: ExecutionContext): RequestWithRoute {
|
||||||
|
const rawRequest = context.switchToHttp().getRequest<unknown>();
|
||||||
|
if (!this.isRequestWithRoute(rawRequest)) {
|
||||||
|
this.logger.error("Unable to determine HTTP request in auth guard");
|
||||||
|
throw new UnauthorizedException("Invalid request context");
|
||||||
|
}
|
||||||
|
return rawRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRequestWithRoute(value: unknown): value is RequestWithRoute {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const candidate = value as Partial<RequestWithRoute>;
|
||||||
|
return (
|
||||||
|
typeof candidate.method === "string" &&
|
||||||
|
typeof candidate.url === "string" &&
|
||||||
|
(!candidate.route || typeof candidate.route === "object")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
CallHandler,
|
CallHandler,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { Observable, throwError } from "rxjs";
|
import { Observable, defer } from "rxjs";
|
||||||
import { tap, catchError } from "rxjs/operators";
|
import { tap, catchError } from "rxjs/operators";
|
||||||
import { FailedLoginThrottleGuard } from "@bff/modules/auth/presentation/http/guards/failed-login-throttle.guard";
|
import { FailedLoginThrottleGuard } from "@bff/modules/auth/presentation/http/guards/failed-login-throttle.guard";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
@ -14,27 +14,74 @@ import type { Request } from "express";
|
|||||||
export class LoginResultInterceptor implements NestInterceptor {
|
export class LoginResultInterceptor implements NestInterceptor {
|
||||||
constructor(private readonly failedLoginGuard: FailedLoginThrottleGuard) {}
|
constructor(private readonly failedLoginGuard: FailedLoginThrottleGuard) {}
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
const rawRequest = context.switchToHttp().getRequest<unknown>();
|
||||||
|
if (!this.isExpressRequest(rawRequest)) {
|
||||||
|
throw new UnauthorizedException("Invalid request context");
|
||||||
|
}
|
||||||
|
const request: Request = rawRequest;
|
||||||
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
tap(async () => {
|
tap(() => {
|
||||||
await this.failedLoginGuard.handleLoginResult(request, true);
|
void this.failedLoginGuard.handleLoginResult(request, true);
|
||||||
}),
|
}),
|
||||||
catchError(async error => {
|
catchError(error =>
|
||||||
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
|
defer(async () => {
|
||||||
const isAuthError =
|
const message = this.extractErrorMessage(error).toLowerCase();
|
||||||
error instanceof UnauthorizedException ||
|
const status = this.extractStatusCode(error);
|
||||||
error?.status === 401 ||
|
const isAuthError =
|
||||||
message.includes("invalid") ||
|
error instanceof UnauthorizedException ||
|
||||||
message.includes("unauthorized");
|
status === 401 ||
|
||||||
|
message.includes("invalid") ||
|
||||||
|
message.includes("unauthorized");
|
||||||
|
|
||||||
if (isAuthError) {
|
if (isAuthError) {
|
||||||
await this.failedLoginGuard.handleLoginResult(request, false);
|
await this.failedLoginGuard.handleLoginResult(request, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwError(() => error);
|
throw error;
|
||||||
})
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isExpressRequest(value: unknown): value is Request {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const candidate = value as Partial<Request> & {
|
||||||
|
headers?: unknown;
|
||||||
|
method?: unknown;
|
||||||
|
url?: unknown;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
typeof candidate.headers === "object" &&
|
||||||
|
candidate.headers !== null &&
|
||||||
|
typeof candidate.method === "string" &&
|
||||||
|
typeof candidate.url === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractErrorMessage(error: unknown): string {
|
||||||
|
if (typeof error === "string") {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (error && typeof error === "object" && "message" in error) {
|
||||||
|
const maybeMessage = (error as { message?: unknown }).message;
|
||||||
|
if (typeof maybeMessage === "string") {
|
||||||
|
return maybeMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractStatusCode(error: unknown): number | undefined {
|
||||||
|
if (error && typeof error === "object" && "status" in error) {
|
||||||
|
const statusValue = (error as { status?: unknown }).status;
|
||||||
|
if (typeof statusValue === "number") {
|
||||||
|
return statusValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./http/auth.controller";
|
export * from "./http/auth.controller";
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import { mapPrismaUserToUserProfile } from "@bff/infra/utils/user-mapper.util";
|
|||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
|
||||||
const cookieExtractor = (req: Request): string | null => {
|
const cookieExtractor = (req: Request): string | null => {
|
||||||
const cookieToken = req?.cookies?.access_token;
|
const cookieSource: unknown = Reflect.get(req, "cookies");
|
||||||
if (typeof cookieToken === "string" && cookieToken.length > 0) {
|
if (!cookieSource || typeof cookieSource !== "object") {
|
||||||
return cookieToken;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
const token = Reflect.get(cookieSource, "access_token") as unknown;
|
||||||
|
return typeof token === "string" && token.length > 0 ? token : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable, BadRequestException, NotFoundException, Inject } from "@nes
|
|||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
||||||
import { OrderPricebookService } from "./order-pricebook.service";
|
import { OrderPricebookService } from "./order-pricebook.service";
|
||||||
import type { PrismaService } from "@bff/infra/database/prisma.service";
|
import { PrismaService } from "@bff/infra/database/prisma.service";
|
||||||
import { createOrderRequestSchema } from "@customer-portal/domain";
|
import { createOrderRequestSchema } from "@customer-portal/domain";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -62,9 +62,8 @@ export class SubscriptionsController {
|
|||||||
async getSubscriptionStats(@Request() req: RequestWithUser): Promise<{
|
async getSubscriptionStats(@Request() req: RequestWithUser): Promise<{
|
||||||
total: number;
|
total: number;
|
||||||
active: number;
|
active: number;
|
||||||
suspended: number;
|
completed: number;
|
||||||
cancelled: number;
|
cancelled: number;
|
||||||
pending: number;
|
|
||||||
}> {
|
}> {
|
||||||
return this.subscriptionsService.getSubscriptionStats(req.user.id);
|
return this.subscriptionsService.getSubscriptionStats(req.user.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,7 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
|||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { subscriptionSchema } from "@customer-portal/domain";
|
import { subscriptionSchema } from "@customer-portal/domain";
|
||||||
import type {
|
import type { WhmcsProduct } from "@bff/integrations/whmcs/types/whmcs-api.types";
|
||||||
WhmcsProduct,
|
|
||||||
WhmcsProductsResponse,
|
|
||||||
} from "@bff/integrations/whmcs/types/whmcs-api.types";
|
|
||||||
|
|
||||||
export interface GetSubscriptionsOptions {
|
export interface GetSubscriptionsOptions {
|
||||||
status?: string;
|
status?: string;
|
||||||
@ -194,13 +191,8 @@ export class SubscriptionsService {
|
|||||||
async getSubscriptionStats(userId: string): Promise<{
|
async getSubscriptionStats(userId: string): Promise<{
|
||||||
total: number;
|
total: number;
|
||||||
active: number;
|
active: number;
|
||||||
suspended: number;
|
|
||||||
cancelled: number;
|
|
||||||
pending: number;
|
|
||||||
completed: number;
|
completed: number;
|
||||||
totalMonthlyRevenue: number;
|
cancelled: number;
|
||||||
activeMonthlyRevenue: number;
|
|
||||||
currency: string;
|
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
// Get WHMCS client ID from user mapping
|
// Get WHMCS client ID from user mapping
|
||||||
@ -209,39 +201,18 @@ export class SubscriptionsService {
|
|||||||
throw new NotFoundException("WHMCS client mapping not found");
|
throw new NotFoundException("WHMCS client mapping not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get basic stats from WHMCS service
|
// Get all subscriptions and aggregate customer-facing stats only
|
||||||
const basicStats = await this.whmcsService.getSubscriptionStats(
|
|
||||||
mapping.whmcsClientId,
|
|
||||||
userId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get all subscriptions for financial calculations
|
|
||||||
const subscriptionList = await this.getSubscriptions(userId);
|
const subscriptionList = await this.getSubscriptions(userId);
|
||||||
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
||||||
|
|
||||||
// Calculate revenue metrics
|
|
||||||
const totalMonthlyRevenue = subscriptions
|
|
||||||
.filter(s => s.cycle === "Monthly")
|
|
||||||
.reduce((sum, s) => sum + s.amount, 0);
|
|
||||||
|
|
||||||
const activeMonthlyRevenue = subscriptions
|
|
||||||
.filter(s => s.status === "Active" && s.cycle === "Monthly")
|
|
||||||
.reduce((sum, s) => sum + s.amount, 0);
|
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
...basicStats,
|
total: subscriptions.length,
|
||||||
|
active: subscriptions.filter(s => s.status === "Active").length,
|
||||||
completed: subscriptions.filter(s => s.status === "Completed").length,
|
completed: subscriptions.filter(s => s.status === "Completed").length,
|
||||||
totalMonthlyRevenue,
|
cancelled: subscriptions.filter(s => s.status === "Cancelled").length,
|
||||||
activeMonthlyRevenue,
|
|
||||||
currency: subscriptions[0]?.currency || "JPY",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.log(`Generated subscription stats for user ${userId}`, {
|
this.logger.log(`Generated subscription stats for user ${userId}`, stats);
|
||||||
...stats,
|
|
||||||
// Don't log revenue amounts for security
|
|
||||||
totalMonthlyRevenue: "[CALCULATED]",
|
|
||||||
activeMonthlyRevenue: "[CALCULATED]",
|
|
||||||
});
|
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -511,7 +482,7 @@ export class SubscriptionsService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const productsResponse: WhmcsProductsResponse = await this.whmcsService.getClientsProducts({
|
const productsResponse = await this.whmcsService.getClientsProducts({
|
||||||
clientid: mapping.whmcsClientId,
|
clientid: mapping.whmcsClientId,
|
||||||
});
|
});
|
||||||
const productContainer = productsResponse.products?.product;
|
const productContainer = productsResponse.products?.product;
|
||||||
|
|||||||
@ -15,11 +15,7 @@ declare module "rate-limiter-flexible" {
|
|||||||
remainingPoints: number;
|
remainingPoints: number;
|
||||||
consumedPoints: number;
|
consumedPoints: number;
|
||||||
msBeforeNext: number;
|
msBeforeNext: number;
|
||||||
constructor(data: {
|
constructor(data: { remainingPoints: number; consumedPoints: number; msBeforeNext: number });
|
||||||
remainingPoints: number;
|
|
||||||
consumedPoints: number;
|
|
||||||
msBeforeNext: number;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RateLimiterRedis {
|
export class RateLimiterRedis {
|
||||||
|
|||||||
@ -20,9 +20,8 @@ const emptySubscriptionList: SubscriptionList = {
|
|||||||
const emptyStats = {
|
const emptyStats = {
|
||||||
total: 0,
|
total: 0,
|
||||||
active: 0,
|
active: 0,
|
||||||
suspended: 0,
|
completed: 0,
|
||||||
cancelled: 0,
|
cancelled: 0,
|
||||||
pending: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyInvoiceList: InvoiceList = {
|
const emptyInvoiceList: InvoiceList = {
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
|||||||
import {
|
import {
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ExclamationTriangleIcon,
|
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
@ -59,12 +58,9 @@ export function SubscriptionsListContainer() {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case "Active":
|
case "Active":
|
||||||
return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
|
return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
|
||||||
case "Suspended":
|
case "Completed":
|
||||||
return <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />;
|
return <CheckCircleIcon className="h-5 w-5 text-blue-500" />;
|
||||||
case "Pending":
|
|
||||||
return <ClockIcon className="h-5 w-5 text-blue-500" />;
|
|
||||||
case "Cancelled":
|
case "Cancelled":
|
||||||
case "Terminated":
|
|
||||||
return <XCircleIcon className="h-5 w-5 text-gray-500" />;
|
return <XCircleIcon className="h-5 w-5 text-gray-500" />;
|
||||||
default:
|
default:
|
||||||
return <ClockIcon className="h-5 w-5 text-gray-500" />;
|
return <ClockIcon className="h-5 w-5 text-gray-500" />;
|
||||||
@ -82,22 +78,17 @@ export function SubscriptionsListContainer() {
|
|||||||
const statusFilterOptions = [
|
const statusFilterOptions = [
|
||||||
{ value: "all", label: "All Status" },
|
{ value: "all", label: "All Status" },
|
||||||
{ value: "Active", label: "Active" },
|
{ value: "Active", label: "Active" },
|
||||||
{ value: "Suspended", label: "Suspended" },
|
{ value: "Completed", label: "Completed" },
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Cancelled", label: "Cancelled" },
|
{ value: "Cancelled", label: "Cancelled" },
|
||||||
{ value: "Terminated", label: "Terminated" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const getStatusVariant = (status: string) => {
|
const getStatusVariant = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "Active":
|
case "Active":
|
||||||
return "success" as const;
|
return "success" as const;
|
||||||
case "Suspended":
|
case "Completed":
|
||||||
return "warning" as const;
|
|
||||||
case "Pending":
|
|
||||||
return "info" as const;
|
return "info" as const;
|
||||||
case "Cancelled":
|
case "Cancelled":
|
||||||
case "Terminated":
|
|
||||||
return "neutral" as const;
|
return "neutral" as const;
|
||||||
default:
|
default:
|
||||||
return "neutral" as const;
|
return "neutral" as const;
|
||||||
@ -239,7 +230,7 @@ export function SubscriptionsListContainer() {
|
|||||||
>
|
>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{stats && (
|
{stats && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
<SubCard>
|
<SubCard>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
@ -256,25 +247,12 @@ export function SubscriptionsListContainer() {
|
|||||||
<SubCard>
|
<SubCard>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<ExclamationTriangleIcon className="h-8 w-8 text-yellow-600" />
|
<CheckCircleIcon className="h-8 w-8 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-5 w-0 flex-1">
|
<div className="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Suspended</dt>
|
<dt className="text-sm font-medium text-gray-500 truncate">Completed</dt>
|
||||||
<dd className="text-lg font-medium text-gray-900">{stats.suspended}</dd>
|
<dd className="text-lg font-medium text-gray-900">{stats.completed}</dd>
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SubCard>
|
|
||||||
<SubCard>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<ClockIcon className="h-8 w-8 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-5 w-0 flex-1">
|
|
||||||
<dl>
|
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Pending</dt>
|
|
||||||
<dd className="text-lg font-medium text-gray-900">{stats.pending}</dd>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user