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.

This commit is contained in:
barsa 2025-09-27 17:51:54 +09:00
parent e339f20ef5
commit 2b54773ebf
28 changed files with 550 additions and 361 deletions

View File

@ -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
};
}

View File

@ -237,17 +237,17 @@ export class WhmcsHttpClientService {
}
/**
* Parse WHMCS API response
* Parse WHMCS API response according to official documentation
*/
private parseResponse<T>(
responseText: string,
action: string,
params: Record<string, unknown>
): WhmcsApiResponse<T> {
let data: WhmcsApiResponse<T>;
let parsedResponse: any;
try {
data = JSON.parse(responseText) as WhmcsApiResponse<T>;
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<T>;
}
/**

View File

@ -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`);
}

View File

@ -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" })

View File

@ -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,

View File

@ -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"],

View File

@ -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<boolean> {
const request = context.switchToHttp().getRequest<Request>();
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<void> {
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
}
}

View File

@ -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<any> {
const request = context.switchToHttp().getRequest<Request>();
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);
})
);
}
}

View File

@ -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

View File

@ -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.");
}

View File

@ -1,4 +1,4 @@
import { PublicLandingLoadingView } from "@/features/marketing";
import { PublicLandingLoadingView } from "@/features/landing-page";
export default function PublicHomeLoading() {
return <PublicLandingLoadingView />;

View File

@ -1,4 +1,4 @@
import { PublicLandingView } from "@/features/marketing";
import { PublicLandingView } from "@/features/landing-page";
export default function PublicHomePage() {
return <PublicLandingView />;

View File

@ -0,0 +1,5 @@
import { PublicLandingView } from "@/features/landing-page";
export default function RootPage() {
return <PublicLandingView />;
}

View File

@ -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({
<div className="mb-6">
<Link
href={backHref}
className="inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 transition-colors"
className="inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 transition-colors duration-200"
>
<ArrowLeftIcon className="h-4 w-4 mr-2" />
{backLabel}
@ -36,13 +37,16 @@ export function AuthLayout({
)}
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight text-gray-900">{title}</h2>
{subtitle && <p className="mt-2 text-sm text-gray-600">{subtitle}</p>}
<div className="flex justify-center mb-8">
<Logo size={72} className="text-blue-600" />
</div>
<h1 className="text-3xl font-bold tracking-tight text-gray-900 mb-3">{title}</h1>
{subtitle && <p className="text-base text-gray-600 leading-relaxed">{subtitle}</p>}
</div>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow-xl rounded-2xl border border-gray-100 sm:px-10">
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-10 px-6 shadow-2xl rounded-3xl border border-gray-100 sm:px-12">
{children}
</div>
</div>

View File

@ -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"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
Remember me
</label>
</div>
@ -111,7 +111,7 @@ export function LoginForm({
<div className="text-sm">
<Link
href="/auth/forgot-password"
className="font-medium text-blue-600 hover:text-blue-500 transition-colors"
className="font-medium text-blue-600 hover:text-blue-500 transition-colors duration-200"
>
Forgot your password?
</Link>
@ -138,7 +138,7 @@ export function LoginForm({
Don&apos;t have an account?{" "}
<Link
href="/auth/signup"
className="font-medium text-blue-600 hover:text-blue-500 transition-colors"
className="font-medium text-blue-600 hover:text-blue-500 transition-colors duration-200"
>
Sign up
</Link>

View File

@ -106,13 +106,6 @@ export function PasswordResetForm({
if (mode === "request") {
return (
<div className={`space-y-6 ${className}`}>
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900">Reset your password</h2>
<p className="mt-2 text-sm text-gray-600">
Enter your email address and we&apos;ll send you a link to reset your password.
</p>
</div>
<form onSubmit={event => void requestForm.handleSubmit(event)} className="space-y-4">
<FormField label="Email address" error={requestForm.errors.email} required>
<Input
@ -140,7 +133,7 @@ export function PasswordResetForm({
{showLoginLink && (
<div className="text-center">
<Link href="/login" className="text-sm text-blue-600 hover:text-blue-500 font-medium">
<Link href="/auth/login" className="text-sm text-blue-600 hover:text-blue-500 font-medium transition-colors duration-200">
Back to login
</Link>
</div>
@ -152,11 +145,6 @@ export function PasswordResetForm({
// Reset mode
return (
<div className={`space-y-6 ${className}`}>
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900">Set new password</h2>
<p className="mt-2 text-sm text-gray-600">Enter your new password below.</p>
</div>
<form onSubmit={event => void resetForm.handleSubmit(event)} className="space-y-4">
<FormField label="New password" error={resetForm.errors.password} required>
<Input
@ -196,7 +184,7 @@ export function PasswordResetForm({
{showLoginLink && (
<div className="text-center">
<Link href="/login" className="text-sm text-blue-600 hover:text-blue-500 font-medium">
<Link href="/auth/login" className="text-sm text-blue-600 hover:text-blue-500 font-medium transition-colors duration-200">
Back to login
</Link>
</div>

View File

@ -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;
}

View File

@ -14,7 +14,7 @@ export function LinkWhmcsView() {
subtitle="Move your existing Assist Solutions account to our new portal"
>
<div className="space-y-8">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="p-5 bg-blue-50 border border-blue-200 rounded-xl">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-blue-500" viewBox="0 0 20 20" fill="currentColor">

View File

@ -17,6 +17,11 @@ function SetPasswordContent() {
}
}, [email, router]);
const handlePasswordSetSuccess = () => {
// Redirect to dashboard after successful password setup
router.push("/dashboard");
};
if (!email) {
return (
<AuthLayout title="Set password" subtitle="Redirecting you to the account transfer flow">
@ -41,8 +46,8 @@ function SetPasswordContent() {
title="Create your new portal password"
subtitle="Complete your account transfer with a secure password"
>
<div className="space-y-6">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="space-y-8">
<div className="p-5 bg-green-50 border border-green-200 rounded-xl">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor">
@ -62,7 +67,7 @@ function SetPasswordContent() {
</div>
</div>
<SetPasswordForm email={email} />
<SetPasswordForm email={email} onSuccess={handlePasswordSetSuccess} />
<div className="pt-6 border-t border-gray-200 space-y-2 text-sm text-gray-600">
<h3 className="font-medium text-gray-900">What happens next?</h3>

View File

@ -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"
>
<div className="space-y-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h2 className="text-sm font-semibold text-blue-800 mb-2">What you&apos;ll need</h2>
<ul className="text-sm text-blue-700 space-y-1 list-disc list-inside">
<div className="space-y-8">
<div className="bg-blue-50 border border-blue-200 rounded-xl p-5">
<h2 className="text-sm font-semibold text-blue-800 mb-3">What you&apos;ll need</h2>
<ul className="text-sm text-blue-700 space-y-2 list-disc list-inside">
<li>Your Assist Solutions customer number</li>
<li>Primary contact details and service address</li>
<li>A secure password that meets our enhanced requirements</li>

View File

@ -36,7 +36,7 @@ export function useDashboardSummary() {
}
try {
const response = await apiClient.GET<DashboardSummary>("/dashboard/summary");
const response = await apiClient.GET<DashboardSummary>("/api/users/summary");
return getDataOrThrow<DashboardSummary>(response, "Dashboard summary response was empty");
} catch (error) {
// Transform API errors to DashboardError format

View File

@ -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";

View File

@ -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 (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 via-indigo-50 to-purple-100">
{/* Header */}
<header className="bg-gradient-to-r from-white/90 via-blue-50/90 to-indigo-50/90 backdrop-blur-sm border-b border-indigo-100/50">
<div className="max-w-6xl mx-auto px-6">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-3">
<Logo size={32} />
<span className="text-lg font-semibold text-gray-900">Assist Solutions</span>
</div>
<div className="flex items-center space-x-4">
<Link
href="/auth/login"
className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-2.5 rounded-lg hover:from-blue-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Login
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="py-24 relative overflow-hidden">
{/* Subtle background decoration */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-32 w-96 h-96 bg-gradient-to-br from-blue-400/20 to-indigo-600/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-32 w-96 h-96 bg-gradient-to-tr from-indigo-400/20 to-purple-600/20 rounded-full blur-3xl"></div>
</div>
<div className="max-w-4xl mx-auto px-6 text-center relative">
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-gray-900 via-blue-900 to-indigo-900 bg-clip-text text-transparent mb-6">
Customer Portal
</h1>
<p className="text-xl text-gray-700 mb-12">
Manage your services, billing, and support in one place
</p>
</div>
</section>
{/* Access Options */}
<section className="py-16 bg-gradient-to-r from-blue-50/80 via-indigo-50/80 to-purple-50/80 backdrop-blur-sm">
<div className="max-w-4xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Existing Users */}
<div className="bg-white/90 backdrop-blur-sm rounded-xl p-8 border border-white/50 shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
<div className="text-center">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
<UserIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Existing Customers</h3>
<p className="text-gray-600 mb-6">
Access your account or migrate from the old system
</p>
<div className="space-y-3">
<Link
href="/auth/login"
className="block bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-3 rounded-lg hover:from-blue-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Login to Portal
</Link>
<Link
href="/auth/link-whmcs"
className="block border-2 border-gray-200 text-gray-700 px-6 py-3 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all duration-200"
>
Migrate Account
</Link>
</div>
</div>
</div>
{/* New Users */}
<div className="bg-white/90 backdrop-blur-sm rounded-xl p-8 border border-white/50 shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
<div className="text-center">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
<SparklesIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">New Customers</h3>
<p className="text-gray-600 mb-6">
Create an account to get started
</p>
<Link
href="/auth/signup"
className="block bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-6 py-3 rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Create Account
</Link>
</div>
</div>
</div>
</div>
</section>
{/* Key Features */}
<section className="py-16 bg-gradient-to-r from-indigo-50/80 via-purple-50/80 to-pink-50/80">
<div className="max-w-4xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="text-2xl font-bold bg-gradient-to-r from-gray-900 to-purple-900 bg-clip-text text-transparent mb-3">Everything you need</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
<CreditCardIcon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">Billing</h3>
<p className="text-sm text-gray-600">Automated invoicing and payments</p>
</div>
<div className="text-center bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Cog6ToothIcon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">Services</h3>
<p className="text-sm text-gray-600">Manage all your subscriptions</p>
</div>
<div className="text-center bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
<PhoneIcon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">Support</h3>
<p className="text-sm text-gray-600">Get help when you need it</p>
</div>
</div>
</div>
</section>
{/* Support Section */}
<section className="py-16 bg-gradient-to-br from-purple-50/80 via-pink-50/80 to-rose-50/80">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="text-2xl font-bold bg-gradient-to-r from-gray-900 to-indigo-900 bg-clip-text text-transparent mb-4">Need help?</h2>
<p className="text-gray-700 mb-8">
Our support team is here to assist you with any questions
</p>
<Link
href="/support"
className="inline-block bg-white/80 backdrop-blur-sm border-2 border-purple-200 text-purple-700 px-8 py-3 rounded-lg hover:bg-purple-50 hover:border-purple-300 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Contact Support
</Link>
</div>
</section>
{/* Footer */}
<footer className="bg-gradient-to-r from-gray-900 via-indigo-900 to-purple-900 text-gray-300 py-8">
<div className="max-w-4xl mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex items-center space-x-3">
<Logo size={24} />
<span className="text-white font-medium">Assist Solutions</span>
</div>
<div className="flex gap-6 text-sm">
<Link href="/support" className="hover:text-white transition-colors duration-200">
Support
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Privacy
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Terms
</Link>
</div>
</div>
<div className="mt-6 pt-6 border-t border-gray-700/50 text-center text-sm">
<p>&copy; {new Date().getFullYear()} Assist Solutions. All rights reserved.</p>
</div>
</div>
</footer>
</div>
);
}

View File

@ -12,354 +12,174 @@ import {
export function PublicLandingView() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 via-indigo-50 to-purple-100">
{/* Header */}
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<header className="bg-gradient-to-r from-white/90 via-blue-50/90 to-indigo-50/90 backdrop-blur-sm border-b border-indigo-100/50">
<div className="max-w-6xl mx-auto px-6">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-3">
<Logo size={40} />
<div>
<h1 className="text-xl font-semibold text-gray-900">Assist Solutions</h1>
<p className="text-xs text-gray-500">Customer Portal</p>
</div>
<Logo size={32} />
<span className="text-lg font-semibold text-gray-900">Assist Solutions</span>
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-4">
<Link
href="/auth/login"
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium text-sm"
className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-2.5 rounded-lg hover:from-blue-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Login
</Link>
<Link
href="/support"
className="text-gray-600 hover:text-gray-900 px-4 py-2 text-sm transition-colors duration-200"
>
Support
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="relative py-20 overflow-hidden">
{/* Abstract background elements */}
<div className="absolute inset-0">
<div className="absolute top-20 left-10 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
<div
className="absolute top-40 right-10 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
style={{ animationDelay: "2s" }}
></div>
<div
className="absolute -bottom-8 left-20 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
style={{ animationDelay: "4s" }}
></div>
<section className="py-24 relative overflow-hidden">
{/* Subtle background decoration */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-32 w-96 h-96 bg-gradient-to-br from-blue-400/20 to-indigo-600/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-32 w-96 h-96 bg-gradient-to-tr from-indigo-400/20 to-purple-600/20 rounded-full blur-3xl"></div>
</div>
<div className="relative max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 mb-[var(--cp-space-2xl)]">
New Assist Solutions Customer Portal
</h1>
<div className="flex justify-center">
<a
href="#portal-access"
className="bg-blue-600 text-white px-10 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1"
>
Get Started
</a>
</div>
</div>
<div className="max-w-4xl mx-auto px-6 text-center relative">
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-gray-900 via-blue-900 to-indigo-900 bg-clip-text text-transparent mb-6">
Customer Portal
</h1>
<p className="text-xl text-gray-700 mb-12">
Manage your services, billing, and support in one place
</p>
</div>
</section>
{/* Customer Portal Access Section */}
<section id="portal-access" className="py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Access Your Portal</h2>
<p className="text-lg text-gray-600">Choose the option that applies to you</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)] items-stretch">
{/* Existing Customers - Migration */}
<div className="bg-white rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-all duration-[var(--cp-transition-slow)] transform hover:-translate-y-2 border-[var(--cp-card-border)]">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
<ArrowPathIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Existing Customers</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Migrate to our new portal and enjoy enhanced security with modern interface.
</p>
<div className="mt-auto">
<Link
href="/auth/link-whmcs"
className="block bg-blue-600 text-white px-8 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg mb-3 shadow-md hover:shadow-lg"
>
Migrate Your Account
</Link>
<p className="text-sm text-gray-500">Takes just a few minutes</p>
</div>
</div>
</div>
{/* Portal Users */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
{/* Access Options */}
<section className="py-16 bg-gradient-to-r from-blue-50/80 via-indigo-50/80 to-purple-50/80 backdrop-blur-sm">
<div className="max-w-4xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Existing Users */}
<div className="bg-white/90 backdrop-blur-sm rounded-xl p-8 border border-white/50 shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
<div className="text-center">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
<UserIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Portal Users</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Sign in to access your dashboard and manage all your services efficiently.
<h3 className="text-xl font-semibold text-gray-900 mb-3">Existing Customers</h3>
<p className="text-gray-600 mb-6">
Access your account or migrate from the old system
</p>
<div className="mt-auto">
<div className="space-y-3">
<Link
href="/auth/login"
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
className="block bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-3 rounded-lg hover:from-blue-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Login to Portal
</Link>
<p className="text-sm text-gray-500">Secure access to your account</p>
<Link
href="/auth/link-whmcs"
className="block border-2 border-gray-200 text-gray-700 px-6 py-3 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all duration-200"
>
Migrate Account
</Link>
</div>
</div>
</div>
{/* New Customers */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
<div className="text-center flex-1 flex flex-col">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
{/* New Users */}
<div className="bg-white/90 backdrop-blur-sm rounded-xl p-8 border border-white/50 shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
<div className="text-center">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
<SparklesIcon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">New Customers</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Create your account and access our full range of IT solutions and services.
<h3 className="text-xl font-semibold text-gray-900 mb-3">New Customers</h3>
<p className="text-gray-600 mb-6">
Create an account to get started
</p>
<div className="mt-auto">
<Link
href="/auth/signup"
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
>
Create Account
</Link>
<p className="text-sm text-gray-500">Start your journey with us</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Portal Features Section */}
<section className="bg-white py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Why Choose Assist Solutions</h2>
<p className="text-lg text-gray-600">
Modern tools to manage your IT services with confidence
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)]">
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<CreditCardIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Automated Billing</h3>
<p className="text-gray-600">
Transparent invoicing, automated payments, and flexible billing options tailored to
your business.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<Cog6ToothIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Service Management</h3>
<p className="text-gray-600">
Control subscriptions, manage network services, and track usage from a single pane
of glass.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<PhoneIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Expert Support</h3>
<p className="text-gray-600">
Dedicated support team with SLA-backed response times and proactive service
monitoring.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<ChartBarIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Actionable Insights</h3>
<p className="text-gray-600">
Real-time analytics and reporting to help you optimize resource usage and forecast
demand.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<ArrowPathIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Seamless Migration</h3>
<p className="text-gray-600">
Guided onboarding for WHMCS customers with automatic data migration and validation.
</p>
</div>
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
<SparklesIcon className="w-7 h-7 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Future-Proof Platform</h3>
<p className="text-gray-600">
Built on modern infrastructure with continuous updates, security patches, and new
features.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-700 py-20">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<h2 className="text-4xl font-bold text-white mb-6">
Ready to experience the new portal?
</h2>
<p className="text-blue-100 text-lg mb-8">
Join thousands of customers who trust Assist Solutions to keep their business
connected and secure.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link
href="/auth/signup"
className="inline-flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-lg text-blue-600 bg-white hover:bg-blue-50 transition-colors duration-200"
className="block bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-6 py-3 rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Create an Account
</Link>
<Link
href="/auth/login"
className="inline-flex items-center justify-center px-8 py-3 border border-blue-300 text-base font-medium rounded-lg text-white bg-transparent hover:bg-blue-800 transition-colors duration-200"
>
Portal Login
Create Account
</Link>
</div>
</div>
</div>
</div>
</section>
<div className="bg-white/10 backdrop-blur rounded-2xl p-8 border border-white/20">
<h3 className="text-xl font-semibold text-white mb-4">Whats included?</h3>
<ul className="space-y-4 text-blue-100">
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Centralized service and subscription management dashboard</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Automated billing with support for multiple payment methods</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Priority access to our customer support specialists</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
<span>Insights and analytics to track usage and growth</span>
</li>
</ul>
{/* Key Features */}
<section className="py-16 bg-gradient-to-r from-indigo-50/80 via-purple-50/80 to-pink-50/80">
<div className="max-w-4xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="text-2xl font-bold bg-gradient-to-r from-gray-900 to-purple-900 bg-clip-text text-transparent mb-3">Everything you need</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
<CreditCardIcon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">Billing</h3>
<p className="text-sm text-gray-600">Automated invoicing and payments</p>
</div>
<div className="text-center bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Cog6ToothIcon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">Services</h3>
<p className="text-sm text-gray-600">Manage all your subscriptions</p>
</div>
<div className="text-center bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-white/50 shadow-lg hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
<PhoneIcon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">Support</h3>
<p className="text-sm text-gray-600">Get help when you need it</p>
</div>
</div>
</div>
</section>
{/* Support Section */}
<section className="py-16 bg-gradient-to-br from-purple-50/80 via-pink-50/80 to-rose-50/80">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="text-2xl font-bold bg-gradient-to-r from-gray-900 to-indigo-900 bg-clip-text text-transparent mb-4">Need help?</h2>
<p className="text-gray-700 mb-8">
Our support team is here to assist you with any questions
</p>
<Link
href="/support"
className="inline-block bg-white/80 backdrop-blur-sm border-2 border-purple-200 text-purple-700 px-8 py-3 rounded-lg hover:bg-purple-50 hover:border-purple-300 transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
Contact Support
</Link>
</div>
</section>
{/* Footer */}
<footer className="bg-blue-950 text-blue-100 py-10">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<Logo size={32} />
<p className="mt-4 text-sm text-blue-200">
Delivering reliable IT solutions and support for businesses of all sizes.
</p>
<footer className="bg-gradient-to-r from-gray-900 via-indigo-900 to-purple-900 text-gray-300 py-8">
<div className="max-w-4xl mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex items-center space-x-3">
<Logo size={24} />
<span className="text-white font-medium">Assist Solutions</span>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">Portal</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>
<Link
href="/auth/login"
className="hover:text-white transition-colors duration-200"
>
Login
</Link>
</li>
<li>
<Link
href="/auth/signup"
className="hover:text-white transition-colors duration-200"
>
Create Account
</Link>
</li>
<li>
<Link href="/support" className="hover:text-white transition-colors duration-200">
Support Center
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">
Solutions
</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>Network Services</li>
<li>Managed Security</li>
<li>Cloud Infrastructure</li>
<li>Professional Services</li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">Contact</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>support@assistsolutions.com</li>
<li>+1 (800) 555-0123</li>
<li>123 Innovation Drive, Suite 400</li>
<li>San Francisco, CA 94105</li>
</ul>
<div className="flex gap-6 text-sm">
<Link href="/support" className="hover:text-white transition-colors duration-200">
Support
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Privacy
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Terms
</Link>
</div>
</div>
<div className="mt-10 border-t border-blue-800 pt-6 text-sm text-blue-300 flex flex-col sm:flex-row justify-between gap-4">
<div className="mt-6 pt-6 border-t border-gray-700/50 text-center text-sm">
<p>&copy; {new Date().getFullYear()} Assist Solutions. All rights reserved.</p>
<div className="flex gap-6">
<Link href="#" className="hover:text-white transition-colors duration-200">
Privacy Policy
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Terms of Service
</Link>
<Link href="#" className="hover:text-white transition-colors duration-200">
Status
</Link>
</div>
</div>
</div>
</footer>

View File

@ -109,7 +109,7 @@ export interface UserProfile extends User {
lastLoginAt?: string;
}
export type UserRole = "user" | "admin";
export type UserRole = "USER" | "ADMIN";
export interface AuthenticatedUser extends UserProfile {
role: UserRole;

View File

@ -93,7 +93,7 @@ export const userProfileSchema = userSchema.extend({
avatar: z.string().optional(),
preferences: z.record(z.string(), z.unknown()).optional(),
lastLoginAt: timestampSchema.optional(),
role: z.enum(["user", "admin"]),
role: z.enum(["USER", "ADMIN"]),
});
export const prismaUserProfileSchema = z.object({