I have a project with react and an java spring boot api. For connect to the api, after SignIn with google login, i do this :
import axios from 'axios'
import { useNavigate } from 'react-router-dom';
import { signInRoute } from '../../routing';
export function SignInGetService() {
axios.get(process.env.REACT_APP_API_SERVER_URL + '/url', {})
.then(response => { });
}
export async function SignInPostService(token) {
return await axios.post(process.env.REACT_APP_API_SERVER_URL + '/mySignInUrl', { "token": token })
.then(response => {
axios.defaults.headers.common['X-AUTH-TOKEN'] = response.data['data'];
return response.data['data']
}).catch(function(error) {
if(error.response.status == 500){
window.localStorage.removeItem('userFromStorage');
window.location.reload()
}
});
}
in app.tsx my code is :
interface AuthContextType {
user: any;
signin: (userMail: string, callback: VoidFunction, userObject: Object) => void;
signout: (callback: VoidFunction) => void;
}
let AuthContext = React.createContext<AuthContextType>(null!);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<any>(window.localStorage.getItem('userFromStorage'));
let signin = (newUserMail: string, callback: VoidFunction, newUserObject: Object) => {
return fakeAuthProvider.signin(() => {
setUser(JSON.stringify(newUserMail));
window.localStorage.setItem('userFromStorage', JSON.stringify(newUserMail));
callback();
});
};
let signout = (callback: VoidFunction) => {
return fakeAuthProvider.signout(() => {
setUser(null);
window.localStorage.removeItem('userFromStorage');
callback();
});
};
const value = { user, signin, signout };
const [isLoading, setIsLoading] = useState(true)
if (user != null) {
let r = SignInPostService(JSON.parse(user).token).then(() => {
setIsLoading(false)
})
}
if (user != null && isLoading == false) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
if (user == null) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
}
export function useAuth() {
return React.useContext(AuthContext);
}
function RequireAuth({ children }: { children: JSX.Element }) {
let auth = useAuth();
let location = useLocation();
if (!auth.user) {
return <Navigate to="/SignIn" state={{ from: location }} replace />;
}
return children;
}
So after this, normally everytime I do a request my X-AUTH-TOKEN is suppose to be well set.
And actually it's work. But sometimes, not often but enough for being annoying, I have an 401 error.
And If I refresh the 401 error disappear.
Do you know where it can come from ?
Thank you all.
I expect that the X-AUTH-TOKEN is set everytime.
Related
I have used the context in other places, such as login, database functions, and more. However, when I try to run functions or variables inside my context in places such as custom api's or getServerSideProps, it returns the following error, TypeError: Cannot read properties of null (reading 'useContext'). I am attaching my auth context, my initialization of the context, and the getServerSideProps function that is returning an error
_app.js
import RootLayout from '../components/Layout'
import { AuthProvider } from '../configs/auth-context'
import '../styles/globals.css'
export default function App({ Component, pageProps }) {
return (
<AuthProvider >
<RootLayout>
<Component {...pageProps} />
</RootLayout>
</AuthProvider>
)}
auth-context
import React, { useContext, useState, useEffect, useRef } from 'react'
import { auth, db, provider } from './firebase-config'
import { GoogleAuthProvider, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged, signInWithPopup } from 'firebase/auth'
import { doc, getDoc, setDoc } from 'firebase/firestore'
import {useRouter} from 'next/router';
const AuthContext = React.createContext({currentUser: {uid: "TestUid", email:"Testeremail#email.com"}})
export function UseAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const router = useRouter();
const [currentUser, setCurrentUser] = useState({uid: "TestUid", email:"Testeremail#email.com"})
const [loading, setLoading] = useState(true)
async function signup(email, password) {
createUserWithEmailAndPassword(auth, email, password)
.then(async (result) => {
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user;
}).catch((error) => {
console.error(error);
})
return
}
async function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
.then(async (result) => {
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user;
}).catch((error) => {
console.error(error)
})
}
function logout() {
router.push('/')
return signOut(auth)
}
async function googleSignIn() {
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then(async (result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user
}).catch((error) => {
console.log(error)
// const errorCode = error.code;
// const errorMessage = error.message;
// The email of the user's account used.
// const email = error.customData.email;
// The AuthCredential type that was used.
// const credential = GoogleAuthProvider.credentialFromError(error);
} )
}
const userToDb = async (user) => {
// await setDoc(doc(db, "users", user.uid), {
// userEmail: user.email,
// userID: user.uid
// }, {merge: false})
let currentRef = doc(db, 'users', user.uid)
let currentUserID = user.uid;
let currentEmail = user.email;
await setDoc(currentRef, {
userEmail: currentEmail,
userID: currentUserID
}, {merge: false})
}
function fixData(docs) {
console.log("this works")
// setDocuments(docs);
let retMap = new Map();
if (currentUser !== null) {
docs?.map(function(doc) {
console.log(doc)
let tic = doc.stockTicker
let data = {
shares: doc.shares,
price: doc.price,
type: doc.type
}
if(!retMap.has(tic)) {
retMap.set(tic, [data]);
console.log(tic + " " + data)
// setMap(new Map(datamap.set(tic, {shares: shares, averagePrice: price})))
}
else {
let x = retMap.get(tic);
x.push(data);
}
})
console.log(retMap)
return retMap;
}
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async user => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
signup,
logout,
googleSignIn,
fixData
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
getServerSideProps
export async function getServerSideProps() {
let allDocs = []
let avgDocs = []
const {currentUser} = UseAuth()
return {
props: {allDocs, avgDocs}
}
}
I don't know the correct answer, but hooks should be used in components and hooks without exception to ssr.
I have following bit of code where <AuthContext> provide authentication objects and methods, <ProtectedRoute> component which guards the authentication required routes.
But the problem is when I login and refresh inside authenticated page, user object fetched from the useAuth hook returns null, but if I use next/link it works fine and user object is preserved.
AuthContext.tsx
const AuthContext = createContext<any>({})
export const useAuth = () => useContext(AuthContext)
type Props = {
children: React.ReactNode
}
const LOGIN = gql`...`
export const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = useState<object | null>(null)
const [loginUser, { data, loading, error }] = useMutation(LOGIN)
const router = useRouter()
useEffect(() => {
// in case of first login set user and token
// push user to route he belongs
if (data != null) {
if (data.authenticate.jwt !== null) {
console.log('Setting user with JWT decoding...')
const decoded = jwt_decode(data.authenticate.jwt)
setUser({
role: decoded.role,
id: decoded.id,
})
localStorage.setItem('token', data.authenticate.jwt)
}
}
const token = localStorage.getItem('token')
// if token is present set the user object
if (token !== null) {
console.log('Token present')
const decoded = jwt_decode(token)
// console.log(user)
console.log(`Decoded token : ${JSON.stringify(decoded)}`)
setUser({
role: decoded.role,
id: decoded.id,
})
} else {
setUser(null)
}
}, [data])
const login = async (username: string, password: string) => {
loginUser({
variables: {
username,
password,
},
})
}
const logOut = async () => {
console.log('Logging out ...')
setUser(null)
data = null
localStorage.removeItem('token')
}
// if (error) return <p>Submission error! ${error.message}</p>
return (
<AuthContext.Provider value={{ user, login, logOut }}>
{error || loading ? null : children}
</AuthContext.Provider>
)
}
ProtectedRoute.tsx
const PUBLIC_PATHS = ['/']
const ADMIN_ROUTES = [...]
const SUPERVISOR_ROUTES = [...]
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user } = useAuth()
const router = useRouter()
const [authorized, setAuthorized] = useState(false)
useEffect(() => {
console.log(`Current User : ${JSON.stringify(user)}`)
const isAdminRoute = ADMIN_ROUTES.includes(router.pathname)
const isSupervisorRoute = SUPERVISOR_ROUTES.includes(router.pathname)
// if an token is present send user to
// authorized route
if (user !== null) {
// #ts-ignore
if (user.role === 1 && isAdminRoute) {
setAuthorized(true)
console.log(`Pushing to ${router.pathname}`)
router.push(router.pathname)
// #ts-ignore
} else if (user.role === 2 && isSupervisorRoute) {
setAuthorized(true)
console.log(`Pushing to ${router.pathname}`)
router.push(router.pathname)
} else {
console.log(`Invalid role! user: ${JSON.stringify(user)}`)
}
} else {
setAuthorized(false)
console.log('Sending you to login page')
// "/" have the login page
router.push('/')
}
}, [user]) // router is avoided from deps to avoid infinite loop
return <>{authorized && children}</>
}
_app.tsx
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
const noAuthRequired = ['/']
function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page)
const router = useRouter()
return (
<ApolloProvider client={CLIENT}>
<AuthContextProvider>
{getLayout(
noAuthRequired.includes(router.pathname) ? (
<Component {...pageProps} />
) : (
<ProtectedRoute>
<Component {...pageProps} />
</ProtectedRoute>
),
)}
</AuthContextProvider>
</ApolloProvider>
)
}
export default App
Current possible workaround is to read JWT in the ProtectedRoute to get user information.
I there anyway to preserve user object on page refresh?
I am using the Auth Provider to manage my Firebase auth information. I want to be able to use currentUser as soon as I sign up, but it won't set without reloading.
I tried to setCurrentUser out of the Auth Provider and set it, but I could not get it to work either.
contexts/Auth.tsx
const AuthContext = createContext<IAuthContext>(null!)
export const AuthProvider = ({
children,
}: {
children: ReactNode
}) => {
const [currentFBUser, setCurrentFBUser] = useState<firebase.User | null>(null)
const [currentUser, setCurrentUser] = useState<any>(null)
const [isLoading, setIsLoading] = useState<boolean>(true)
const { update } = useIntercom()
/**
* SUBSCRIBE user auth state from firebase
*/
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (!user) {
setCurrentFBUser(null)
setIsLoading(false)
return
}
await setCurrentFBUser(user)
const storeUser = await userRepository.findById(user.uid)
if (!storeUser) {
setCurrentUser(null)
setIsLoading(false)
return
}
await setCurrentUser(storeUser)
/* UPDATE Intercom props */
if(currentUser) {
update({
name: currentUser.name,
email: currentUser.email
})
}
setIsLoading(false)
return () => {
unsubscribe()
}
})
}, [])
const logout = useCallback(() => {
const auth = getAuth();
signOut(auth).then(() => {
window.location.reload()
}).catch((err) => {
toast.error(err.message)
});
}, [])
return (
<AuthContext.Provider value={{
currentFBUser,
currentUser,
setCurrentUser,
isLoading,
logout,
}}>
{children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within the AuthProvider')
}
return context
}
signup.tsx
...
const { currentFBUser, isLoading, setCurrentUser } = useAuthContext()
const signup = handleSubmit(
async (data) => {
if (data.password != data.confirmPassword) {
toast.error('Your password is not matched!')
return
}
const auth = getAuth()
createUserWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
const auth = userCredential.user
if (!auth) return
const { email, uid } = auth
if (!email) return
const user = userRepository.findOrCreate(email, uid)
setCurrentUser(user)
})
.catch((err) => {
toast.error(err.message)
});
},
(err: any) => {
toast.error(err.message)
},
)
...
Try to call again getAuth() instead of using the response from createUserWithEmailAndPassword
const { currentFBUser, isLoading, setCurrentUser } = useAuthContext()
const signup = handleSubmit(
async (data) => {
if (data.password != data.confirmPassword) {
toast.error('Your password is not matched!')
return
}
const auth = getAuth()
createUserWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
// const auth = userCredential.user
const auth = getAuth().currentUser
if (!auth) return
const { email, uid } = auth
if (!email) return
const user = userRepository.findOrCreate(email, uid)
setCurrentUser(user)
})
.catch((err) => {
toast.error(err.message)
});
},
(err: any) => {
toast.error(err.message)
},
)
I'm working on a larger web-application, which makes several calls to the server from different places. We are using a token which has a lifetime of 15min. Which means it uses the refreshtoken to generate a new token and a new refreshtoken when it gets expired. We are using an interceptor of axios to see that a token got expired. Based on that we are executing a call to generate a new one.
The issue right now is, that all API calls that are made during the time when we make a request to get a new token, will fail!
So my question I have right now is if it is possible to prevent sending requests before a specific call is completely executed (using axios)?
export const runRefreshToken = (errorCallback: () => void): void => {
Axios.interceptors.response.use(
(response: AxiosResponse<any>): AxiosResponse<any> => {
return response;
}, (error: any): any => {
const original = error.config;
const loginVerification = !(original.url as string).startsWith("api/login");
if (error.response && error.response.status === 401 && error.config && loginVerification && !error.config.__retry) {
return new Promise((resolve): void => {
original.__retry = true;
const response = fetch("api/login/refresh-token", { method: 'POST' })
.then((): AxiosPromise<any> => {
return Axios(original);
})
.catch((): void => errorCallback())
resolve(response);
});
} else if (error.config.__retry && original.url !== "api/login/verify-authentication") {
history.push("/login");
}
return Promise.reject(error);
}
);
}
Thanks in advance
This is something I have run into on several teams. The solution we have used is a component at the top level of the app that makes the initial request and does not render any of the children until it has succeeded.
import React from 'react';
import { useAuthUser } from './hooks';
export const AuthenticateUser = ({ children }) => {
const { hasError, isLoading, shouldRedirectToLoginPage, handleRedirectToLoginPage } = useAuthUser();
if (isLoading) {
return <LoadingPage>;
}
if (hasError) {
return <ErrorPage>;
}
return children;
};
The hook file looks like
import { userSlice } from 'state/user';
import { useAppDispatch } from 'state/hooks';
import { useEffect } from 'react';
import { useUserDetails } from 'api/userRequests';
export function useUserDetailsProps(): UserDetailsProps {
const { data, isLoading, error } = useUserDetails();
const dispatch = useAppDispatch();
useEffect(() => {
if (data != null) {
dispatch(userSlice.actions.authenticatedUserReceived(data));
}
}, [data, dispatch]);
return {
hasError: !!error,
isLoading,
shouldRedirectToLoginPage: !!error && error.response?.status === 401,
handleRedirectToLoginPage,
};
}
I create an API service like this:
export default class ApiService {
static init(store) {
axios.interceptors.request.use(config => {
const { token } = store.getState().user;
config.baseURL = ${process.env.NEXT_STATIC_API_URL};
if (token) {
config.headers.Authorization = Bearer ${token};
}
return config;
});
}
}
And init it here in the main app component:
class EnhancedApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
const { store } = ctx;
if (!isClient()) ApiService.init(store);
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
componentDidMount() {
ApiService.init(this.props.store);
}
render() {
const { Component, pageProps, store } = this.props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
export default withRedux(configureStore, { debug: true })(
withReduxSaga(EnhancedApp)
);
I'm using a wrapper to get user data for every page:
const checkAuth = async (ctx, isProtected) => {
const { auth } = nextCookie(ctx);
if (isProtected && !auth) {
if (!isClient() && ctx.res) {
ctx.res.writeHead(302, { Location: '/' });
ctx.res.end();
} else {
await Router.push('/');
}
}
return auth || null;
};
export const withAuth = (isProtected = false) => (WrappedComponent) => {
const WrappedWithAuth = (props) => {
const { token } = useSelector(
state => state.user
);
useEffect(() => {
if (isProtected && !token) Router.push('/');
}, [token]);
return <WrappedComponent {...props} />;
};
WrappedWithAuth.getInitialProps = async (ctx) => {
const token = await checkAuth(ctx, isProtected);
const { store } = ctx;
if (token) {
store.dispatch(userSetToken(token));
try {
const { data } = await UserService.getProfile();
store.dispatch(userGetProfileSuccess(data));
} catch (e) {
store.dispatch(userGetProfileFailure(e));
}
}
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps({ ...ctx, token }));
return { ...componentProps };
};
return WrappedWithAuth;
};
Everything works properly on the client-side, but when I change the user and refresh the page I see that get profile API call on the server-side continues to use a previous token.