Redux Saga call api before token is set - reactjs

I m trying to implements a react application with authentification using keycloak, all sounds good but when I refresh the page and there is fetching of an api, Saga perform the call before the token is set
there is my saga call
function* getAPI(action) {
const state = yield select();
try {
let response = yield call(
axiosRequest,
"get",
BaseURL,
`/foo/mini`,
{},
setAuthorizationBearer(state.auth.token),
{ sendToken: true },
"application/json"
);
yield put({ type: `${action.type}_SUCCESS`, payload: response, metadata: action.metadata })
} catch (e) {
yield put({ type: `${action.type}_ERROR`, payload: e })
}
}
and here is my axios request instance
import axios from "axios";
let authorizationBearer = null;
export const setAuthorizationBearer = token => {
authorizationBearer = token;
};
const instance = (
method,
baseURL = process.env.REACT_APP_ENDPOINT,
url,
data = null,
headers = null,
sendToken = true,
contentType
) => {
return new Promise((resolve, reject) => {
const p = {
sendToken: sendToken.sendToken,
data: {
...data,
},
};
const req = axios.create({
method,
baseURL,
url,
timeout: 30000,
headers: headers,
crossDomain: true,
});
headers = {};
if (p.sendToken && authorizationBearer) {
headers.Authorization = `Bearer ${authorizationBearer}`;
headers["Content-Type"] = contentType;
}
req({
method,
baseURL,
url,
data,
headers,
sendToken,
})
.then((payload) => {
if (payload) {
if (payload.status < 400) {
resolve(payload);
} else {
reject(payload);
}
} else {
reject(payload);
}
})
.catch((e) => {
if (axios.isCancel(e)) {
console.log("Request canceled", e.message);
} else {
// handle error
}
reject(e);
});
});
};
export default instance;
And finally i set my token on authentification with a dispatch
const dispatch = useDispatch()
<ReactKeycloakProvider onTokens={({token}) => dispatch(authUser(token))} authClient={Keycloak(config)}
initOptions={{
onLoad: 'login-required',
checkLoginIframe: false,
timeSkew: "0",
refreshToken: ""
}}
LoadingComponent={<div />}
>
....
</ReactKeycloakProvider>

Most probably the application content is being rendered before the onTokens is being executed. Try checking on the existence of the token in the store state before rendering anything (or show a loading screen).

Related

Getting access token with the refresh token after expiration(JWT)

I get 401 error after a while when the page is reloaded, I figured it could be because the access token is expired. How do I set a new token with my refresh token? The below function runs every time the user visits a new page or refreshes the page. But it doesn't seem to work.
export async function currentAccount() {
if (store.get('refreshToken')) {
const query = {
grant_type: 'refresh_token',
companyId: store.get('lastCompanyId'),
refresh_token: store.get('refreshToken'),
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
})
.catch(err => {
console.log('error', err)
store.clearAll()
})
}
return false
}
Login sets the access tokens
export async function login(email, password) {
const query = {
grant_type: 'password',
username: email,
password,
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const {
data: {
access_token: accessToken,
refresh_token: refreshToken,
},
} = response
const decoded = jsonwebtoken.decode(accessToken)
response.data.authUser = decoded.authUser
const { userId, profileId, companyId } = decoded.authUser
if (accessToken) {
store.set('accessToken', accessToken)
store.set('refreshToken', refreshToken)
}
return response.data
}
return false
})
.catch(err => console.log(err))
}
saga users.js
export function* LOAD_CURRENT_ACCOUNT() {
yield put({
type: 'user/SET_STATE',
payload: {
loading: true,
},
})
const { authProvider } = yield select((state) => state.settings)
const response = yield call(mapAuthProviders[authProvider].currentAccount)
if (response) {
const decoded = jsonwebtoken.decode(response.access_token)
response.authUser = decoded.authUser
yield store.set('id', id)
try {
const user = yield call(LoadUserProfile)
if (user) {
const { company } = user
yield put({
type: 'user/SET_STATE',
payload: {
...user,
preferredDateFormat: user.preferredDateFormat || 'DD/MM/YYYY',
userId,
id,
},
})
}
} catch (error) {
}
}else{
store.set('refreshToken', response.refreshToken)
}
yield put({
type: 'user/SET_STATE',
payload: {
loading: false,
},
})
}
You can get a new access token with your refresh token using interceptors. Intercept and check for response status code 401, and get a new access token with your refresh token and add the new access token to the header.
Example:
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) { // check for the status code 401 and make call with refresh token to get new access token and set in the auth header
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
});
Simple Interceptor example,
axios.interceptors.request.use(req => {
req.headers.authorization = 'token';
return req;
});
Interceptor example for 401
axios.interceptors.response.use(response => response, error => {
if (error.response.status === 401) {
// Fetch new access token with your refresh token
// set the auth header with the new access token fetched
}
});
There are several good posts on Interceptors usage for getting a new access token with your refresh token
https://thedutchlab.com/blog/using-axios-interceptors-for-refreshing-your-api-token
https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da
Automating access token refreshing via interceptors in axios
https://stackoverflow.com/a/52737325/8370370
The above answer is good. But I found below method is better than that also using Axios Interceptors and "jwt-decode". Give it a try. (I'm using session storage for this example. You can use your own way to store the tokens securely)
Methodology
Login to get an access token and long live refresh token and then store them securely.
Create an axios instance to check the access token expiration with "jwt-decode". Then add the access token into the request if there is a valid access token, or else request a new access token using the stored refresh token and then apply the new access token into the request.
Login:
import axios from 'axios'
const handleLogin = async (login) => {
await axios
.post('/api/login', login, {
headers: {
'Content-Type': 'application/json'
}
})
.then(async response => {
sessionStorage.setItem('accessToken', response.data.accessToken)
sessionStorage.setItem('refreshToken', response.data.refreshToken)
})
.catch(err => {
if (errorCallback) errorCallback(err)
})
}
Create axios instance:
import axios from 'axios'
import jwt_decode from 'jwt-decode'
import dayjs from 'dayjs'
const axiosInstance = axios.create({
headers: { 'Content-Type': 'application/json' }
})
axiosInstance.interceptors.request.use(async req => {
const accessToken = sessionStorage.getItem('accessToken') ? sessionStorage.getItem('accessToken') : null
if (accessToken) {
req.headers.Authorization = `Bearer ${accessToken}`
}
const tokenData = jwt_decode(accessToken)
const isExpired = dayjs.unix(tokenData.exp).diff(dayjs()) < 1
if (!isExpired) return req
const refreshToken = sessionStorage.getItem('refreshToken')
const response = await axios.post('/api/refresh', { refreshToken }, {
headers: {
'Content-Type': 'application/json'
}
})
req.headers.Authorization = `Bearer ${response.data.accessToken}`
sessionStorage.setItem('accessToken', response.data.accessToken)
return req
})
export default axiosInstance
Use axios instance in all the requests (Redux Toolkit Example):
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
// Import axiosInstance
import axiosInstance from 'src/utils/axiosInstance'
export const getItems = createAsyncThunk(
'appItems/getItems',
async (args, { rejectedWithValue }) => {
try {
const response = await axiosInstance.get('/api/items')
return response.data
} catch ({ response }) {
return rejectedWithValue({ code: response.status, ...response.data })
}
}
)

Generic function to request api with Axios

I am trying to build a generic function for my endpoints, using Axios and React. Generic because I have always the same header and I do not want to repeat a lot of code for each of my components.
To do that, I built this function (sorry, a lot of comments that I will remove after of course) :
export const getRequest = ( endpoint ) => axios
.get( env._URL_SERVER_ + endpoint, { headers: getHeaders() } )
.then((res) => {
// Success
console.log(res);
return {error: false, response: res.data};
})
.catch((error) => {
// Error
if (error.response) {
/*
* The request was made and the server responded with a
* status code that falls out of the range of 2xx
*/
console.log(error.response.data);
console.log(error.response.status);
return {error: true, status: error.response.status, data: error.response.data};
} else if (error.request) {
/*
* The request was made but no response was received, `error.request`
* is an instance of XMLHttpRequest in the browser and an instance
* of http.ClientRequest in Node.js
*/
console.log(error.request);
return {error: true, data: error.request };
} else {
// Something happened in setting up the request and triggered an Error
console.log('Error', error.message);
return {error: true, data: error.message}
}
});
Ant then in my components I do that :
getSchools = () => {
this.setState({
loadingSchools: true
}, () => {
getRequest(`/schools?name=${this.state.filterByName}&city=${this.state.filterByCity}&school_type_id=${this.state.filterBySchoolTypeId}&page=${this.state.selectedPage}`)
.then((response) => {
// there is an error
if (!response.error) {
this.setState({
schools: response.response.data,
meta: response.response.meta,
links: response.response.links
})
} else {
this.setState({
error: true,
errorMessage: response.data,
})
}
})
.then(() => {
this.setState({loadingSchools : false});
})
})
}
It works fine. I tested it in several situation (all is OK - 200, not found - 404, no response). But is it a good practice ? I feel that there is a lot of codes in the parent component. Maybe I complicate my life?
Here is how I've done it:
var URL_BACKEND = "http://localhost:5000/";
// Create Function to handle requests from the backend
callToBackend = async (ENDPOINT, METHOD) => {
const options = {
url: `${URL_BACKEND}${ENDPOINT}`,
method: METHOD,
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
};
const response = await axios(options);
return response.data;
}
// Then you make a call with the exact endpoint and method:
const response = await this.callToBackend('createSetupIntent', 'POST');
console.log(JSON.stringify(response));
create one common file for base URL let's say api.js
// api.js file code
export const apiUrl = axios.create({
baseURL: 'http://localhost:5000',
});
Register file
// register.js file code
import { apiUrl } from './api';
try {
const resp = await apiUrl.post('/api/register', {
username,
email,
password,
});
const { data, status } = resp;
if (Object.keys(data).length && status === 200) {
// received api data successfully
console.log('API response', data);
}
} catch (err) {
console.log(err);
}
// For auth request
try {
const token = localstorage.getItem('token');
const res = await apiUrl.post(
'/authroute',
{
name: fullName,
originCountry: country,
career: careerStatus,
},
{
headers: { Authorization: `Bearer ${token}` },
}
);
const { data, status } = strapiRes;
if (Object.keys(data).length && status === 200) {
return res.status(status).json(data);
}
} catch (error) {
throw new Error(error);
}
// same for all request
apiUrl.get(endpoint);
apiUrl.post(endpoint, body);
apiUrl.put(endpoint, body);
apiUrl.delete(endpoint, body);

react & redux with hooks: Actions must be plain objects. Use custom middleware for async actions

i tried looking for similar answers to help solve my problem but i couldn't find anything using react redux hooks. This code was from a tutorial and originally written using the Context api. I wanted to trying using it with react-redux-hooks, but i got stuck. Basically i'm trying to register a user with a name, email and password, then pass these three as an object to the express server which will validated it and give me back a jwt token. Then come back to the client side and send the token to the reducer, which adds the token to localstorage and sets the state to isAuthenticated. The error i get is on the dispatch.
Dispatch
const onSubmit = e => {
e.preventDefault();
if (name === "" || email === "" || password === "") {
dispatch(setAlert("Please enter all fields", "danger"));
} else if (password !== password2) {
dispatch(setAlert("Passwords do not match", "danger"));
} else {
dispatch(register({ name, email, password })); // Error is here
}
setTimeout(() => {
dispatch(removeAlert());
}, 5000);
};
Action
export const register = async formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
try {
const res = await axios.post("/api/users", formData, config);
return {
type: "REGISTER_SUCCESS",
payload: res.data
};
} catch (err) {
return {
type: "REGISTER_FAIL",
payload: err.response.data.msg
};
}
};
Reducer
const authReducer = (
state = {
token: localStorage.getItem("token"),
isAuthenticated: null,
loading: true,
user: null,
error: null
},
action
) => {
switch (action.type) {
case "REGISTER_SUCCESS":
console.log("register success");
localStorage.setItem("token", action.payload.token);
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false
};
case "REGISTER_FAIL":
console.log("register failed");
localStorage.removeItem("token");
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
error: action.payload
};
default:
return state;
}
};
Store
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|| compose;
const store = createStore(
allReducers,
composeEnhancers(applyMiddleware(thunk))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Express server
router.post(
"/",
[
check("name", "Please a name")
.not()
.isEmpty(),
check("email", "Please include a valid email").isEmail(),
check(
"password",
"Please enter a password with 6 or more characters"
).isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const { name, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
msg: "User already exists"
});
}
user = new User({
name,
email,
password
});
// hash passsword
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
config.get("jwtSecret"),
{
expiresIn: 360000
},
(err, token) => {
if (err) throw err;
res.json({
token
});
}
);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error");
}
}
);
I believe this question has answers to the issue your experiencing here: how to async/await redux-thunk actions?
Using this example, it may look something like this (wasn't able to test it):
export const register = formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
const request = axios.post("/api/users", formData, config);
return dispatch => {
const onSuccess = success => {
dispatch({
type: "REGISTER_SUCCESS",
payload: success.data
});
return success;
};
const onError = error => {
dispatch({
type: "REGISTER_FAIL",
payload: error.response.data.msg
});
return error;
};
request.then(onSuccess, onError);
};
};
export const register = formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
return async dispatch => {
const onSuccess = success => {
dispatch({
type: "REGISTER_SUCCESS",
payload: success.data
});
return success;
};
const onError = error => {
dispatch({
type: "REGISTER_FAIL",
payload: error.response.data.msg
});
return error;
};
try {
const success = await axios.post("/api/users", formData, config);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
};

How to call ToastsStore.success or ToastsStore.error after API response in React component?

I created a component that contains the "New Article" form. The user can add a new article after clicking the Save button. The click event calls this.props.fetchAddPaper(data), which saves the article to the database.
If the response is 200, I would like to display information on the page for the user that the article has been successfully saved.
If the response is 500 or 400 or 401, I would like to display information that 'something went wrong try again'. To display alerts I use react-toasts. My question is: how can I get a response from the API after clicking the Save button so that you can display a success or error alert? How do I get a response from this.props.fetchAddPaper (data) in the handleSubmit method that I am calling?
Below is the fetchAddPaper that connects to the API. How do I get a response from such a method in a component?
const apiMiddleware = ({ dispatch }) => next => action => {
next(action);
if (action.type !== 'API')
return;
let {
url, // Endpoint address, relative to $HOST/api/
method, // http method (GET, POST, DELETE etc.)
params, // URI string params
data, // Post data
onSuccess, // Function accepting response. If redux action is returned, it will be dispatched
onFinish, // Function run on either success or error
onError, // Function accepting error
onValidationError, // Function accepting response with validation error
text, // Loading text. If not provided there will be no overlay while loading data
successText // Success text, shown on green bar. If not provided it won't be shown
} = action.payload;
// Allow for onSuccess, onFinish and onError to be either redux (and thunk) actions or normal functions
const conditionalDispatch = (action) =>
action && _.isFunction(action) ? dispatch(action) : action;
const request = {
headers: {
'Accept': 'application/json'
},
url: `${host}/api/${url}`,
method,
timeout: 180000
};
if (params) {
params = { ...params };
for (let prop in params) {
if (Array.isArray(params[prop])) {
const arrayData = arrayToGetParameters(params[prop], prop);
delete params[prop];
Object.assign(params, arrayData);
}
}
}
if (data) {
if (method.toUpperCase() === "GET" || method.toUpperCase() === "DELETE") {
throw new Error("Can't add request data to get or delete method");
}
request.headers['Content-Type'] = 'application/json;text/plain;text/json';
}
request.data = data;
request.params = params;
text && dispatch(onLoadingStart(text));
let notificationId = shortId.generate();
axios.request(request)
.then((response) => {
text && dispatch(onLoadingEnd());
onSuccess && dispatch(onSuccess(response.data));
onFinish && conditionalDispatch(onFinish);
if (successText) {
dispatch(onAddFlashMessage({type: 'success', text: successText, id: notificationId}));
setTimeout(() => {
dispatch(onDeleteFlashMessage(notificationId));
}, 5000);
}
})
.catch((error) => {
onFinish && conditionalDispatch(onFinish);
// onError && conditionalDispatch(onError(error));
onError && dispatch(onError(error));
dispatch(onLoadingEnd());
if (error.response && error.response.status === 401) {
//dispatch(onLogOut()); todo: wylogowanie
return;
}
if (error.response && error.response.status === 422 && onValidationError) {
conditionalDispatch(onValidationError(error));
}
else {
dispatch(onAddFlashMessage({...httpReqErrorHandler(error), id: notificationId}));
}
setTimeout(() => {
dispatch(onDeleteFlashMessage(notificationId));
}, 5000);
});
};
export const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.ON_FETCH_ADD_PAPER:
return {
...state,
paper: action.response
};
default:
return state;
}
const onFetchAddPaper = (response) => ({ type: actionTypes.ON_FETCH_ADD_PAPER, response });
export const fetchAddPaper = (data) => {
return (dispatch) => {
dispatch({
type: 'API',
payload: {
url: 'Papers/addPaper',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
},
data: data,
onSuccess: (response) => onFetchAddPaper(response),
onError: (error) => onFetchAddPaper(error)
}
});
};
};
handleSubmit(e) {
e.preventDefault();
let data = {
title: this.state.title,
header: this.state.header
}
this.props.fetchAddPaper(data);
console.log(this.props.paper);
//when the user first clicks the save button, the response is empty, but the second time the response has a value 200
}
function mapStateToProps(state) {
return {
paper: state.paper.paper
}
};
function mapDispatchToProps(dispatch) {
return {
fetchAddPaper: data => dispatch(fetchAddPaper(data))
}
}
//initialstore.jsx
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import apiMiddleware from './ApiMiddleware';
import rootReducers from '../RootReducers';
export default function initStore() {
const store = createStore(
rootReducers,
compose(
applyMiddleware(thunk, consoleMessages, apiMiddleware),
window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f
)
);
if (module.hot) {
module.hot.accept('../RootReducers', () => {
const nextRootReducer = require('../RootReducers').default;
store.replaceReducer(nextRootReducer);
});
}
return store;
}
You can return a promise from your fetchAddPaper action
Something like this:
export const fetchAddPaper = (data) => {
return (dispatch) => {
return new Promise((resolve,reject) => {
dispatch({
type: 'API',
payload: {
url: 'Papers/addPaper',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
},
data: data,
onSuccess: (response) => {
onFetchAddPaper(response);
resolve(response); //return your promise on success
},
onError: (error) => {
onFetchAddPaper(error);
reject(error); //return your promise on failure
}
}
});
})
};
};
So, whenever your action executes, it'll be a promise which you can then evaluate like -
this.props.fetchAddPaper(data).then(response => {..do something})

Fetch Post Request not returning payload but return status code (200)

So I am trying to create a user using redux-form. I have an express post route on the backend. NOTE: using redux-thunk for middleware, whatwg-fetch with webpack and babel-polyfill.
routes.post('/signup', async (req, res) => {
try {
const createdUser = await userController.createUser(req.body);
const JSONCreatedUser = JSON.stringify(createdUser);
res.json({
confirmation: 'success',
result: createdUser,
});
return JSONCreatedUser;
} catch (error) {
res.statusMessage = error.toString();
res.status(409).json({
confirmation: 'failure',
error: error.toString(),
});
}
});
So the problem I am having is that when I use postman. I will get the entire user object back.
But when I submit it using form I only get
Apimanager.js
export const signUserUpApi = async (url, params) => {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
const { status, statusText } = response;
if (status === 409) {
throw new Error(statusText);
}
return response;
} catch (error) {
throw new Error(error.toString());
}
};
action.js
import constants from '../constants';
import { signUserUpApi } from '../utils/APIManager';
const signUserUpUrl = process.env.SIGN_USER_UP_URL || 'http://localhost:3000/user/signup';
export const signUserUp = (user) => {
return async (dispatch) => {
try {
const createdUser = await signUserUpApi(signUserUpUrl, user);
dispatch({
type: constants.SIGN_USER_UP,
user: createdUser,
});
return createdUser;
} catch (error) {
throw new Error(error);
}
};
};
export const signUserIn = (user) => {
return {
type: constants.SIGN_USER_UP,
user,
};
};
What I am trying to do is to get the User Object I created when I submit the form and redirect back to the page.
This is what I get back and it did create the user.
First thing, I need is why am I getting the https status code back and not the user object?
Second thing, what are the ways to redirect to the home page when a user successfully signed up logged in.

Resources