I have a strange problem where you can navigate to the login page and login, but if you try to enter the address manually into the browser it will just reload the login page every time you hit send. Whats even more strange is that it will show you parts of the navigation that are only visible to logged in users after the first login attempt but it will redirect to the login page still. And if i try to go to other pages that are only for logged in users it won't show them.
If i hit refresh in the browser the login page will instantly start to work again and i can login. I'm using React Router on a docker container locally.
Is there a way i can fix this?
This is what my React side of things looks like:
App.js
import React, { useContext } from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate
} from "react-router-dom";
import { AuthContext, AuthContextProvider } from './contexts/AuthContext'
import { FacilityDetail } from './components/FacilityDetail'
import { Settings } from './components/Settings'
import { Login } from './components/Login'
import { Reset } from './components/Reset'
import { Navbar } from "./components/Navbar";
import { FacilityUpdate } from "./components/FacilityUpdate";
import { Signup } from "./components/Signup"
import { ConfirmEmail } from "./components/ConfirmEmail";
import { FacilityList } from './components/FacilityList'
import { ResetConfirm } from './components/ResetConfirm'
import { Home } from "./components/Home";
const EnforceAuthOnRoute = ({ children }) => {
const { shouldGoToLogin, user } = useContext(AuthContext)
return user && !shouldGoToLogin ? children : <Navigate replace to="/login" />
}
export default function App() {
return (
<Router>
<AuthContextProvider>
<div>
<Navbar />
{/* A <Routes> looks through its children <Route>s and
renders the first one that matches the current URL. */}
<div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8">
<Routes>
<Route path="/about" element={<About/>} />
<Route path="/users" element={<Users />} />
<Route path="/facilities/:id" element={<EnforceAuthOnRoute><FacilityDetail /></EnforceAuthOnRoute>} exact />
<Route path="/facilities/:id/update" element={<EnforceAuthOnRoute><FacilityUpdate /></EnforceAuthOnRoute>} exact />
<Route path="/settings" element={<EnforceAuthOnRoute><Settings /></EnforceAuthOnRoute>} exact />
<Route path="/login" element={<Login />} exact />
<Route path="/signup" element={<Signup />} exact />
<Route path="/reset" element={<Reset />} exact />
<Route path="/password-reset/confirm/:uid/:token" element={<ResetConfirm />} exact />
<Route path="/accounts/confirm-email/:key" element={<ConfirmEmail />} exact />
<Route path="/facilities" element={<EnforceAuthOnRoute><FacilityList /></EnforceAuthOnRoute>} exact />
<Route path="/" element={<Home />} exact />
</Routes>
</div>
</div>
</AuthContextProvider>
</Router>
);
}
function About() {
return <h2>About</h2>;
}
function Users() {
return <h2>Users</h2>;
}
Login.js
import { useContext, useState } from 'react';
import { Formik, Field, Form } from 'formik';
import { useNavigate } from "react-router-dom"
import { AuthContext } from '../contexts/AuthContext'
export function Login() {
const [loading, setLoading] = useState(false)
const { login } = useContext(AuthContext)
const navigate = useNavigate()
function handleSubmit(values) {
setLoading(true)
login(values).then(() => {
navigate('/facilities')
}).finally(() => setLoading(false))
}
return (
<div>
{loading && "Loading..."}
<Formik
initialValues={{
email: '',
password: '',
}}
onSubmit={handleSubmit}>
{({ errors, touched }) => (
<Form>
<Field name="email">
{({ field, form }) => (
<label className="mt-3 block">
<span className="text-gray-700">Email</span>
<input
{...field}
type="text"
className="
mt-1
block
w-full
rounded-md
border-gray-300
shadow-sm
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
"
placeholder=""
style={
form.touched.email && form.errors.email ? (
{ border: '2px solid var(--primary-red)'}
) : null
}
/>
</label>
)}
</Field>
<Field name="password">
{({ field, form }) => (
<label className="mt-3 block">
<span className="text-gray-700">Password</span>
<input
{...field}
type="password"
className="
mt-1
block
w-full
rounded-md
border-gray-300
shadow-sm
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
"
placeholder=""
style={
form.touched.password && form.errors.password ? (
{ border: '2px solid var(--primary-red)'}
) : null
}
/>
</label>
)}
</Field>
<button className="btn btn-gr"
type="submit">
Submit
</button>
</Form>
)}
</Formik>
</div>
)
}
AuthContext.js
import React, { useEffect, useState } from 'react'
import { API } from "../api"
import axios from "axios"
import { isAfter, isEqual, parseISO, sub } from 'date-fns'
export const AuthContext = React.createContext(null)
export function AuthContextProvider({ children }) {
const [accessTokenExpiration, setAccessTokenExpiraton] = useState(undefined);
const getUser = () => {
return JSON.parse(localStorage.getItem('user'))
}
const isLoggedIn = () => {
return localStorage.getItem('user') !== null
}
const [user, setUser] = useState(() => {
return isLoggedIn() ? getUser() : null;
})
const [shouldGoToLogin, setShouldGoToLogin] = useState(() => {
if (!user || !user.access_token || !user.refresh_token) {
return true;
}
return false;
})
const logout = async () => {
if (!user) {
return;
}
const { access_token } = user;
localStorage.removeItem('user')
setUser(null);
return axios.post(API.auth.logout, {
headers: {
"Authorization": `Bearer ${access_token}`,
"Content-Type": "application/json"
},
withCredentials: true
});
}
const login = async (values) => {
console.log(values);
const correctedValues = { ...values, username: values.email };
return axios.post(API.auth.login, correctedValues)
.then(res => {
const data = res.data;
processApiData(data);
})
}
const refreshToken = async () => {
const user = getUser();
const redirectToLogout = () => {
localStorage.clear(); // Clear our localStorage
setShouldGoToLogin(true);
};
if (!user) { // No user
redirectToLogout();
}
console.log(API.auth.refreshToken);
const resp = await fetch(API.auth.refreshToken, {
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({'refresh': user?.refresh_token}),
method: "POST",
withCredentials: true
})
console.log("status", resp.status);
if (resp.status === 200) {
const data = await resp.json(); // Convert to JSON
console.log("refresh token data", data);
processApiData(data);
} else {
redirectToLogout();
}
}
const resetPassword = async (values) => {
return axios.post(API.auth.passwordReset, values);
}
const processApiData = (resp) => {
let newUser = { ...user, ...resp };
delete(newUser.user); // Delete the user sub-object since we merged that directly into the top-level object
saveUser(newUser); // Save the user
const { access_token_expiration } = newUser;
if (access_token_expiration) {
console.log("have expiration", access_token_expiration);
const nextExpiration = parseISO(access_token_expiration); // Convert from ISO 8601 to a Date Object
const earlyRefreshTime = sub(nextExpiration, { minutes: 55 }); // Do an hourish early
setAccessTokenExpiraton(earlyRefreshTime); // Set the upcoming expiraton
}
}
const saveUser = async (newUser) => {
localStorage.setItem('user', JSON.stringify(newUser))
setUser(newUser)
}
const signup = async (values) => {
return axios.post(API.auth.signup, values);
}
useEffect(() => {
if (!user) {
return;
}
const interval = setInterval(()=> {
if(!user){
return false;
}
if (accessTokenExpiration) {
const now = new Date(); // Get the current time
console.log(now);
console.log(accessTokenExpiration);
if (isAfter(now, accessTokenExpiration) || isEqual(now, accessTokenExpiration)) { // If we are late to the party or the stars have aligned
refreshToken(); // Refresh the token
}
} else { // We do not have an access token expiration yet
refreshToken(); // Refresh the token immediately so we get a time
}
}, 1000 * 15)
return ()=> clearInterval(interval)
}, [accessTokenExpiration, refreshToken, user])
return (
<AuthContext.Provider value={{
getUser,
isLoggedIn,
logout,
login,
resetPassword,
signup,
user,
shouldGoToLogin
}}>
{children}
</AuthContext.Provider>
)
}
My hunch is, that you are setting the shouldGoToLogin state in AuthContext.js in several places that get mixed up.
This might cause the EnforceAuthOnRoute function to run before shouldGoToLogin is set to false.
Try setting the default simply to true and handle the logic of when it is set to false somewhere else:
const [shouldGoToLogin, setShouldGoToLogin] = useState(true);
Alternatively try to have the handleSubmit await shouldGoToLogin to be false before calling navigate():
const { shouldGoToLogin } = useContext(AuthContext)
useEffect(() => {
if(shouldGoToLogin) navigate('/facilities');
},[shouldGoToLogin])
function handleSubmit(values) {
setLoading(true)
login(values).finally(() => setLoading(false))
Your question is bit unclear. As far as i have understood the question, here goes the following response.
If you want the reacter-router to work even when typed manually in the browser. Do the following changes in the webpack server like this.
devServer: {
historyApiFallback: true,
contentBase: './',
hot: true
},
The historyApiFallback would fix the issue. Now routing would work correctly and you can refresh the page or type in the URL directly. The above answer works if you used Webpack.
For further details follow this link
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With