Files
patchmon.net/frontend/src/pages/Login.jsx
Muhammad Ibrahim 48ce1951de fix: Resolve all linting errors
- Remove unused imports and variables in metricsRoutes.js
- Prefix unused error variables with underscore
- Fix useEffect dependency in Login.jsx
- Add aria-label and title to all SVG elements for accessibility
2025-10-28 16:06:36 +00:00

914 lines
29 KiB
JavaScript

import {
AlertCircle,
ArrowLeft,
BookOpen,
Eye,
EyeOff,
Github,
Globe,
Lock,
Mail,
Route,
Star,
User,
} from "lucide-react";
import { useEffect, useId, useRef, useState } from "react";
import { FaReddit, FaYoutube } from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import trianglify from "trianglify";
import DiscordIcon from "../components/DiscordIcon";
import { useAuth } from "../contexts/AuthContext";
import { useColorTheme } from "../contexts/ColorThemeContext";
import { authAPI, isCorsError } from "../utils/api";
const Login = () => {
const usernameId = useId();
const firstNameId = useId();
const lastNameId = useId();
const emailId = useId();
const passwordId = useId();
const tokenId = useId();
const rememberMeId = useId();
const { login, setAuthState } = useAuth();
const [isSignupMode, setIsSignupMode] = useState(false);
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
firstName: "",
lastName: "",
});
const [tfaData, setTfaData] = useState({
token: "",
remember_me: false,
});
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [requiresTfa, setRequiresTfa] = useState(false);
const [tfaUsername, setTfaUsername] = useState("");
const [signupEnabled, setSignupEnabled] = useState(false);
const [latestRelease, setLatestRelease] = useState(null);
const [githubStars, setGithubStars] = useState(null);
const canvasRef = useRef(null);
const { themeConfig } = useColorTheme();
const navigate = useNavigate();
// Generate Trianglify background based on selected theme
useEffect(() => {
const generateBackground = () => {
if (canvasRef.current && themeConfig?.login) {
// Get current date as seed for daily variation
const today = new Date();
const dateSeed = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
// Generate pattern with selected theme configuration
const pattern = trianglify({
width: canvasRef.current.offsetWidth,
height: canvasRef.current.offsetHeight,
cellSize: themeConfig.login.cellSize,
variance: themeConfig.login.variance,
seed: dateSeed,
xColors: themeConfig.login.xColors,
yColors: themeConfig.login.yColors,
});
// Render to canvas
pattern.toCanvas(canvasRef.current);
}
};
generateBackground();
// Regenerate on window resize
const handleResize = () => {
generateBackground();
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [themeConfig]); // Regenerate when theme changes
// Check if signup is enabled
useEffect(() => {
const checkSignupEnabled = async () => {
try {
const response = await fetch("/api/v1/auth/signup-enabled");
if (response.ok) {
const data = await response.json();
setSignupEnabled(data.signupEnabled);
}
} catch (error) {
console.error("Failed to check signup status:", error);
// Default to disabled on error for security
setSignupEnabled(false);
}
};
checkSignupEnabled();
}, []);
// Fetch latest release and stars from GitHub
useEffect(() => {
const fetchGitHubData = async () => {
try {
// Try to get cached data first
const cachedRelease = localStorage.getItem("githubLatestRelease");
const cachedStars = localStorage.getItem("githubStarsCount");
const cacheTime = localStorage.getItem("githubReleaseCacheTime");
const now = Date.now();
// Load cached data immediately
if (cachedRelease) {
setLatestRelease(JSON.parse(cachedRelease));
}
if (cachedStars) {
setGithubStars(parseInt(cachedStars, 10));
}
// Use cache if less than 1 hour old
if (cacheTime && now - parseInt(cacheTime, 10) < 3600000) {
return;
}
// Fetch repository info (includes star count)
const repoResponse = await fetch(
"https://api.github.com/repos/PatchMon/PatchMon",
{
headers: {
Accept: "application/vnd.github.v3+json",
},
},
);
if (repoResponse.ok) {
const repoData = await repoResponse.json();
setGithubStars(repoData.stargazers_count);
localStorage.setItem(
"githubStarsCount",
repoData.stargazers_count.toString(),
);
}
// Fetch latest release
const releaseResponse = await fetch(
"https://api.github.com/repos/PatchMon/PatchMon/releases/latest",
{
headers: {
Accept: "application/vnd.github.v3+json",
},
},
);
if (releaseResponse.ok) {
const data = await releaseResponse.json();
const releaseInfo = {
version: data.tag_name,
name: data.name,
publishedAt: new Date(data.published_at).toLocaleDateString(
"en-US",
{
year: "numeric",
month: "long",
day: "numeric",
},
),
body: data.body?.split("\n").slice(0, 3).join("\n") || "", // First 3 lines
};
setLatestRelease(releaseInfo);
localStorage.setItem(
"githubLatestRelease",
JSON.stringify(releaseInfo),
);
}
localStorage.setItem("githubReleaseCacheTime", now.toString());
} catch (error) {
console.error("Failed to fetch GitHub data:", error);
// Set fallback data if nothing cached
if (!latestRelease) {
setLatestRelease({
version: "v1.3.0",
name: "Latest Release",
publishedAt: "Recently",
body: "Monitor and manage your Linux package updates",
});
}
}
};
fetchGitHubData();
}, [latestRelease]);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError("");
try {
// Use the AuthContext login function which handles everything
const result = await login(formData.username, formData.password);
if (result.requiresTfa) {
setRequiresTfa(true);
setTfaUsername(formData.username);
setError("");
} else if (result.success) {
navigate("/");
} else {
setError(result.error || "Login failed");
}
} catch (err) {
// Check for CORS/network errors first
if (isCorsError(err)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
err.name === "TypeError" &&
err.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
setError(err.response?.data?.error || "Login failed");
}
} finally {
setIsLoading(false);
}
};
const handleSignupSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError("");
try {
const response = await authAPI.signup(
formData.username,
formData.email,
formData.password,
formData.firstName,
formData.lastName,
);
if (response.data?.token) {
// Update AuthContext state and localStorage
setAuthState(response.data.token, response.data.user);
// Redirect to dashboard
navigate("/");
} else {
setError("Signup failed - invalid response");
}
} catch (err) {
console.error("Signup error:", err);
if (isCorsError(err)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
err.name === "TypeError" &&
err.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
const errorMessage =
err.response?.data?.error ||
(err.response?.data?.errors && err.response.data.errors.length > 0
? err.response.data.errors.map((e) => e.msg).join(", ")
: err.message || "Signup failed");
setError(errorMessage);
}
} finally {
setIsLoading(false);
}
};
const handleTfaSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError("");
try {
const response = await authAPI.verifyTfa(
tfaUsername,
tfaData.token,
tfaData.remember_me,
);
if (response.data?.token) {
// Update AuthContext with the new authentication state
setAuthState(response.data.token, response.data.user);
// Redirect to dashboard
navigate("/");
} else {
setError("TFA verification failed - invalid response");
}
} catch (err) {
console.error("TFA verification error:", err);
if (isCorsError(err)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
err.name === "TypeError" &&
err.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
const errorMessage =
err.response?.data?.error || err.message || "TFA verification failed";
setError(errorMessage);
}
// Clear the token input for security
setTfaData({ token: "" });
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleTfaInputChange = (e) => {
const { name, value, type, checked } = e.target;
setTfaData({
...tfaData,
[name]:
type === "checkbox" ? checked : value.replace(/\D/g, "").slice(0, 6),
});
// Clear error when user starts typing
if (error) {
setError("");
}
};
const handleBackToLogin = () => {
setRequiresTfa(false);
setTfaData({ token: "", remember_me: false });
setError("");
};
const toggleMode = () => {
// Only allow signup mode if signup is enabled
if (!signupEnabled && !isSignupMode) {
return; // Don't allow switching to signup if disabled
}
setIsSignupMode(!isSignupMode);
setFormData({
username: "",
email: "",
password: "",
firstName: "",
lastName: "",
});
setError("");
};
return (
<div className="min-h-screen relative flex">
{/* Full-screen Trianglify Background */}
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
<div className="absolute inset-0 bg-gradient-to-br from-black/40 to-black/60" />
{/* Left side - Info Panel (hidden on mobile) */}
<div className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative z-10">
<div className="flex flex-col justify-between text-white p-12 h-full w-full">
<div className="flex-1 flex flex-col justify-center items-start max-w-xl mx-auto">
<div className="space-y-6">
<div>
<img
src="/assets/logo_dark.png"
alt="PatchMon"
className="h-16 mb-4"
/>
<p className="text-sm text-blue-200 font-medium tracking-wide uppercase">
Linux Patch Monitoring
</p>
</div>
{latestRelease ? (
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-green-300 text-sm font-semibold">
Latest Release
</span>
</div>
<span className="text-2xl font-bold text-white">
{latestRelease.version}
</span>
</div>
{latestRelease.name && (
<h3 className="text-lg font-semibold text-white">
{latestRelease.name}
</h3>
)}
<div className="flex items-center gap-2 text-sm text-gray-300">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-label="Release date"
>
<title>Release date</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>Released {latestRelease.publishedAt}</span>
</div>
{latestRelease.body && (
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
{latestRelease.body}
</p>
)}
<a
href="https://github.com/PatchMon/PatchMon/releases/latest"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-blue-300 hover:text-blue-200 transition-colors font-medium"
>
View Release Notes
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-label="External link"
>
<title>External link</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
) : (
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
<div className="animate-pulse space-y-3">
<div className="h-6 bg-white/20 rounded w-3/4" />
<div className="h-4 bg-white/20 rounded w-1/2" />
<div className="h-4 bg-white/20 rounded w-full" />
</div>
</div>
)}
</div>
</div>
{/* Social Links Footer */}
<div className="max-w-xl mx-auto w-full">
<div className="border-t border-white/10 pt-6">
<p className="text-sm text-gray-400 mb-4">Connect with us</p>
<div className="flex flex-wrap items-center gap-2">
{/* GitHub */}
<a
href="https://github.com/PatchMon/PatchMon"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1.5 px-3 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="GitHub Repository"
>
<Github className="h-5 w-5 text-white" />
{githubStars !== null && (
<div className="flex items-center gap-1">
<Star className="h-3.5 w-3.5 fill-current text-yellow-400" />
<span className="text-sm font-medium text-white">
{githubStars}
</span>
</div>
)}
</a>
{/* Roadmap */}
<a
href="https://github.com/orgs/PatchMon/projects/2/views/1"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Roadmap"
>
<Route className="h-5 w-5 text-white" />
</a>
{/* Docs */}
<a
href="https://docs.patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Documentation"
>
<BookOpen className="h-5 w-5 text-white" />
</a>
{/* Discord */}
<a
href="https://patchmon.net/discord"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Discord Community"
>
<DiscordIcon className="h-5 w-5 text-white" />
</a>
{/* Email */}
<a
href="mailto:support@patchmon.net"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Email Support"
>
<Mail className="h-5 w-5 text-white" />
</a>
{/* YouTube */}
<a
href="https://youtube.com/@patchmonTV"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="YouTube Channel"
>
<FaYoutube className="h-5 w-5 text-white" />
</a>
{/* Reddit */}
<a
href="https://www.reddit.com/r/patchmon"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Reddit Community"
>
<FaReddit className="h-5 w-5 text-white" />
</a>
{/* Website */}
<a
href="https://patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Visit patchmon.net"
>
<Globe className="h-5 w-5 text-white" />
</a>
</div>
</div>
</div>
</div>
</div>
{/* Right side - Login Form */}
<div className="flex-1 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
<div className="max-w-md w-full space-y-8 bg-white dark:bg-secondary-900 rounded-2xl shadow-2xl p-8 lg:p-10">
<div>
<div className="mx-auto h-16 w-16 flex items-center justify-center">
<img
src="/assets/favicon.svg"
alt="PatchMon Logo"
className="h-16 w-16"
/>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-secondary-900 dark:text-secondary-100">
{isSignupMode ? "Create PatchMon Account" : "Sign in to PatchMon"}
</h2>
<p className="mt-2 text-center text-sm text-secondary-600 dark:text-secondary-400">
Monitor and manage your Linux package updates
</p>
</div>
{!requiresTfa ? (
<form
className="mt-8 space-y-6"
onSubmit={isSignupMode ? handleSignupSubmit : handleSubmit}
>
<div className="space-y-4">
<div>
<label
htmlFor={usernameId}
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
>
{isSignupMode ? "Username" : "Username or Email"}
</label>
<div className="mt-1 relative">
<input
id={usernameId}
name="username"
type="text"
required
value={formData.username}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder={
isSignupMode
? "Enter your username"
: "Enter your username or email"
}
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
<User size={20} color="#64748b" strokeWidth={2} />
</div>
</div>
</div>
{isSignupMode && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label
htmlFor={firstNameId}
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
>
First Name
</label>
<div className="mt-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-secondary-400" />
</div>
<input
id={firstNameId}
name="firstName"
type="text"
required
value={formData.firstName}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Enter your first name"
/>
</div>
</div>
<div>
<label
htmlFor={lastNameId}
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
>
Last Name
</label>
<div className="mt-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-secondary-400" />
</div>
<input
id={lastNameId}
name="lastName"
type="text"
required
value={formData.lastName}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Enter your last name"
/>
</div>
</div>
</div>
<div>
<label
htmlFor={emailId}
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
>
Email
</label>
<div className="mt-1 relative">
<input
id={emailId}
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Enter your email"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
<Mail size={20} color="#64748b" strokeWidth={2} />
</div>
</div>
</div>
</>
)}
<div>
<label
htmlFor={passwordId}
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
>
Password
</label>
<div className="mt-1 relative">
<input
id={passwordId}
name="password"
type={showPassword ? "text" : "password"}
required
value={formData.password}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-10 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Enter your password"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
<Lock size={20} color="#64748b" strokeWidth={2} />
</div>
<div className="absolute right-3 top-1/2 -translate-y-1/2 z-20 flex items-center">
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="bg-transparent border-none cursor-pointer p-1 flex items-center justify-center"
>
{showPassword ? (
<EyeOff size={20} color="#64748b" strokeWidth={2} />
) : (
<Eye size={20} color="#64748b" strokeWidth={2} />
)}
</button>
</div>
</div>
</div>
</div>
{error && (
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
<div className="flex">
<AlertCircle size={20} color="#dc2626" strokeWidth={2} />
<div className="ml-3">
<p className="text-sm text-danger-700">{error}</p>
</div>
</div>
</div>
)}
<div>
<button
type="submit"
disabled={isLoading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
{isSignupMode ? "Creating account..." : "Signing in..."}
</div>
) : isSignupMode ? (
"Create Account"
) : (
"Sign in"
)}
</button>
</div>
{signupEnabled && (
<div className="text-center">
<p className="text-sm text-secondary-700 dark:text-secondary-300">
{isSignupMode
? "Already have an account?"
: "Don't have an account?"}{" "}
<button
type="button"
onClick={toggleMode}
className="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 focus:outline-none focus:underline"
>
{isSignupMode ? "Sign in" : "Sign up"}
</button>
</p>
</div>
)}
</form>
) : (
<form className="mt-8 space-y-6" onSubmit={handleTfaSubmit}>
<div className="text-center">
<div className="mx-auto h-16 w-16 flex items-center justify-center">
<img
src="/assets/favicon.svg"
alt="PatchMon Logo"
className="h-16 w-16"
/>
</div>
<h3 className="mt-4 text-lg font-medium text-secondary-900 dark:text-secondary-100">
Two-Factor Authentication
</h3>
<p className="mt-2 text-sm text-secondary-600 dark:text-secondary-400">
Enter the 6-digit code from your authenticator app
</p>
</div>
<div>
<label
htmlFor={tokenId}
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
>
Verification Code
</label>
<div className="mt-1">
<input
id={tokenId}
name="token"
type="text"
required
value={tfaData.token}
onChange={handleTfaInputChange}
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm text-center text-lg font-mono tracking-widest"
placeholder="000000"
maxLength="6"
/>
</div>
</div>
<div className="flex items-center">
<input
id={rememberMeId}
name="remember_me"
type="checkbox"
checked={tfaData.remember_me}
onChange={handleTfaInputChange}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
/>
<label
htmlFor={rememberMeId}
className="ml-2 block text-sm text-secondary-900 dark:text-secondary-200"
>
Remember me on this computer (skip TFA for 30 days)
</label>
</div>
{error && (
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
<div className="flex">
<AlertCircle size={20} color="#dc2626" strokeWidth={2} />
<div className="ml-3">
<p className="text-sm text-danger-700">{error}</p>
</div>
</div>
</div>
)}
<div className="space-y-3">
<button
type="submit"
disabled={isLoading || tfaData.token.length !== 6}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Verifying...
</div>
) : (
"Verify Code"
)}
</button>
<button
type="button"
onClick={handleBackToLogin}
className="group relative w-full flex justify-center py-2 px-4 border border-secondary-300 dark:border-secondary-600 text-sm font-medium rounded-md text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-800 hover:bg-secondary-50 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 items-center gap-2"
>
<ArrowLeft
size={16}
className="text-secondary-700 dark:text-secondary-200"
strokeWidth={2}
/>
Back to Login
</button>
</div>
<div className="text-center">
<p className="text-sm text-secondary-600 dark:text-secondary-400">
Don't have access to your authenticator? Use a backup code.
</p>
</div>
</form>
)}
</div>
</div>
</div>
);
};
export default Login;