From 2b54773ebf36278f10f22bd4a0adb365b8c3a821 Mon Sep 17 00:00:00 2001 From: barsa Date: Sat, 27 Sep 2025 17:51:54 +0900 Subject: [PATCH] Enhance WHMCS API response handling and error logging across services. Update response parsing to align with official documentation, ensuring proper validation of response structures. Improve error messages for client lookups and credential validation, providing more context for debugging. Refactor SetPasswordView to redirect to the dashboard upon successful password setup. --- apps/bff/src/infra/utils/user-mapper.util.ts | 3 +- .../services/whmcs-http-client.service.ts | 41 +- .../whmcs/services/whmcs-client.service.ts | 16 +- .../src/modules/auth/auth-zod.controller.ts | 21 +- apps/bff/src/modules/auth/auth.module.ts | 4 + apps/bff/src/modules/auth/auth.service.ts | 2 +- .../guards/failed-login-throttle.guard.ts | 75 ++++ .../interceptors/login-result.interceptor.ts | 42 ++ .../modules/auth/services/token.service.ts | 2 +- .../workflows/whmcs-link-workflow.service.ts | 25 +- apps/portal/src/app/(public)/loading.tsx | 2 +- apps/portal/src/app/(public)/page.tsx | 2 +- apps/portal/src/app/page.tsx | 5 + .../templates/AuthLayout/AuthLayout.tsx | 14 +- .../auth/components/LoginForm/LoginForm.tsx | 8 +- .../PasswordResetForm/PasswordResetForm.tsx | 16 +- .../features/auth/utils/route-protection.ts | 4 +- .../src/features/auth/views/LinkWhmcsView.tsx | 2 +- .../features/auth/views/SetPasswordView.tsx | 11 +- .../src/features/auth/views/SignupView.tsx | 8 +- .../dashboard/hooks/useDashboardSummary.ts | 2 +- apps/portal/src/features/index.ts | 2 +- .../{marketing => landing-page}/index.ts | 0 .../views/PublicLandingLoadingView.tsx | 0 .../landing-page/views/PublicLandingView.tsx | 188 ++++++++ .../marketing/views/PublicLandingView.tsx | 412 +++++------------- packages/domain/src/entities/user.ts | 2 +- .../domain/src/validation/shared/entities.ts | 2 +- 28 files changed, 550 insertions(+), 361 deletions(-) create mode 100644 apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts create mode 100644 apps/bff/src/modules/auth/interceptors/login-result.interceptor.ts create mode 100644 apps/portal/src/app/page.tsx rename apps/portal/src/features/{marketing => landing-page}/index.ts (100%) rename apps/portal/src/features/{marketing => landing-page}/views/PublicLandingLoadingView.tsx (100%) create mode 100644 apps/portal/src/features/landing-page/views/PublicLandingView.tsx diff --git a/apps/bff/src/infra/utils/user-mapper.util.ts b/apps/bff/src/infra/utils/user-mapper.util.ts index 82f1ff5f..ea8db105 100644 --- a/apps/bff/src/infra/utils/user-mapper.util.ts +++ b/apps/bff/src/infra/utils/user-mapper.util.ts @@ -44,13 +44,12 @@ export function mapPrismaUserToEnhancedBase(user: PrismaUser): { export function mapPrismaUserToUserProfile(user: PrismaUser): AuthenticatedUser { const shared = mapPrismaUserToSharedUser(user); - const normalizedRole = user.role?.toLowerCase() === "admin" ? "admin" : "user"; return { ...shared, avatar: undefined, preferences: {}, lastLoginAt: user.lastLoginAt ? user.lastLoginAt.toISOString() : undefined, - role: normalizedRole, + role: user.role || "USER", // Return the actual Prisma enum value, default to USER }; } diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts index 7368b9f9..c91b3f26 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts @@ -237,17 +237,17 @@ export class WhmcsHttpClientService { } /** - * Parse WHMCS API response + * Parse WHMCS API response according to official documentation */ private parseResponse( responseText: string, action: string, params: Record ): WhmcsApiResponse { - let data: WhmcsApiResponse; + let parsedResponse: any; try { - data = JSON.parse(responseText) as WhmcsApiResponse; + parsedResponse = JSON.parse(responseText); } catch (parseError) { this.logger.error(`Invalid JSON response from WHMCS API [${action}]`, { responseText: responseText.substring(0, 500), @@ -257,14 +257,37 @@ export class WhmcsHttpClientService { throw new Error("Invalid JSON response from WHMCS API"); } - if (data.result === "error") { - const errorResponse = data as WhmcsErrorResponse; - throw new Error( - `WHMCS API Error: ${errorResponse.message} (${errorResponse.errorcode || "unknown"})` - ); + // Validate basic response structure + if (!parsedResponse || typeof parsedResponse !== 'object') { + this.logger.error(`WHMCS API returned invalid response structure [${action}]`, { + responseType: typeof parsedResponse, + responseText: responseText.substring(0, 500), + params: this.sanitizeLogParams(params), + }); + throw new Error("Invalid response structure from WHMCS API"); } - return data; + // Handle error responses according to WHMCS API documentation + if (parsedResponse.result === "error") { + const errorMessage = parsedResponse.message || parsedResponse.error || "Unknown WHMCS API error"; + const errorCode = parsedResponse.errorcode || "unknown"; + + this.logger.error(`WHMCS API returned error [${action}]`, { + errorMessage, + errorCode, + params: this.sanitizeLogParams(params), + }); + + throw new Error(`WHMCS API Error: ${errorMessage} (${errorCode})`); + } + + // For successful responses, WHMCS API returns data directly at the root level + // The response structure is: { "result": "success", ...actualData } + // We need to wrap this in our expected format + return { + result: "success", + data: parsedResponse as T + } as WhmcsApiResponse; } /** diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts index 09603c74..6868d0c2 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts @@ -59,7 +59,13 @@ export class WhmcsClientService { const response = await this.connectionService.getClientDetails(clientId); - if (!response.client) { + // According to WHMCS API documentation, successful responses have the client data directly + // The response structure is: { result: "success", client: {...}, ...otherFields } + if (!response || !response.client) { + this.logger.error(`WHMCS API did not return client data for client ID: ${clientId}`, { + hasResponse: !!response, + responseKeys: response ? Object.keys(response) : [], + }); throw new NotFoundException(`Client ${clientId} not found`); } @@ -83,7 +89,13 @@ export class WhmcsClientService { try { const response = await this.connectionService.getClientDetailsByEmail(email); - if (!response.client) { + // According to WHMCS API documentation, successful responses have the client data directly + // The response structure is: { result: "success", client: {...}, ...otherFields } + if (!response || !response.client) { + this.logger.error(`WHMCS API did not return client data for email: ${email}`, { + hasResponse: !!response, + responseKeys: response ? Object.keys(response) : [], + }); throw new NotFoundException(`Client with email ${email} not found`); } diff --git a/apps/bff/src/modules/auth/auth-zod.controller.ts b/apps/bff/src/modules/auth/auth-zod.controller.ts index 85ce0cc4..defd9d44 100644 --- a/apps/bff/src/modules/auth/auth-zod.controller.ts +++ b/apps/bff/src/modules/auth/auth-zod.controller.ts @@ -3,6 +3,7 @@ import { Post, Body, UseGuards, + UseInterceptors, Get, Req, HttpCode, @@ -14,6 +15,8 @@ import { Throttle } from "@nestjs/throttler"; import { AuthService } from "./auth.service"; import { LocalAuthGuard } from "./guards/local-auth.guard"; import { AuthThrottleGuard } from "./guards/auth-throttle.guard"; +import { FailedLoginThrottleGuard } from "./guards/failed-login-throttle.guard"; +import { LoginResultInterceptor } from "./interceptors/login-result.interceptor"; import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse } from "@nestjs/swagger"; import { Public } from "./decorators/public.decorator"; import { ZodValidationPipe } from "@bff/core/validation"; @@ -102,7 +105,7 @@ export class AuthController { @Public() @Post("validate-signup") @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 10, ttl: 900000 } }) // 10 validations per 15 minutes per IP + @Throttle({ default: { limit: 20, ttl: 600000 } }) // 20 validations per 10 minutes per IP @UsePipes(new ZodValidationPipe(validateSignupRequestSchema)) @ApiOperation({ summary: "Validate customer number for signup" }) @ApiResponse({ status: 200, description: "Validation successful" }) @@ -124,7 +127,7 @@ export class AuthController { @Public() @Post("signup-preflight") @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 10, ttl: 900000 } }) + @Throttle({ default: { limit: 20, ttl: 600000 } }) // 20 validations per 10 minutes per IP @UsePipes(new ZodValidationPipe(signupRequestSchema)) @HttpCode(200) @ApiOperation({ summary: "Validate full signup data without creating anything" }) @@ -145,7 +148,7 @@ export class AuthController { @Public() @Post("signup") @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 signups per 15 minutes per IP + @Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 signups per 15 minutes per IP (reasonable for account creation) @UsePipes(new ZodValidationPipe(signupRequestSchema)) @ApiOperation({ summary: "Create new user account" }) @ApiResponse({ status: 201, description: "User created successfully" }) @@ -162,8 +165,8 @@ export class AuthController { } @Public() - @UseGuards(LocalAuthGuard, AuthThrottleGuard) - @Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 login attempts per 15 minutes per IP+UA + @UseGuards(LocalAuthGuard, FailedLoginThrottleGuard) + @UseInterceptors(LoginResultInterceptor) @Post("login") @ApiOperation({ summary: "Authenticate user" }) @ApiResponse({ status: 200, description: "Login successful" }) @@ -216,7 +219,7 @@ export class AuthController { @Public() @Post("link-whmcs") @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 attempts per 15 minutes per IP + @Throttle({ default: { limit: 5, ttl: 600000 } }) // 5 attempts per 10 minutes per IP (industry standard) @UsePipes(new ZodValidationPipe(linkWhmcsRequestSchema)) @ApiOperation({ summary: "Link existing WHMCS user" }) @ApiResponse({ @@ -232,7 +235,7 @@ export class AuthController { @Public() @Post("set-password") @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 3, ttl: 300000 } }) // 3 attempts per 5 minutes per IP+UA + @Throttle({ default: { limit: 5, ttl: 600000 } }) // 5 attempts per 10 minutes per IP+UA (industry standard) @UsePipes(new ZodValidationPipe(setPasswordRequestSchema)) @ApiOperation({ summary: "Set password for linked user" }) @ApiResponse({ status: 200, description: "Password set successfully" }) @@ -260,7 +263,7 @@ export class AuthController { @Public() @Post("request-password-reset") - @Throttle({ default: { limit: 5, ttl: 900000 } }) + @Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 attempts per 15 minutes (standard for password operations) @UsePipes(new ZodValidationPipe(passwordResetRequestSchema)) @ApiOperation({ summary: "Request password reset email" }) @ApiResponse({ status: 200, description: "Reset email sent if account exists" }) @@ -271,7 +274,7 @@ export class AuthController { @Public() @Post("reset-password") - @Throttle({ default: { limit: 5, ttl: 900000 } }) + @Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 attempts per 15 minutes (standard for password operations) @UsePipes(new ZodValidationPipe(passwordResetSchema)) @ApiOperation({ summary: "Reset password with token" }) @ApiResponse({ status: 200, description: "Password reset successful" }) diff --git a/apps/bff/src/modules/auth/auth.module.ts b/apps/bff/src/modules/auth/auth.module.ts index 14834b62..2aa6522e 100644 --- a/apps/bff/src/modules/auth/auth.module.ts +++ b/apps/bff/src/modules/auth/auth.module.ts @@ -19,6 +19,8 @@ import { TokenMigrationService } from "./services/token-migration.service"; import { SignupWorkflowService } from "./services/workflows/signup-workflow.service"; import { PasswordWorkflowService } from "./services/workflows/password-workflow.service"; import { WhmcsLinkWorkflowService } from "./services/workflows/whmcs-link-workflow.service"; +import { FailedLoginThrottleGuard } from "./guards/failed-login-throttle.guard"; +import { LoginResultInterceptor } from "./interceptors/login-result.interceptor"; @Module({ imports: [ @@ -46,6 +48,8 @@ import { WhmcsLinkWorkflowService } from "./services/workflows/whmcs-link-workfl SignupWorkflowService, PasswordWorkflowService, WhmcsLinkWorkflowService, + FailedLoginThrottleGuard, + LoginResultInterceptor, { provide: APP_GUARD, useClass: GlobalAuthGuard, diff --git a/apps/bff/src/modules/auth/auth.service.ts b/apps/bff/src/modules/auth/auth.service.ts index 01b1ebeb..2aded862 100644 --- a/apps/bff/src/modules/auth/auth.service.ts +++ b/apps/bff/src/modules/auth/auth.service.ts @@ -140,7 +140,7 @@ export class AuthService { { id: profile.id, email: profile.email, - role: prismaUser.role, + role: prismaUser.role || "USER", }, { userAgent: request?.headers["user-agent"], diff --git a/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts b/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts new file mode 100644 index 00000000..baf5cc26 --- /dev/null +++ b/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts @@ -0,0 +1,75 @@ +import { Injectable, ExecutionContext, Inject } from "@nestjs/common"; +import { ThrottlerException } from "@nestjs/throttler"; +import { Redis } from "ioredis"; +import { createHash } from "crypto"; +import type { Request } from "express"; + +@Injectable() +export class FailedLoginThrottleGuard { + constructor(@Inject("REDIS") private readonly redis: Redis) {} + + private getTracker(req: Request): string { + // Track by IP address + User Agent for failed login attempts only + const forwarded = req.headers["x-forwarded-for"]; + const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded; + const ip = + (typeof forwardedIp === "string" ? forwardedIp.split(",")[0]?.trim() : undefined) || + (req.headers["x-real-ip"] as string | undefined) || + req.socket?.remoteAddress || + req.ip || + "unknown"; + + const userAgent = req.headers["user-agent"] || "unknown"; + const userAgentHash = createHash("sha256").update(userAgent).digest("hex").slice(0, 16); + + const normalizedIp = ip.replace(/^::ffff:/, ""); + + return `failed_login_${normalizedIp}_${userAgentHash}`; + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const tracker = this.getTracker(request); + + // Get throttle configuration (10 attempts per 10 minutes) + const limit = 10; + const ttlSeconds = 600; // 10 minutes in seconds + + // Check current failed attempts count + const currentCount = await this.redis.get(tracker); + const attempts = currentCount ? parseInt(currentCount, 10) : 0; + + // If over limit, block the request with remaining time info + if (attempts >= limit) { + const ttl = await this.redis.ttl(tracker); + const remainingTime = Math.max(ttl, 0); + const minutes = Math.floor(remainingTime / 60); + const seconds = remainingTime % 60; + const timeMessage = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; + + throw new ThrottlerException(`Too many failed login attempts. Try again in ${timeMessage}.`); + } + + // Increment counter for this attempt + await this.redis.incr(tracker); + await this.redis.expire(tracker, ttlSeconds); + + // Store tracker info for post-processing + (request as any).__failedLoginTracker = tracker; + + return true; + } + + // Method to be called after login attempt to handle success/failure + async handleLoginResult(request: Request, wasSuccessful: boolean): Promise { + const tracker = (request as any).__failedLoginTracker; + + if (!tracker) return; + + if (wasSuccessful) { + // Reset failed attempts counter on successful login + await this.redis.del(tracker); + } + // For failed logins, we keep the incremented counter + } +} diff --git a/apps/bff/src/modules/auth/interceptors/login-result.interceptor.ts b/apps/bff/src/modules/auth/interceptors/login-result.interceptor.ts new file mode 100644 index 00000000..ca31a3f9 --- /dev/null +++ b/apps/bff/src/modules/auth/interceptors/login-result.interceptor.ts @@ -0,0 +1,42 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + UnauthorizedException, +} from "@nestjs/common"; +import { Observable, throwError } from "rxjs"; +import { tap, catchError } from "rxjs/operators"; +import { FailedLoginThrottleGuard } from "../guards/failed-login-throttle.guard"; +import type { Request } from "express"; + +@Injectable() +export class LoginResultInterceptor implements NestInterceptor { + constructor(private readonly failedLoginGuard: FailedLoginThrottleGuard) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + + return next.handle().pipe( + tap(async (result) => { + // Login was successful + await this.failedLoginGuard.handleLoginResult(request, true); + }), + catchError(async (error) => { + // Check if this is an authentication error (failed login) + const isAuthError = + error instanceof UnauthorizedException || + error?.status === 401 || + error?.message?.toLowerCase().includes('invalid') || + error?.message?.toLowerCase().includes('unauthorized'); + + if (isAuthError) { + // Login failed - keep the failed attempt count + await this.failedLoginGuard.handleLoginResult(request, false); + } + + return throwError(() => error); + }) + ); + } +} diff --git a/apps/bff/src/modules/auth/services/token.service.ts b/apps/bff/src/modules/auth/services/token.service.ts index e312d71d..97f25470 100644 --- a/apps/bff/src/modules/auth/services/token.service.ts +++ b/apps/bff/src/modules/auth/services/token.service.ts @@ -268,7 +268,7 @@ export class AuthTokenService { const user = { id: prismaUser.id, email: prismaUser.email, - role: prismaUser.role, + role: prismaUser.role || "USER", }; // Invalidate current refresh token diff --git a/apps/bff/src/modules/auth/services/workflows/whmcs-link-workflow.service.ts b/apps/bff/src/modules/auth/services/workflows/whmcs-link-workflow.service.ts index f9ed93ac..ce299903 100644 --- a/apps/bff/src/modules/auth/services/workflows/whmcs-link-workflow.service.ts +++ b/apps/bff/src/modules/auth/services/workflows/whmcs-link-workflow.service.ts @@ -48,7 +48,16 @@ export class WhmcsLinkWorkflowService { try { clientDetails = await this.whmcsService.getClientDetailsByEmail(email); } catch (error) { - this.logger.error("WHMCS client lookup failed", { error: getErrorMessage(error) }); + this.logger.error("WHMCS client lookup failed", { + error: getErrorMessage(error), + email: email // Safe to log email for debugging since it's not sensitive + }); + + // Provide more specific error messages based on the error type + if (error instanceof Error && error.message.includes('not found')) { + throw new UnauthorizedException("No billing account found with this email address. Please check your email or contact support."); + } + throw new UnauthorizedException("Unable to verify account. Please try again later."); } @@ -70,7 +79,19 @@ export class WhmcsLinkWorkflowService { } } catch (error) { if (error instanceof UnauthorizedException) throw error; - this.logger.error("WHMCS credential validation failed", { error: getErrorMessage(error) }); + + const errorMessage = getErrorMessage(error); + this.logger.error("WHMCS credential validation failed", { error: errorMessage }); + + // Check if this is a WHMCS authentication error and provide user-friendly message + if (errorMessage.toLowerCase().includes('email or password invalid') || + errorMessage.toLowerCase().includes('invalid email or password') || + errorMessage.toLowerCase().includes('authentication failed') || + errorMessage.toLowerCase().includes('login failed')) { + throw new UnauthorizedException("Invalid email or password. Please check your credentials and try again."); + } + + // For other errors, provide generic message to avoid exposing system details throw new UnauthorizedException("Unable to verify credentials. Please try again later."); } diff --git a/apps/portal/src/app/(public)/loading.tsx b/apps/portal/src/app/(public)/loading.tsx index 51bac016..26cd4573 100644 --- a/apps/portal/src/app/(public)/loading.tsx +++ b/apps/portal/src/app/(public)/loading.tsx @@ -1,4 +1,4 @@ -import { PublicLandingLoadingView } from "@/features/marketing"; +import { PublicLandingLoadingView } from "@/features/landing-page"; export default function PublicHomeLoading() { return ; diff --git a/apps/portal/src/app/(public)/page.tsx b/apps/portal/src/app/(public)/page.tsx index f54fd694..bae0be3d 100644 --- a/apps/portal/src/app/(public)/page.tsx +++ b/apps/portal/src/app/(public)/page.tsx @@ -1,4 +1,4 @@ -import { PublicLandingView } from "@/features/marketing"; +import { PublicLandingView } from "@/features/landing-page"; export default function PublicHomePage() { return ; diff --git a/apps/portal/src/app/page.tsx b/apps/portal/src/app/page.tsx new file mode 100644 index 00000000..4eedc930 --- /dev/null +++ b/apps/portal/src/app/page.tsx @@ -0,0 +1,5 @@ +import { PublicLandingView } from "@/features/landing-page"; + +export default function RootPage() { + return ; +} diff --git a/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx b/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx index 36a78ed4..717fdae3 100644 --- a/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx +++ b/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { Logo } from "@/components/atoms/logo"; export interface AuthLayoutProps { children: React.ReactNode; @@ -27,7 +28,7 @@ export function AuthLayout({
{backLabel} @@ -36,13 +37,16 @@ export function AuthLayout({ )}
-

{title}

- {subtitle &&

{subtitle}

} +
+ +
+

{title}

+ {subtitle &&

{subtitle}

}
-
-
+
+
{children}
diff --git a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx index a7f07a7a..ee12a338 100644 --- a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx +++ b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx @@ -100,9 +100,9 @@ export function LoginForm({ checked={values.rememberMe} onChange={e => setValue("rememberMe", e.target.checked)} disabled={isSubmitting || loading} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded transition-colors" /> -
@@ -111,7 +111,7 @@ export function LoginForm({
Forgot your password? @@ -138,7 +138,7 @@ export function LoginForm({ Don't have an account?{" "} Sign up diff --git a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx index 03c1127e..3b7be341 100644 --- a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx +++ b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx @@ -106,13 +106,6 @@ export function PasswordResetForm({ if (mode === "request") { return (
-
-

Reset your password

-

- Enter your email address and we'll send you a link to reset your password. -

-
-
void requestForm.handleSubmit(event)} className="space-y-4"> - + Back to login
@@ -152,11 +145,6 @@ export function PasswordResetForm({ // Reset mode return (
-
-

Set new password

-

Enter your new password below.

-
- void resetForm.handleSubmit(event)} className="space-y-4"> - + Back to login
diff --git a/apps/portal/src/features/auth/utils/route-protection.ts b/apps/portal/src/features/auth/utils/route-protection.ts index 51d44960..33408eac 100644 --- a/apps/portal/src/features/auth/utils/route-protection.ts +++ b/apps/portal/src/features/auth/utils/route-protection.ts @@ -1,8 +1,8 @@ import type { ReadonlyURLSearchParams } from "next/navigation"; export function getPostLoginRedirect(searchParams: ReadonlyURLSearchParams): string { - const dest = searchParams.get("redirect") || "/"; + const dest = searchParams.get("redirect") || "/dashboard"; // prevent open redirects - if (dest.startsWith("http://") || dest.startsWith("https://")) return "/"; + if (dest.startsWith("http://") || dest.startsWith("https://")) return "/dashboard"; return dest; } diff --git a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx index 7612b33f..b904d391 100644 --- a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx +++ b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx @@ -14,7 +14,7 @@ export function LinkWhmcsView() { subtitle="Move your existing Assist Solutions account to our new portal" >
-
+
diff --git a/apps/portal/src/features/auth/views/SetPasswordView.tsx b/apps/portal/src/features/auth/views/SetPasswordView.tsx index 3dd61210..b39a1bd3 100644 --- a/apps/portal/src/features/auth/views/SetPasswordView.tsx +++ b/apps/portal/src/features/auth/views/SetPasswordView.tsx @@ -17,6 +17,11 @@ function SetPasswordContent() { } }, [email, router]); + const handlePasswordSetSuccess = () => { + // Redirect to dashboard after successful password setup + router.push("/dashboard"); + }; + if (!email) { return ( @@ -41,8 +46,8 @@ function SetPasswordContent() { title="Create your new portal password" subtitle="Complete your account transfer with a secure password" > -
-
+
+
@@ -62,7 +67,7 @@ function SetPasswordContent() {
- +

What happens next?

diff --git a/apps/portal/src/features/auth/views/SignupView.tsx b/apps/portal/src/features/auth/views/SignupView.tsx index 148b1c2b..a5f52d90 100644 --- a/apps/portal/src/features/auth/views/SignupView.tsx +++ b/apps/portal/src/features/auth/views/SignupView.tsx @@ -9,10 +9,10 @@ export function SignupView() { title="Create your portal account" subtitle="Verify your details and set up secure access in a few guided steps" > -
-
-

What you'll need

-
    +
    +
    +

    What you'll need

    +
    • Your Assist Solutions customer number
    • Primary contact details and service address
    • A secure password that meets our enhanced requirements
    • diff --git a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts index 0f514f9b..0e45dd6b 100644 --- a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts +++ b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts @@ -36,7 +36,7 @@ export function useDashboardSummary() { } try { - const response = await apiClient.GET("/dashboard/summary"); + const response = await apiClient.GET("/api/users/summary"); return getDataOrThrow(response, "Dashboard summary response was empty"); } catch (error) { // Transform API errors to DashboardError format diff --git a/apps/portal/src/features/index.ts b/apps/portal/src/features/index.ts index bcc4ac7c..6fe24a95 100644 --- a/apps/portal/src/features/index.ts +++ b/apps/portal/src/features/index.ts @@ -1,5 +1,5 @@ export * as billing from "./billing"; export * as subscriptions from "./subscriptions"; export * as dashboard from "./dashboard"; -export * as marketing from "./marketing"; +export * as landingPage from "./landing-page"; export * as support from "./support"; diff --git a/apps/portal/src/features/marketing/index.ts b/apps/portal/src/features/landing-page/index.ts similarity index 100% rename from apps/portal/src/features/marketing/index.ts rename to apps/portal/src/features/landing-page/index.ts diff --git a/apps/portal/src/features/marketing/views/PublicLandingLoadingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingLoadingView.tsx similarity index 100% rename from apps/portal/src/features/marketing/views/PublicLandingLoadingView.tsx rename to apps/portal/src/features/landing-page/views/PublicLandingLoadingView.tsx diff --git a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx new file mode 100644 index 00000000..b2d8a8fc --- /dev/null +++ b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx @@ -0,0 +1,188 @@ +import Link from "next/link"; +import { Logo } from "@/components/atoms/logo"; +import { + ArrowPathIcon, + UserIcon, + SparklesIcon, + CreditCardIcon, + Cog6ToothIcon, + PhoneIcon, + ChartBarIcon, +} from "@heroicons/react/24/outline"; + +export function PublicLandingView() { + return ( +
      + {/* Header */} +
      +
      +
      +
      + + Assist Solutions +
      + +
      + + Login + +
      +
      +
      +
      + + {/* Hero Section */} +
      + {/* Subtle background decoration */} +
      +
      +
      +
      + +
      +

      + Customer Portal +

      +

      + Manage your services, billing, and support in one place +

      +
      +
      + + {/* Access Options */} +
      +
      +
      + {/* Existing Users */} +
      +
      +
      + +
      +

      Existing Customers

      +

      + Access your account or migrate from the old system +

      +
      + + Login to Portal + + + Migrate Account + +
      +
      +
      + + {/* New Users */} +
      +
      +
      + +
      +

      New Customers

      +

      + Create an account to get started +

      + + Create Account + +
      +
      +
      +
      +
      + + {/* Key Features */} +
      +
      +
      +

      Everything you need

      +
      + +
      +
      +
      + +
      +

      Billing

      +

      Automated invoicing and payments

      +
      + +
      +
      + +
      +

      Services

      +

      Manage all your subscriptions

      +
      + +
      +
      + +
      +

      Support

      +

      Get help when you need it

      +
      +
      +
      +
      + + {/* Support Section */} +
      +
      +

      Need help?

      +

      + Our support team is here to assist you with any questions +

      + + Contact Support + +
      +
      + + {/* Footer */} +
      +
      +
      +
      + + Assist Solutions +
      + +
      + + Support + + + Privacy + + + Terms + +
      +
      + +
      +

      © {new Date().getFullYear()} Assist Solutions. All rights reserved.

      +
      +
      +
      +
      + ); +} diff --git a/apps/portal/src/features/marketing/views/PublicLandingView.tsx b/apps/portal/src/features/marketing/views/PublicLandingView.tsx index 005bac69..b2d8a8fc 100644 --- a/apps/portal/src/features/marketing/views/PublicLandingView.tsx +++ b/apps/portal/src/features/marketing/views/PublicLandingView.tsx @@ -12,354 +12,174 @@ import { export function PublicLandingView() { return ( -
      +
      {/* Header */} -
      -
      +
      +
      - -
      -

      Assist Solutions

      -

      Customer Portal

      -
      + + Assist Solutions
      -
      +
      Login - - Support -
      {/* Hero Section */} -
      - {/* Abstract background elements */} -
      -
      -
      -
      +
      + {/* Subtle background decoration */} +
      +
      +
      - -
      -
      -

      - New Assist Solutions Customer Portal -

      - - -
      + +
      +

      + Customer Portal +

      +

      + Manage your services, billing, and support in one place +

      - {/* Customer Portal Access Section */} -
      -
      -
      -

      Access Your Portal

      -

      Choose the option that applies to you

      -
      - -
      - {/* Existing Customers - Migration */} -
      -
      -
      - -
      -

      Existing Customers

      -

      - Migrate to our new portal and enjoy enhanced security with modern interface. -

      - -
      - - Migrate Your Account - -

      Takes just a few minutes

      -
      -
      -
      - - {/* Portal Users */} -
      -
      -
      + {/* Access Options */} +
      +
      +
      + {/* Existing Users */} +
      +
      +
      -

      Portal Users

      -

      - Sign in to access your dashboard and manage all your services efficiently. +

      Existing Customers

      +

      + Access your account or migrate from the old system

      - -
      +
      Login to Portal -

      Secure access to your account

      + + Migrate Account +
      - {/* New Customers */} -
      -
      -
      + {/* New Users */} +
      +
      +
      -

      New Customers

      -

      - Create your account and access our full range of IT solutions and services. +

      New Customers

      +

      + Create an account to get started

      - -
      - - Create Account - -

      Start your journey with us

      -
      -
      -
      -
      -
      -
      - - {/* Portal Features Section */} -
      -
      -
      -

      Why Choose Assist Solutions

      -

      - Modern tools to manage your IT services with confidence -

      -
      - -
      -
      -
      - -
      -

      Automated Billing

      -

      - Transparent invoicing, automated payments, and flexible billing options tailored to - your business. -

      -
      - -
      -
      - -
      -

      Service Management

      -

      - Control subscriptions, manage network services, and track usage from a single pane - of glass. -

      -
      - -
      -
      - -
      -

      Expert Support

      -

      - Dedicated support team with SLA-backed response times and proactive service - monitoring. -

      -
      - -
      -
      - -
      -

      Actionable Insights

      -

      - Real-time analytics and reporting to help you optimize resource usage and forecast - demand. -

      -
      - -
      -
      - -
      -

      Seamless Migration

      -

      - Guided onboarding for WHMCS customers with automatic data migration and validation. -

      -
      - -
      -
      - -
      -

      Future-Proof Platform

      -

      - Built on modern infrastructure with continuous updates, security patches, and new - features. -

      -
      -
      -
      -
      - - {/* CTA Section */} -
      -
      -
      -
      -

      - Ready to experience the new portal? -

      -

      - Join thousands of customers who trust Assist Solutions to keep their business - connected and secure. -

      -
      - Create an Account - - - Portal Login + Create Account
      +
      +
      +
      -
      -

      What’s included?

      -
        -
      • - - Centralized service and subscription management dashboard -
      • -
      • - - Automated billing with support for multiple payment methods -
      • -
      • - - Priority access to our customer support specialists -
      • -
      • - - Insights and analytics to track usage and growth -
      • -
      + {/* Key Features */} +
      +
      +
      +

      Everything you need

      +
      + +
      +
      +
      + +
      +

      Billing

      +

      Automated invoicing and payments

      +
      + +
      +
      + +
      +

      Services

      +

      Manage all your subscriptions

      +
      + +
      +
      + +
      +

      Support

      +

      Get help when you need it

      + {/* Support Section */} +
      +
      +

      Need help?

      +

      + Our support team is here to assist you with any questions +

      + + Contact Support + +
      +
      + {/* Footer */} -