tema 675f7d5cfd Remove cached profile fields migration and update CSRF middleware for new public auth endpoints
- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS.
- Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking.
- Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience.
- Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
2025-11-21 17:12:34 +09:00

304 lines
9.5 KiB
TypeScript

import { Injectable, Inject, BadRequestException } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { SubscriptionsService } from "../../subscriptions.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
import type { SimValidationResult } from "../interfaces/sim-base.interface";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
@Injectable()
export class SimValidationService {
constructor(
private readonly subscriptionsService: SubscriptionsService,
private readonly mappingsService: MappingsService,
private readonly whmcsService: WhmcsService,
@Inject(Logger) private readonly logger: Logger
) {}
/**
* Check if a subscription is a SIM service and extract account identifier
*/
async validateSimSubscription(
userId: string,
subscriptionId: number
): Promise<SimValidationResult> {
try {
// Get subscription details to verify it's a SIM service
const subscription = await this.subscriptionsService.getSubscriptionById(
userId,
subscriptionId
);
// Check if this is a SIM service
const isSimService =
subscription.productName.toLowerCase().includes("sim") ||
subscription.groupName?.toLowerCase().includes("sim");
if (!isSimService) {
throw new BadRequestException("This subscription is not a SIM service");
}
// For SIM services, the account identifier (phone number) can be stored in multiple places
let account = "";
let accountSource = "";
// 1. Try domain field first
if (subscription.domain && subscription.domain.trim()) {
account = subscription.domain.trim();
accountSource = "subscription.domain";
this.logger.log(`Found SIM account in domain field: ${account}`, {
userId,
subscriptionId,
source: accountSource,
});
}
// 2. If no domain, check custom fields for phone number/MSISDN
if (!account && subscription.customFields) {
account = this.extractAccountFromCustomFields(subscription.customFields, subscriptionId);
if (account) {
accountSource = "subscription.customFields";
this.logger.log(`Found SIM account in custom fields: ${account}`, {
userId,
subscriptionId,
source: accountSource,
});
}
}
// 3. If still no account, check if subscription ID looks like a phone number
if (!account && subscription.orderNumber) {
const orderNum = subscription.orderNumber.toString();
if (/^\d{10,11}$/.test(orderNum)) {
account = orderNum;
accountSource = "subscription.orderNumber";
this.logger.log(`Found SIM account in order number: ${account}`, {
userId,
subscriptionId,
source: accountSource,
});
}
}
// 4. Final fallback - get phone number from WHMCS account
if (!account) {
try {
const mapping = await this.mappingsService.findByUserId(userId);
if (mapping?.whmcsClientId) {
const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
if (client?.phonenumber) {
account = client.phonenumber;
accountSource = "whmcs.account.phonenumber";
this.logger.log(
`Found SIM account in WHMCS account phone number: ${account}`,
{
userId,
subscriptionId,
productName: subscription.productName,
whmcsClientId: mapping.whmcsClientId,
source: accountSource,
}
);
}
}
} catch (error) {
this.logger.warn(
`Failed to retrieve phone number from WHMCS account for user ${userId}`,
{
error: getErrorMessage(error),
userId,
subscriptionId,
}
);
}
// If still no account found, throw an error
if (!account) {
throw new BadRequestException(
`No SIM account identifier (phone number) found for subscription ${subscriptionId}. ` +
`Please ensure the subscription has a phone number in the domain field, custom fields, or in your WHMCS account profile.`
);
}
}
// Clean up the account format (remove hyphens, spaces, etc.)
account = account.replace(/[-\s()]/g, "");
// Skip phone number format validation for testing
// In production, you might want to add validation back:
// const cleanAccount = account.replace(/^\+81/, '0'); // Convert +81 to 0
// if (!/^0\d{9,10}$/.test(cleanAccount)) {
// throw new BadRequestException(`Invalid SIM account format: ${account}. Expected Japanese phone number format (10-11 digits starting with 0).`);
// }
// account = cleanAccount;
this.logger.log(`Using SIM account: ${account} (from ${accountSource})`, {
userId,
subscriptionId,
account,
source: accountSource,
note: "Phone number format validation skipped for testing",
});
return { account };
} catch (error) {
const sanitizedError = getErrorMessage(error);
this.logger.error(
`Failed to validate SIM subscription ${subscriptionId} for user ${userId}`,
{
error: sanitizedError,
}
);
throw error;
}
}
/**
* Extract account identifier from custom fields
*/
private extractAccountFromCustomFields(
customFields: Record<string, unknown>,
subscriptionId: number
): string {
// Common field names for SIM phone numbers in WHMCS
const phoneFields = [
"phone",
"msisdn",
"phonenumber",
"phone_number",
"mobile",
"sim_phone",
"Phone Number",
"MSISDN",
"Phone",
"Mobile",
"SIM Phone",
"PhoneNumber",
"phone_number",
"mobile_number",
"sim_number",
"account_number",
"Account Number",
"SIM Account",
"Phone Number (SIM)",
"Mobile Number",
// Specific field names that might contain the SIM number
"SIM Number",
"SIM_Number",
"sim_number",
"SIM_Phone_Number",
"Phone_Number_SIM",
"Mobile_SIM_Number",
"SIM_Account_Number",
"ICCID",
"iccid",
"IMSI",
"imsi",
"EID",
"eid",
// Additional variations
"SIM_Data",
"SIM_Info",
"SIM_Details",
];
for (const fieldName of phoneFields) {
const rawValue = customFields[fieldName];
if (rawValue !== undefined && rawValue !== null && rawValue !== "") {
const accountValue = this.formatCustomFieldValue(rawValue);
this.logger.log(`Found SIM account in custom field '${fieldName}': ${accountValue}`, {
subscriptionId,
fieldName,
account: accountValue,
});
return accountValue;
}
}
// If still no account found, log all available custom fields for debugging
this.logger.warn(`No SIM account found in custom fields for subscription ${subscriptionId}`, {
subscriptionId,
availableFields: Object.keys(customFields),
customFields,
searchedFields: phoneFields,
});
return "";
}
/**
* Debug method to check subscription data for SIM services
*/
async debugSimSubscription(
userId: string,
subscriptionId: number
): Promise<Record<string, unknown>> {
try {
const subscription = await this.subscriptionsService.getSubscriptionById(
userId,
subscriptionId
);
// Get WHMCS account phone number for debugging
let whmcsAccountPhone: string | undefined;
try {
const mapping = await this.mappingsService.findByUserId(userId);
if (mapping?.whmcsClientId) {
const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
whmcsAccountPhone = client?.phonenumber;
}
} catch (error) {
this.logger.warn(`Failed to fetch WHMCS account phone for debugging`, {
error: getErrorMessage(error),
});
}
return {
subscriptionId,
productName: subscription.productName,
domain: subscription.domain,
orderNumber: subscription.orderNumber,
customFields: subscription.customFields,
isSimService:
subscription.productName.toLowerCase().includes("sim") ||
subscription.groupName?.toLowerCase().includes("sim"),
groupName: subscription.groupName,
status: subscription.status,
whmcsAccountPhone,
allCustomFieldKeys: Object.keys(subscription.customFields || {}),
allCustomFieldValues: subscription.customFields,
};
} catch (error) {
const sanitizedError = getErrorMessage(error);
this.logger.error(`Failed to debug subscription ${subscriptionId}`, {
error: sanitizedError,
});
throw error;
}
}
private formatCustomFieldValue(value: unknown): string {
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value === "object" && value !== null) {
try {
return JSON.stringify(value);
} catch {
return "[unserializable]";
}
}
return "";
}
}