- {/* Logo Section */}
-
-
-
- Welcome Back
-
-
- Sign in to your account to continue
-
-
-
- {/* General Error Message - Prioritize local error over Redux error */}
- {generalError && (
-
- )}
- {/* Show Redux error only if no local error and no field errors */}
- {!generalError && error && !errors.email && !errors.password && (
-
- )}
-
-
+
+ {/* Header */}
+
+ {/* Heading */}
+
+
+ Sign in
+
- {/* Footer */}
-
-
- © 2026 QAssure. All rights reserved.
+ {/* Description */}
+
+
+ Use your work email or configured SSO provider to access the
+ admin portal.
-
+
+ {/* Form Card */}
+
+
);
};
diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx
index 57c016b..539ffec 100644
--- a/src/pages/ResetPassword.tsx
+++ b/src/pages/ResetPassword.tsx
@@ -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
(false);
- const [success, setSuccess] = useState('');
+ const [success, setSuccess] = useState("");
- 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({
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 => {
- 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 (
-
-
- {/* Reset Password Card */}
-
- {/* Logo Section */}
-
-
-
- {reason === 'FIRST_LOGIN' ? 'Set Initial Password' :
- reason === 'EXPIRED' ? 'Update Expired Password' :
- 'Reset Password'}
-
-
- {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'}
-
-
-
- {/* Success Message */}
- {success && (
-
- )}
-
- {!success ? (
-
- ) : (
-
-
-
navigate('/')}
- className="w-full h-12 text-sm font-medium"
- >
- Go to Login
-
-
-
- )}
-
- {/* Back to Login Link */}
- {!success && !reason && (
-
- )}
+
+ {/* Header */}
+
+ {/* Heading */}
+
+
+ {reason === "FIRST_LOGIN"
+ ? "Set Initial Password"
+ : reason === "EXPIRED"
+ ? "Update Expired Password"
+ : "Set New Password"}
+
- {/* Footer */}
-
-
- © 2026 QAssure. All rights reserved.
+ {/* Description */}
+
+
+ {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"}
-
+
+ {/* Success Message */}
+ {success && (
+
+ )}
+
+ {!success ? (
+
+ ) : (
+
+
+
navigate("/")}
+ className="w-full h-11 text-base font-semibold rounded-md shadow-sm"
+ >
+ Go to Login
+
+
+
+ )}
+
);
};
diff --git a/src/pages/tenant/TenantLogin.tsx b/src/pages/tenant/TenantLogin.tsx
index dc073d9..41f4eec 100644
--- a/src/pages/tenant/TenantLogin.tsx
+++ b/src/pages/tenant/TenantLogin.tsx
@@ -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
;
@@ -30,9 +30,11 @@ type LoginFormData = z.infer;
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({
resolver: zodResolver(loginSchema),
- mode: 'onBlur',
+ mode: "onBlur",
});
- const [generalError, setGeneralError] = useState('');
+ const [generalError, setGeneralError] = useState("");
const [rememberMe, setRememberMe] = useState(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 => {
- 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 (
-
- {/* Left Side - Blue Background */}
-
+ {/* Header */}
+
+ {/* Heading */}
+
+
+ Sign in to your tenant
+
+
+
+ {/* Description */}
+
+
+ Use your work email or configured SSO provider to access the admin
+ portal.
+
+
+
+
+ {/* Login Card Form */}
+