359 lines
16 KiB
TypeScript
359 lines
16 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
|
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
|
|
import Label from "../form/Label";
|
|
import Input from "../form/input/InputField";
|
|
import Checkbox from "../form/input/Checkbox";
|
|
import Button from "../ui/button/Button";
|
|
import { useAuthStore } from "../../store/authStore";
|
|
|
|
interface LogoutReason {
|
|
code: string;
|
|
message: string;
|
|
path: string;
|
|
context?: any;
|
|
timestamp: string;
|
|
source: string;
|
|
}
|
|
|
|
export default function SignInForm() {
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [isChecked, setIsChecked] = useState(false);
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [logoutReason, setLogoutReason] = useState<LogoutReason | null>(null);
|
|
const [showLogoutDetails, setShowLogoutDetails] = useState(false);
|
|
const [sessionConflict, setSessionConflict] = useState<{
|
|
message: string;
|
|
existingUser: { email: string; username: string; id: number };
|
|
requestedUser: { email: string; username: string; id: number };
|
|
} | null>(null);
|
|
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { login, loading } = useAuthStore();
|
|
|
|
// Check for logout reason on component mount
|
|
useEffect(() => {
|
|
try {
|
|
const storedReason = localStorage.getItem('logout_reason');
|
|
|
|
if (storedReason) {
|
|
const reason = JSON.parse(storedReason);
|
|
setLogoutReason(reason);
|
|
|
|
// Check if we've already logged this (prevent StrictMode duplicates)
|
|
const loggedKey = 'logout_reason_logged';
|
|
const alreadyLogged = sessionStorage.getItem(loggedKey);
|
|
|
|
if (!alreadyLogged) {
|
|
// Single consolidated log
|
|
console.error('🚨 LOGOUT:', reason.code, '-', reason.message, 'on', reason.path);
|
|
sessionStorage.setItem(loggedKey, 'true');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to read logout reason:', e);
|
|
}
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: React.FormEvent, forceLogout = false) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
setSessionConflict(null);
|
|
|
|
if (!email || !password) {
|
|
setError("Please enter both email and password");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await login(email, password, isChecked, forceLogout);
|
|
// Redirect to the page user was trying to access, or home
|
|
const from = (location.state as any)?.from?.pathname || "/";
|
|
navigate(from, { replace: true });
|
|
} catch (err: any) {
|
|
// Handle session conflict
|
|
if (err?.type === 'SESSION_CONFLICT') {
|
|
setSessionConflict({
|
|
message: err.message,
|
|
existingUser: err.existingUser,
|
|
requestedUser: err.requestedUser,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (err?.code === 'PLAN_REQUIRED') {
|
|
window.location.href = 'https://igny8.com/pricing';
|
|
return;
|
|
}
|
|
setError(err.message || "Login failed. Please check your credentials.");
|
|
}
|
|
};
|
|
|
|
const handleForceLogout = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
await handleSubmit(e, true);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col flex-1">
|
|
<div className="w-full max-w-md pt-10 mx-auto">
|
|
{/* Back link removed - users shouldn't go back to dashboard when not logged in */}
|
|
</div>
|
|
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
|
|
<div>
|
|
<div className="mb-5 sm:mb-8">
|
|
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
|
|
Sign In
|
|
</h1>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Enter your email and password to sign in!
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
|
|
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
|
<svg
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 20 20"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M18.7511 10.1944C18.7511 9.47495 18.6915 8.94995 18.5626 8.40552H10.1797V11.6527H15.1003C15.0011 12.4597 14.4654 13.675 13.2749 14.4916L13.2582 14.6003L15.9087 16.6126L16.0924 16.6305C17.7788 15.1041 18.7511 12.8583 18.7511 10.1944Z"
|
|
fill="#4285F4"
|
|
/>
|
|
<path
|
|
d="M10.1788 18.75C12.5895 18.75 14.6133 17.9722 16.0915 16.6305L13.274 14.4916C12.5201 15.0068 11.5081 15.3666 10.1788 15.3666C7.81773 15.3666 5.81379 13.8402 5.09944 11.7305L4.99473 11.7392L2.23868 13.8295L2.20264 13.9277C3.67087 16.786 6.68674 18.75 10.1788 18.75Z"
|
|
fill="#34A853"
|
|
/>
|
|
<path
|
|
d="M5.10014 11.7305C4.91165 11.186 4.80257 10.6027 4.80257 9.99992C4.80257 9.3971 4.91165 8.81379 5.09022 8.26935L5.08523 8.1534L2.29464 6.02954L2.20333 6.0721C1.5982 7.25823 1.25098 8.5902 1.25098 9.99992C1.25098 11.4096 1.5982 12.7415 2.20333 13.9277L5.10014 11.7305Z"
|
|
fill="#FBBC05"
|
|
/>
|
|
<path
|
|
d="M10.1789 4.63331C11.8554 4.63331 12.9864 5.34303 13.6312 5.93612L16.1511 3.525C14.6035 2.11528 12.5895 1.25 10.1789 1.25C6.68676 1.25 3.67088 3.21387 2.20264 6.07218L5.08953 8.26943C5.81381 6.15972 7.81776 4.63331 10.1789 4.63331Z"
|
|
fill="#EB4335"
|
|
/>
|
|
</svg>
|
|
Sign in with Google
|
|
</Button>
|
|
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
|
<svg
|
|
width="21"
|
|
className="fill-current"
|
|
height="20"
|
|
viewBox="0 0 21 20"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
|
|
</svg>
|
|
Sign in with X
|
|
</Button>
|
|
</div>
|
|
<div className="relative py-3 sm:py-5">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-gray-200 dark:border-gray-800"></div>
|
|
</div>
|
|
<div className="relative flex justify-center text-sm">
|
|
<span className="p-2 text-gray-400 bg-white dark:bg-gray-900 sm:px-5 sm:py-2">
|
|
Or
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="space-y-6">
|
|
{/* Logout Reason Display */}
|
|
{logoutReason && (
|
|
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg dark:bg-warning-900/20 dark:border-warning-800">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<svg className="w-5 h-5 text-warning-600 dark:text-warning-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
<h4 className="font-semibold text-warning-800 dark:text-warning-300">
|
|
Session Ended
|
|
</h4>
|
|
</div>
|
|
<p className="text-sm text-warning-700 dark:text-warning-400 mb-2">
|
|
{logoutReason.message}
|
|
</p>
|
|
{logoutReason.path && logoutReason.path !== '/signin' && (
|
|
<p className="text-xs text-warning-600 dark:text-warning-500">
|
|
Original page: <span className="font-mono">{logoutReason.path}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowLogoutDetails(!showLogoutDetails)}
|
|
className="ml-2 p-1 text-warning-600 hover:text-warning-800 dark:text-warning-400 dark:hover:text-warning-300"
|
|
title="Toggle technical details"
|
|
>
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Expandable Technical Details */}
|
|
{showLogoutDetails && (
|
|
<div className="mt-3 pt-3 border-t border-warning-300 dark:border-warning-700">
|
|
<p className="text-xs font-semibold text-warning-800 dark:text-warning-300 mb-2">
|
|
Technical Details:
|
|
</p>
|
|
<div className="space-y-1 text-xs font-mono text-warning-700 dark:text-warning-400">
|
|
<div><span className="font-bold">Code:</span> {logoutReason.code}</div>
|
|
<div><span className="font-bold">Source:</span> {logoutReason.source}</div>
|
|
<div><span className="font-bold">Time:</span> {new Date(logoutReason.timestamp).toLocaleString()}</div>
|
|
{logoutReason.context && Object.keys(logoutReason.context).length > 0 && (
|
|
<div className="mt-2">
|
|
<span className="font-bold">Context:</span>
|
|
<pre className="mt-1 p-2 bg-warning-100 dark:bg-warning-900/30 rounded text-xs overflow-x-auto">
|
|
{JSON.stringify(logoutReason.context, null, 2)}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Session Conflict Alert */}
|
|
{sessionConflict && (
|
|
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg dark:bg-warning-900/20 dark:border-warning-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<svg className="w-5 h-5 text-warning-600 dark:text-warning-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
<h4 className="font-semibold text-warning-800 dark:text-warning-300">
|
|
Active Session Detected
|
|
</h4>
|
|
</div>
|
|
<p className="text-sm text-warning-700 dark:text-warning-400 mb-2">
|
|
You have an active session for:
|
|
</p>
|
|
<p className="text-sm font-semibold text-warning-800 dark:text-warning-300 mb-3">
|
|
{sessionConflict.existingUser.email}
|
|
</p>
|
|
<p className="text-sm text-warning-700 dark:text-warning-400 mb-4">
|
|
You're trying to login as: <strong>{sessionConflict.requestedUser.email}</strong>
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
tone="warning"
|
|
size="sm"
|
|
onClick={handleForceLogout}
|
|
disabled={loading}
|
|
className="flex-1"
|
|
>
|
|
{loading ? 'Logging out...' : 'Logout Previous & Continue'}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
tone="warning"
|
|
size="sm"
|
|
onClick={() => setSessionConflict(null)}
|
|
className="flex-1"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="p-3 text-sm text-error-600 bg-error-50 border border-error-200 rounded-lg dark:bg-error-900/20 dark:text-error-400 dark:border-error-800">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<Label>
|
|
Email <span className="text-error-500">*</span>{" "}
|
|
</Label>
|
|
<Input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="info@gmail.com"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>
|
|
Password <span className="text-error-500">*</span>{" "}
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
type={showPassword ? "text" : "password"}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Enter your password"
|
|
required
|
|
/>
|
|
<span
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
|
>
|
|
{showPassword ? (
|
|
<EyeIcon className="fill-gray-500 dark:fill-gray-400 size-5" />
|
|
) : (
|
|
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400 size-5" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Checkbox checked={isChecked} onChange={setIsChecked} />
|
|
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
|
|
Keep me logged in
|
|
</span>
|
|
</div>
|
|
<Link
|
|
to="/reset-password"
|
|
className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
|
>
|
|
Forgot password?
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
<Button
|
|
className="w-full"
|
|
size="sm"
|
|
type="submit"
|
|
disabled={loading}
|
|
>
|
|
{loading ? "Signing in..." : "Sign in"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<div className="mt-5">
|
|
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
|
Don't have an account? {""}
|
|
<Link
|
|
to="/signup"
|
|
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
|
>
|
|
Sign Up
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|