I have this a Dashboard component which makes three API calls to fetch widget data.
If any API call fails it refreshes token.
But, when Dashboard renders it makes API call individually and they don't wait to check if first api call failed or token is refreshed. Each api call ends up making another call to refresh the token.
It should stop at first API call fail and refresh the token.
But it does so for each request. How can I prevent this behaviour.
It seems I need to make request sequentially.
const Dashboard = () => {
const { response: studentResponse } = useAxios(ApiConfig.STUDENT.GET_STUDENTS);
const { response: courseResponse } = useAxios(ApiConfig.COURSE.GET_COURSES);
const { response: feesResponse } = useAxios(ApiConfig.FEES.GET_TOTAL);
return (
<Box padding={2} width="100%">
<Stack direction={'row'} justifyContent="space-between" gap={2} mb={10}>
<NewWidget type={'student'} counter={studentResponse?.data?.length} />
<NewWidget type={'course'} counter={courseResponse?.data?.length} />
<NewWidget type={'earning'} counter={feesResponse?.data} />
</Stack>
</Box>
);
};
export default Dashboard;
use-axios.js
import { useState, useEffect } from 'react';
import axios from 'axios';
import history from '../utils/history';
import refreshToken from './refresh-token';
const Client = axios.create();
Client.defaults.baseURL = 'http://localhost:3000/api/v1';
const getUser = () => {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
};
const updateLocalStorageAccessToken = (accessToken) => {
const user = getUser();
user.accessToken = accessToken;
localStorage.setItem('user', JSON.stringify(user));
};
Client.interceptors.request.use(
(config) => {
const user = getUser();
config.headers.Authorization = user?.accessToken;
return config;
},
(error) =>
// Do something with request error
Promise.reject(error)
);
Client.interceptors.response.use(
(response) => response,
(error) => {
// Reject promise if usual error
if (error.response.status !== 401) {
return Promise.reject(error);
}
const user = getUser();
const status = error.response ? error.response.status : null;
const originalRequest = error.config;
console.log(originalRequest);
if (status === 401 && originalRequest.url !== '/auth/refresh-token') {
refreshToken(user.refreshToken)
.then((res) => {
const { accessToken } = res.data.data;
Client.defaults.headers.common.Authorization = accessToken;
// update local storage
updateLocalStorageAccessToken(accessToken);
return Client(originalRequest);
})
.catch((err) => {
console.log(err);
if (err.response.status === 401) {
localStorage.setItem('user', null);
history.push('/login');
}
return Promise.reject(err);
});
}
history.push('/login');
return Promise.reject(error);
}
);
export const useAxios = (axiosParams, isAuto = true) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const fetchData = async (params) => {
try {
const result = await Client.request({
...params,
method: params.method || 'GET',
headers: {
accept: 'application/json',
},
});
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isAuto) fetchData(axiosParams);
}, [axiosParams, isAuto]); // execute once only
return { fetch: () => fetchData(axiosParams), response, error, loading };
};
In the interceptor for the response, you check if there's an error. I would keep a state which contains the previous success of a call, implement that how you wish - after that, create an interceptor for requests which checks if that error occurred, and if so, cancel the request:
axios.interceptors.request.use((req: AxiosRequestConfig) => {
if(error){
throw new axios.Cancel('Operation canceled due to previous failure.');
}
else {
return req
}
})
Also see: Axios: how to cancel request inside request interceptor properly?
Related
I am using firebase for authentication in my Next.js app and also I have an express server that serves a REST API, which has a middleware that uses firebase-admin to verify idToken that is sent from my app, to pass the authenticated routes
Currently
The idToken generated by firebase lasts for one hour and if the client is still on my app and
hits any route that needs idToken and if the idToken is expired then the server just throws an error as unauthenticated, which is pretty good work, but this is not desired, I know my user is in there and just idToken is expired
Question
How do I refresh my idToken of a user if it has expired, without having to do a full refresh in the browser to get new idToken
Some Code
AuthContext.tsx
/* eslint-disable no-unused-vars */
import { useRouter } from 'next/router'
import nookies from 'nookies'
import { createContext, useContext, useEffect, useState } from 'react'
import { axios } from '../config/axios'
import firebase from '../config/firebase'
import { AuthUser } from '../types'
import { BaseUser } from '../types/user'
import { getProvider } from '../utils/oAuthProviders'
type AuthContextType = {
user: AuthUser | null
login: (email: string, password: string) => Promise<any>
signup: (email: string, password: string) => Promise<any>
logout: () => Promise<any>
oAuthLogin: (provider: string) => Promise<any>
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => useContext(AuthContext)
const fromPaths = ['/login', '/signup']
const formatUser = (user: BaseUser, idToken: string): AuthUser => {
return {
...user,
idToken,
}
}
export const AuthContextProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
console.log(user)
useEffect(() => {
const unsub = firebase.auth().onIdTokenChanged((user) => {
if (user) {
user
.getIdToken()
.then(async (idToken) => {
try {
const userResp = await axios.get('/user/me', {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
nookies.set(undefined, 'idk', idToken, { path: '/' })
const {
data: { userFullDetials },
} = userResp
setUser(formatUser(userFullDetials, idToken))
setLoading(false)
if (fromPaths.includes(router.pathname)) {
router.push('/home')
}
} catch (err) {
console.log(err)
setUser(null)
setLoading(false)
}
})
.catch((err) => {
console.log(err.message)
setUser(null)
setLoading(false)
})
} else {
setLoading(false)
setUser(null)
}
})
return () => unsub()
}, [router])
const login = (email: string, password: string) => {
return firebase.auth().signInWithEmailAndPassword(email, password)
}
const signup = (email: string, password: string) => {
return firebase.auth().createUserWithEmailAndPassword(email, password)
}
const oAuthLogin = (provider: string) => {
return firebase.auth().signInWithPopup(getProvider(provider))
}
const logout = async () => {
setUser(null)
await firebase.auth().signOut()
}
const returnObj = {
user,
login,
signup,
logout,
oAuthLogin,
}
return (
<AuthContext.Provider value={returnObj}>
{loading ? (
<div className="flex items-center justify-center w-full h-screen bg-gray-100">
<h1 className="text-indigo-600 text-8xl">S2Media</h1>
</div>
) : (
children
)}
</AuthContext.Provider>
)
}
// auth.ts
// Auth Middleware in express
import { NextFunction, Request, Response } from 'express'
import fbadmin from 'firebase-admin'
import { DecodedIdToken } from '../types/index'
export default async (req: Request, res: Response, next: NextFunction) => {
const authorization = req.header('Authorization')
if (!authorization || !authorization.startsWith('Bearer')) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
const idToken = authorization.split(' ')[1]
if (!idToken) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
try {
const decodedToken = await fbadmin.auth().verifyIdToken(idToken)
req.user = decodedToken as DecodedIdToken
return next()
} catch (err) {
console.log(err.message)
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
}
The Firebase SDK does that for you. Whenever you call user.getIdToken() it will return a valid token for sure. If the existing token has expired, it will refresh and return a new token.
You can use onIdTokenChanged()and which will trigger whenever a token is refreshed and store it in your state.
However, I don't see any cons in using getIdToken() method whenever you are making an API request to server. You won't have to deal with IdToken observer and get valid token always.
const makeAPIRequest = async () => {
// get token before making API request
const token = await user.getIdToken()
// pass the token in request headers
}
Right now your code makes a request to server to get user's information whenever a token refreshes and that may be redundant.
I am using Next.js. I have created an Axios interceptor where a rejected Promise will be returned. But where there is a server-specific error that I need. Next.js is showing the error in the application like this.
And there is the code of the Axios interceptor and instance.
import axios from "axios";
import store from "../redux/store";
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
let token = "";
if (typeof window !== 'undefined') {
const item = localStorage.getItem('key')
token = item;
}
const axiosInstance = axios.create({
baseURL: publicRuntimeConfig.backendURL,
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
});
axiosInstance.interceptors.request.use(
function (config) {
const { auth } = store.getState();
if (auth.token) {
config.headers.Authorization = `Bearer ${auth.token}`;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(res) => {
console.log(res)
return res;
},
(error) => {
console.log(error)
return Promise.reject(error);
}
);
export default axiosInstance;
Also, I am using redux and there is the action.
import axios from "../../api/axios";
import { authConstants } from "../types";
export const login = (data) => {
return async (dispatch) => {
try {
dispatch({
type: authConstants.LOGIN_REQUEST,
});
const res = axios.post("/user/login", data);
if (res.status === 200) {
dispatch({
type: authConstants.LOGIN_SUCCESS,
payload: res.data,
});
}
} catch (error) {
console.log(error, authConstants);
dispatch({
type: authConstants.LOGIN_FAILURE,
payload: { error: error.response?.data?.error },
});
}
};
};
Your problem is here...
const res = axios.post("/user/login", data);
You're missing await to wait for the response
const res = await axios.post("/user/login", data);
This fixes two things...
Your code now waits for the response and res.status on the next line will be defined
Any errors thrown by Axios (which surface as rejected promises) will trigger your catch block. Without the await this does not happen and any eventual promise failure bubbles up to the top-level Next.js error handler, resulting in the popup in your screenshot.
I want to add a Clickable “Remember Me” checkbox in my login page that tells the browser to save a cookie so that if you close out the window for the site without signing out, the next time you go back, you will be signed back in automatically.that can save username and password
export const getUser = () => {
const userStr = sessionStorage.getItem("user");
if (userStr) return JSON.parse(userStr);
else return null;
};
export const getToken = () => {
return sessionStorage.getItem("token") || null;
};
export const setUserSession = (token, user) => {
sessionStorage.setItem("token", token);
sessionStorage.setItem("user", JSON.stringify(user));
};
export const removeUserSession = () => {
sessionStorage.removeItem("token");
sessionStorage.removeItem("user");
};
export const handleSuccessfulLogin = async (token, rememberMe) => {
localStorage.setItem("token", token);
localStorage.setItem("rememberme", rememberMe);
};
export const handleLogout = () => {
localStorage.clear();
};
This is my login that work with api
const handelLogin = () => {
setError(null);
setLoading(true);
axios
.post("https://www.mecallapi.com/api/login", {
username: username,
password: password,
})
.then((response) => {
setLoading(false);
setUserSession(response.data.token, response.data.user);
navigate("/Dashboard");
})
.catch((error) => {
setLoading(false);
if (error.response.status === 401 || error.response.status === 400) {
setError(error.response.data.message);
} else {
setError("somthing went wrong ,please try again");
}
});
};
This is my remember me checkbox
<div className="login-bottom">
<Checkbox {...label} />
</div>
I am using firebase for authentication in my Next.js app and also I have an express server that serves a REST API, which has a middleware that uses firebase-admin to verify idToken that is sent from my app, to pass the authenticated routes
Currently
The idToken generated by firebase lasts for one hour and if the client is still on my app and
hits any route that needs idToken and if the idToken is expired then the server just throws an error as unauthenticated, which is pretty good work, but this is not desired, I know my user is in there and just idToken is expired
Question
How do I refresh my idToken of a user if it has expired, without having to do a full refresh in the browser to get new idToken
Some Code
AuthContext.tsx
/* eslint-disable no-unused-vars */
import { useRouter } from 'next/router'
import nookies from 'nookies'
import { createContext, useContext, useEffect, useState } from 'react'
import { axios } from '../config/axios'
import firebase from '../config/firebase'
import { AuthUser } from '../types'
import { BaseUser } from '../types/user'
import { getProvider } from '../utils/oAuthProviders'
type AuthContextType = {
user: AuthUser | null
login: (email: string, password: string) => Promise<any>
signup: (email: string, password: string) => Promise<any>
logout: () => Promise<any>
oAuthLogin: (provider: string) => Promise<any>
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => useContext(AuthContext)
const fromPaths = ['/login', '/signup']
const formatUser = (user: BaseUser, idToken: string): AuthUser => {
return {
...user,
idToken,
}
}
export const AuthContextProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
console.log(user)
useEffect(() => {
const unsub = firebase.auth().onIdTokenChanged((user) => {
if (user) {
user
.getIdToken()
.then(async (idToken) => {
try {
const userResp = await axios.get('/user/me', {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
nookies.set(undefined, 'idk', idToken, { path: '/' })
const {
data: { userFullDetials },
} = userResp
setUser(formatUser(userFullDetials, idToken))
setLoading(false)
if (fromPaths.includes(router.pathname)) {
router.push('/home')
}
} catch (err) {
console.log(err)
setUser(null)
setLoading(false)
}
})
.catch((err) => {
console.log(err.message)
setUser(null)
setLoading(false)
})
} else {
setLoading(false)
setUser(null)
}
})
return () => unsub()
}, [router])
const login = (email: string, password: string) => {
return firebase.auth().signInWithEmailAndPassword(email, password)
}
const signup = (email: string, password: string) => {
return firebase.auth().createUserWithEmailAndPassword(email, password)
}
const oAuthLogin = (provider: string) => {
return firebase.auth().signInWithPopup(getProvider(provider))
}
const logout = async () => {
setUser(null)
await firebase.auth().signOut()
}
const returnObj = {
user,
login,
signup,
logout,
oAuthLogin,
}
return (
<AuthContext.Provider value={returnObj}>
{loading ? (
<div className="flex items-center justify-center w-full h-screen bg-gray-100">
<h1 className="text-indigo-600 text-8xl">S2Media</h1>
</div>
) : (
children
)}
</AuthContext.Provider>
)
}
// auth.ts
// Auth Middleware in express
import { NextFunction, Request, Response } from 'express'
import fbadmin from 'firebase-admin'
import { DecodedIdToken } from '../types/index'
export default async (req: Request, res: Response, next: NextFunction) => {
const authorization = req.header('Authorization')
if (!authorization || !authorization.startsWith('Bearer')) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
const idToken = authorization.split(' ')[1]
if (!idToken) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
try {
const decodedToken = await fbadmin.auth().verifyIdToken(idToken)
req.user = decodedToken as DecodedIdToken
return next()
} catch (err) {
console.log(err.message)
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
}
The Firebase SDK does that for you. Whenever you call user.getIdToken() it will return a valid token for sure. If the existing token has expired, it will refresh and return a new token.
You can use onIdTokenChanged()and which will trigger whenever a token is refreshed and store it in your state.
However, I don't see any cons in using getIdToken() method whenever you are making an API request to server. You won't have to deal with IdToken observer and get valid token always.
const makeAPIRequest = async () => {
// get token before making API request
const token = await user.getIdToken()
// pass the token in request headers
}
Right now your code makes a request to server to get user's information whenever a token refreshes and that may be redundant.
I did follow a tutorial of how to integrate mailchimp with node backend. I have never touched back end, so am pretty lame at it.
When I POST to their API I get the subscriber's credentials, but I get an error back - "Assignment to constant variable". Reading through the web and other SO questions, it seems like I am trying to reassign a CONST value.
I had a goooooooooood look at my code and the only thing I have noticed that might be issues here is
request(options, (error, response, body) => {
try {
const resObj = {};
if (response.statusCode == 200) {
resObj = {
success: `Subscibed using ${email}`,
message: JSON.parse(response.body),
};
} else {
resObj = {
error: ` Error trying to subscribe ${email}. Please, try again`,
message: JSON.parse(response.body),
};
}
res.send(respObj);
} catch (err) {
const respErrorObj = {
error: " There was an error with your request",
message: err.message,
};
res.send(respErrorObj);
}
});
I have noticed I am creating an empty object called "resObj", then trying to assign a value to it.
I have tried changing the CONST to LET, but I get an error saying: "resObj is not defined".
Here is my front end code:
import React, { useState } from "react";
import "./App.css";
import Subscribe from "./components/Subscribe";
import Loading from "./components/Loading/Loading";
import axios from "axios";
import apiUrl from "./helpers/apiUrl";
function App() {
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const handleSendEmail = (e) => {
setLoading(true);
console.log(email);
axios
.post(`${apiUrl}/subscribe`, { email: email })
.then((res) => {
if (res.data.success) {
alert(`You have successfully subscribed!, ${res.data.success}`);
setEmail("");
setLoading(false);
} else {
alert(`Unable to subscribe, ${res.data.error}`);
console.log(res);
setLoading(false);
setEmail("");
}
})
.catch((err) => {
setLoading(false);
alert("Oops, something went wrong...");
console.log(err);
setEmail("");
});
e.preventDefault();
};
const handleInput = (event) => {
setEmail(event.target.value);
};
// const handleLoadingState = (isLoading) => {
// setLoading({ isLoading: loading });
// console.log(loading);
// };
return (
<div className='App'>
<h1>Subscribe for offers and discounts</h1>
{loading ? (
<Loading message='Working on it...' />
) : (
<Subscribe
buttonText='Subscribe'
value={email}
handleOnChange={handleInput}
handleOnSubmit={handleSendEmail}
/>
)}
</div>
);
}
export default App;
And the Back end code:
const restify = require("restify");
const server = restify.createServer();
const corsMiddleware = require("restify-cors-middleware");
const request = require("request");
require("dotenv").config({ path: __dirname + "/variables.env" });
const subscribe = (req, res, next) => {
const email = req.body.email;
const dataCenter = process.env.DATA_CENTER;
const apiKey = process.env.MAILCHIMP_API_KEY;
const listID = process.env.LIST_ID;
const options = {
url: `https://${dataCenter}.api.mailchimp.com/3.0/lists/${listID}/members`,
method: "POST",
headers: {
"content-type": "application/json",
Authorization: `apikey ${apiKey}`,
},
body: JSON.stringify({ email_address: email, status: "subscribed" }),
};
request(options, (error, response, body) => {
try {
const resObj = {};
if (response.statusCode == 200) {
resObj = {
success: `Subscibed using ${email}`,
message: JSON.parse(response.body),
};
} else {
resObj = {
error: ` Error trying to subscribe ${email}. Please, try again`,
message: JSON.parse(response.body),
};
}
res.send(respObj);
} catch (err) {
const respErrorObj = {
error: " There was an error with your request",
message: err.message,
};
res.send(respErrorObj);
}
});
next();
};
const cors = corsMiddleware({
origins: ["http://localhost:3001"],
});
server.pre(cors.preflight);
server.use(restify.plugins.bodyParser());
server.use(cors.actual);
server.post("/subscribe", subscribe);
server.listen(8080, () => {
console.log("%s listening at %s", server.name, server.url);
});
If anyone could help I would be very grateful. The subscription form works, but I need to clear that bug in order for my front end to work correctly onto submission of the form.
Maybe what you are looking for is Object.assign(resObj, { whatyouwant: value} )
This way you do not reassign resObj reference (which cannot be reassigned since resObj is const), but just change its properties.
Reference at MDN website
Edit: moreover, instead of res.send(respObj) you should write res.send(resObj), it's just a typo