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: { select: {
id: true, id: true,
email: true, email: true,
firstName: true,
lastName: true,
}, },
}, },
}, },

View File

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

View File

@ -83,7 +83,16 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
this.logger.debug(`Authenticated access to: ${route}`); this.logger.debug(`Authenticated access to: ${route}`);
return true; return true;
} catch (error) { } 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; throw error;
} }
} }

View File

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

View File

@ -2,7 +2,8 @@
import { PageLayout } from "@/components/templates/PageLayout"; import { PageLayout } from "@/components/templates/PageLayout";
import { ProgressSteps } from "@/components/molecules"; 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 { import type {
InternetPlanCatalogItem, InternetPlanCatalogItem,
InternetInstallationCatalogItem, InternetInstallationCatalogItem,
@ -84,6 +85,19 @@ export function InternetConfigureContainer({
description="Set up your internet service options" description="Set up your internet service options"
> >
<div className="max-w-4xl mx-auto"> <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 */} {/* Plan Header */}
<PlanHeader plan={plan} monthlyTotal={monthlyTotal} oneTimeTotal={oneTimeTotal} /> <PlanHeader plan={plan} monthlyTotal={monthlyTotal} oneTimeTotal={oneTimeTotal} />

View File

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

View File

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

View File

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

View File

@ -61,10 +61,9 @@ export function ServiceConfigurationStep({ plan, mode, setMode, isTransitioning,
<Button <Button
onClick={onNext} onClick={onNext}
disabled={plan?.internetPlanTier === "Silver" && !mode} disabled={plan?.internetPlanTier === "Silver" && !mode}
className="flex items-center" rightIcon={<ArrowRightIcon className="w-4 h-4" />}
> >
Continue to Installation Continue to Installation
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button> </Button>
</div> </div>
</AnimatedCard> </AnimatedCard>
@ -122,12 +121,14 @@ function ModeSelectionCard({
return ( return (
<button <button
type="button"
onClick={() => onSelect(mode)} 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 isSelected
? "border-blue-500 bg-blue-50 shadow-lg transform scale-105" ? "border-blue-500 bg-blue-50 shadow-md"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50 hover: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"> <div className="flex items-center justify-between mb-3">
<h5 className="text-lg font-semibold text-gray-900">{title}</h5> <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="max-w-4xl mx-auto space-y-8">
<div className="mb-6"> <div className="mb-6">
<Button as="a" href="/catalog/sim" variant="outline" size="sm" className="group"> <Button
<ArrowLeftIcon className="w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform duration-300" /> 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 Back to SIM Plans
</Button> </Button>
</div> </div>
@ -221,9 +227,11 @@ export function SimConfigureView({
errors={errors} errors={errors}
/> />
<div className="flex justify-end mt-6"> <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 Continue to Activation
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button> </Button>
</div> </div>
</AnimatedCard> </AnimatedCard>
@ -252,14 +260,15 @@ export function SimConfigureView({
<Button <Button
onClick={() => transitionToStep(1)} onClick={() => transitionToStep(1)}
variant="outline" 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 Back to SIM Type
</Button> </Button>
<Button onClick={() => transitionToStep(3)} className="flex items-center"> <Button
onClick={() => transitionToStep(3)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Add-ons Continue to Add-ons
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button> </Button>
</div> </div>
</AnimatedCard> </AnimatedCard>
@ -297,14 +306,15 @@ export function SimConfigureView({
<Button <Button
onClick={() => transitionToStep(2)} onClick={() => transitionToStep(2)}
variant="outline" variant="outline"
className="flex items-center" leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
> >
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Activation Back to Activation
</Button> </Button>
<Button onClick={() => transitionToStep(4)} className="flex items-center"> <Button
onClick={() => transitionToStep(4)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Number Porting Continue to Number Porting
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button> </Button>
</div> </div>
</AnimatedCard> </AnimatedCard>
@ -333,9 +343,8 @@ export function SimConfigureView({
<Button <Button
onClick={() => transitionToStep(3)} onClick={() => transitionToStep(3)}
variant="outline" 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 Back to Add-ons
</Button> </Button>
<Button <Button
@ -343,10 +352,9 @@ export function SimConfigureView({
if (wantsMnp && !validate()) return; if (wantsMnp && !validate()) return;
transitionToStep(5); transitionToStep(5);
}} }}
className="flex items-center" rightIcon={<ArrowRightIcon className="w-4 h-4" />}
> >
Review Order Review Order
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button> </Button>
</div> </div>
</AnimatedCard> </AnimatedCard>
@ -490,13 +498,17 @@ export function SimConfigureView({
variant="outline" variant="outline"
size="lg" size="lg"
className="px-8 py-4 text-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 Back to Number Porting
</Button> </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 Proceed to Checkout
<ArrowRightIcon className="w-5 h-5 ml-2" />
</Button> </Button>
</div> </div>
</AnimatedCard> </AnimatedCard>

View File

@ -19,9 +19,9 @@ export function SimTypeSelector({
<div> <div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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 === "Physical SIM" 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" : "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`} }`}
> >
@ -43,9 +43,9 @@ export function SimTypeSelector({
</label> </label>
<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" 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" : "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`} }`}
> >
@ -68,7 +68,12 @@ export function SimTypeSelector({
</div> </div>
{/* EID Input for eSIM */} {/* 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"> <div className="mt-4 p-4 bg-blue-50 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">eSIM Device Information</h4> <h4 className="font-medium text-blue-900 mb-2">eSIM Device Information</h4>
<div> <div>
@ -91,7 +96,7 @@ export function SimTypeSelector({
</p> </p>
</div> </div>
</div> </div>
)} </div>
</div> </div>
); );
} }

View File

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

View File

@ -23,6 +23,12 @@ import {
// Use domain Address type // Use domain Address type
import type { Address } from "@customer-portal/domain/customer"; 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() { export function useCheckout() {
const params = useSearchParams(); const params = useSearchParams();
const router = useRouter(); const router = useRouter();
@ -36,6 +42,17 @@ export function useCheckout() {
// Load active subscriptions to enforce business rules client-side before submission // Load active subscriptions to enforce business rules client-side before submission
const { data: activeSubs } = useActiveSubscriptions(); 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 { const {
data: paymentMethods, data: paymentMethods,
@ -91,6 +108,18 @@ export function useCheckout() {
} }
}, [orderType, params]); }, [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(() => { useEffect(() => {
let mounted = true; let mounted = true;
@ -160,18 +189,8 @@ export function useCheckout() {
}; };
// Client-side guard: prevent Internet orders if an Internet subscription already exists // Client-side guard: prevent Internet orders if an Internet subscription already exists
if (orderType === "Internet" && Array.isArray(activeSubs)) { if (orderType === ORDER_TYPE.INTERNET && hasActiveInternetSubscription && !isDevEnvironment) {
const hasActiveInternet = activeSubs.some( throw new Error(ACTIVE_INTERNET_WARNING_MESSAGE);
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."
);
}
} }
const response = await ordersService.createOrder(orderData); const response = await ordersService.createOrder(orderData);
@ -183,7 +202,7 @@ export function useCheckout() {
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
}, [checkoutState, orderType, activeSubs, router]); }, [checkoutState, orderType, hasActiveInternetSubscription, router]);
const confirmAddress = useCallback((address?: Address) => { const confirmAddress = useCallback((address?: Address) => {
setAddressConfirmed(true); setAddressConfirmed(true);
@ -218,5 +237,6 @@ export function useCheckout() {
markAddressIncomplete, markAddressIncomplete,
handleSubmitOrder, handleSubmitOrder,
navigateBackToConfigure, navigateBackToConfigure,
activeInternetWarning,
} as const; } as const;
} }

View File

@ -25,6 +25,7 @@ export function CheckoutContainer() {
markAddressIncomplete, markAddressIncomplete,
handleSubmitOrder, handleSubmitOrder,
navigateBackToConfigure, navigateBackToConfigure,
activeInternetWarning,
} = useCheckout(); } = useCheckout();
if (isLoading(checkoutState)) { if (isLoading(checkoutState)) {
@ -98,6 +99,16 @@ export function CheckoutContainer() {
tone={paymentRefresh.toast.tone} 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="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"> <div className="flex items-center gap-3 mb-6">
<ShieldCheckIcon className="w-6 h-6 text-blue-600" /> <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.