Intercepting Auth0 getSession with MSW.js and Cypress - reactjs

I'm building NextJS app with SSR. I've written the getServerSideProps function that makes a call to supabase. Before making the call I'm trying to get user session by calling getSession function from #auth0/nextjs-auth0 package.
I'm trying to mock it in the handlers.ts file:
import { rest } from 'msw';
export const handlers = [
// this is the endpoint called by getSession
rest.get('/api/auth/session', (_req, res, ctx) => {
return res(ctx.json(USER_DATA));
}),
rest.get('https://<supabase-id>.supabase.co/rest/v1/something', (_req, res, ctx) => {
return res(ctx.json(SOMETHING));
}),
];
My mocks file: requestMocks/index.ts:
export const initMockServer = async () => {
const { server } = await import('./server');
server.listen();
return server;
};
export const initMockBrowser = async () => {
const { worker } = await import('./browser');
worker.start();
return worker;
};
export const initMocks = async () => {
if (typeof window === 'undefined') {
console.log('<<<< setup server');
return initMockServer();
}
console.log('<<<< setup browser');
return initMockBrowser();
};
initMocks();
Finally, I'm calling it in the _app.tsx file:
if (process.env.NEXT_PUBLIC_API_MOCKING === 'true') {
require('../requestMocks');
}
Unfortunately, it does work for me. I'm getting no user session data in the getServerSideProps function in my page component:
import { getSession } from '#auth0/nextjs-auth0';
export const getServerSideProps = async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) => {
const session = getSession(req, res);
if (!session?.user.accessToken) {
// I'm constantly falling here
console.log('no.session');
return { props: { something: [] } };
}
// DO something else
};
Any suggestions on how to make it working in Cypress tests would be great.
I'm expecting that I will be able to mock requests made in getServerSideProps function with MSW.js library.

I made it finally. Looks like I don't have to mock any calls. I need to copy my user appSession cookie and save it in cypress/fixtures/appSessionCookie.json file:
{
"appSession": "<cookie-value>"
}
Then use it in tests as follows:
before(() => {
cy.fixture('appSessionCookie').then((cookie) => {
cy.setCookie('appSession', cookie.appSession);
});
});
This makes a user automatically logged in with Auth0.

Related

Fetch wont work in getStaticProps for ISR but works for CSR

I'm trying to fetch some data using the -
import { useBhagwadGitaQuote, useTime } from '../api/Endpoints';
...
...
(default export component code)
...
...
export async function getStaticProps() {
const { timeData } = await useTime();
console.log(timeData);
return {
props: {
timeData
},
revalidate: 86400,
};
}
The useTime is defined as -
export const useTime = async () => {
const options = {
method: 'GET',
};
const link = 'https://worldtimeapi.org/api/timezone/Asia/Kolkata';
const raw = await fetch(link, options);
const timeData = await raw.json();
console.log(timeData);
return { timeData, isLoading: !timeData };
};
When I run it with yarn run dev it does not works and throws this error -
However, it works fine with Client Side Rendering, When I call the Hook useTime, inside the functional component, at the client's end.

Access React context in an API service

In my React application I use the context API to store the user information through the useContext hook:
const AuthContext = createContext<AuthContextType>(null!);
const useAuth = () => useContext(AuthContext);
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User>();
// Implementations of values
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export { AuthProvider, useAuth };
Accessing the auth information works all fine and dandy in the components:
export default function CoolComponent() {
const auth = useAuth();
if (auth.user) {
// Do something
}
return <div>Hello {auth.user}</div>;
}
The thing is that my jwt-token is stored in the user object and I need it for my API calls in my service, but hooks are not allowed outside functional components. Can I circumvent this in a clever way? Some things that I can think of is to pass the token on every call to the service (not very DRY) or save the token in localStorage and then retrieve it from there in the service, but it seems unnecessary to store the same information in two different places?
Update:
Now with the service code:
const baseUrl = environment.apiUrl;
function getToken() {
// This is what I would like to get some help with
}
const headers = {
...(getToken() && { Authorization: `Bearer ${getToken()}` }),
"Content-Type": "application/json",
};
function getAllProjects(): Promise<IProject[]> {
return fetch(`${baseUrl}projects`, {
headers,
}).then((response) => response.json());
}
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
// + many more
export { getAllProjects, createProject };
Calling the service in a component:
useEffect(() => {
const fetchProjects = async () => {
setIsLoading(true);
try {
const allProjects = await getAllProjects();
setProjects(allProjects);
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
fetchProjects();
}, []);
The React documentation says that you cannot call hooks inside JavaScript functions.
What can you do?
Use custom hooks. rename functions as useCreateProject and return your function. Then you will be able to call useAuth inside your custom hook:
const useCreateProject =() =>{
const {user} = useAuth();
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
return createProject
}
Then call it like this:
const createProject = useCreateProject()
useEffect(() => {
const create = async () => {
setIsLoading(true);
try {
await createProject()
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
create();
}, []);
But my advice is to store the token on localStorage or in cookies. Context data will be lost when user refreshes page. However, if that is not case for you, you can continue using context.

how to use an Axios interceptor with Next-Auth

I am converting my CRA app to Nextjs and running into some issues with my Axios interceptor pattern.
It works, but I am forced to create and pass an Axios instance to every api call.
Is there a better way to do this?
Here is what I have now:
Profile.js:
import { useSession } from 'next-auth/react'
function Profile(props) {
const { data: session } = useSession()
const [user, setUser] = useState()
useEffect(()=> {
const proc= async ()=> {
const user = await getUser(session?.user?.userId)
setUser(user)
}
proc()
},[])
return <div> Hello {user.userName}<div>
}
getUser.js:
export default async function getUser(userId) {
const axiosInstance = useAxios()
const url = apiBase + `/user/${userId}`
const { data } = await axiosInstance.get(url)
return data
}
useAxios.js:
import axios from 'axios'
import { useSession } from 'next-auth/react'
const getInstance = (token) => {
const axiosApiInstance = axios.create()
axiosApiInstance.interceptors.request.use(
(config) => {
if (token && !config.url.includes('authenticate')) {
config.headers.common = {
Authorization: `${token}`
}
}
return config
},
(error) => {
Promise.reject(error)
}
)
return axiosApiInstance
}
export default function useAxios() {
const session = useSession()
const token = session?.data?.token?.accessToken
return getInstance(token)
}
In case anyone else has this problem, this was how i solved it (using getSession):
credit to:
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
import axios from 'axios'
import { getSession } from 'next-auth/react'
const ApiClient = () => {
const instance = axios.create()
instance.interceptors.request.use(async (request) => {
const session = await getSession()
if (session) {
request.headers.common = {
Authorization: `${session.token.accessToken}`
}
}
return request
})
instance.interceptors.response.use(
(response) => {
return response
},
(error) => {
console.log(`error`, error)
}
)
return instance
}
export default ApiClient()
There is actually a neat way on including user extended details to session object
// /api/[...nextauth].ts
...
callbacks: {
session({ session, user, token }) {
// fetch user profile here. you could utilize contents of token and user
const profile = getUser(user.userId)
// once done above, you can now attach profile to session object
session.profile = profile;
return session;
}
},
The you could utilize it as:
const { data: session } = useSession()
// Should display profile details not included in session.user
console.log(session.profile)
I know one way to do this is to use
const session = await getSession()
Is there any other way to go about it without using await getSession() because what this does is that it makes a network request to get your session every time your Axios request runs?

Server-side redirects using react HOC when a httpOnly cookie is not present [duplicate]

So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.
The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.
This is my HOC code:
(cookie are stored under userToken name)
import React from 'react';
const jwt = require('jsonwebtoken');
const RequireAuthentication = (WrappedComponent) => {
return WrappedComponent;
};
export async function getServerSideProps({req,res}) {
const token = req.cookies.userToken || null;
// no token so i take user to login page
if (!token) {
res.statusCode = 302;
res.setHeader('Location', '/admin/login')
return {props: {}}
} else {
// we have token so i return nothing without changing location
return;
}
}
export default RequireAuthentication;
If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.
You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.
For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.
export function requireAuthentication(gssp) {
return async (context) => {
const { req, res } = context;
const token = req.cookies.userToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/admin/login',
statusCode: 302
}
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
}
}
You would then use it in your page by wrapping the getServerSideProps function.
// pages/index.js (or some other page)
export const getServerSideProps = requireAuthentication(context => {
// Your normal `getServerSideProps` code here
})
Based on Julio's answer, I made it work for iron-session:
import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '#/utils/index'
export const withAuth = (gssp: any) => {
return async (context: GetServerSidePropsContext) => {
const { req } = context
const user = req.session.user
if (!user) {
return {
redirect: {
destination: '/',
statusCode: 302,
},
}
}
return await gssp(context)
}
}
export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))
And then I use it like:
export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
return {
props: {},
}
})
My withSessionSsr function looks like:
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'
const IRON_OPTIONS: IronSessionOptions = {
cookieName: process.env.IRON_COOKIE_NAME,
password: process.env.IRON_PASSWORD,
ttl: 60 * 2,
}
function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, IRON_OPTIONS)
}
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
return withIronSessionSsr(handler, IRON_OPTIONS)
}
export { withSessionRoute, withSessionSsr }

How to fetch API data from Axios inside the getServerSideProps function in NextJS?

I'm building an App with Next.js, and I need to connect to specific API routes (set up with API Platform) and populate pages with the route's responses.
The API is working fine, but no matter how I try to implement my Axios call inside the getServerSideProps, I always get the same error, ECONNREFUSED, from my Node stack.
I tried to get the data from useEffect() and it's working fine, but I would like to know if there's a way to call it directly in getServerSideProps.
I'm using a Node container for Docker, and the routes are authenticated through a JWT Token (stored in the session and the client cookies for the server-side connection)
Here are is my code:
pages/accounts.js:
export async function getServerSideProps(context) {
const cookies = new Cookies(context.req.headers.cookie)
const adminToken = cookies.get('jwtToken')
const res = await getAllAccounts(adminToken)
return {
props: {
testdata: ''
},
}
}
lib/accounts.js:
import service from '../service'
export const getAllAccounts = async (adminToken) => {
const res = service({ jwtToken : adminToken }).get(`/accounts`).then((response) => {
}).catch((error) => {
console.dir(error)
})
}
HTTP wrapper:
import axios from 'axios';
import jwt_decode from "jwt-decode";
import mockAdapter from 'axios-mock-adapter';
const service = ({ jwtToken = null, store = null, mockURL = null, mockResponse = null, multipart = false } = {}) => {
const options = {};
options.baseURL = process.env.NEXT_PUBLIC_API_URL + '/api';
if(multipart === true) {
options.headers = {
'Content-Type': 'multipart/form-data'
}
} else {
options.headers = {
'Content-Type': 'application/ld+json',
accept: 'application/ld+json'
}
}
const instance = axios.create(options);
instance.interceptors.response.use(response => {
return response;
}, error => {
return Promise.reject(error);
})
if (mockURL !== null && mockResponse !== null) {
let mock = new mockAdapter(instance);
mock.onAny(mockURL).reply(200, mockResponse)
}
return instance;
};
export default service;
Through the error dump in the node stack, I managed to see that the request headers are correct, and the JWT correctly passed through.
Do not use Axios. Just use fetch().
Next.js polyfills fetch() by default on both the client and server, so you can just use it:
In addition to fetch() on the client-side, Next.js polyfills fetch() in the Node.js environment. You can use fetch() in your server code (such as getStaticProps/getServerSideProps) without using polyfills such as isomorphic-unfetch or node-fetch.
Source.
getServerSideProps works well with axios if we return response.data
export const getServerSideProps: GetStaticProps = async ({ params }) => {
const { brandName } = params as IParams;
const brandData = await $host.get(`api/brand/${brandName}`).then(response => response.data);
return {
props: {
brand: brandData,
},
};
};
Your problem is that your async method does not return a promise.
import service from '../service'
export const getAllAccounts = async (adminToken) => {
const res = service({ jwtToken : adminToken }).get(`/accounts`);
return res;
}
In my NextJS begining I followed this tutorial , and I changed fetch to axios in this way:
export const getStaticPaths = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await res.json();
const paths = data.map((ninja) => {
return {
params: { id: ninja.id.toString() },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context) => {
const id = context.params.id;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
const data = await res.json();
return {
props: { ninja: data },
};
};
I applied the change using useEffect()
useEffect(() => {
// const data = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
// const res = await data.json();
// setninja(res);
const fetchData = async () => {
const result = await axios(`https://jsonplaceholder.typicode.com/users/${id}`);
setninja(result.data);
};
fetchData();
console.log(data);
}, []);
I hope this info will be useful for you.
I Used Axios in getServerSideProps without any problems.
export const getServerSideProps: GetServerSideProps = async({
params,
res
}) => {
try {
const response = await axios.get(`/api/test`);
return {
props: {
data: response.data
},
}
} catch {
res.statusCode = 404;
return {
props: {}
};
}
};

Resources