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>
)
}