Fix login race condition.
All checks were successful
homepanel/HomePanel/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-02-02 01:27:42 +00:00
parent f7b4f9e8e4
commit fc89bba581
3 changed files with 39 additions and 7 deletions

View File

@ -16,7 +16,7 @@ import PasswordResetHelp from "./PasswordResetHelp";
function Login() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const { login, isLoading, isAuthenticated, passwordIsSet, siteName } =
const { login, isLoading, isAuthenticated, passwordIsSet, siteName, lastLoggedIn } =
useAuth();
const navigate = useNavigate();
const location = useLocation();
@ -38,6 +38,17 @@ function Login() {
}
}, [isAuthenticated, isLoading, navigate, from]);
// Navigate after successful login using lastLoggedIn timestamp
useEffect(() => {
if (lastLoggedIn && !isLoading) {
// Small delay to ensure state has propagated
const timer = setTimeout(() => {
navigate(from, { replace: true });
}, 50);
return () => clearTimeout(timer);
}
}, [lastLoggedIn, isLoading, navigate, from]);
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
@ -48,12 +59,10 @@ function Login() {
}
const result = await login(password);
if (result.success) {
// Navigation will be handled by the useEffect when isAuthenticated becomes true
// No need to navigate here - the useEffect will catch the state change
} else {
if (!result.success) {
setError(result.error || "Invalid password");
}
// Navigation will be handled by the useEffect when lastLoggedIn is set
};
return (

View File

@ -3,7 +3,7 @@ import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "@contexts/AuthContext";
function ProtectedRoute({ children }) {
const { isAuthenticated, isLoading } = useAuth();
const { isAuthenticated, isLoading, lastLoggedIn } = useAuth();
const location = useLocation();
if (isLoading) {
@ -11,8 +11,13 @@ function ProtectedRoute({ children }) {
return <div>Loading...</div>;
}
// Check if user just logged in (within last 2 seconds) to handle race conditions
const justLoggedIn = lastLoggedIn && (Date.now() - lastLoggedIn) < 2000;
// Check both state and sessionStorage to handle race conditions after login
const isAuth = isAuthenticated || sessionStorage.getItem("adminAuthenticated") === "true";
const isAuth = isAuthenticated ||
sessionStorage.getItem("adminAuthenticated") === "true" ||
justLoggedIn;
if (!isAuth) {
// Redirect to login page with the current location as state

View File

@ -32,6 +32,11 @@ export const AuthProvider = ({ children }) => {
// Get session token from storage if available
return sessionStorage.getItem("adminSessionToken") || null;
});
const [lastLoggedIn, setLastLoggedIn] = useState(() => {
// Get last login timestamp from storage if available
const stored = sessionStorage.getItem("adminLastLoggedIn");
return stored ? parseInt(stored, 10) : null;
});
const [passwordIsSet, setPasswordIsSet] = useState(true); // Default to true to avoid redirect loop
const [siteName, setSiteName] = useState(null);
const [isLoading, setIsLoading] = useState(false);
@ -58,6 +63,10 @@ export const AuthProvider = ({ children }) => {
setSessionToken(data.data.sessionToken);
sessionStorage.setItem("adminSessionToken", data.data.sessionToken);
}
// Set lastLoggedIn timestamp to handle race conditions
const loginTime = Date.now();
setLastLoggedIn(loginTime);
sessionStorage.setItem("adminLastLoggedIn", loginTime.toString());
return { success: true };
} else {
return {
@ -78,8 +87,10 @@ export const AuthProvider = ({ children }) => {
const logout = useCallback(async () => {
setIsAuthenticated(false);
setSessionToken(null);
setLastLoggedIn(null);
sessionStorage.removeItem("adminAuthenticated");
sessionStorage.removeItem("adminSessionToken");
sessionStorage.removeItem("adminLastLoggedIn");
// Notify server about logout
try {
await fetch(`${getApiUrl()}/logout`, {
@ -156,8 +167,10 @@ export const AuthProvider = ({ children }) => {
} else {
setIsAuthenticated(false);
setSessionToken(null);
setLastLoggedIn(null);
sessionStorage.removeItem("adminAuthenticated");
sessionStorage.removeItem("adminSessionToken");
sessionStorage.removeItem("adminLastLoggedIn");
}
}
}
@ -166,8 +179,10 @@ export const AuthProvider = ({ children }) => {
if (isAuthenticated) {
setIsAuthenticated(false);
setSessionToken(null);
setLastLoggedIn(null);
sessionStorage.removeItem("adminAuthenticated");
sessionStorage.removeItem("adminSessionToken");
sessionStorage.removeItem("adminLastLoggedIn");
}
});
}, [isAuthenticated]); // Only run on mount or when isAuthenticated changes
@ -175,8 +190,10 @@ export const AuthProvider = ({ children }) => {
const setUnauthorized = useCallback(() => {
setIsAuthenticated(false);
setSessionToken(null);
setLastLoggedIn(null);
sessionStorage.removeItem("adminAuthenticated");
sessionStorage.removeItem("adminSessionToken");
sessionStorage.removeItem("adminLastLoggedIn");
}, []);
const value = {
@ -189,6 +206,7 @@ export const AuthProvider = ({ children }) => {
setPassword,
siteName,
setUnauthorized,
lastLoggedIn,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;