I have create a log in page, which has to enter username and password to authenticate. After the authentication, I push the history to the Homepage by using react-router and set a cookie. The code is like:
const handleLogin = () =>{
const options = {
url: "http://test/login",
method: "POST",
withCredentials: true,
headers:{
"Accept": "application/json",
"content-Type": "application/json;charset-UTF-8"
},
data: {
"username": user,
"password": pass,
}
};
Axios(options).then(Response =>{
if(Response.data.JWT){
let decode = jwt_decode(Response.data.JWT);
if(decode.id){
setLoginUser(decode.username);
setAuthenticated(true);
history.push('/Homepage');
Cookies.set("isLogged", {expires: 1} )
}
}else{
alert("Invalid user");
}
});
}
I also used JWT from back-end, here the application is fine. But when I refresh the web page, I check through the cookies for the refresh token to stay in the Homepage, and remove it for log out.
const readCookie = () => {
let user = false;
if(user = Cookies.get("isLogged")){
history.push('/Homepage');
setAuthenticated(true);
}else{
history.push('/');
}
}
useEffect(() => {
readCookie()
},[]);
Is it OK to get refresh token like this?
Your function, on the outside, should be a dependency of your useEffect. To avoid that, just move it inside the hook. Other than that, looks good to me. Reading a cookie is a side-effect, so you're doing it the right way.
useEffect(() => {
const readCookie = () => {
let user = false;
if(user = Cookies.get("isLogged")){
history.push('/Homepage');
setAuthenticated(true);
}else{
history.push('/');
}
}
readCookie()
},[]);
If you wanted to leave readCookies outside of the hook, you'll have to wrap it in a useCallback hook and use that in your dependency, like so:
const readCookiesCallback = useCallback(() => {/* your read cookies code */}, [])
useEffect(() => {
readCookiesCallback()
}, [readCookiesCallback])
For what you're doing, that's unnecessary. My first example is the way to go.
Related
I'm writing an authentication application in Next Js (v12.2.5). The application also uses React (v18.2.0).
The problem is with persisting the authentication state. When the browser is refreshed, the login session is killed. Why is this happening even though I am getting and setting the token in the local storage. I would like to persist the login session to survive browser refresh and only kill the session when the user logs out.
Application flow
Users authenticates through a login form which calls a Spring API to fetch credentials from a mySQL database. Everything works as expected. The user is able to login, conditional rendering and route protection all function as expected.
Persisting the session relies on the localstorage API to store the JWT token once the user logs in. The Chrome browser console shows that the token is successfully set and stored throughout the authentication process. The get method for getting the initial token also seems to work.
Background
There are several questions on SFO that cover this topic but most seem to cover the use of cookies like this example. This question covers localstorage, but simply says to wrap the token get method is useEffect which doesn't address the actual questions and problems I'm having.
This example also covers localstorage but takes a different approach, using useReducer where my approach is trying to use use Effect. I'm open to restructure my whole application to use useReducer if this is the correct way, but first I want to make sure I understand if I'm taking the right approach.
I also suspect there is a difference between persisting the user state using React and Next. From researching, the difference seems to be in the way Next also includes SSR which may explain why I'm not able to persist the state in Next?
Application code
auth-context.js
const AuthContext = React.createContext({
token: '',
admintoken: '',
isLoggedIn: false,
isAdmin: false,
login: (token) => { },
adminAccess: (admintoken) => { },
logout: () => { },
});
export const AuthContextProvider = (props) => {
useEffect(()=> {
if(typeof window !== 'undefined') {
console.log('You are on the browser');
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
} else {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
}
},[AuthContext])
const [token, setToken] = useState(initialToken);
const [admintoken, setAdminToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const userHasAdmin = !!admintoken;
const loginHandler = (token) => {
setToken(token);
localStorage.setItem('token', token);
console.log("token stored " + token);
};
const logoutHandler = () => {
setToken(null);
localStorage.removeItem('token');
};
const adminTokenHandler = (admintoken) => {
setAdminToken(admintoken);
}
const contextValue = {
token: token,
admintoken: admintoken,
isAdmin: userHasAdmin,
isLoggedIn: userIsLoggedIn,
adminAccess: adminTokenHandler,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
ProtectRoute.js
const ProtectRoute = ({ children }) => {
const authCtx = useContext(AuthContext);
const isLoggedIn = authCtx.isLoggedIn;
if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname == '/') {
return <HomePage />;
} else {
if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname !== '/auth') {
return <RestrictedSection />;
}
else {
console.log("User logged in");
// return <RestrictedSection />;
return children;
}
}
}
export default ProtectRoute;
Authform.js (login page)
const AuthForm = () => {
const emailInputRef = useRef();
const passwordInputRef = useRef();
const [isLoading, setIsLoading] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const router = useRouter();
const authCtx = useContext(AuthContext);
const submitHandler = (event) => {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("username", enteredEmail);
urlencoded.append("password", enteredPassword);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: urlencoded,
redirect: 'follow'
};
fetch(API_LOGIN_URL, requestOptions)
.then(async (res) => {
setIsLoading(false);
if (res.ok) {
return res.json();
} else {
const data = await res.json();
let errorMessage = 'Authentication failed!';
throw new Error(errorMessage);
}
})
.then((data)=> {
authCtx.login(data.access_token);
router.replace('/');
const processedData = JSON.stringify(data);
console.log("Admin status "+ processedData);
for(let i = 0; i < processedData.length; i++) {
if(processedData.includes("ROLE_SUPER_ADMIN")) {
console.log("Found Admin");
authCtx.adminAccess(true);
}
if(processedData.includes("ROLE_USER")) {
console.log("Found User");
break;
}
else {
console.log("Not Found");
}
}})
.catch((err) => {
alert(err.message);
});
};
return (
<section className={classes.auth}>
<h1>Login</h1>
<form onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor='email'>Your Email</label>
<input type='email' id='email' required ref={emailInputRef} />
</div>
<div className={classes.control}>
<label htmlFor='password'>Your Password</label>
<input type='password' id='password' required ref={passwordInputRef} />
</div>
<div className={classes.actions}>
{!isLoading && <button>Login</button>}
{isLoading && <p>Sending request</p>}
</div>
</form>
</section>
);
};
export default AuthForm;
Issue 1
From your code in auth-context.js, you are calling useEffect inside a condition.
if(typeof window !== 'undefined') {
console.log('You are on the browser');
useEffect(()=> {
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
})
} else {
useEffect(()=> {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
})
}
You SHOULD NOT call your useEffect(or any other hook) inside conditions, loops and nested functions.
Doc reference: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level.
Consider moving your conditions code inside the hook.
useEffect(()=> {
if(condition)
{run your localstorage related logic here...}
})
Issue 2
I think you should consider adding a dependency array to your useEffect hook because getting your token on every rerender seems quite expensive.
useEffect(()=> {
if(condition)
{run your localstorage related logic here...}
},[])
Still, its just a suggestion, as I don't know your code in much depth.
Issue 3
The initial token is not getting set in the use effect.
Kindly add setToken(initialToken) in the useEffect after initial token assignment.
initialToken = localStorage.getItem('token');
setToken(initialToken);
The main issue is with you trying to run serverside code on the fronted:
useEffect(()=> {
if(typeof window !== 'undefined') {
console.log('You are on the browser');
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
} else {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
}
},[AuthContext])
The above part of the code will always run on the front end(so you don't need the if part).
If you want to clear your concepts on what part of the code will work on the server and what part will run on the client, kindly refer to these documentations:
SSR: https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props
SSG: https://nextjs.org/docs/basic-features/data-fetching/get-static-props
ISR: https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
I explain my problem:
I have two functions, one to create a garden and another a plot. So it's POST requests to my API but when I click submit, I have another function (which is a GET request) that retrieves the garden and the plots, this function works but I have to reload the page for them to appear: it's not added automatically. How can I do this?
I tried putting in the array of useEffect (which repeats my function to retrieve data from the Garden and the plots) the variable garden and plot or setGarden and setPlot but it doesn't work.
Here is the code of the GET Request :
const [garden, setGarden] = useState([]);
const [plot, setPlot] = useState([]);
const [loading, setLoading] = useState(false);
const gardenData = async () => {
setLoading(true);
const user = await AsyncStorage.getItem('user');
const parsedUserData = JSON.parse(user);
try {
const response = await axios.get(
`http://127.0.0.1/api/garden?user=${parsedUserData.user.id}`,
{
headers: {
Authorization: `Token ${parsedUserData.token}`,
},
},
);
if (response.status === 200) {
setGarden(response.data);
setLoading(false);
try {
const plotResponse = await axios.get(
`http://127.0.0.1/api/plots?garden=${response.data[0].id}`,
{
headers: {
Authorization: `Token ${parsedUserData.token}`,
},
},
);
if (plotResponse.status === 200) {
setPlot(plotResponse.data);
}
} catch (e) {
alert(e);
}
}
} catch (e) {
console.log('Erreur ' + e);
setLoading(false);
}
};
useEffect(() => {
gardenData();
}, []);
Thanks for the help !
I am assuming that the mentioned code is a part of an existing component with a return. It would be great to see where you run the POST method.
The useEffect hook gets triggered only on the mount of the component (as defined with the second argument - [])
So you need to call the gardenData function after successfully performing the POST request. After that, the data will refresh and state will take care of the rest.
I have a problem (in React):
After the fetch occurs, when I console.log the User_ID value it shows me the right updated id value which is 1, but when I set the state of UserID and then I console.log it, it still shows me the default value which is 0 and not the updated one which equals to 1.
Can someone please take a look in my code and tell me where the problem is? And if the problem is delay, can someone tell me how to fix it?
The code:
export default function Login() {
const [Email, setEmail] = useState("");
const [UserID, setUserID] = useState(0);
const [Password, setPassword] = useState("");
const navigate = useNavigate();
const btnLogin = async () => {
console.log(1);
console.log(Email + "," + Password);
let s = await checkUserDetails(
Email,
Password
);
console.log("returned value=" + s + "and id = " +s.User_ID); //here the it returns the right User ID ( id = 1)
setUserID(s.User_ID) // for some reason here it doesnt update the right value of the User ID
if (s != null)
{
console.log("user_id is =" + UserID); // here it still prints User ID = 0 .. why?
alert("Logged In Successfully!!");
navigate('/')
console.log("h1");
} else {
console.log("err login!");
alert("failed to login");
}
};
const checkUserDetails = async (Email, Password) => {
console.log("we are in func checkUserDetails, email = " + Email);
try {
let result = await fetch(url + `?email=${Email}&pass=${Password}`, {
method: "GET", // 'GET', 'POST', 'PUT', 'DELETE', etc.
headers: new Headers({
"Content-Type": "application/json",
Accept: "application/json",
}),
});
let resultData = result.json();
console.log("meowwww");
console.log(resultData);
if (resultData) {
return resultData;
} else {
alert("unable to login");
}
} catch (e) {
console.log("Error Login", e);
alert("lo mevin");
}
};
return (
<div className="login-form">
<button
type="button"
className="button-LoginSign"
onClick={btnLogin}
>
Login
</button>
</div>
Thanks for all the helpers
that is because react state updates are not synchronous !!!!!
if you want to use the latest value inside the function where you are using you can directly use the response or create a useEffect with userId as a dependency
also your variable names should be camelCase - password, userId etc.
useEffect(()=> { this effect will have the latest value of ID}, [userId]
and will run whenever setUserId is called
The UserId piece of state will not show the updated value until the next render of that component. So in that initial run through the btnLogin function it still has the original value. Use s.User_ID there again if you need to. But I don't see that you really need access to that value other than a console log.
If you need to do something once that UserId has been set to state, put that inside a useEffect
useEffect(() => {
if (UserId > 0) {
// do something with id
}
}, [UserID])
You can never get your updated state this way. Because, you are writing console statement write after setUserId() hook, Which is asynchronous in nature. To get the updated value you need to write a useEffect hook with the state in dependency array like this. So whenever you set UserID the updated state will be reflected in this useEffect.
useEffect(()=>{
console.log(userId)
//Now desired computation on base of updated state
},[userId])
I am using magic login in javascript and next js to add users to my app, it works just fine, but the problem I am having is. When a user lands back on the page I have to manually refresh the page to get my data. I have tried checking for the url param, and reloading if it exists then changing the changing the url to not have the url param so it doesn't loop.
But the router isn't even found after clicking the login button from the email sent from magic login.
import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.css";
import { gql, useQuery } from "#apollo/client";
import useSWR from "swr";
import { useEffect } from "react";
import Layout from "../components/layout";
import { useRouter } from "next/router";
export const GET_DAD_HAT = gql`
query FindUserByID($id: ID!) {
findUserByID(id: $id) {
_id
hats {
data {
name
}
}
}
}
`;
const fetcher = (url) => fetch(url).then((r) => r.json());
function useAuth() {
const { data: user, error, mutate } = useSWR("/api/user", fetcher);
const loading = user?.token === false || user === undefined;
return {
user,
loading,
error,
};
}
export default function Profile() {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
console.log("window", window);
console.log(Object.keys(router.query)[0]);
if (Object.keys(router.query)[0] === "magic_credentials") {
router.reload(window.location.pathname);
window.history.pushState({}, document.title, "/" + "profile");
}
if (loading) {
}
}, []);
return (
<Layout>
<main>{loading ? "Loading..." : <Data user={user} />}</main>
</Layout>
);
}
const Data = ({ user }) => {
const { loading, error, data } = useQuery(GET_DAD_HAT, {
variables: { id: user.id },
});
if (loading) return <h1>Loading...</h1>;
if (error) return <h1>{error.message}</h1>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
};
What happens is the data is just stuck on Loading until I manually hit refresh, after being redirected to the app from the email login flow.
UPDATE: I made a reproducible sandbox. https://omg5u.sse.codesandbox.io/login-magic
Enter your email, click the login link sent to email.
Feel free to use disposable email service like https://getnada.com/
if it's not blocked
When you arrive on profile page see that it is just loading, until you hit refresh then it should show your user id, and an empty array for data.
UPDATE: It looks like when I first land on the page the cookie lookup for fauna_client in the fetch to the user api route returns undefined. However after refreshing it returns the cookie. If I inspect the cookie is there before hitting refresh, but if I look in the terminal for next, the cookie is created after it is looked for. I think it has something to do with Magic redirect in the login api route creating the cookie after magic is logged in.
Still quite confused.
Had to do the following on a auth-verify page and a login-verify api route
useEffect(() => {
finishEmailRedirectLogin();
}, [router.query]);
const finishEmailRedirectLogin = () => {
if (router.query.magic_credential)
magicClient.auth
.loginWithCredential()
.then((didToken) => authenticateWithServer(didToken));
};
// Send token to server to validate
const authenticateWithServer = async (didToken) => {
let res = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + didToken,
},
});
if (res.status === 200) {
// Set the UserContext to the now logged in user
let userMetadata = await magicClient.user.getMetadata();
// await setUser(userMetadata);
router.push("/profile");
}
};
import { Magic } from "#magic-sdk/admin";
// Initiating Magic instance for server-side methods
const magic = new Magic(process.env.MAGIC_SECRET_KEY);
export default async function login(req, res) {
try {
const didToken = req.headers.authorization.substr(7);
await magic.token.validate(didToken);
res.status(200).json({ authenticated: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
action.js:
export const login = creds => {
console.log(`${url}/login`);
const requestOptions = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: creds
};
return function(dispatch) {
dispatch({ type: LOGIN_REQUEST });
function timer() {
return fetch(`${url}/login`, requestOptions).then(response => {
if (!response.ok) {
console.log(response);
return response.json().then(json => {
var error = new Error(json.message);
error.response = response;
throw error;
});
} else {
console.log("3");
return response.json();
}
}).then(user => {
if (user.message === "ok") {
localStorage.setItem("token", user.token);
dispatch({ type: LOGIN_SUCCESS, payload: user.token });
window.location.href = `${app}/dashboard`;
} else {
const error = user.message;
throw new Error(error);
}
}).catch(function(error) {
dispatch(loginError(error));
});
}
setTimeout(timer, 5000)
}
};
I am unable to redirect in a single-page manner to my dashboard, I searched a lot but I didn't get anything useful. I am using React Router v4. Can you please suggest whether I am doing this user login with JWT the right way or not.
create a your own history in history.js file using this history library.
//history.js
import createHistory from 'history/createBrowserHistory'
const history = createHistory()
export default history
Supply it to your router:
<Router history = {history}>.....</Router>
Then you can use this history object to redirect from anywhere. In your action:
import history from './history'
history.push(`${app}/dashboard`)
You can add react-router-redux which will conveniently add all routing info to the Redux state, but also you can use push from react-router-redux by:
dispatch(push('/dashboard'))
You can access it by
import { push } from 'react-router-redux'
I think you are not doing anything wrong with JWT. I would abstract your fetch call with the helper function. I assume you need to add your access token to all future requests after authorization, and using helper function you can do it easier and keep you code cleaner.
window.location.href will hit your server and you'll loose all the benefits of a single page application, you should use a client side routing instead with the history object
In this scenario a good solution could be pass a callback from the component that triggers the first dispatch containing a history.push to the new state
e.g.
Assuming that inside your component props you have access to history
login(body, () => {
this.props.history.push('/dashboard');
})
then in place of window.location.href = ${app}/dashboard you invoke the callback