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 */} -