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:
parent
e56d6f5e20
commit
3d17f36c2f
@ -142,8 +142,6 @@ export class AuditService {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -277,10 +277,6 @@ export class SignupWorkflowService {
|
||||
data: {
|
||||
email,
|
||||
passwordHash,
|
||||
firstName,
|
||||
lastName,
|
||||
company: company || null,
|
||||
phone: phone || null,
|
||||
emailVerified: false,
|
||||
failedLoginAttempts: 0,
|
||||
lockedUntil: null,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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} />
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}, []);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
4
cookies.txt
Normal 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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user