From be3af76e010687a48acf594a2417030fc90225f1 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 25 Sep 2025 15:12:20 +0900 Subject: [PATCH] Update import paths for DataTable component in InvoiceTable and SubscriptionsList to improve module structure and maintainability. --- .../connection/config/whmcs-config.service.ts | 153 ++++++++++++++++++ .../components/SignupForm/AddressStep.tsx | 2 +- .../components/SignupForm/PasswordStep.tsx | 2 +- .../components/SignupForm/PersonalStep.tsx | 2 +- .../components/InvoiceTable/InvoiceTable.tsx | 2 +- .../subscriptions/views/SubscriptionsList.tsx | 2 +- 6 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts diff --git a/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts b/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts new file mode 100644 index 00000000..2989ec78 --- /dev/null +++ b/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts @@ -0,0 +1,153 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import type { WhmcsApiConfig } from "../types/connection.types"; + +/** + * Service for managing WHMCS API configuration + * Handles environment-based configuration loading with dev/prod separation + */ +@Injectable() +export class WhmcsConfigService { + private readonly config: WhmcsApiConfig; + private readonly accessKey?: string; + + constructor(private readonly configService: ConfigService) { + this.config = this.loadConfiguration(); + this.accessKey = this.loadAccessKey(); + } + + /** + * Get the complete WHMCS API configuration + */ + getConfig(): WhmcsApiConfig { + return { ...this.config }; + } + + /** + * Get the API access key if available + */ + getAccessKey(): string | undefined { + return this.accessKey; + } + + /** + * Check if admin authentication is available + */ + hasAdminAuth(): boolean { + return Boolean(this.config.adminUsername && this.config.adminPasswordHash); + } + + /** + * Get admin authentication credentials + */ + getAdminAuth(): { username: string; passwordHash: string } | null { + if (!this.hasAdminAuth()) { + return null; + } + return { + username: this.config.adminUsername!, + passwordHash: this.config.adminPasswordHash!, + }; + } + + /** + * Validate that required configuration is present + */ + validateConfig(): void { + const required = ['baseUrl', 'identifier', 'secret']; + const missing = required.filter(key => !this.config[key as keyof WhmcsApiConfig]); + + if (missing.length > 0) { + throw new Error(`Missing required WHMCS configuration: ${missing.join(', ')}`); + } + + if (!this.config.baseUrl.startsWith('http')) { + throw new Error('WHMCS baseUrl must start with http:// or https://'); + } + } + + /** + * Load configuration from environment variables + */ + private loadConfiguration(): WhmcsApiConfig { + const nodeEnv = this.configService.get("NODE_ENV", "development"); + const isDev = nodeEnv !== "production"; + + // Resolve and normalize base URL (trim trailing slashes) + const rawBaseUrl = this.getFirst([ + isDev ? "WHMCS_DEV_BASE_URL" : undefined, + "WHMCS_BASE_URL" + ]) || ""; + const baseUrl = rawBaseUrl.replace(/\/+$/, ""); + + const identifier = this.getFirst([ + isDev ? "WHMCS_DEV_API_IDENTIFIER" : undefined, + "WHMCS_API_IDENTIFIER" + ]) || ""; + + const secret = this.getFirst([ + isDev ? "WHMCS_DEV_API_SECRET" : undefined, + "WHMCS_API_SECRET" + ]) || ""; + + const adminUsername = this.getFirst([ + isDev ? "WHMCS_DEV_ADMIN_USERNAME" : undefined, + "WHMCS_ADMIN_USERNAME", + ]); + + const adminPasswordHash = this.getFirst([ + isDev ? "WHMCS_DEV_ADMIN_PASSWORD_MD5" : undefined, + "WHMCS_ADMIN_PASSWORD_MD5", + "WHMCS_ADMIN_PASSWORD_HASH", + ]); + + return { + baseUrl, + identifier, + secret, + timeout: this.getNumberConfig("WHMCS_API_TIMEOUT", 30000), + retryAttempts: this.getNumberConfig("WHMCS_API_RETRY_ATTEMPTS", 3), + retryDelay: this.getNumberConfig("WHMCS_API_RETRY_DELAY", 1000), + adminUsername, + adminPasswordHash, + }; + } + + /** + * Load API access key + */ + private loadAccessKey(): string | undefined { + const nodeEnv = this.configService.get("NODE_ENV", "development"); + const isDev = nodeEnv !== "production"; + + return this.getFirst([ + isDev ? "WHMCS_DEV_API_ACCESS_KEY" : undefined, + "WHMCS_API_ACCESS_KEY", + ]); + } + + /** + * Helper: read the first defined value across a list of keys + */ + private getFirst(keys: Array): string | undefined { + for (const key of keys) { + if (!key) continue; + const v = this.configService.get(key); + if (v && `${v}`.length > 0) return v; + const raw = process.env[key]; + if (raw && `${raw}`.length > 0) return raw; + } + return undefined; + } + + /** + * Get numeric configuration value with fallback + */ + private getNumberConfig(key: string, defaultValue: number): number { + const value = this.configService.get(key); + if (!value) return defaultValue; + + const parsed = parseInt(value, 10); + return isNaN(parsed) ? defaultValue : parsed; + } +} diff --git a/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx b/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx index 82a7990f..c9fbf656 100644 --- a/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx @@ -7,7 +7,7 @@ import { useCallback } from "react"; import { Input } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import type { SignupFormData } from "@customer-portal/domain"; import type { FormErrors, FormTouched, UseZodFormReturn } from "@customer-portal/validation"; diff --git a/apps/portal/src/features/auth/components/SignupForm/PasswordStep.tsx b/apps/portal/src/features/auth/components/SignupForm/PasswordStep.tsx index 9af2dbf2..ef9fa215 100644 --- a/apps/portal/src/features/auth/components/SignupForm/PasswordStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/PasswordStep.tsx @@ -6,7 +6,7 @@ "use client"; import { Input, Checkbox } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import { type SignupFormData } from "@customer-portal/domain"; import type { UseZodFormReturn } from "@customer-portal/validation"; diff --git a/apps/portal/src/features/auth/components/SignupForm/PersonalStep.tsx b/apps/portal/src/features/auth/components/SignupForm/PersonalStep.tsx index 551be4b5..1afccb49 100644 --- a/apps/portal/src/features/auth/components/SignupForm/PersonalStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/PersonalStep.tsx @@ -6,7 +6,7 @@ "use client"; import { Input } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import { type SignupFormData } from "@customer-portal/domain"; import type { FormErrors, FormTouched, UseZodFormReturn } from "@customer-portal/validation"; diff --git a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx index 4744385e..a2f2911c 100644 --- a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx +++ b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx @@ -11,7 +11,7 @@ import { ExclamationTriangleIcon, ClockIcon, } from "@heroicons/react/24/outline"; -import { DataTable } from "@/components/molecules/DataTable"; +import { DataTable } from "@/components/molecules/DataTable/DataTable"; import { BillingStatusBadge } from "../BillingStatusBadge"; import type { Invoice } from "@customer-portal/domain"; import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index f6b75d73..5984fb8c 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { Button } from "@/components/atoms/button"; import { ErrorBoundary } from "@/components/molecules"; import { PageLayout } from "@/components/templates/PageLayout"; -import { DataTable } from "@/components/molecules/DataTable"; +import { DataTable } from "@/components/molecules/DataTable/DataTable"; import { StatusPill } from "@/components/atoms/status-pill"; import { SubCard } from "@/components/molecules/SubCard"; import { SearchFilterBar } from "@/components/molecules/SearchFilterBar";