feat: introduce AuthLayout and update password reset validation to require terms acceptance

This commit is contained in:
Yashwin 2026-06-10 11:08:59 +05:30
parent 72a3c99299
commit e6fb7687ba
5 changed files with 858 additions and 710 deletions

View File

@ -0,0 +1,198 @@
import type { ReactElement, ReactNode } from "react";
import { Shield } from "lucide-react";
interface AuthLayoutProps {
children: ReactNode;
portalType?: "admin" | "tenant";
primaryColor?: string;
logoUrl?: string;
secondaryColor?: string;
}
const adminStats = [
{
title: "Tenants Managed",
value: "50+",
},
{
title: "Regions supported",
value: "12+",
},
{
title: "System Uptime",
value: "99.9%",
},
{
title: "Active Users",
value: "1200+",
},
];
const tenantStats = [
{
title: "Avg. Response time",
value: "< 2min",
},
{
title: "Regions supported",
value: "12+",
},
{
title: "Monthly Logins",
value: "1240+",
},
{
title: "Compliance checks",
value: "24/7",
},
];
export const AuthLayout = ({
children,
portalType = "admin",
primaryColor,
logoUrl,
secondaryColor,
}: AuthLayoutProps): ReactElement => {
const stats = portalType === "admin" ? adminStats : tenantStats;
const isTenant = portalType === "tenant";
return (
<div className="min-h-screen lg:h-screen lg:overflow-hidden flex flex-col lg:flex-row bg-[#F9F9F9]">
{/* Left Section */}
<div
className="hidden lg:flex lg:w-1/2 h-full px-8 pt-8 pb-10 flex-col justify-between items-start"
style={{ backgroundColor: primaryColor || "#112868" }}
>
<div className="w-full flex flex-col items-start gap-[10px] pt-8 pb-8">
<div className="flex flex-col items-start w-full gap-[10px]">
{/* Content Block */}
<div className="flex flex-col items-start gap-3 max-w-[280px]">
<div className="flex items-center gap-3">
{logoUrl ? (
<img
src={logoUrl}
alt="Logo"
className="h-9 w-auto max-w-[180px] object-contain"
onError={(e) => {
e.currentTarget.style.display = "none";
const fallback = e.currentTarget
.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = "flex";
}}
/>
) : null}
<div
className="w-9 h-9 rounded-xl flex items-center justify-center shadow-[0_4px_12px_rgba(0,0,0,0.15)]"
style={{
display: logoUrl ? "none" : "flex",
backgroundColor: secondaryColor || "#00d2c4",
}}
>
<Shield className="w-5 h-5 text-white" strokeWidth={2} />
</div>
<div className="flex items-center gap-2">
<span
className="text-[18px] font-semibold tracking-tight"
style={{ color: secondaryColor || "#00d2c4" }}
>
QAssure
</span>
<span className="text-white text-[13px] font-medium tracking-wider uppercase">
- {portalType}
</span>
</div>
</div>
<h1 className="w-[373px] self-stretch text-white text-[24px] font-semibold leading-normal">
{isTenant
? "Secure access for every tenant."
: "Secure control across all products"}
</h1>
<p className="w-[373px] text-white text-[14px] font-medium leading-[21px]">
{isTenant
? "Log in to manage projects, approvals, documents, and more from a single, compliant control center."
: "QAssure unifies multiple products into a single platform. The admin console enables centralized management of users, tenants, configurations, and operations."}
</p>
</div>
{/* Stats Grid */}
<div className="inline-grid grid-cols-2 gap-x-[10px] gap-y-[10px]">
{stats.map((item) => (
<div
key={item.title}
className="flex flex-col items-start w-[183.59px] p-3 gap-1 rounded-[6px] border border-white backdrop-blur-[20px]"
style={{
background:
"linear-gradient(96deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.30) 100.2%)",
}}
>
<p className="text-white text-xs font-medium opacity-90">
{item.title}
</p>
<p className="text-white text-xl font-semibold">
{item.value}
</p>
</div>
))}
</div>
</div>
</div>
{/* Footer */}
<div className="flex flex-col items-start self-stretch">
<p className="text-white text-sm leading-[21px]">
{isTenant
? "SSO, MFA, and audit-ready access for every tenant."
: "Centralized control with secure access for all admin operations."}
</p>
<p className="text-white text-sm leading-[21px]">
Need help? Contact your workspace administrator.
</p>
</div>
</div>
{/* Right Section */}
<div className="w-full lg:w-1/2 min-h-screen lg:h-full p-4 sm:p-8 lg:p-16 flex flex-col justify-center items-center">
<div className="w-full max-w-[507px] bg-white p-5 sm:p-7 flex flex-col items-start gap-5">
<div className="flex justify-between items-center w-full">
<span className="flex flex-col items-start rounded-full bg-[#EDF3FE] px-[10px] py-1 text-xs font-medium text-[#2563EB] text-[11px]">
{isTenant ? "Tenant Admin Portal" : "Admin Portal"}
</span>
</div>
{children}
{/* Footer Links */}
<div className="flex items-start gap-4 w-full mt-auto">
<button
type="button"
className="text-xs font-normal text-[#6B7280] hover:text-[#374151]"
>
Status page
</button>
<button
type="button"
className="text-xs font-normal text-[#6B7280] hover:text-[#374151]"
>
Security
</button>
<button
type="button"
className="text-xs font-normal text-[#6B7280] hover:text-[#374151]"
>
Help
</button>
</div>
</div>
</div>
</div>
);
};

View File

@ -1,13 +1,16 @@
import { useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useNavigate, Link, useLocation } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { ReactElement } from "react";
import { Shield, ArrowLeft } from "lucide-react";
import { ArrowLeft } from "lucide-react";
import { authService } from "@/services/auth-service";
import { FormField } from "@/components/shared";
import { PrimaryButton } from "@/components/shared";
import { useAppSelector } from "@/hooks/redux-hooks";
import { useTenantTheme } from "@/hooks/useTenantTheme";
import { AuthLayout } from "@/components/layout/AuthLayout";
// Zod validation schema
const forgotPasswordSchema = z.object({
@ -21,10 +24,20 @@ type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
const ForgotPassword = (): ReactElement => {
const navigate = useNavigate();
const location = useLocation();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [success, setSuccess] = useState<string>("");
const { theme, logoUrl } = useAppSelector((state) => state.theme);
// Fetch and apply tenant theme if on tenant route
useTenantTheme();
const isTenantRoute = location.pathname.startsWith("/tenant");
const portalType = isTenantRoute ? "tenant" : "admin";
const loginPath = isTenantRoute ? "/tenant/login" : "/";
const {
register,
handleSubmit,
@ -70,114 +83,103 @@ const ForgotPassword = (): ReactElement => {
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#f6f9ff] px-4">
<div className="w-full max-w-md">
{/* Forgot Password Card */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-6 md:p-8">
{/* Logo Section */}
<div className="flex justify-center mb-8">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#112868] rounded-xl flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)]">
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
</div>
<div className="text-2xl font-bold text-[#0f1724] tracking-[-0.4px]">
QAssure
</div>
</div>
</div>
<div className="mb-6">
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] mb-2">
Forgot Password
</h1>
<p className="text-sm md:text-base text-[#6b7280]">
Enter your email address and we'll send you a link to reset your
password
</p>
</div>
{/* Success Message */}
{success && (
<div className="mb-4 p-3 bg-[rgba(16,185,129,0.1)] border border-[#10b981] rounded-md">
<p className="text-sm text-[#10b981]">{success}</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{error}</p>
</div>
)}
{!success ? (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="space-y-4"
noValidate
>
{/* Email Field */}
<FormField
label="Email"
type="email"
placeholder="Enter your email"
required
error={errors.email?.message}
{...register("email")}
/>
{/* Submit Button */}
<div className="pt-2">
<PrimaryButton
type="submit"
size="large"
disabled={isLoading}
className="w-full h-12 text-sm font-medium"
>
{isLoading ? "Sending..." : "Send Reset Link"}
</PrimaryButton>
</div>
</form>
) : (
<div className="space-y-4">
<div className="pt-2">
<PrimaryButton
type="button"
size="large"
onClick={() => navigate("/")}
className="w-full h-12 text-sm font-medium"
>
Back to Login
</PrimaryButton>
</div>
</div>
)}
{/* Back to Login Link */}
{!success && (
<div className="mt-6 text-center">
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-[#112868] hover:text-[#0d1f4d] transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Login
</Link>
</div>
)}
<AuthLayout
portalType={portalType}
primaryColor={theme?.primary_color || undefined}
secondaryColor={theme?.secondary_color || undefined}
logoUrl={logoUrl || undefined}
>
{/* Header */}
<div className="flex flex-col items-start gap-2 w-full">
{/* Heading */}
<div className="flex flex-col items-start w-full">
<h2 className="text-[20px] font-semibold text-[#0F1724] leading-none">
Forgot Password
</h2>
</div>
{/* Footer */}
<div className="mt-6 text-center">
<p className="text-xs md:text-sm text-[#6b7280]">
© 2026 QAssure. All rights reserved.
{/* Description */}
<div className="flex flex-col items-start self-stretch">
<p className="w-full max-w-[503px] font-inter text-[12px] font-normal leading-normal text-[#6B7280]">
Enter your email address and we'll send you a link to reset your
password
</p>
</div>
</div>
</div>
{/* Success Message */}
{success && (
<div className="w-full p-3 bg-[rgba(16,185,129,0.1)] border border-[#10b981] rounded-md">
<p className="text-sm text-[#10b981] font-medium">{success}</p>
</div>
)}
{!success ? (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="flex flex-col w-full p-5 border border-gray-300 rounded-lg bg-white space-y-4"
noValidate
>
{/* Error Message */}
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600 font-medium">{error}</p>
</div>
)}
{/* Email Field */}
<FormField
label="Email"
type="email"
placeholder="Enter your email"
required
error={errors.email?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("email")}
/>
{/* Submit Button */}
<div className="pt-2">
<PrimaryButton
type="submit"
disabled={isLoading}
className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
>
{isLoading ? "Sending..." : "Send Reset Link"}
</PrimaryButton>
</div>
</form>
) : (
<div className="w-full space-y-4">
<div className="pt-2">
<PrimaryButton
type="button"
onClick={() => navigate(loginPath)}
className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
>
Back to Login
</PrimaryButton>
</div>
</div>
)}
{/* Back to Login Link */}
{!success && (
<div className="mt-6 text-center w-full flex justify-center">
<Link
to={loginPath}
className="inline-flex items-center gap-2 text-sm text-[#112868] hover:text-[#0d1f4d] transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Login
</Link>
</div>
)}
</AuthLayout>
);
};

View File

@ -1,27 +1,27 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ReactElement } from 'react';
import { Shield } from 'lucide-react';
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
import { loginAsync, clearError } from '@/store/authSlice';
import { FormField } from '@/components/shared';
import { PrimaryButton } from '@/components/shared';
import type { LoginError } from '@/services/auth-service';
import { showToast } from '@/utils/toast';
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { ReactElement } from "react";
import { useAppDispatch, useAppSelector } from "@/hooks/redux-hooks";
import { loginAsync, clearError } from "@/store/authSlice";
import { FormField } from "@/components/shared";
import { PrimaryButton } from "@/components/shared";
import type { LoginError } from "@/services/auth-service";
import { showToast } from "@/utils/toast";
import { AuthLayout } from "@/components/layout/AuthLayout";
// Zod validation schema
const loginSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
.min(1, "Email is required")
.email("Please enter a valid email address"),
password: z
.string()
.min(1, 'Password is required')
.min(6, 'Password must be at least 6 characters'),
.min(1, "Password is required")
.min(6, "Password must be at least 6 characters"),
});
type LoginFormData = z.infer<typeof loginSchema>;
@ -29,7 +29,9 @@ type LoginFormData = z.infer<typeof loginSchema>;
const Login = (): ReactElement => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { isLoading, error, isAuthenticated, roles } = useAppSelector((state) => state.auth);
const { isLoading, error, isAuthenticated, roles } = useAppSelector(
(state) => state.auth,
);
const {
register,
@ -39,10 +41,10 @@ const Login = (): ReactElement => {
clearErrors,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: 'onBlur', // Validate on blur for better UX
mode: "onBlur", // Validate on blur for better UX
});
const [generalError, setGeneralError] = useState<string>('');
const [generalError, setGeneralError] = useState<string>("");
// Redirect if already authenticated
useEffect(() => {
@ -52,34 +54,33 @@ const Login = (): ReactElement => {
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
rolesArray = roles;
} else if (typeof roles === 'string') {
} else if (typeof roles === "string") {
try {
rolesArray = JSON.parse(roles);
} catch {
rolesArray = [];
}
}
if (rolesArray.includes('super_admin')) {
navigate('/dashboard');
if (rolesArray.includes("super_admin")) {
navigate("/dashboard");
} else {
// Tenant admin - redirect to tenant landing page (workspace selector)
navigate('/tenant/landing');
navigate("/tenant/landing");
}
}
}, [isAuthenticated, roles, navigate]);
// Clear errors only on component mount, not on every auth state change
useEffect(() => {
// Only clear errors on initial mount
dispatch(clearError());
setGeneralError('');
setGeneralError("");
clearErrors();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Empty dependency array - only run on mount
const onSubmit = async (data: LoginFormData): Promise<void> => {
// Clear previous errors
setGeneralError('');
setGeneralError("");
clearErrors();
dispatch(clearError());
@ -89,173 +90,172 @@ const Login = (): ReactElement => {
// Check if password reset is required
if (result.data.requirePasswordReset) {
const { tempToken, resetReason } = result.data;
showToast.info('Password reset required', 'For security reasons, please update your password.');
showToast.info(
"Password reset required",
"For security reasons, please update your password.",
);
navigate(`/reset-password?token=${tempToken}&reason=${resetReason}`);
return;
}
const message = result.message || 'Login successful';
const description = result.message ? undefined : 'Welcome back!';
const message = result.message || "Login successful";
const description = result.message ? undefined : "Welcome back!";
showToast.success(message, description);
// Check roles after login to redirect appropriately
const userRoles = result.data.roles || [];
if (userRoles.includes('super_admin')) {
navigate('/dashboard');
if (userRoles.includes("super_admin")) {
navigate("/dashboard");
} else {
navigate('/tenant/landing');
navigate("/tenant/landing");
}
}
} catch (error: any) {
// Clear Redux error state since we're handling errors locally
dispatch(clearError());
// Handle error from unwrap() - error is the rejected value from rejectWithValue
const loginError = error as LoginError;
if (loginError && typeof loginError === 'object') {
if (loginError && typeof loginError === "object") {
// Check for validation errors with details array
if ('details' in loginError && Array.isArray(loginError.details)) {
if ("details" in loginError && Array.isArray(loginError.details)) {
// Validation errors from server - set field-specific errors
loginError.details.forEach((detail) => {
if (detail.path === 'email' || detail.path === 'password') {
if (detail.path === "email" || detail.path === "password") {
setError(detail.path as keyof LoginFormData, {
type: 'server',
type: "server",
message: detail.message,
});
} else {
// If error is for a field we don't handle, show as general error
setGeneralError(detail.message);
}
});
} else if ('error' in loginError) {
// Check if error is an object with message property
if (typeof loginError.error === 'object' && loginError.error !== null && 'message' in loginError.error) {
// General error from server with object structure
setGeneralError(loginError.error.message || 'Login failed');
} else if (typeof loginError.error === 'string') {
// Error is a string
} else if ("error" in loginError) {
if (
typeof loginError.error === "object" &&
loginError.error !== null &&
"message" in loginError.error
) {
setGeneralError(loginError.error.message || "Login failed");
} else if (typeof loginError.error === "string") {
setGeneralError(loginError.error);
} else {
setGeneralError('Login failed. Please check your credentials.');
setGeneralError("Login failed. Please check your credentials.");
}
} else {
// Fallback for unknown error structure
setGeneralError('An unexpected error occurred. Please try again.');
setGeneralError("An unexpected error occurred. Please try again.");
}
} else if (error?.message) {
// Network error or other error
setGeneralError(error.message);
} else {
// Complete fallback
setGeneralError('Login failed. Please check your credentials and try again.');
setGeneralError(
"Login failed. Please check your credentials and try again.",
);
}
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#f6f9ff] px-4">
<div className="w-full max-w-md">
{/* Login Card */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-6 md:p-8">
{/* Logo Section */}
<div className="flex justify-center mb-8">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#112868] rounded-xl flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)]">
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
</div>
<div className="text-2xl font-bold text-[#0f1724] tracking-[-0.4px]">
QAssure
</div>
</div>
</div>
<div className="mb-6">
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] mb-2">
Welcome Back
</h1>
<p className="text-sm md:text-base text-[#6b7280]">
Sign in to your account to continue
</p>
</div>
{/* General Error Message - Prioritize local error over Redux error */}
{generalError && (
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{generalError}</p>
</div>
)}
{/* Show Redux error only if no local error and no field errors */}
{!generalError && error && !errors.email && !errors.password && (
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{error}</p>
</div>
)}
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="space-y-4"
noValidate
>
{/* Email Field */}
<FormField
label="Email"
type="email"
placeholder="Enter your email"
required
error={errors.email?.message}
{...register('email')}
/>
{/* Password Field */}
<FormField
label="Password"
type="password"
placeholder="Enter your password"
required
error={errors.password?.message}
{...register('password')}
/>
{/* Forgot Password Link */}
<div className="flex justify-end -mt-2">
<a
href="/forgot-password"
onClick={(e) => {
e.preventDefault();
navigate('/forgot-password');
}}
className="text-sm text-[#112868] hover:text-[#0d1f4d] transition-colors"
>
Forgot Password?
</a>
</div>
{/* Submit Button */}
<div className="pt-2">
<PrimaryButton
type="submit"
size="large"
disabled={isLoading}
className="w-full h-12 text-sm font-medium"
>
{isLoading ? 'Signing in...' : 'Sign In'}
</PrimaryButton>
</div>
</form>
<AuthLayout portalType="admin">
{/* Header */}
<div className="flex flex-col items-start gap-2 w-full">
{/* Heading */}
<div className="flex flex-col items-start w-full">
<h2 className="text-[20px] font-semibold text-[#0F1724] leading-none">
Sign in
</h2>
</div>
{/* Footer */}
<div className="mt-6 text-center">
<p className="text-xs md:text-sm text-[#6b7280]">
© 2026 QAssure. All rights reserved.
{/* Description */}
<div className="flex flex-col items-start self-stretch">
<p className="w-full max-w-[503px] font-inter text-[12px] font-normal leading-normal text-[#6B7280]">
Use your work email or configured SSO provider to access the
admin portal.
</p>
</div>
</div>
</div>
{/* Form Card */}
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="flex flex-col w-full p-5 border border-gray-300 rounded-lg bg-white"
noValidate
>
{/* General Error Message - Prioritize local error over Redux error */}
{generalError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600 font-medium">
{generalError}
</p>
</div>
)}
{/* Show Redux error only if no local error and no field errors */}
{!generalError && error && !errors.email && !errors.password && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600 font-medium">{error}</p>
</div>
)}
{/* Email */}
<FormField
label="Email"
type="email"
placeholder="user@company.com"
required
helperText="Enter your work email"
error={errors.email?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("email")}
/>
{/* Password */}
<FormField
label="Password"
type="password"
placeholder="••••••••"
required
helperText="Enter your password (min 8 characters)"
error={errors.password?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("password")}
/>
{/* Remember Me */}
<div className="flex items-center justify-between mt-1">
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
<input
type="checkbox"
className="rounded border-gray-300 text-[#112868] focus:ring-[#112868]/20"
/>
Remember Me
</label>
<button
type="button"
onClick={() => navigate("/forgot-password")}
className="text-sm font-medium text-[#112868] hover:underline"
>
Forgot Password?
</button>
</div>
{/* Login Button */}
<div className="pt-1">
<PrimaryButton
type="submit"
disabled={isLoading}
className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
>
{isLoading ? "Signing in..." : "Login"}
</PrimaryButton>
</div>
</form>
</AuthLayout>
);
};

View File

@ -1,14 +1,14 @@
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ReactElement } from 'react';
import { Shield, ArrowLeft } from 'lucide-react';
import { authService } from '@/services/auth-service';
import { FormField } from '@/components/shared';
import { PrimaryButton } from '@/components/shared';
import { showToast } from '@/utils/toast';
import { useState, useEffect } from "react";
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { ReactElement } from "react";
import { authService } from "@/services/auth-service";
import { FormField } from "@/components/shared";
import { PrimaryButton } from "@/components/shared";
import { showToast } from "@/utils/toast";
import { AuthLayout } from "@/components/layout/AuthLayout";
// Zod validation schema - token is optional if provided in URL
const createResetPasswordSchema = (hasTokenFromUrl: boolean) =>
@ -16,26 +16,33 @@ const createResetPasswordSchema = (hasTokenFromUrl: boolean) =>
.object({
token: hasTokenFromUrl
? z.string().optional()
: z.string().min(1, 'Reset token is required'),
: z.string().min(1, "Reset token is required"),
password: z
.string()
.min(1, 'Password is required')
.min(6, 'Password must be at least 6 characters'),
confirmPassword: z.string().min(1, 'Please confirm your password'),
.min(1, "Password is required")
.min(8, "Password must be at least 8 characters"),
confirmPassword: z.string().min(1, "Please confirm your password"),
acceptTerms: z.boolean().refine((val) => val === true, {
message: "You must accept the Terms of Service and Privacy Policy",
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
message: "Passwords do not match",
path: ["confirmPassword"],
});
const ResetPassword = (): ReactElement => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const location = useLocation();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [success, setSuccess] = useState<string>('');
const [success, setSuccess] = useState<string>("");
const tokenFromUrl = searchParams.get('token') || '';
const reason = searchParams.get('reason') || '';
const isTenantRoute = location.pathname.startsWith("/tenant");
const portalType = isTenantRoute ? "tenant" : "admin";
const tokenFromUrl = searchParams.get("token") || "";
const reason = searchParams.get("reason") || "";
const hasTokenFromUrl = Boolean(tokenFromUrl);
const resetPasswordSchema = createResetPasswordSchema(hasTokenFromUrl);
@ -50,57 +57,72 @@ const ResetPassword = (): ReactElement => {
clearErrors,
} = useForm<ResetPasswordFormData>({
resolver: zodResolver(resetPasswordSchema),
mode: 'onBlur',
mode: "onBlur",
defaultValues: {
token: tokenFromUrl || undefined,
password: '',
confirmPassword: '',
password: "",
confirmPassword: "",
acceptTerms: false as any,
},
});
// Set token from URL if available
useEffect(() => {
if (tokenFromUrl) {
setValue('token', tokenFromUrl);
setValue("token", tokenFromUrl);
}
}, [tokenFromUrl, setValue]);
const onSubmit = async (data: ResetPasswordFormData): Promise<void> => {
setSuccess('');
setSuccess("");
clearErrors();
setIsLoading(true);
try {
// Always use token from URL if available, otherwise use form data
const tokenToUse = tokenFromUrl || data.token || '';
const tokenToUse = tokenFromUrl || data.token || "";
const response = await authService.resetPassword({
token: tokenToUse,
password: data.password,
});
if (response.success) {
const message = response.message || response.data?.message || 'Password reset successfully!';
const description = (response.message || response.data?.message) ? undefined : 'You can now login with your new password';
const successMessage = response.message || response.data?.message || 'Password reset successfully! You can now login with your new password.';
const message =
response.message ||
response.data?.message ||
"Password reset successfully!";
const description =
response.message || response.data?.message
? undefined
: "You can now login with your new password";
const successMessage =
response.message ||
response.data?.message ||
"Password reset successfully! You can now login with your new password.";
setSuccess(successMessage);
showToast.success(message, description);
// Redirect to login after 2 seconds
setTimeout(() => {
navigate('/');
navigate("/");
}, 2000);
}
} catch (err: any) {
console.error('Reset password error:', err);
console.error("Reset password error:", err);
// Handle validation errors from API
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
if (
err?.response?.data?.details &&
Array.isArray(err.response.data.details)
) {
const validationErrors = err.response.data.details;
validationErrors.forEach((detail: { path: string; message: string }) => {
if (detail.path === 'token' || detail.path === 'password') {
setError(detail.path as keyof ResetPasswordFormData, {
type: 'server',
message: detail.message,
});
}
});
validationErrors.forEach(
(detail: { path: string; message: string }) => {
if (detail.path === "token" || detail.path === "password") {
setError(detail.path as keyof ResetPasswordFormData, {
type: "server",
message: detail.message,
});
}
},
);
} else {
// Handle general errors
const errorMessage =
@ -108,10 +130,13 @@ const ResetPassword = (): ReactElement => {
err?.response?.data?.error ||
err?.response?.data?.message ||
err?.message ||
'Failed to reset password. Please try again.';
setError('root', {
type: 'server',
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to reset password. Please try again.',
"Failed to reset password. Please try again.";
setError("root", {
type: "server",
message:
typeof errorMessage === "string"
? errorMessage
: "Failed to reset password. Please try again.",
});
}
} finally {
@ -120,140 +145,144 @@ const ResetPassword = (): ReactElement => {
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#f6f9ff] px-4">
<div className="w-full max-w-md">
{/* Reset Password Card */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-6 md:p-8">
{/* Logo Section */}
<div className="flex justify-center mb-8">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#112868] rounded-xl flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)]">
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
</div>
<div className="text-2xl font-bold text-[#0f1724] tracking-[-0.4px]">
QAssure
</div>
</div>
</div>
<div className="mb-6">
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] mb-2">
{reason === 'FIRST_LOGIN' ? 'Set Initial Password' :
reason === 'EXPIRED' ? 'Update Expired Password' :
'Reset Password'}
</h1>
<p className="text-sm md:text-base text-[#6b7280]">
{reason === 'FIRST_LOGIN' ? 'Welcome to QAssure! Please set a new password for your account to continue.' :
reason === 'EXPIRED' ? 'Your password has expired. For security, please choose a new password.' :
tokenFromUrl ? 'Enter your new password to reset' :
'Enter your reset token and new password'}
</p>
</div>
{/* Success Message */}
{success && (
<div className="mb-4 p-3 bg-[rgba(16,185,129,0.1)] border border-[#10b981] rounded-md">
<p className="text-sm text-[#10b981]">{success}</p>
</div>
)}
{!success ? (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="space-y-4"
noValidate
>
{/* General Error Display */}
{errors.root && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
</div>
)}
{/* Token Field - Only show if not in URL */}
{!hasTokenFromUrl && (
<FormField
label="Reset Token"
type="text"
placeholder="Enter reset token from email"
required
error={errors.token?.message}
{...register('token')}
/>
)}
{/* Password Field */}
<FormField
label="New Password"
type="password"
placeholder="Enter your new password"
required
error={errors.password?.message}
{...register('password')}
/>
{/* Confirm Password Field */}
<FormField
label="Confirm Password"
type="password"
placeholder="Confirm your new password"
required
error={errors.confirmPassword?.message}
{...register('confirmPassword')}
/>
{/* Submit Button */}
<div className="pt-2">
<PrimaryButton
type="submit"
size="large"
disabled={isLoading}
className="w-full h-12 text-sm font-medium"
>
{isLoading ? 'Resetting...' : 'Reset Password'}
</PrimaryButton>
</div>
</form>
) : (
<div className="space-y-4">
<div className="pt-2">
<PrimaryButton
type="button"
size="large"
onClick={() => navigate('/')}
className="w-full h-12 text-sm font-medium"
>
Go to Login
</PrimaryButton>
</div>
</div>
)}
{/* Back to Login Link */}
{!success && !reason && (
<div className="mt-6 text-center">
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-[#112868] hover:text-[#0d1f4d] transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Login
</Link>
</div>
)}
<AuthLayout portalType={portalType}>
{/* Header */}
<div className="flex flex-col items-start gap-2 w-full">
{/* Heading */}
<div className="flex flex-col items-start w-full">
<h2 className="text-[20px] font-semibold text-[#0F1724] leading-none">
{reason === "FIRST_LOGIN"
? "Set Initial Password"
: reason === "EXPIRED"
? "Update Expired Password"
: "Set New Password"}
</h2>
</div>
{/* Footer */}
<div className="mt-6 text-center">
<p className="text-xs md:text-sm text-[#6b7280]">
© 2026 QAssure. All rights reserved.
{/* Description */}
<div className="flex flex-col items-start self-stretch">
<p className="w-full max-w-[503px] font-inter text-[12px] font-normal leading-normal text-[#6B7280]">
{reason === "FIRST_LOGIN"
? "Welcome to QAssure! Please set a new password for your account to continue."
: reason === "EXPIRED"
? "Your password has expired. For security, please choose a new password."
: "Create a strong password to activate your account and access the portal"}
</p>
</div>
</div>
</div>
{/* Success Message */}
{success && (
<div className="w-full p-3 bg-[rgba(16,185,129,0.1)] border border-[#10b981] rounded-md">
<p className="text-sm text-[#10b981] font-medium">{success}</p>
</div>
)}
{!success ? (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="flex flex-col w-full p-5 border border-gray-300 rounded-lg bg-white space-y-4"
noValidate
>
{/* General Error Display */}
{errors.root && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600 font-medium">
{errors.root.message}
</p>
</div>
)}
{/* Token Field - Only show if not in URL */}
{!hasTokenFromUrl && (
<FormField
label="Reset Token"
type="text"
placeholder="Enter reset token from email"
required
error={errors.token?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("token")}
/>
)}
{/* Password Field */}
<FormField
label="New Password"
type="password"
placeholder="Create a strong password"
required
helperText="Min 8 characters with letters, numbers, and a symbol."
error={errors.password?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("password")}
/>
{/* Confirm Password Field */}
<FormField
label="Confirm Password"
type="password"
placeholder="Re-enter your password"
required
helperText="Must match the new password exactly."
error={errors.confirmPassword?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("confirmPassword")}
/>
{/* Accept Terms Checkbox */}
<div className="flex flex-col gap-1">
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
<input
type="checkbox"
className="rounded border-gray-300 text-[#112868] focus:ring-[#112868]/20"
{...register("acceptTerms")}
/>
I accept the Terms of Service and Privacy Policy
</label>
{errors.acceptTerms ? (
<span className="text-[11px] text-red-600">
{errors.acceptTerms.message}
</span>
) : (
<span className="text-[11px] text-[#6B7280]">
You must accept to continue.
</span>
)}
</div>
{/* Submit Button */}
<div className="pt-2">
<PrimaryButton
type="submit"
disabled={isLoading}
className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
>
{isLoading ? "Resetting..." : "Login"}
</PrimaryButton>
<p className="text-xs text-[#6B7280] text-center mt-2">
You will be redirected to the Tenant Admin Portal after saving.
</p>
</div>
</form>
) : (
<div className="w-full space-y-4">
<div className="pt-2">
<PrimaryButton
type="button"
onClick={() => navigate("/")}
className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
>
Go to Login
</PrimaryButton>
</div>
</div>
)}
</AuthLayout>
);
};

View File

@ -1,28 +1,28 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ReactElement } from 'react';
import { Shield } from 'lucide-react';
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
import { loginAsync, clearError } from '@/store/authSlice';
import { FormField } from '@/components/shared';
import { PrimaryButton } from '@/components/shared';
import type { LoginError } from '@/services/auth-service';
import { showToast } from '@/utils/toast';
import { useTenantTheme } from '@/hooks/useTenantTheme';
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { ReactElement } from "react";
import { useAppDispatch, useAppSelector } from "@/hooks/redux-hooks";
import { loginAsync, clearError } from "@/store/authSlice";
import { FormField } from "@/components/shared";
import { PrimaryButton } from "@/components/shared";
import type { LoginError } from "@/services/auth-service";
import { showToast } from "@/utils/toast";
import { useTenantTheme } from "@/hooks/useTenantTheme";
import { AuthLayout } from "@/components/layout/AuthLayout";
// Zod validation schema
const loginSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
.min(1, "Email is required")
.email("Please enter a valid email address"),
password: z
.string()
.min(1, 'Password is required')
.min(6, 'Password must be at least 6 characters'),
.min(1, "Password is required")
.min(6, "Password must be at least 6 characters"),
});
type LoginFormData = z.infer<typeof loginSchema>;
@ -30,9 +30,11 @@ type LoginFormData = z.infer<typeof loginSchema>;
const TenantLogin = (): ReactElement => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { isLoading, error, isAuthenticated, roles } = useAppSelector((state) => state.auth);
const { isLoading, error, isAuthenticated, roles } = useAppSelector(
(state) => state.auth,
);
const { theme, logoUrl } = useAppSelector((state) => state.theme);
// Fetch and apply tenant theme
useTenantTheme();
@ -44,10 +46,10 @@ const TenantLogin = (): ReactElement => {
clearErrors,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: 'onBlur',
mode: "onBlur",
});
const [generalError, setGeneralError] = useState<string>('');
const [generalError, setGeneralError] = useState<string>("");
const [rememberMe, setRememberMe] = useState<boolean>(false);
// Redirect if already authenticated
@ -58,18 +60,18 @@ const TenantLogin = (): ReactElement => {
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
rolesArray = roles;
} else if (typeof roles === 'string') {
} else if (typeof roles === "string") {
try {
rolesArray = JSON.parse(roles);
} catch {
rolesArray = [];
}
}
if (rolesArray.includes('super_admin')) {
navigate('/dashboard');
if (rolesArray.includes("super_admin")) {
navigate("/dashboard");
} else {
// Tenant admin - redirect to tenant landing page (workspace selector)
navigate('/tenant/landing');
navigate("/tenant/landing");
}
}
}, [isAuthenticated, roles, navigate]);
@ -77,13 +79,13 @@ const TenantLogin = (): ReactElement => {
// Clear errors on component mount
useEffect(() => {
dispatch(clearError());
setGeneralError('');
setGeneralError("");
clearErrors();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmit = async (data: LoginFormData): Promise<void> => {
setGeneralError('');
setGeneralError("");
clearErrors();
dispatch(clearError());
@ -93,315 +95,232 @@ const TenantLogin = (): ReactElement => {
// Check if password reset is required
if (result.data.requirePasswordReset) {
const { tempToken, resetReason } = result.data;
showToast.info('Password reset required', 'For security reasons, please update your password.');
showToast.info(
"Password reset required",
"For security reasons, please update your password.",
);
navigate(`/reset-password?token=${tempToken}&reason=${resetReason}`);
return;
}
const message = result.message || 'Login successful';
const message = result.message || "Login successful";
showToast.success(message);
// Check roles after login to redirect appropriately
// Handle both array and JSON string formats
const userRoles = result.data.roles || [];
let rolesArray: string[] = [];
if (Array.isArray(userRoles)) {
rolesArray = userRoles;
} else if (typeof userRoles === 'string') {
} else if (typeof userRoles === "string") {
try {
rolesArray = JSON.parse(userRoles);
} catch {
rolesArray = [];
}
}
if (rolesArray.includes('super_admin')) {
navigate('/dashboard');
if (rolesArray.includes("super_admin")) {
navigate("/dashboard");
} else {
navigate('/tenant/landing');
navigate("/tenant/landing");
}
}
} catch (error: any) {
dispatch(clearError());
const loginError = error as LoginError;
if (loginError && typeof loginError === 'object') {
if ('details' in loginError && Array.isArray(loginError.details)) {
if (loginError && typeof loginError === "object") {
if ("details" in loginError && Array.isArray(loginError.details)) {
loginError.details.forEach((detail) => {
if (detail.path === 'email' || detail.path === 'password') {
if (detail.path === "email" || detail.path === "password") {
setError(detail.path as keyof LoginFormData, {
type: 'server',
type: "server",
message: detail.message,
});
} else {
setGeneralError(detail.message);
}
});
} else if ('error' in loginError) {
if (typeof loginError.error === 'object' && loginError.error !== null && 'message' in loginError.error) {
setGeneralError(loginError.error.message || 'Login failed');
} else if (typeof loginError.error === 'string') {
} else if ("error" in loginError) {
if (
typeof loginError.error === "object" &&
loginError.error !== null &&
"message" in loginError.error
) {
setGeneralError(loginError.error.message || "Login failed");
} else if (typeof loginError.error === "string") {
setGeneralError(loginError.error);
} else {
setGeneralError('Login failed. Please check your credentials.');
setGeneralError("Login failed. Please check your credentials.");
}
} else {
setGeneralError('An unexpected error occurred. Please try again.');
setGeneralError("An unexpected error occurred. Please try again.");
}
} else if (error?.message) {
setGeneralError(error.message);
} else {
setGeneralError('Login failed. Please check your credentials and try again.');
setGeneralError(
"Login failed. Please check your credentials and try again.",
);
}
}
};
return (
<div className="min-h-screen bg-[#f6f9ff] relative flex">
{/* Left Side - Blue Background */}
<div
className="hidden lg:flex lg:w-[48%] flex-col justify-between px-8 py-8 min-w-[320px]"
style={{
backgroundColor: theme?.primary_color || '#112868',
<AuthLayout
portalType="tenant"
primaryColor={theme?.primary_color || undefined}
secondaryColor={theme?.secondary_color || undefined}
logoUrl={logoUrl || undefined}
>
{/* Header */}
<div className="flex flex-col items-start gap-2 w-full">
{/* Heading */}
<div className="flex flex-col items-start w-full">
<h2 className="text-[20px] font-semibold text-[#0F1724] leading-none">
Sign in to your tenant
</h2>
</div>
{/* Description */}
<div className="flex flex-col items-start self-stretch">
<p className="w-full max-w-[503px] font-inter text-[12px] font-normal leading-normal text-[#6B7280]">
Use your work email or configured SSO provider to access the admin
portal.
</p>
</div>
</div>
{/* Login Card Form */}
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="flex flex-col w-full p-5 border border-gray-300 rounded-lg bg-white"
noValidate
>
<div className="flex flex-col gap-2.5 py-22">
{/* Logo Section */}
<div className="flex flex-col gap-3 max-w-[280px]">
<div className="flex items-center justify-between px-2 w-[206px]">
{logoUrl ? (
<img
src={logoUrl}
alt="Logo"
className="h-9 w-auto max-w-[180px] object-contain"
onError={(e) => {
// Fallback to icon if image fails
e.currentTarget.style.display = 'none';
const fallback = e.currentTarget.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'flex';
}}
/>
) : null}
<div
className="rounded-[10px] shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] w-9 h-9 flex items-center justify-center shrink-0"
style={{
display: logoUrl ? 'none' : 'flex',
backgroundColor: theme?.secondary_color || '#23dce1',
}}
>
<Shield className="w-5 h-5 text-white" strokeWidth={1.67} />
</div>
<div
className="text-[18px] font-bold tracking-[-0.36px]"
style={{ color: theme?.secondary_color || '#23dce1' }}
>
{/* {logoUrl ? '' : 'QAssure'} */}
QAssure
</div>
<div className="text-[13px] font-medium text-white uppercase">-</div>
<div className="text-[13px] font-medium text-white uppercase">Tenant</div>
</div>
{/* Heading */}
<div className="flex flex-col">
<h2 className="text-2xl font-semibold text-white leading-normal">
Secure access for<br />every tenant.
</h2>
</div>
{/* Description */}
<div className="opacity-90">
<p className="text-sm font-medium text-white leading-[21px]">
Log in to manage projects, approvals, documents, and more from a single, compliant control center.
</p>
</div>
{/* General Error Message */}
{generalError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600 font-medium">{generalError}</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-2.5 mt-16 max-w-[377px]">
{[
{ label: 'Avg. tenant uptime', value: '99.98%' },
{ label: 'Regions supported', value: '12+' },
{ label: 'Active tenants', value: '480+' },
{ label: 'Compliance checks', value: '24/7' },
].map((stat, index) => (
<div
key={index}
className="backdrop-blur-[20px] border border-white rounded-md p-3"
style={{
backgroundImage: 'linear-gradient(108.34deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.3) 100.2%)',
}}
>
<div className="opacity-90 mb-1">
<p className="text-xs font-normal text-white leading-normal">{stat.label}</p>
</div>
<div>
<p className="text-base font-semibold text-white leading-normal">{stat.value}</p>
</div>
</div>
))}
)}
{!generalError && error && !errors.email && !errors.password && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600 font-medium">{error}</p>
</div>
</div>
)}
{/* Footer Text */}
<div className="flex flex-col gap-1 opacity-90">
<p className="text-sm font-normal text-white">SSO, MFA, and audit-ready access for every tenant.</p>
<p className="text-sm font-normal text-white">Need help? Contact your workspace administrator.</p>
</div>
</div>
{/* Email Field */}
<FormField
label="Email"
type="email"
placeholder="Enter your email"
required
error={errors.email?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("email")}
/>
{/* Right Side - Login Form */}
<div className="flex-1 flex items-center justify-center px-9 py-8">
<div className="w-full max-w-[507px] flex flex-col gap-5">
{/* Header */}
<div className="flex flex-col gap-2">
{/* <div className="flex items-center justify-between">
<div className="bg-[#edf3fe] px-2.5 py-1 rounded-full">
<span className="text-[11px] font-medium text-[#0f1724]">Tenant Admin Portal</span>
</div>
</div> */}
<h1 className="text-[22px] font-semibold text-[#0f1724]">Sign in to your tenant</h1>
<p className="text-sm font-normal text-[#6b7280]">
Use your work email or configured SSO provider to access the admin portal.
</p>
</div>
{/* Password Field */}
<FormField
label="Password"
type="password"
placeholder="Enter your password"
required
error={errors.password?.message}
className="h-11 px-4 border border-gray-300 rounded-md focus-visible:ring-blue-500 focus-visible:border-blue-500"
{...register("password")}
/>
{/* Login Card */}
<div className="bg-white border border-[#d1d5db] rounded-lg p-5 flex flex-col gap-2.5">
{/* General Error Message */}
{generalError && (
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{generalError}</p>
</div>
)}
{!generalError && error && !errors.email && !errors.password && (
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{error}</p>
</div>
)}
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
className="space-y-4"
noValidate
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between pt-1">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="remember-me"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="w-[18px] h-[18px] rounded border-[#d1d5db] focus:ring-2"
style={
{
backgroundColor: theme?.primary_color || "#112868",
accentColor: theme?.primary_color || "#112868",
} as any
}
/>
<label
htmlFor="remember-me"
className="text-sm font-normal text-[#0f1724] cursor-pointer"
>
{/* Email Field */}
<FormField
label="Email"
type="email"
placeholder="Enter your email"
required
error={errors.email?.message}
{...register('email')}
/>
{/* Password Field */}
<FormField
label="Password"
type="password"
placeholder="Enter your password"
required
error={errors.password?.message}
{...register('password')}
/>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between pt-1">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="remember-me"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="w-[18px] h-[18px] rounded border-[#d1d5db] focus:ring-2"
style={{
backgroundColor: theme?.primary_color || '#112868',
accentColor: theme?.primary_color || '#112868',
} as React.CSSProperties & { accentColor?: string }}
/>
<label htmlFor="remember-me" className="text-sm font-normal text-[#0f1724] cursor-pointer">
Remember Me
</label>
</div>
<a
href="/tenant/forgot-password"
onClick={(e) => {
e.preventDefault();
navigate('/tenant/forgot-password');
}}
className="text-[13px] font-medium underline"
style={{ color: theme?.primary_color || '#112868' }}
>
Forgot Password?
</a>
</div>
{/* Submit Button */}
<div className="pt-1">
<PrimaryButton
type="submit"
size="large"
disabled={isLoading}
className="w-full h-12 text-base font-medium"
>
{isLoading ? 'Signing in...' : 'Sign In'}
</PrimaryButton>
</div>
{/* New to tenant */}
<div className="flex items-center justify-between h-8">
<p className="text-xs font-normal text-[#6b7280]">New to this tenant?</p>
<a
href="/tenant/request-access"
onClick={(e) => {
e.preventDefault();
navigate('/tenant/request-access');
}}
className="text-[13px] font-medium underline"
style={{ color: theme?.secondary_color || '#23dce1' }}
>
Request Access
</a>
</div>
{/* SSO Section */}
<div className="pt-1">
<div className="flex flex-col gap-2.5 h-[60px]">
<p className="text-xs font-normal text-[#6b7280] uppercase tracking-[0.48px]">Single sign-on</p>
<div className="grid grid-cols-3 gap-2">
{['Google', 'Microsoft', 'Okta'].map((provider) => (
<button
key={provider}
type="button"
className="bg-white border border-[#9ca3af] rounded px-4 py-1 flex items-center justify-center gap-2 h-10 text-sm font-medium text-[#0f1724] hover:bg-[#f5f7fa] transition-colors"
>
{provider}
</button>
))}
</div>
</div>
</div>
</form>
</div>
{/* Footer Links */}
<div className="flex gap-4 items-start">
<a href="/tenant/status" className="text-xs font-normal text-[#6b7280]">
Status page
</a>
<a href="/tenant/security" className="text-xs font-normal text-[#6b7280]">
Security
</a>
<a href="/tenant/help" className="text-xs font-normal text-[#6b7280]">
Help
</a>
Remember Me
</label>
</div>
<a
href="/tenant/forgot-password"
onClick={(e) => {
e.preventDefault();
navigate("/tenant/forgot-password");
}}
className="text-[13px] font-medium underline"
style={{ color: theme?.primary_color || "#112868" }}
>
Forgot Password?
</a>
</div>
</div>
</div>
{/* Submit Button */}
<div className="pt-3">
<PrimaryButton
type="submit"
disabled={isLoading}
className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
>
{isLoading ? "Signing in..." : "Sign In"}
</PrimaryButton>
</div>
{/* New to tenant */}
{/* <div className="flex items-center justify-between h-8 pt-1">
<p className="text-xs font-normal text-[#6b7280]">
New to this tenant?
</p>
<a
href="/tenant/request-access"
onClick={(e) => {
e.preventDefault();
navigate("/tenant/request-access");
}}
className="text-[13px] font-medium underline"
style={{ color: theme?.secondary_color || "#23dce1" }}
>
Request Access
</a>
</div> */}
{/* SSO Section */}
{/* <div className="pt-1">
<div className="flex flex-col gap-2.5">
<p className="text-xs font-normal text-[#6b7280] uppercase tracking-[0.48px]">
Single sign-on
</p>
<div className="grid grid-cols-3 gap-2">
{["Google", "Microsoft", "Okta"].map((provider) => (
<button
key={provider}
type="button"
className="bg-white border border-[#9ca3af] rounded px-4 py-1 flex items-center justify-center gap-2 h-10 text-sm font-medium text-[#0f1724] hover:bg-[#f5f7fa] transition-colors"
>
{provider}
</button>
))}
</div>
</div>
</div> */}
</form>
</AuthLayout>
);
};