Refactor audit and signup workflows to streamline user data handling

- Removed unnecessary fields (firstName, lastName, company, phone) from user creation in AuditService and SignupWorkflowService for cleaner data management.
- Enhanced error logging in GlobalAuthGuard to differentiate between unauthorized access attempts and other authentication errors.
- Updated CurrencyController to mark endpoints as public for improved access control.
- Improved button components across various steps in the internet and SIM configuration processes for better user experience and consistency.
- Added active internet subscription warning in checkout process to prevent duplicate subscriptions.
This commit is contained in:
barsa 2025-10-22 14:19:31 +09:00
parent e56d6f5e20
commit 3d17f36c2f
15 changed files with 167 additions and 65 deletions

View File

@ -142,8 +142,6 @@ export class AuditService {
select: {
id: true,
email: true,
firstName: true,
lastName: true,
},
},
},

View File

@ -277,10 +277,6 @@ export class SignupWorkflowService {
data: {
email,
passwordHash,
firstName,
lastName,
company: company || null,
phone: phone || null,
emailVerified: false,
failedLoginAttempts: 0,
lockedUntil: null,

View File

@ -83,7 +83,16 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
this.logger.debug(`Authenticated access to: ${route}`);
return true;
} catch (error) {
this.logger.error(`Authentication error for route ${route}: ${getErrorMessage(error)}`);
if (error instanceof UnauthorizedException) {
const token = extractTokenFromRequest(request);
const log =
typeof token === "string"
? () => this.logger.warn(`Unauthorized access attempt to ${route}`)
: () => this.logger.debug(`Unauthenticated request blocked for ${route}`);
log();
} else {
this.logger.error(`Authentication error for route ${route}: ${getErrorMessage(error)}`);
}
throw error;
}
}

View File

@ -1,10 +1,12 @@
import { Controller, Get } from "@nestjs/common";
import { Public } from "@bff/modules/auth/decorators/public.decorator";
import { WhmcsCurrencyService } from "../../integrations/whmcs/services/whmcs-currency.service";
@Controller("currency")
export class CurrencyController {
constructor(private readonly currencyService: WhmcsCurrencyService) {}
@Public()
@Get("default")
getDefaultCurrency() {
const defaultCurrency = this.currencyService.getDefaultCurrency();
@ -17,6 +19,7 @@ export class CurrencyController {
};
}
@Public()
@Get("all")
getAllCurrencies() {
return this.currencyService.getAllCurrencies();

View File

@ -2,7 +2,8 @@
import { PageLayout } from "@/components/templates/PageLayout";
import { ProgressSteps } from "@/components/molecules";
import { ServerIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/atoms/button";
import { ServerIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
import type {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
@ -84,6 +85,19 @@ export function InternetConfigureContainer({
description="Set up your internet service options"
>
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<Button
as="a"
href="/catalog/internet"
variant="ghost"
size="sm"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
className="text-gray-600 hover:text-gray-900"
>
Back to Internet Plans
</Button>
</div>
{/* Plan Header */}
<PlanHeader plan={plan} monthlyTotal={monthlyTotal} oneTimeTotal={oneTimeTotal} />

View File

@ -47,13 +47,15 @@ export function AddonsStep({
/>
<div className="flex justify-between mt-6">
<Button onClick={onBack} variant="outline" className="flex items-center">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
<Button
onClick={onBack}
variant="outline"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
Back to Installation
</Button>
<Button onClick={onNext} className="flex items-center">
<Button onClick={onNext} rightIcon={<ArrowRightIcon className="w-4 h-4" />}>
Review Order
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>

View File

@ -48,13 +48,19 @@ export function InstallationStep({
/>
<div className="flex justify-between mt-6">
<Button onClick={onBack} variant="outline" className="flex items-center">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
<Button
onClick={onBack}
variant="outline"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
Back to Configuration
</Button>
<Button onClick={onNext} disabled={!selectedInstallation} className="flex items-center">
<Button
onClick={onNext}
disabled={!selectedInstallation}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Add-ons
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>

View File

@ -65,13 +65,22 @@ export function ReviewOrderStep({
</div>
<div className="flex justify-between items-center pt-6 border-t">
<Button onClick={onBack} variant="outline" size="lg" className="px-8 py-4 text-lg">
<ArrowLeftIcon className="w-5 h-5 mr-2" />
<Button
onClick={onBack}
variant="outline"
size="lg"
className="px-8 py-4 text-lg"
leftIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
Back to Add-ons
</Button>
<Button onClick={onConfirm} size="lg" className="px-12 py-4 text-lg font-semibold">
<Button
onClick={onConfirm}
size="lg"
className="px-12 py-4 text-lg font-semibold"
rightIcon={<ArrowRightIcon className="w-5 h-5" />}
>
Proceed to Checkout
<ArrowRightIcon className="w-5 h-5 ml-2" />
</Button>
</div>
</AnimatedCard>

View File

@ -61,10 +61,9 @@ export function ServiceConfigurationStep({ plan, mode, setMode, isTransitioning,
<Button
onClick={onNext}
disabled={plan?.internetPlanTier === "Silver" && !mode}
className="flex items-center"
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Installation
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>
@ -122,12 +121,14 @@ function ModeSelectionCard({
return (
<button
type="button"
onClick={() => onSelect(mode)}
className={`p-6 rounded-xl border-2 text-left transition-all duration-200 ${
className={`p-6 rounded-xl border-2 text-left transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
isSelected
? "border-blue-500 bg-blue-50 shadow-lg transform scale-105"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50 hover:shadow-md"
? "border-blue-500 bg-blue-50 shadow-md"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`}
aria-pressed={isSelected}
>
<div className="flex items-center justify-between mb-3">
<h5 className="text-lg font-semibold text-gray-900">{title}</h5>

View File

@ -139,8 +139,14 @@ export function SimConfigureView({
>
<div className="max-w-4xl mx-auto space-y-8">
<div className="mb-6">
<Button as="a" href="/catalog/sim" variant="outline" size="sm" className="group">
<ArrowLeftIcon className="w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform duration-300" />
<Button
as="a"
href="/catalog/sim"
variant="ghost"
size="sm"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
className="text-gray-600 hover:text-gray-900"
>
Back to SIM Plans
</Button>
</div>
@ -221,9 +227,11 @@ export function SimConfigureView({
errors={errors}
/>
<div className="flex justify-end mt-6">
<Button onClick={() => transitionToStep(2)} className="flex items-center">
<Button
onClick={() => transitionToStep(2)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Activation
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>
@ -252,14 +260,15 @@ export function SimConfigureView({
<Button
onClick={() => transitionToStep(1)}
variant="outline"
className="flex items-center"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to SIM Type
</Button>
<Button onClick={() => transitionToStep(3)} className="flex items-center">
<Button
onClick={() => transitionToStep(3)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Add-ons
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>
@ -297,14 +306,15 @@ export function SimConfigureView({
<Button
onClick={() => transitionToStep(2)}
variant="outline"
className="flex items-center"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Activation
</Button>
<Button onClick={() => transitionToStep(4)} className="flex items-center">
<Button
onClick={() => transitionToStep(4)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Number Porting
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>
@ -333,9 +343,8 @@ export function SimConfigureView({
<Button
onClick={() => transitionToStep(3)}
variant="outline"
className="flex items-center"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Add-ons
</Button>
<Button
@ -343,10 +352,9 @@ export function SimConfigureView({
if (wantsMnp && !validate()) return;
transitionToStep(5);
}}
className="flex items-center"
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Review Order
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</AnimatedCard>
@ -490,13 +498,17 @@ export function SimConfigureView({
variant="outline"
size="lg"
className="px-8 py-4 text-lg"
leftIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
<ArrowLeftIcon className="w-5 h-5 mr-2" />
Back to Number Porting
</Button>
<Button onClick={onConfirm} size="lg" className="px-12 py-4 text-lg font-semibold">
<Button
onClick={onConfirm}
size="lg"
className="px-12 py-4 text-lg font-semibold"
rightIcon={<ArrowRightIcon className="w-5 h-5" />}
>
Proceed to Checkout
<ArrowRightIcon className="w-5 h-5 ml-2" />
</Button>
</div>
</AnimatedCard>

View File

@ -19,9 +19,9 @@ export function SimTypeSelector({
<div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.01] ${
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out ${
simType === "Physical SIM"
? "border-blue-500 bg-blue-50 ring-2 ring-blue-100 shadow-sm"
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`}
>
@ -43,9 +43,9 @@ export function SimTypeSelector({
</label>
<label
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.01] ${
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out ${
simType === "eSIM"
? "border-blue-500 bg-blue-50 ring-2 ring-blue-100 shadow-sm"
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`}
>
@ -68,7 +68,12 @@ export function SimTypeSelector({
</div>
{/* EID Input for eSIM */}
{simType === "eSIM" && (
<div
className={`overflow-hidden transition-[max-height,opacity] duration-300 ease-out ${
simType === "eSIM" ? "max-h-[360px] opacity-100" : "max-h-0 opacity-0"
}`}
aria-hidden={simType !== "eSIM"}
>
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">eSIM Device Information</h4>
<div>
@ -91,7 +96,7 @@ export function SimTypeSelector({
</p>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -83,6 +83,16 @@ const parsePortingGenderParam = (value: string | null): MnpData["portingGender"]
return undefined;
};
const MIN_STEP = 1;
const MAX_STEP = 5;
const parseStepParam = (value: string | null): number => {
if (!value) return MIN_STEP;
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed)) return MIN_STEP;
return Math.min(Math.max(parsed, MIN_STEP), MAX_STEP);
};
export function useSimConfigure(planId?: string): UseSimConfigureResult {
const searchParams = useSearchParams();
const { data: simData, isLoading: simLoading } = useSimCatalog();
@ -90,7 +100,9 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
const configureParams = useSimConfigureParams();
// Step orchestration state
const [currentStep, setCurrentStep] = useState(0);
const [currentStep, setCurrentStep] = useState<number>(() =>
parseStepParam(searchParams.get("step"))
);
const [isTransitioning, setIsTransitioning] = useState(false);
// Initialize form with Zod
@ -349,7 +361,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
const transitionToStep = useCallback((nextStep: number) => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentStep(nextStep);
setCurrentStep(Math.min(Math.max(nextStep, MIN_STEP), MAX_STEP));
setIsTransitioning(false);
}, 150);
}, []);

View File

@ -23,6 +23,12 @@ import {
// Use domain Address type
import type { Address } from "@customer-portal/domain/customer";
const ACTIVE_INTERNET_WARNING_MESSAGE =
"You already have an active Internet subscription. Please contact support to modify your service.";
const DEVELOPMENT_WARNING_SUFFIX =
"Development mode override allows checkout to continue for testing.";
const isDevEnvironment = process.env.NODE_ENV === "development";
export function useCheckout() {
const params = useSearchParams();
const router = useRouter();
@ -36,6 +42,17 @@ export function useCheckout() {
// Load active subscriptions to enforce business rules client-side before submission
const { data: activeSubs } = useActiveSubscriptions();
const hasActiveInternetSubscription = useMemo(() => {
if (!Array.isArray(activeSubs)) return false;
return activeSubs.some(
subscription =>
String(subscription.groupName || subscription.productName || "")
.toLowerCase()
.includes("internet") &&
String(subscription.status || "").toLowerCase() === "active"
);
}, [activeSubs]);
const [activeInternetWarning, setActiveInternetWarning] = useState<string | null>(null);
const {
data: paymentMethods,
@ -91,6 +108,18 @@ export function useCheckout() {
}
}, [orderType, params]);
useEffect(() => {
if (orderType !== ORDER_TYPE.INTERNET || !hasActiveInternetSubscription) {
setActiveInternetWarning(null);
return;
}
const warningMessage = isDevEnvironment
? `${ACTIVE_INTERNET_WARNING_MESSAGE} ${DEVELOPMENT_WARNING_SUFFIX}`
: ACTIVE_INTERNET_WARNING_MESSAGE;
setActiveInternetWarning(warningMessage);
}, [orderType, hasActiveInternetSubscription]);
useEffect(() => {
let mounted = true;
@ -160,18 +189,8 @@ export function useCheckout() {
};
// Client-side guard: prevent Internet orders if an Internet subscription already exists
if (orderType === "Internet" && Array.isArray(activeSubs)) {
const hasActiveInternet = activeSubs.some(
s =>
String(s.groupName || s.productName || "")
.toLowerCase()
.includes("internet") && String(s.status || "").toLowerCase() === "active"
);
if (hasActiveInternet) {
throw new Error(
"You already have an active Internet subscription. Please contact support to modify your service."
);
}
if (orderType === ORDER_TYPE.INTERNET && hasActiveInternetSubscription && !isDevEnvironment) {
throw new Error(ACTIVE_INTERNET_WARNING_MESSAGE);
}
const response = await ordersService.createOrder(orderData);
@ -183,7 +202,7 @@ export function useCheckout() {
} finally {
setSubmitting(false);
}
}, [checkoutState, orderType, activeSubs, router]);
}, [checkoutState, orderType, hasActiveInternetSubscription, router]);
const confirmAddress = useCallback((address?: Address) => {
setAddressConfirmed(true);
@ -218,5 +237,6 @@ export function useCheckout() {
markAddressIncomplete,
handleSubmitOrder,
navigateBackToConfigure,
activeInternetWarning,
} as const;
}

View File

@ -25,6 +25,7 @@ export function CheckoutContainer() {
markAddressIncomplete,
handleSubmitOrder,
navigateBackToConfigure,
activeInternetWarning,
} = useCheckout();
if (isLoading(checkoutState)) {
@ -98,6 +99,16 @@ export function CheckoutContainer() {
tone={paymentRefresh.toast.tone}
/>
{activeInternetWarning && (
<AlertBanner
variant="warning"
title="Existing Internet Subscription"
elevated
>
<span className="text-sm text-gray-700">{activeInternetWarning}</span>
</AlertBanner>
)}
<div className="bg-gray-50 border border-gray-200 rounded-2xl p-6 md:p-7 shadow-sm">
<div className="flex items-center gap-3 mb-6">
<ShieldCheckIcon className="w-6 h-6 text-blue-600" />

4
cookies.txt Normal file
View File

@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.