- 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.
222 lines
6.6 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|