logo out issues fixes
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
|
||||
import Label from "../form/Label";
|
||||
@@ -7,16 +7,51 @@ 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 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) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
@@ -27,7 +62,7 @@ export default function SignInForm() {
|
||||
}
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
await login(email, password, isChecked);
|
||||
// Redirect to the page user was trying to access, or home
|
||||
const from = (location.state as any)?.from?.pathname || "/";
|
||||
navigate(from, { replace: true });
|
||||
@@ -109,6 +144,64 @@ export default function SignInForm() {
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-6">
|
||||
{/* Logout Reason Display */}
|
||||
{logoutReason && (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-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-yellow-600 dark:text-yellow-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-yellow-800 dark:text-yellow-300">
|
||||
Session Ended
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-400 mb-2">
|
||||
{logoutReason.message}
|
||||
</p>
|
||||
{logoutReason.path && logoutReason.path !== '/signin' && (
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-500">
|
||||
Original page: <span className="font-mono">{logoutReason.path}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLogoutDetails(!showLogoutDetails)}
|
||||
className="ml-2 p-1 text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-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-yellow-300 dark:border-yellow-700">
|
||||
<p className="text-xs font-semibold text-yellow-800 dark:text-yellow-300 mb-2">
|
||||
Technical Details:
|
||||
</p>
|
||||
<div className="space-y-1 text-xs font-mono text-yellow-700 dark:text-yellow-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-yellow-100 dark:bg-yellow-900/30 rounded text-xs overflow-x-auto">
|
||||
{JSON.stringify(logoutReason.context, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg dark:bg-red-900/20 dark:text-red-400 dark:border-red-800">
|
||||
{error}
|
||||
|
||||
@@ -195,6 +195,27 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
// Don't logout for permission errors or plan issues
|
||||
const authState = useAuthStore.getState();
|
||||
if (authState?.isAuthenticated || authState?.token) {
|
||||
const logoutReasonData = {
|
||||
code: 'AUTH_CREDENTIALS_MISSING',
|
||||
message: errorMessage,
|
||||
path: window.location.pathname,
|
||||
context: {
|
||||
errorData,
|
||||
hasToken: !!authState?.token,
|
||||
isAuthenticated: authState?.isAuthenticated
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'api_403_auth_error'
|
||||
};
|
||||
console.error('🚨 LOGOUT TRIGGERED - Authentication Credentials Missing:', logoutReasonData);
|
||||
|
||||
// Store logout reason before logout
|
||||
try {
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutReasonData));
|
||||
} catch (e) {
|
||||
console.warn('Failed to store logout reason:', e);
|
||||
}
|
||||
|
||||
console.warn('Authentication credentials missing - forcing logout');
|
||||
const { logout } = useAuthStore.getState();
|
||||
logout();
|
||||
@@ -244,6 +265,50 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
|
||||
// Handle 401 Unauthorized - try to refresh token
|
||||
if (response.status === 401) {
|
||||
// Parse error to check for logout reason from backend
|
||||
let logoutReason = null;
|
||||
try {
|
||||
const errorData = text ? JSON.parse(text) : null;
|
||||
if (errorData?.logout_reason) {
|
||||
logoutReason = {
|
||||
code: errorData.logout_reason,
|
||||
message: errorData.logout_message || errorData.error,
|
||||
path: errorData.logout_path || window.location.pathname,
|
||||
context: errorData.logout_context || {},
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'backend_middleware'
|
||||
};
|
||||
console.error('🚨 BACKEND FORCED LOGOUT:', logoutReason);
|
||||
|
||||
// CRITICAL: Store logout reason IMMEDIATELY
|
||||
try {
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutReason));
|
||||
console.error('✅ Stored backend logout reason');
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to store logout reason:', e);
|
||||
}
|
||||
|
||||
// If backend explicitly logged us out (session contamination, etc),
|
||||
// DON'T try to refresh - respect the forced logout
|
||||
console.error('⛔ Backend forced logout - not attempting token refresh');
|
||||
const { logout } = useAuthStore.getState();
|
||||
logout();
|
||||
|
||||
// Throw error to stop request processing
|
||||
let err: any = new Error(errorData.error || 'Session ended');
|
||||
err.status = 401;
|
||||
err.data = errorData;
|
||||
throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
// If we just threw the error above, re-throw it
|
||||
if (e instanceof Error && (e as any).status === 401) {
|
||||
throw e;
|
||||
}
|
||||
console.warn('Failed to parse logout reason from 401 response:', e);
|
||||
}
|
||||
|
||||
// No explicit logout reason from backend, try token refresh
|
||||
const refreshToken = getRefreshToken();
|
||||
if (refreshToken) {
|
||||
try {
|
||||
@@ -318,12 +383,49 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// Refresh failed, clear auth state and force re-login
|
||||
const logoutReasonData = {
|
||||
code: 'TOKEN_REFRESH_FAILED',
|
||||
message: 'Token refresh failed - session expired',
|
||||
path: window.location.pathname,
|
||||
context: {
|
||||
error: refreshError instanceof Error ? refreshError.message : String(refreshError),
|
||||
endpoint,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'token_refresh_failure'
|
||||
};
|
||||
console.error('🚨 LOGOUT TRIGGERED - Token Refresh Failed:', logoutReasonData);
|
||||
|
||||
// Store logout reason before logout
|
||||
try {
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutReasonData));
|
||||
} catch (e) {
|
||||
console.warn('Failed to store logout reason:', e);
|
||||
}
|
||||
|
||||
const { logout } = useAuthStore.getState();
|
||||
logout();
|
||||
throw refreshError;
|
||||
}
|
||||
} else {
|
||||
// No refresh token available, clear auth state
|
||||
const logoutReasonData = {
|
||||
code: 'NO_REFRESH_TOKEN',
|
||||
message: 'No refresh token available - please login again',
|
||||
path: window.location.pathname,
|
||||
context: { endpoint },
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'missing_refresh_token'
|
||||
};
|
||||
console.error('🚨 LOGOUT TRIGGERED - No Refresh Token:', logoutReasonData);
|
||||
|
||||
// Store logout reason before logout
|
||||
try {
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutReasonData));
|
||||
} catch (e) {
|
||||
console.warn('Failed to store logout reason:', e);
|
||||
}
|
||||
|
||||
const { logout } = useAuthStore.getState();
|
||||
logout();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
isAuthenticated: false,
|
||||
loading: false, // Always start with loading false - will be set true only during login/register
|
||||
|
||||
login: async (email, password) => {
|
||||
login: async (email, password, rememberMe = false) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
||||
@@ -63,7 +63,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
body: JSON.stringify({ email, password, remember_me: rememberMe }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -144,6 +144,37 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
// Check if there's already a logout reason from automatic logout
|
||||
const existingReason = localStorage.getItem('logout_reason');
|
||||
|
||||
if (!existingReason) {
|
||||
// Only store manual logout reason if no automatic reason exists
|
||||
const currentPath = typeof window !== 'undefined' ? window.location.pathname : 'unknown';
|
||||
const logoutContext = {
|
||||
code: 'MANUAL_LOGOUT',
|
||||
message: 'You have been logged out',
|
||||
path: currentPath,
|
||||
context: {
|
||||
user: get().user?.email || 'unknown',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'manual_user_action'
|
||||
};
|
||||
|
||||
console.error('🚪 MANUAL LOGOUT from page:', currentPath);
|
||||
|
||||
try {
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutContext));
|
||||
console.error('✅ Stored manual logout_reason');
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to store logout reason:', e);
|
||||
}
|
||||
} else {
|
||||
console.error('⚠️ Automatic logout reason already exists, not overwriting with manual logout');
|
||||
console.error('Existing reason:', existingReason);
|
||||
}
|
||||
|
||||
// CRITICAL: Properly clear ALL cookies to prevent session contamination
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
@@ -157,8 +188,9 @@ export const useAuthStore = create<AuthState>()(
|
||||
}
|
||||
|
||||
// IMPORTANT: Selectively clear auth-related localStorage items
|
||||
// DO NOT clear 'logout_reason' - it needs to persist for signin page display!
|
||||
// DO NOT use localStorage.clear() as it breaks Zustand persist middleware
|
||||
const authKeys = ['auth-storage', 'auth-store', 'site-storage', 'sector-storage', 'billing-storage'];
|
||||
const authKeys = ['auth-storage', 'auth-store', 'site-storage', 'sector-storage', 'billing-storage', 'access_token', 'refresh_token'];
|
||||
authKeys.forEach(key => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
@@ -389,9 +421,36 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
const refreshedUser = response.user;
|
||||
if (!refreshedUser.account) {
|
||||
const logoutReasonData = {
|
||||
code: 'ACCOUNT_REQUIRED',
|
||||
message: 'Account not configured for this user. Please contact support.',
|
||||
path: typeof window !== 'undefined' ? window.location.pathname : 'unknown',
|
||||
context: {
|
||||
user_email: refreshedUser.email,
|
||||
user_id: refreshedUser.id
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'refresh_user_validation'
|
||||
};
|
||||
console.error('🚨 LOGOUT TRIGGERED - Account Required:', logoutReasonData);
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutReasonData));
|
||||
throw createAuthError('Account not configured for this user. Please contact support.', 'ACCOUNT_REQUIRED');
|
||||
}
|
||||
if (!refreshedUser.account.plan) {
|
||||
const logoutReasonData = {
|
||||
code: 'PLAN_REQUIRED',
|
||||
message: 'Active subscription required. Visit igny8.com/pricing to subscribe.',
|
||||
path: typeof window !== 'undefined' ? window.location.pathname : 'unknown',
|
||||
context: {
|
||||
user_email: refreshedUser.email,
|
||||
account_name: refreshedUser.account.name,
|
||||
account_id: refreshedUser.account.id
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'refresh_user_validation'
|
||||
};
|
||||
console.error('🚨 LOGOUT TRIGGERED - Plan Required:', logoutReasonData);
|
||||
localStorage.setItem('logout_reason', JSON.stringify(logoutReasonData));
|
||||
throw createAuthError('Active subscription required. Visit igny8.com/pricing to subscribe.', 'PLAN_REQUIRED');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user