AUTH in next js with React Query - reactjs

Tell me I want to make middleware to protect the administrator pages, I have authorization through sessions on the project. The project itself is on next js . I want to use React Query to protect pages, but I get the error: An error has occurred: Unexpected token < in JSON at position 0 react query
APi:
import type { NextApiRequest, NextApiResponse } from 'next'
import { route } from 'next/dist/server/router'
import { useRouter } from 'next/router'
import checkSession from '../../src/services/checkCookie'
export async function middleware(req: NextApiRequest,res:NextApiResponse) {
if (req.method === 'GET') {
try {
const router= useRouter()
const sid = req.cookies['sid']
const admin = await checkSession(sid)
console.log(router.pathname)
// if (router.pathname === '/admin/login' || router.pathname === '/admin/regAdmin' || admin) {
// return res.next()
// }
res.send(admin)
const host = process.env.NODE_ENV === 'production' ? process.env.HOST : 'http://localhost:3000'
// return res.redirect(host + '/admin/login')
return res.send({ redirectUrl: '/admin/login' })
}catch (error) {
console.error(error)
res.status(500).send({ message: "Server error" })
}
}else{
res.status(404).send({ message: "adress error" })
}
}
Service in api (checkSessin) :
export default async function checkSession (token: string) {
// const token = req.cookies['sid']
if (typeof window === 'undefined' && token) {
const unsign = (await import('./signature')).unsign
const sessionToken = unsign(token, process.env.SECRET!)
if (sessionToken && typeof sessionToken === 'string') {
const db = (await import('../../prisma')).default
const session = db.session.findUnique({ where: { sessionToken },
include: { admin: true } })
if (session) {
return { admin: session.admin }
}
}
}
}
page admin :
import { NextPage } from "next"
import AdminLayout from "../../src/component/admin/AdminLayout"
import { SalesAdminComponent } from "../../src/component/admin/SalesAdmin"
import { useQuery } from 'react-query'
const AdminTable: NextPage = () => {
const { isLoading, error, data,isSuccess} = useQuery('sid', () =>
fetch('api/checkSession',{
method:'GET',
headers: {
"Content-Type": "application/json"
}
}).then(res =>res.json())
)
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<>
{isSuccess &&
<AdminLayout title="OPEL Admin">
<SalesAdminComponent />
</AdminLayout>
}
{isLoading && <p>Loading..</p>}
{error && <p>Error occurred!</p>}
</>
)
}
export default AdminTable

Lots of things are going on here. The response is probably an HTML page that cannot be parsed to JSON. Please include logs both from the server and the browser.
Also, why are you using the client router on the server?

Related

Next.js | HOC from REST API with Typescript not being read when wrapping a child component to access it's data

I have this HOC line of code from withUser.tsx. When a user is authenticated, the authenticated pages will then be wrapped by it so that the specified user-role will be the one to only have access to pages intended.
import axios, { AxiosError } from "axios";
import { API } from "../config";
import { getCookie } from "../helpers/auth";
const withUser = (Page: any) => {
const WithAuthUser = (props: any): JSX.Element => <Page {...props} />;
WithAuthUser.getInitialProps = async (context: any): Promise<any> => {
const token = getCookie("token", context.req);
let user = null;
let userLinks = [];
if (token) {
try {
const response = await axios.get(`${API}/user`, {
headers: {
authorization: `Bearer ${token}`,
contentType: "application/json",
},
});
console.log("Response in withUser: ", response);
user = response.data.user;
userLinks = response.data.links;
} catch (err: unknown) {
const error = err as AxiosError;
if (error.response?.status === 401) {
user = null;
}
}
}
if (user === null) {
// redirect
context.res.writeHead(302, {
Location: "/",
});
context.res.end();
} else {
return {
...(Page.getInitialProps ? await Page.getInitialProps(context) : {}),
user,
token,
userLinks,
};
}
};
return WithAuthUser;
};
export default withUser;
Now, the above code is not my final writing of TypeScript, I could be wrong but this is how I converted it from JS, please feel free to give me a refactored TSX codes here, here is the JS version:
import axios from "axios";
import { API } from "../config";
import { getCookie } from "../helpers/auth";
const withUser = (Page) => {
const WithAuthUser = (props) => <Page {...props} />;
WithAuthUser.getInitialProps = async (context) => {
const token = getCookie("token", context.req);
let user = null;
let userLinks = [];
if (token) {
try {
const response = await axios.get(`${API}/user`, {
headers: {
authorization: `Bearer ${token}`,
contentType: "application/json",
},
});
console.log("Response in withUser: ", response);
user = response.data.user;
userLinks = response.data.links;
} catch (error) {
if (error.response.status === 401) {
user = null;
}
}
}
if (user === null) {
// redirect
context.res.writeHead(302, {
Location: "/",
});
context.res.end();
} else {
return {
...(Page.getInitialProps ? await Page.getInitialProps(context) : {}),
user,
token,
userLinks,
};
}
};
return WithAuthUser;
};
export default withUser;
But now, when using it when an Authenticated /user page, I could not get any data from the user. It will give me an undefined and for example, user.first_name will not be shown:
import withUser from "../withUser";
const User = ({ user }: any): JSX.Element => (
<div className="flex min-h-screen flex-col items-center justify-center">
{user.first_name}
</div>
);
export default withUser(User);
Any correct ways of implementing this would be very much appreciated. Thanks!
Whether you are in Reactjs or Nextjs, I think there needs to have a correct type definitions of your HOC component in the first place.
First you need to define your HOC component as a React.ComponentType:
const withUser = (ChildComp: React.ComponentType<any | string>) => { /* code follows */ }
you also need to define an interface for the expected values for these"
const token = getCookie("token", context.req);
let user = null;
let userLinks = [];
and when you wrap your child component, say user.tsx, do it like this:
type UserType = {
first_name: string
}
const User: React.SFC<ContainerProps> = ({ user}: UserType)=> (
<h1>{user.first_name ?? "User not found"}</h1>
);
export default withUser(User);
You can read more about here: Create a TypeScript HOC in React
Okay, sorry this was just a bug and I figure out that I did not have any userLinks from the REST API that I was passing in. So I can already consider this question as resolved as I have already fixed it.
Here is the code of my fix:
import axios, { AxiosError } from "axios";
import { API } from "../config";
import { getCookie } from "../helpers/auth";
const withUser = (Page: any) => {
const WithAuthUser = (props: any): JSX.Element => <Page {...props} />;
WithAuthUser.getInitialProps = async (context: any): Promise<any> => {
const token = getCookie("token", context.req);
console.log("token: ", token);
let user = null;
if (token) {
try {
const response = await axios.get(`${API}/user`, {
headers: {
authorization: `Bearer ${token}`,
contentType: "application/json",
},
});
console.log("response: ", response);
user = response.data;
} catch (err: unknown) {
const error = err as AxiosError;
if (error.response?.status === 401) {
user = null;
}
}
}
if (user === null) {
// redirect
context.res.writeHead(302, {
Location: "/",
});
context.res.end();
} else {
return {
...(Page.getInitialProps ? await Page.getInitialProps(context) : {}),
user,
token,
};
}
}
return WithAuthUser;
}
export default withUser;

React relay auth middleware

I am trying to build a react app using relay following instructions from the react-relay step by step guide. In the guide the auth token is stored in the env file, and I am trying to retrieve my token from memory which is created when the user logs in and is passed down to all components using the context API. I am not storing it in local storage and have a refresh token to automatically refresh the JWT.
From the tutorial, the relay environment class is not a React component because of which I cannot access the context object.
Is there a way to pass the token from my context to the relay environment class or any middleware implementation to accomplish this.
Any help is greatly appreciated.
import { useContext } from 'react';
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
import axios from "axios";
import { AppConstants } from './app-constants';
import { AuthContext, AuthSteps } from "./context/auth-context";
import { useCookie } from './hooks/useCookie';
interface IGraphQLResponse {
data: any;
errors: any;
}
async function fetchRelay(params: { text: any; name: any; }, variables: any, _cacheConfig: any) {
const authContext = useContext(AuthContext); //Error - cannot access context
const { getCookie } = useCookie(); //Error - cannot access context
axios.interceptors.request.use(
(config) => {
const accessToken = authContext && authContext.state && authContext.state.token;
if(accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
return config;
},
(error) => {
Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
const refreshToken = getCookie(AppConstants.AUTH_COOKIE_NAME);
if(refreshToken && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const response = await axios
.post(process.env.REACT_APP_REFRESH_TOKEN_API_URL!, { refreshToken: refreshToken });
if (response.status === 200 && response.data && response.data.accessToken) {
authContext && authContext.dispatch && authContext.dispatch({
payload: {
token: response.data.accessToken
},
type: AuthSteps.SIGN_IN
});
accessToken = response.data.accessToken;
return axios(originalRequest);
}
}
return Promise.reject(error);
}
);
const data: IGraphQLResponse = await axios.post(process.env.REACT_APP_GRAPHQL_URL!, {
query: params.text,
variables
});
if(Array.isArray(data.errors)) {
throw new Error(
`Error fetching GraphQL query '${
params.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
data.errors,
)}`,
);
}
return data;
}
export default new Environment({
network: Network.create(fetchRelay),
store: new Store(new RecordSource(), {
gcReleaseBufferSize: 10,
}),
});

useContext inside axios interceptor

I cant figure out why my useContext is not being called in this function:
import { useContext } from "react";
import { MyContext } from "../contexts/MyContext.js";
import axios from "axios";
const baseURL = "...";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
.
.
.
});
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const { setUser } = useContext(MyContext);
console.log("anything after this line is not running!!!!");
setUser(null)
.
.
.
My goal is to use an interceptor to check if the token is live and if its not clear the user and do the login. I'm using the same context in my other react components. And its working fine there, its just not running here! any idea whats I'm doing wrong?
I had the same issue as you. Here is how I solved it:
You can only use useContext inside a functional component which is why you can't execute setUser inside your axios interceptors.
What you can do though is to create a separate file called WithAxios:
// WithAxios.js
import { useContext, useEffect } from 'react'
import axios from 'axios'
const WithAxios = ({ children }) => {
const { setUser } = useContext(MyContext);
useEffect(() => {
axios.interceptors.response.use(response => response, async (error) => {
setUser(null)
})
}, [setUser])
return children
}
export default WithAxios
And then add WithAxios after MyContext.Provider to get access to your context like this for example:
// App.js
const App = () => {
const [user, setUser] = useState(initialState)
return (
<MyContext.Provider value={{ setUser }}>
<WithAxios>
{/* render the rest of your components here */}
</WithAxios>
</MyContext.Provider>
)
}
I don't have any issues catching the errors in this schema. are you catching them in the axios interceptor? here how I modified it:
useMemo(() => {
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Prevent infinite loops
if (
error.response.status === 401 &&
originalRequest.url === // your auth url ***
) {
handleLogout();
return Promise.reject(error);
}
if (
error.response.status === 401 &&
error.response.data.detail === "Token is invalid or expired"
) {
handleLogout(); // a function to handle logout (house keeping ... )
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = // get the refresh token from where you store
if (refreshToken && refreshToken !== "undefined") {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const response = await axiosInstance.post(
"***your auth url****",
{
//your refresh parameters
refresh: refreshToken,
}
);
// some internal stuff here ***
return axiosInstance(originalRequest);
} catch (err) {
console.log(err);
handleLogout();
}
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
handleLogout();
}
} else {
console.log("Refresh token not available.");
handleLogout();
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
}, [setUser]);

ReactJs showing global alert dialog from global helper

I have global helper for axios like this:
import Axios from 'axios';
const CustomAxios = Axios;
CustomAxios.interceptors.response.use(response => {
return response;
}, error => {
const isLogin = error.response.config.url.match(/login/);
if (+error.response.status === 401 && !isLogin) { // Invalid token
localStorage.removeItem('jwt_token');
window.location.href = '/';
} else if (+error.response.status === 440) { // Expired token, try to refresh first
return CustomAxios
.post(`${process.env.REACT_APP_API_URL}/refresh`, null, {
headers: {
Authorization: `Bearer ${localStorage.getItem('jwt_token')}`
}
})
.then(res => {
localStorage.setItem('jwt_token', res.data);
error.config.headers['Authorization'] = `Bearer ${res.data}`
return CustomAxios.request(error.config);
})
} else if (process.env.NODE_ENV === 'production') {
alert("Oops.. Something went wrong..\nPlease contact the admin");
}
return Promise.reject(error);
})
export default CustomAxios;
It went great until my boss tell me to change the alert message to something better like alert dialog from material-ui. How do I show this 'global alert' from my global helper? Thanks for answering !

Auth0 SPA TypeError: Cannot read property 'idTokenPayload' of undefined

I’m getting an error on my Gatsby+Auth0 app when trying to log in. Here is the error:
TypeError: Cannot read property 'idTokenPayload' of undefined
(anonymous function)
src/utils/auth.js:1
> 1 | import auth0 from "auth0-js"
2 | import { navigate } from "gatsby"
3 |
4 | const isBrowser = typeof window !== "undefined"
When I log in to my app it works the first time and I can see that authResult is console logging proper values, but the moment I click on any routes or refresh the page, it returns this error. I was working fine until this morning, I didn’t change anything other than some CSS styles.
Here is my auth.js file:
import auth0 from "auth0-js"
import { navigate } from "gatsby"
const isBrowser = typeof window !== "undefined"
const auth = isBrowser
? new auth0.WebAuth({
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENTID,
redirectUri: process.env.AUTH0_CALLBACK,
responseType: "token id_token",
scope: "openid profile email",
})
: {}
const tokens = {
accessToken: false,
idToken: false,
expiresAt: false,
}
let user = {}
export const isAuthenticated = () => {
if (!isBrowser) {
return
}
return localStorage.getItem("isLoggedIn") === "true"
}
export const login = () => {
if (!isBrowser) {
return
}
auth.authorize()
}
const setSession = (cb = () => {}) => (err, authResult) => {
console.log(authResult);
localStorage.setItem("userAuthID", authResult.idTokenPayload.sub)
localStorage.setItem("userIdToken", 'Bearer '+authResult.idToken)
if (err) {
navigate("/")
cb()
return
}
if (authResult && authResult.accessToken && authResult.idToken) {
let expiresAt = authResult.expiresIn * 1000 + new Date().getTime()
tokens.accessToken = authResult.accessToken
tokens.idToken = authResult.idToken
tokens.expiresAt = expiresAt
user = authResult.idTokenPayload
localStorage.setItem("isLoggedIn", true)
navigate("/account")
cb()
}
}
export const silentAuth = callback => {
if (!isAuthenticated()) return callback()
auth.checkSession({}, setSession(callback))
}
export const handleAuthentication = () => {
if (!isBrowser) {
return
}
auth.parseHash(setSession())
}
export const getProfile = () => {
return user
}
export const logout = () => {
localStorage.setItem("isLoggedIn", false)
localStorage.removeItem("userIdToken");
auth.logout()
}
And here is my callback.js file:
import React, { Component } from 'react'
import { handleAuthentication } from "../utils/auth"
import { navigate } from 'gatsby';
export default class callback extends Component {
componentDidMount() {
handleAuthentication();
setTimeout(() => {
navigate('/account')
}, 1500);
}
render() {
return (
<div>
<div className="hero is-white is-fullheight">
<div className="hero-body">
<div className="container">
<h1 className="title is-1 is-spaced has-text-centered logo-text">Fottom</h1>
</div>
</div>
</div>
</div>
)
}
}
Can some please help me find what is missing here? Thanks!
Your problem here is that you are trying to access the property idTokenPayload of authResult, without checking before if authResult is null. In your setSession() function, in case of error, you are accessing authResult. You should only set the localStorage items when the login was successful.
Try this code:
const setSession = (cb = () => {}) => (err, authResult) => {
console.log(authResult);
if (err) {
navigate("/")
cb()
return
}
if (authResult && authResult.accessToken && authResult.idToken) {
localStorage.setItem("userAuthID", authResult.idTokenPayload.sub) // moved after error control
localStorage.setItem("userIdToken", 'Bearer '+authResult.idToken) // moved after error control
let expiresAt = authResult.expiresIn * 1000 + new Date().getTime()
tokens.accessToken = authResult.accessToken
tokens.idToken = authResult.idToken
tokens.expiresAt = expiresAt
user = authResult.idTokenPayload
localStorage.setItem("isLoggedIn", true)
navigate("/account")
cb()
}
}
I agree with #mhSangar that you should probably only attempt to access the payload AFTER ensuring that it's not null.
But I think there's also another layer to the issue that you're experiencing. You mentioned that everything works fine until you refresh, which I'm assuming is when the token refresh is attempted. My guess is that the issue lies within the checkSession method.
Currently, it's being invoked like this:
auth.checkSession({}, setSession(callback))
You have to pass a config as the first argument to that method (not just an empty object). When I invoke it in my app, I pass the following args:
{
audience,
callbackURL,
clientID,
domain
}
Try passing those arguments to auth.checkSession and see if that fixes it.
i.e.
export const silentAuth = callback => {
if (!isAuthenticated()) return callback()
auth.checkSession(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENTID,
redirectUri: process.env.AUTH0_CALLBACK,
audience: YOUR_AUDIENCE (looks like AUTH0_DOMAIN/api/v2/)
},
setSession(callback)
)
}

Resources