I am using next-auth for user authentification, when the user signin I store the accessToken in the session
in my page [trackId].js each time the user updates the track the value of refresh (which is a useState hook) changes so useEffect runs and get the new track.
useEffect(() => {
async function refreshPage() {
console.log(session); // <-- output in the image below
await axiosPrivateWithToken(session.data.accessToken)
.get(`/track/get/${trackId}`)
.then((res) => {
setGetedTrack(res.data);
})
.catch((error) => {
console.log("refreshPage for track has catched an error : ", error);
});
}
refreshPage();
}, [refresh]);
so far so good all works fine the problem is when the user enters the URL manually, I get this error
TypeError: Cannot read properties of undefined (reading 'accessToken')
and this is because when the window reloads the session is loading and does not have the accessToken yet, data is still undefined
any suggestions ? thank you for your attention
Instead of checking if session is defined each time you need it, a good pattern is this one, you declare an Auth component :
function Auth({ children }: { children: JSX.Element }) {
// if `{ required: true }` is supplied, `status` can only be "loading" or "authenticated"
const router = useRouter();
const { status } = useSession({ required: true, onUnauthenticated() {
router.push('/login');
}, })
if (status === "loading") {
return <div>Loading...</div>
}
return children
}
And in your _app.tsx, you add it like this so session will always be defined:
<Layout>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</Layout>
So if you need a Page where session has always to be defined, you juste have to add an Auth property to your page, for exmaple if you have a page named 'account' :
Account.auth = {
}
And then, session will always be defined in your page or you'll be redirect to /login
Can't you just add session.data.accessToken to the dependency list, so it refreshes once you have your access token?
useEffect(() => {
async function refreshPage() {
if (!session.data || !session.data.accessToken) return;
console.log(session); // <-- output in the image below
await axiosPrivateWithToken(session.data.accessToken)
.get(`/track/get/${trackId}`)
.then((res) => {
setGetedTrack(res.data);
})
.catch((error) => {
console.log("refreshPage for track has catched an error : ", error);
});
}
refreshPage();
}, [refresh, (session.data || { accessToken: '' }).accessToken]);
Related
I have a redux action called academyRedirect.js which is invoked whenever an user is redirected to the path /user/academy when he pushes the 'academy' button in my main page.
export const getAcademyAutoLogin = () => {
axios.get(`/user/academy`)
.then((response) => {
window.location.replace(response.data); // this is replaced by an URL coming from backend
})
.catch((error) => {
reject(error.response);
});
return null;
};
Whenever the user is not allowed to access the academy (for not having credentials) or whenever i get an error 500 or 404, i need to display a modal or something to inform the user that an error occurred while trying to log into the academy. Right now im not being able to do it, the page just stays blank and the console output is the error.response.
Any help is appreciated
Redux Store
export const messageSlice = createSlice({
name: 'message',
initialState: { isDisplayed: false, errorMessage: ''},
reducers: {
displayError(state, action) {
state.isDisplayed = true
state.errorMessage = action.message
},
resetErrorState() {
state.isDisplayed = false
state.errorMessage = ''
},
}
})
export const messageActions = messageSlice.actions;
Inside the component:-
const Login = () => {
const errorState = useSelector(globalState => globalState.message)
const onClickHandler = async => {
axios.get(`/user/academy`)
.then((response) => { window.location.replace(response.data) })
.catch((error) => {
dispatch(messageActions.displayError(error))
});
}
return (
{errorState.isDisplayed && <div>{errorState.errorMessage}</div>}
{!errorState.isDisplayed &&<button onClick={onClickHandler}>Fetch Data </button>}
)
}
Maybe this is of help to you
You can try to add interceptor to your axios.
Find a place where you create your axios instance and apply an interceptor to it like this
const instance = axios.create({
baseURL: *YOUR API URL*,
headers: { "Content-Type": "application/json" },
});
instance.interceptors.request.use(requestResolveInterceptor, requestRejectInterceptor);
And then, in your requestRejectInterceptor you can configure default behavior for the case when you get an error from your request. You can show user an error using toast for example, or call an action to add your error to redux.
For second case, when you want to put your error to redux, its better to use some tools that created to make async calls to api and work with redux, for example it can be redux-saga, redux-thunk, redux-axios-middleware etc.
With their docs you would be able to configure your app and handle all cases easily.
Creating an app which allows you to sign up and sign in user in the database. After sign up / sign in process the app moves you to the content.
Code for authorization process:
const Auth = ({ handleSetAuthorized }) => {
const { refetch } = useQuery(CURRENT_USER)
const { error } = useSelector(({ error }) => error)
const history = useHistory()
const dispatch = useDispatch()
const [signup] = useMutation(SIGN_UP, {
onCompleted: async (data) => {
const token = data.signup
localStorage.setItem("token", token)
await refetch()
handleSetAuthorized()
history.push("/pages/edituser")
},
onError: error => dispatch(getError(error))
})
const [login] = useMutation(SIGN_IN, {
onCompleted: async (data) => {
const token = data.login.token
localStorage.setItem("token", token)
await refetch()
handleSetAuthorized()
history.push("/pages/process")
},
onError: error => dispatch(getError(error))
})
const signUp = (values) => {
signup({
variables: {
firstName: values.firstName,
secondName: values.secondName,
email: values.email,
password: values.password,
}
})
}
const signIn = (values) =>{
login({
variables: {
email: values.email,
password: values.password,
}
})
}
const removeError = () => {
dispatch(cleanError())
}
return (
<>
<div className="auth">
<img
className="auth__logo"
src={logo}
alt="logo"
/>
<div className="auth__content">
<Switch>
<Route path="/auth/signin" >
<SignIn onSubmit={signIn} removeError={removeError}/>
</Route>
<Route path="/auth/signup">
<SignUp onSubmit={signUp} removeError={removeError}/>
</Route>
</Switch>
{error &&
<ErrorMessage className="mistake">
{error.message}
</ErrorMessage>
}
</div>
</div>
</>
)
}
export default Auth
As you can see after mutation is completed, I need to refetch my CURRENT_USER query to understand who is current user and to move user to the content. I do that here:
const { refetch } = useQuery(CURRENT_USER)
const [signup] = useMutation(SIGN_UP, {
onCompleted: async (data) => {
const token = data.signup
localStorage.setItem("token", token)
await refetch() // <- HERE!
handleSetAuthorized()
history.push("/pages/edituser")
},
onError: error => dispatch(getError(error))
})
Code works but the problem is I don't want to refetch query like that: import CURRENT_USER query itself, get refetch function from useQuery hook and use it inside onCompleted option of mutation.
ApolloClient provides next:
I can put refetchQueries option inside useMutation hook to refetch my CURRENT_USER query like that:
const [signup] = useMutation(SIGN_UP, {
refetchQueries: [{query: CURRENT_USER }], // <- HERE!
onCompleted: (data) => {
const token = data.signup
localStorage.setItem("token", token)
handleSetAuthorized()
history.push("/pages/edituser")
},
onError: error => dispatch(getError(error))
})
That approach doesn't work because refetchQueries option updates CURRENT_USER query before I get and put token from mutation result in localStorage. So user is not moved to content because CURRENT_USER query result is empty and app shows user a mistake.
I can use 'refetch-queries' npm package. It provides the option to refetch Apollo queries anywhere by name and partial variables. Perfect for my case and look like that:
const [signup] = useMutation(SIGN_UP, {
onCompleted: (data) => {
const token = data.signup
localStorage.setItem("token", token)
refetchQueries: [{query: CURRENT_USER }], // <- HERE!
handleSetAuthorized()
history.push("/pages/edituser")
},
onError: error => dispatch(getError(error))
})
So here I use that option inside onCompleted option and after putting token in localStorage. Seems perfect in my case but doesn't work. I have no idea why, it throws the same mistake which shows that CURRENT_USER query result is empty. Maybe package is not supported.
My wish is to refetch CURRENT_USER query after pushing user to the content and before content (Pages component) initialization. But how to do that using useEffect hook inside content (Pages component) and without componentWillMount method.
And what is the best practice for refetching queries after mutation in my situation?
I'm using nextjs and apollo (with react hooks). I am trying to update the user object in the apollo cache (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).
I want to logout the user immediately after login , so I could see in redux if It works and I get this error: Uncaught Error: Actions may not have an undefined "type" property. Have you misspelled a constant?
I should get AUTH_LOGOUT action in my redux after the success- example image.
As I understand the error Is in the checkAuthTimeout:
export const checkAuthTimeout = (expirationTime) => {
return dispatch => {
setTimeout(() => {
dispatch(logout())
}, expirationTime )
}}
My logout:
export const logout = () => {
return {
type: actionTypes.AUTH_LOGOUT
}}
And auth:
export const auth = (email, password) => {
return dispatch => {
dispatch(authStart());
const authData = {
email:email,
password:password,
returnSecureToken: true
}
axios.post('https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyCpqBy-KjAJWCMUYLHVWAIu_HWZd3yzHVE', authData)
.then(response => {
console.log(response);
dispatch(authSuccess(response.data.idToken, response.data.localId));
dispatch(checkAuthTimeout(response.data.expiresIn));
})
.catch(err => {
dispatch(authFail(err.response.data.error));
});
}}
The error itself is pretty clear, your action type has value of undefined. Meaning actionTypes.AUTH_LOGOUT is undefined.
Try do a console.log on actionTypes.AUTH_LOGOUT, probably it's not defined or not imported properly. Show us the file where it is defined and imported, but I am positive the problem is either on how you exported or imported it.
Our React Native Redux app uses JWT tokens for authentication. There are many actions that require such tokens and a lot of them are dispatched simultaneously e.g. when app loads.
E.g.
componentDidMount() {
dispath(loadProfile());
dispatch(loadAssets());
...
}
Both loadProfile and loadAssets require JWT. We save the token in the state and AsyncStorage. My question is how to handle token expiration.
Originally I was going to use middleware for handling token expiration
// jwt-middleware.js
export function refreshJWTToken({ dispatch, getState }) {
return (next) => (action) => {
if (isExpired(getState().auth.token)) {
return dispatch(refreshToken())
.then(() => next(action))
.catch(e => console.log('error refreshing token', e));
}
return next(action);
};
}
The problem that I ran into was that refreshing of the token will happen for both loadProfile and loadAssets actions because at the time when they are dispatch the token will be expired. Ideally I would like to "pause" actions that require authentication until the token is refreshed. Is there a way to do that with middleware?
I found a way to solve this. I am not sure if this is best practice approach and there are probably some improvements that could be made to it.
My original idea stays: JWT refresh is in the middleware. That middleware has to come before thunk if thunk is used.
...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);
Then in the middleware code we check to see if token is expired before any async action. If it is expired we also check if we are already are refreshing the token -- to be able to have such check we add promise for fresh token to the state.
import { refreshToken } from '../actions/auth';
export function jwt({ dispatch, getState }) {
return (next) => (action) => {
// only worry about expiring token for async actions
if (typeof action === 'function') {
if (getState().auth && getState().auth.token) {
// decode jwt so that we know if and when it expires
var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;
if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {
// make sure we are not already refreshing the token
if (!getState().auth.freshTokenPromise) {
return refreshToken(dispatch).then(() => next(action));
} else {
return getState().auth.freshTokenPromise.then(() => next(action));
}
}
}
}
return next(action);
};
}
The most important part is refreshToken function. That function needs to dispatch action when token is being refreshed so that the state will contain the promise for the fresh token. That way if we dispatch multiple async actions that use token auth simultaneously the token gets refreshed only once.
export function refreshToken(dispatch) {
var freshTokenPromise = fetchJWTToken()
.then(t => {
dispatch({
type: DONE_REFRESHING_TOKEN
});
dispatch(saveAppToken(t.token));
return t.token ? Promise.resolve(t.token) : Promise.reject({
message: 'could not refresh token'
});
})
.catch(e => {
console.log('error refreshing token', e);
dispatch({
type: DONE_REFRESHING_TOKEN
});
return Promise.reject(e);
});
dispatch({
type: REFRESHING_TOKEN,
// we want to keep track of token promise in the state so that we don't try to refresh
// the token again while refreshing is in process
freshTokenPromise
});
return freshTokenPromise;
}
I realize that this is pretty complicated. I am also a bit worried about dispatching actions in refreshToken which is not an action itself. Please let me know of any other approach you know that handles expiring JWT token with redux.
Instead of "waiting" for an action to finish, you could instead keep a store variable to know if you're still fetching tokens:
Sample reducer
const initialState = {
fetching: false,
};
export function reducer(state = initialState, action) {
switch(action.type) {
case 'LOAD_FETCHING':
return {
...state,
fetching: action.fetching,
}
}
}
Now the action creator:
export function loadThings() {
return (dispatch, getState) => {
const { auth, isLoading } = getState();
if (!isExpired(auth.token)) {
dispatch({ type: 'LOAD_FETCHING', fetching: false })
dispatch(loadProfile());
dispatch(loadAssets());
} else {
dispatch({ type: 'LOAD_FETCHING', fetching: true })
dispatch(refreshToken());
}
};
}
This gets called when the component mounted. If the auth key is stale, it will dispatch an action to set fetching to true and also refresh the token. Notice that we aren't going to load the profile or assets yet.
New component:
componentDidMount() {
dispath(loadThings());
// ...
}
componentWillReceiveProps(newProps) {
const { fetching, token } = newProps; // bound from store
// assuming you have the current token stored somewhere
if (token === storedToken) {
return; // exit early
}
if (!fetching) {
loadThings()
}
}
Notice that now you attempt to load your things on mount but also under certain conditions when receiving props (this will get called when the store changes so we can keep fetching there) When the initial fetch fails, it will trigger the refreshToken. When that is done, it'll set the new token in the store, updating the component and hence calling componentWillReceiveProps. If it's not still fetching (not sure this check is necessary), it will load things.
I made a simple wrapper around redux-api-middleware to postpone actions and refresh access token.
middleware.js
import { isRSAA, apiMiddleware } from 'redux-api-middleware';
import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'
export function createApiMiddleware() {
const postponedRSAAs = []
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({dispatch, getState})
return (next) => (action) => {
const nextCheckPostponed = (nextAction) => {
// Run postponed actions after token refresh
if (nextAction.type === TOKEN_RECEIVED) {
next(nextAction);
postponedRSAAs.forEach((postponed) => {
rsaaMiddleware(next)(postponed)
})
} else {
next(nextAction)
}
}
if(isRSAA(action)) {
const state = getState(),
token = refreshToken(state)
if(token && isAccessTokenExpired(state)) {
postponedRSAAs.push(action)
if(postponedRSAAs.length === 1) {
return rsaaMiddleware(nextCheckPostponed)(refreshAccessToken(token))
} else {
return
}
}
return rsaaMiddleware(next)(action);
}
return next(action);
}
}
}
export default createApiMiddleware();
I keep tokens in the state, and use a simple helper to inject Acess token into a request headers
export function withAuth(headers={}) {
return (state) => ({
...headers,
'Authorization': `Bearer ${accessToken(state)}`
})
}
So redux-api-middleware actions stays almost unchanged
export const echo = (message) => ({
[RSAA]: {
endpoint: '/api/echo/',
method: 'POST',
body: JSON.stringify({message: message}),
headers: withAuth({ 'Content-Type': 'application/json' }),
types: [
ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
]
}
})
I wrote the article and shared the project example, that shows JWT refresh token workflow in action
I think that redux is not the right tool for enforcing the atomicity of token refresh.
Instead I can offer you an atomic function that can be called from anywhere and ensures that you will always get a valid token:
/*
The non-atomic refresh function
*/
const refreshToken = async () => {
// Do whatever you need to do here ...
}
/*
Promise locking-queueing structure
*/
var promiesCallbacks = [];
const resolveQueue = value => {
promiesCallbacks.forEach(x => x.resolve(value));
promiesCallbacks = [];
};
const rejectQueue = value => {
promiesCallbacks.forEach(x => x.reject(value));
promiesCallbacks = [];
};
const enqueuePromise = () => {
return new Promise((resolve, reject) => {
promiesCallbacks.push({resolve, reject});
});
};
/*
The atomic function!
*/
var actionInProgress = false;
const refreshTokenAtomically = () => {
if (actionInProgress) {
return enqueuePromise();
}
actionInProgress = true;
return refreshToken()
.then(({ access }) => {
resolveQueue(access);
return access;
})
.catch((error) => {
rejectQueue(error);
throw error;
})
.finally(() => {
actionInProgress = false;
});
};
Posted also here: https://stackoverflow.com/a/68154638/683763