barsa fcd324df09 Refactor Salesforce utility and improve order handling in checkout process
- Imported salesforceIdSchema and nonEmptyStringSchema from the common domain for better consistency.
- Simplified the creation of SOQL field name validation schema.
- Refactored the InvoiceTable component to utilize useCallback for improved performance.
- Streamlined payment method handling in the PaymentMethodCard component by removing unused props.
- Enhanced the checkout process by integrating new validation schemas and improving cart handling logic.
- Updated various components to ensure better type safety and clarity in data handling.
2025-10-22 11:33:23 +09:00

222 lines
6.6 KiB
TypeScript

import { getErrorMessage } from "@bff/core/utils/error.util";
import { Logger } from "nestjs-pino";
import { Injectable, Inject } from "@nestjs/common";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer";
import type { WhmcsSsoResponse } from "@customer-portal/domain/customer";
@Injectable()
export class WhmcsSsoService {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly connectionService: WhmcsConnectionOrchestratorService
) {}
/**
* Create SSO token for WHMCS access
*/
async createSsoToken(
clientId: number,
destination?: string,
ssoRedirectPath?: string
): Promise<{ url: string; expiresAt: string }> {
try {
const params: WhmcsCreateSsoTokenParams = {
client_id: clientId,
...(destination && { destination }),
...(ssoRedirectPath && { sso_redirect_path: ssoRedirectPath }),
};
const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(params);
const url = this.resolveRedirectUrl(response.redirect_url);
this.debugLogRedirectHost(url);
const result = {
url,
expiresAt: new Date(Date.now() + 60000).toISOString(), // ~60 seconds (per WHMCS spec)
};
this.logger.log(`Created SSO token for client ${clientId}`, {
destination,
ssoRedirectPath,
});
return result;
} catch (error) {
this.logger.error(`Failed to create SSO token for client ${clientId}`, {
error: getErrorMessage(error),
destination,
ssoRedirectPath,
});
throw error;
}
}
/**
* Helper function to create SSO links for invoices (following WHMCS best practices)
*/
async whmcsSsoForInvoice(
clientId: number,
invoiceId: number,
target: "view" | "download" | "pay"
): Promise<string> {
let path: string;
switch (target) {
case "pay":
// Direct payment page using Friendly URLs
path = `index.php?rp=/invoice/${invoiceId}/pay`;
break;
case "download":
// PDF download
path = `dl.php?type=i&id=${invoiceId}`;
break;
case "view":
default:
// Invoice view page
path = `viewinvoice.php?id=${invoiceId}`;
break;
}
const params: WhmcsCreateSsoTokenParams = {
client_id: clientId,
destination: "sso:custom_redirect",
sso_redirect_path: path,
};
const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(params);
// Return the 60s, one-time URL (resolved to absolute)
const url = this.resolveRedirectUrl(response.redirect_url);
this.debugLogRedirectHost(url);
return url;
}
/**
* Create SSO token for direct WHMCS admin access
*/
async createAdminSsoToken(
clientId: number,
adminPath?: string
): Promise<{ url: string; expiresAt: string }> {
try {
const params: WhmcsCreateSsoTokenParams = {
client_id: clientId,
destination: adminPath || "clientarea.php",
};
const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(params);
const url = this.resolveRedirectUrl(response.redirect_url);
this.debugLogRedirectHost(url);
const result = {
url,
expiresAt: new Date(Date.now() + 60000).toISOString(), // ~60 seconds
};
this.logger.log(`Created admin SSO token for client ${clientId}`, {
adminPath,
});
return result;
} catch (error) {
this.logger.error(`Failed to create admin SSO token for client ${clientId}`, {
error: getErrorMessage(error),
adminPath,
});
throw error;
}
}
/**
* Create SSO token for specific WHMCS module/page
*/
async createModuleSsoToken(
clientId: number,
module: string,
action?: string,
params?: Record<string, unknown>
): Promise<{ url: string; expiresAt: string }> {
try {
// Build the module path
let modulePath = `index.php?m=${module}`;
if (action) {
modulePath += `&a=${action}`;
}
if (params) {
const stringParams: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
stringParams[key] = typeof value === "string" ? value : JSON.stringify(value);
}
const queryParams = new URLSearchParams(stringParams).toString();
if (queryParams) {
modulePath += `&${queryParams}`;
}
}
const ssoParams: WhmcsCreateSsoTokenParams = {
client_id: clientId,
destination: "sso:custom_redirect",
sso_redirect_path: modulePath,
};
const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(ssoParams);
const url = this.resolveRedirectUrl(response.redirect_url);
this.debugLogRedirectHost(url);
const result = {
url,
expiresAt: new Date(Date.now() + 60000).toISOString(), // ~60 seconds
};
this.logger.log(`Created module SSO token for client ${clientId}`, {
module,
action,
modulePath,
});
return result;
} catch (error) {
this.logger.error(`Failed to create module SSO token for client ${clientId}`, {
error: getErrorMessage(error),
module,
action,
params,
});
throw error;
}
}
/**
* Ensure the returned redirect URL is absolute and points to the active WHMCS base URL.
* WHMCS typically returns an absolute URL, but we normalize in case it's relative.
*/
private resolveRedirectUrl(redirectUrl: string): string {
if (!redirectUrl) return redirectUrl;
const base = this.connectionService.getBaseUrl().replace(/\/+$/, "");
const isAbsolute = /^https?:\/\//i.test(redirectUrl);
if (!isAbsolute) {
const path = redirectUrl.replace(/^\/+/, "");
return `${base}/${path}`;
}
// Absolute URL returned by WHMCS — return as-is
return redirectUrl;
}
/**
* Debug helper: log only the host of the SSO URL (never the token) in non-production.
*/
private debugLogRedirectHost(url: string): void {
if (process.env.NODE_ENV === "production") return;
try {
const target = new URL(url);
const base = new URL(this.connectionService.getBaseUrl());
this.logger.debug("WHMCS SSO redirect host", {
redirectHost: target.host,
redirectOrigin: target.origin,
baseOrigin: base.origin,
});
} catch {
// Ignore parse errors silently
}
}
}