MOBX is concatenating an observable instead of updating it - reactjs

I am building a login page with Mobx, MUI V5, react-router V6 and react-hook-form.
My first API call is to authenticate the application, apiAuth() will return a token that needs to be passed to all subsequent API calls.
On the next call, userAuth(), I try to validate the user credential.
As you can see, the method takes 3 arguments (a token, card number and, password)
When the user credentials are valid, I can login successfully.
When the user credentials are not valid on the first try, it works as
expected. I receive 400 (Bad Request) error from the API and display the error message on the
interface.
That said when I entered the user credentials once more, I get a 401 (Unauthorized) error.
Upon further inspection of the request headers, when I compared the authorization header in both userAuth() calls, I see that the token's value on the second call was concatenated with the previous token
Any ideas as to why for this behavior?
My AuthStore looks as follow:
class AuthStore {
isAuth = false
isAuthFail = false
AuthFailObj = {}
bearerToken = ''
cardNum = ''
password=''
constructor() {
makeObservable(this, {
isAuth: observable,
AuthFailObj: observable,
isAuthFail:observable,
bearerToken: observable,
cardNum: observable,
password: observable,
auth: action,
setIsAuth: action,
setToken: action,
setCardNum: action,
setPassword: action,
setIsAuthFail: action,
setAuthFailObj: action
})
}
setIsAuth = isAuth => {
this.isAuth = isAuth
}
setToken = bearerToken => {
this.bearerToken = bearerToken
}
setCardNum = cardNum => {
this.cardNum = cardNum
}
setPassword = password => {
this.password = password
}
setIsAuthFail = b => {
this.isAuthFail = b
}
setAuthFailObj = ojb => {
this.AuthFailObj = ojb
}
auth = async () => {
const apiRes = await apiAuth()
if (apiRes.status === 200){
const apiData = await apiRes.text()
this.setToken(JSON.parse(apiData)[0].token)
}
const userAuthRes = await userAuth(this.bearerToken, this.password, this.cardNum)
if (!userAuthRes.ok){
this.setIsAuthFail(true)
const errRes = await userAuthRes.text()
userAuthRes.status === 400 && this.setAuthFailObj(JSON.parse(errRes))
userAuthRes.status === 401 && this.setAuthFailObj('401 (Unauthorized)')
}
if (userAuthRes.ok){
const userAuthData = await userAuthRes.text()
userStore.updateUserProfile(JSON.parse(userAuthData))
this.setIsAuth(true)
}
}
}
export default new AuthStore()
In the login form, the submit method looks like this:
const submit = async (data) => {
AuthStore.setCardNum(data.Card_Number)
AuthStore.setPassword(data.Password)
setToggle(true)
await AuthStore.auth()
if (AuthStore.isAuth) {
navigate('/dashboard')
} else {
// clear form
}
}
Finally, the PrivateRoute logic reads is simple:
const PrivateRoute = () => {
return AuthStore.isAuth ? <Outlet /> : <Navigate to='/' />
}
The function userAuth()
const myHeaders = new window.Headers()
const { REACT_APP_API_ACC_MNG_AUTH_URL } = process.env
const userAuth = async (bearerToken, password, cardNum) => {
myHeaders.append('Authorization', `Bearer ${bearerToken}`)
myHeaders.append('Content-Type', 'application/json')
const raw = JSON.stringify({
cardNumber: cardNum,
pinNumber: password
})
const requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
}
const response = await window.fetch(REACT_APP_API_ACC_MNG_AUTH_URL, requestOptions)
return response
}

The issue is that you're using the Headers API and appending to the headers instead of setting them, which exist outside the function scope and are updated. From MDN:
The append() method of the Headers interface appends a new value onto an existing header inside a Headers object, or adds the header if it does not already exist.
So every time you make a request, if you append the header, it will be added on to the existing value. You could move your headers declaration inside of the function, and create a new object each time you make a request:
const { REACT_APP_API_ACC_MNG_AUTH_URL } = process.env
const userAuth = async (bearerToken, password, cardNum) => {
const myHeaders = new window.Headers()
myHeaders.append('Authorization', `Bearer ${bearerToken}`)
myHeaders.append('Content-Type', 'application/json')
const raw = JSON.stringify({
cardNumber: cardNum,
pinNumber: password
})
const requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
}
const response = await window.fetch(REACT_APP_API_ACC_MNG_AUTH_URL, requestOptions)
return response
}
Or you could just pass them in as an object, which is allowed by the Fetch API:
const userAuth = async (bearerToken, password, cardNum) => {
const raw = JSON.stringify({
cardNumber: cardNum,
pinNumber: password
});
const requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow',
headers: {
'Authorization': `Bearer ${bearerToken}`,
'Content-Type': 'application/json',
}
};
const response = await window.fetch(REACT_APP_API_ACC_MNG_AUTH_URL, requestOptions);
return response;
}

Related

Authorization headers taking the previous token value redux toolkit

I'm using redux toolkit and i want to send the token to verify if the user is logged in or not so he can like a post.
My code is as follow :
const userInfo = localStorage.getItem('userInfo') ?
JSON.parse(localStorage.getItem('userInfo')) : null
const config = {
headers: {
Accept: "application/json",
'Content-type': 'application/json',
}
}
const auth = {
headers: {
Authorization: `Bearer ${userInfo?.token}`
}
}
export const likePost = createAsyncThunk("posts/likePost",
async (id,{ rejectWithValue }) => {
try {
const { data } = await axios.patch(`http://localhost:5000/posts/like/${id}/`,
config,
auth,
id,
)
console.log(userInfo?.token)
return data
} catch (error) {
return rejectWithValue(error.response.data)
}
}
)
when i check the headers i find that it takes the token of the previous logged in person

How to create a global 401 unauthroized handler using React + ReactQuery + Axios stack?

So I architected frontend in the way which encapsulates every API operation tied to a single resource inside custom hook like this:
export default function useSubjects() {
const queryClient: QueryClient = useQueryClient();
const token: string | null = useStore((state) => state.user.token);
const { yearCourseId } = useParams<{ yearCourseId: string }>();
const getSubjects = async () => {
const response = await axios.get(`yearCourses/${yearCourseId}/subjects`, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
};
const postSubject = async (subject: SubjectType) => {
const response = await axios.post(`yearCourses/${yearCourseId}/subjects`, subject, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
};
const query = useQuery(SUBJECTS_QUERY_KEY, getSubjects);
const postMutation = useMutation(postSubject, {
onSuccess: (subject: SubjectType) => {
queryClient.setQueryData(SUBJECTS_QUERY_KEY, (old: any) => [...old, subject]);
},
});
return { query, postMutation };
}
Now what is the way to globally handle 401 unauthorized? I would like to navigate user to /login on every unauthorized request. Note that I have more hooks like this tied to other resources.
use the onError callback. You can also do this globally as a callback on the queryCache
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: error => {
// check for 401 and redirect here
}
})
})

how to solve problem with oAuth Zoom in Nextjs?

I am trying to authenticate the user in order to get data to use to create or update meetings later. but it full of errors.
Here I am sending Post Requests in order to get the AccessToken and then get the UserData as props.
export async function getServerSideProps(res){
const oauth = async() => {
const zoomUserData = [];
const b = Buffer.from(process.env.ZOOM_API_KEY + ":" + process.env.ZOOM_API_SECRET);
const zoomRes = await fetch(`https://zoom.us/oauth/token?grant_type=authorization_code&code=${req.body.code}&redirect_uri=${process.env.ZOOM_REDIRECT_URL}`, {
method: "POST",
headers: {
Authorization: `Basic ${b.toString("base64")}`,
},
});
const zoomData = await zoomRes.json();
const zoomUserRes = await fetch("https://api.zoom.us/v2/users/me", {
method: "GET",
headers: {
Authorization: `Bearer ${zoomData.access_token}`,
},
});
const zoomUserData = await zoomUserRes.json();
/*
Encrypt and store below details to your database:
zoomUserData.email
zoomUserData.account_id
zoomData.access_token
zoomData.refresh_token
zoomData.expires_in // convert it to time by adding these seconds to current time
*/
}
return{
props:{zoomUserData}
}
}
and then i am passing the props to a page component like that :
export default function Meeting({zoomUserData}) {
const router = useRouter();
useEffect(() => {
if (router.query.code) {
fetch('/connectZoom',
{ method: 'POST',
headers: {
'ContType': 'application/json',
},
body: JSON.stringify({ code: router.query.code }),
}).then(() => {
console.log("success")
}).catch(() => {
console.log("No!")
});
}
}, [router.query.code]);
console.log(zoomUserData)
return (
<a href={`https://zoom.us/oauth/authorize?response_type=code&client_id=${process.env.ZOOM_API_KEY}&redirect_uri=${process.env.ZOOM_REDIRECT_URL}`}>
Connect Zoom
</a>
)
}

Access session data in Next.js page using withIronSession()

After reading the following post, https://dev.to/chrsgrrtt/easy-user-authentication-with-next-js-18oe and consulting the following question Using next-iron-session's "withIronSession" with Next.JS to perform simple authentication, I am still unable to access the session using req.session.get('user'). Below is my implementation in a Next.js project:
Create a util
import {withIronSession} from 'next-iron-session';
const cookie = {
cookieName: process.env.COOKIE_NAME,
password: process.env.COOKIE_SECRET,
cookieOptions: {secure: process.env.NODE_ENV === 'production'},
};
export const guard = (handler) => {
return withIronSession(handler, cookie);
};
export default cookie;
Create an API endpoint
const zlib = require('zlib');
import cookie from '#/utils/cookie';
const fetch = require('node-fetch');
import {withIronSession} from 'next-iron-session';
export default withIronSession(
async (req, res) => {
if (req.method === 'POST') {
try {
const request = await fetch(
process.env.NEXT_PUBLIC_API_BASE_URL + '/api/login',
{
method: 'post',
body: req.body,
headers: {
'Content-Type': 'application/json',
'Origin': req.headers.host || req.headers.origin,
},
}
);
const response = await request.text();
const {success, data, message} = JSON.parse(response);
// set JWT in session
compressor(data, (x) => req.session.set('user', x));
// persist session value
await req.session.save();
// console.log(req.session.get('user'));
return res.status(201).json({success, message});
} catch (error) {
console.log(error);
}
}
return res.status(404).json('Not found');
},
cookie
);
Access session data in a page
export const getServerSideProps = guard(async (ctx) => {
const {req} = ctx;
const session = req.session.get();
console.log({session});
return {redirect: {destination: '/sign-in', permanent: false}};
});
The above terminal log gives an empty object. Is there something am doing wrong??
Try the following:
export const getServerSideProps = guard(async function ({
req,
res,
query,
}) {
//Assuming you have "user" session object
const user = req.session.get("user");
...
});
Harel

LocalStorage not working after storing token

I am storing JWT token in LocalStorage after Login success is dispatched and routing to next component. But the next component API call is not able to take LocalStored token.
If i refresh the page and click again, it is accepting the token. Dont know the issue.
This is Axios Instance and Login Dispatch respectively
const instance = axios.create({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8',
'x-access-token': localStorage.getItem('accessToken'),
},
withCredentials: true,
validateStatus: (status) => status === 200
});
export function checkLogin(data, history) {
return function (dispatch) {
return dispatch(makeAPIRequest(loginAPI, data)).then(function (response) {
if (response.data.success == 1) {
localStorage.removeItem('accessToken')
localStorage.setItem('accessToken', response.data.data.token)
dispatch({ type: STORE_SESSION_TOKEN, authenticated: response.data.data.auth, token: response.data.data.token,userDetails: response.data.data.user });
history.push('/dashboard')
}
})
}
}
Expecting to take token from Localstorage from very next call from Dashboard. But that doesn't happen. says no token and redirects to Login
I think the problem is when you create the axios instance, the token is not available in localStorage.
You should try the default headers of axios:
export const AUTHORIZATION = 'authorization'
export const API = axios.create({
baseURL: `http://localhost:3000/api`
})
export const authorize = (token) => {
if (token) {
API.defaults.headers.Authorization = `Bearer ${token}`
localStorage.setItem(AUTHORIZATION, token)
}
}
export const unauthorize = () => {
delete API.defaults.headers.Authorization
localStorage.removeItem(AUTHORIZATION)
}
authorize(localStorage.getItem(AUTHORIZATION))
When you receive the token from the API in your actions, you have to save it to localStorage.
You need to get your header dynamically, because the token is not there while creating the instance and it might change later:
const JSON_HEADERS = {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8'
};
const getTokenFromStorage = () => localStorage.getItem('token');
const getHeaders = () => getTokenFromStorage().then((token) => {
if (!token) {
return JSON_HEADERS;
}
return {
... JSON_HEADERS,
'x-access-token': token
};
});
export const makeAPIRequest = ({ method, url, data }) => {
return axios({
method,
url,
data,
headers: getHeaders()
});
}

Resources