Apollo Authentication with Gatsby - reactjs

I am trying to implement the Authentication workflow officially described in the Apollo docs as I did before in other projects, but this time using Gatsby.
The idea seems pretty straightforward. Need to create/update gatsby-browser.js like when using redux but to initialise the ApolloClient and pass through ApolloProvider.
Something like:
import React from 'react'
import { Router } from 'react-router-dom'
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context'
const httpLink = createHttpLink({
uri: process.env.ENDPOINT_URI,
credentials: 'same-origin',
})
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache().restore({}),
})
exports.replaceRouterComponent = ({ history }) => {
const ConnectedRouterWrapper = ({ children }) => (
<ApolloProvider client={client}>
<Router history={history}>{children}</Router>
</ApolloProvider>
)
return ConnectedRouterWrapper
}
Then login using
<Mutation
mutation={LOGIN_MUTATION}
refetchQueries={[{ query: CURRENT_USER_QUERY }]}
>
{/*...*/}
</Mutation>
and in another Component with something like the Status (where, if logged in shows x otherwise y):
<Query query={CURRENT_USER_QUERY} fetchPolicy="network-only">
{/*...*/}
</Query>
The problem
With this configuration, when the login receives data (so, credentials are ok), I save the token received in localStorage but the Status component do the Query with the previous (empty) token, so shows logged out, also doing something like history.push('/') after login.
Refreshing the page, since the item in localStorage was saved before, the Status shows as logged in.
Expected behaviour
The idea is to have the Status component updated after login avoiding doing a refresh of the page, for this reason I added refetchQueries={[{ query: CURRENT_USER_QUERY }]}.
I tried some things like pass props (no needed in case I change the route and use withRouter in the Status component), but seems to late, the Query was fired without the new token.

The main issue here is probably your Status component needs to be re-render only after the the token saved into localStorage. Right now it's probably re-render too early.
You can try wrapping your Status component with another wrapper component, and check for the token existence before rendering the Status component.

I believe your issue is related to how the links are defined. IMO, the AuthLink should be defined before the HttpLink (as middleware):
const client = new ApolloClient({
link: ApolloLink.from([
authLink,
httpLink
]),
cache: new InMemoryCache().restore({}),
})
While the documentation says that concat and from do the same thing, order is important. You want to have the context set before sending the request.

Related

Can't upload a file to Strapi using GraphQL and Apollo

I have a React/Next.js app which is using GraphQL and Apollo to connect and interact with a headless API. I am using Strapi as my headless API/CMS which is working great, except for one issue. I am trying to upload a file from my React app to a content type in my Strapi CMS using a GraphQL mutation and it keeps failing on the upload part.
When I upload a file using Altair(my playground environment) to Strapi with the exact same mutation everything works fine, but once I try to run the same mutation from my React app I get this error:
Variable \"$file\" got invalid value {}; Upload value invalid.. Everything I see online brings up using apollo-upload-client and adding something like link: createUploadLink({ uri: "http://localhost:4300/graphql" }), to my Apollo client initiation. I have tried that but whenever I use it, it breaks my app and I get this GraphQL error Error: Cannot return null for non-nullable field UsersPermissionsMe.username..
It seems like the only answer I can find is using apollo-upload-client but when I use it, it seems to break my app. I don't know if I need to use it differently maybe because I am using Strapi, or Next, or #apollo/client. I am a little lost on this one.
This is how I initiate my Apollo client and when everything works except uploading a file.
import { ApolloClient, InMemoryCache, NormalizedCacheObject } from "#apollo/client";
import { parseCookies } from "nookies";
import { useMemo } from 'react'
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
let apolloClient: ApolloClient<NormalizedCacheObject>;
function createApolloClient(ctx) {
return new ApolloClient({
ssrMode: typeof window === "undefined",
uri: publicRuntimeConfig.PUBLIC_GRAPHQL_API_URL,
cache: new InMemoryCache(),
headers: {
Authorization: `Bearer ${ctx ? parseCookies(ctx).jwt : parseCookies().jwt}`,
},
});
}
My app runs perfect with this code except for the fact that I get "message":"Variable \"$file\" got invalid value {}; Upload value invalid.", whenever I try to upload a file. So when I try to use apollo-upload-client to fix that like this:
import { ApolloClient, InMemoryCache, NormalizedCacheObject } from "#apollo/client";
import { createUploadLink } from 'apollo-upload-client';
import { parseCookies } from "nookies";
import { useMemo } from 'react'
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
let apolloClient: ApolloClient<NormalizedCacheObject>;
function createApolloClient(ctx) {
return new ApolloClient({
ssrMode: typeof window === "undefined",
// uri: publicRuntimeConfig.PUBLIC_GRAPHQL_API_URL,
link: createUploadLink({ uri: publicRuntimeConfig.PUBLIC_GRAPHQL_API_URL }),
cache: new InMemoryCache(),
headers: {
Authorization: `Bearer ${ctx ? parseCookies(ctx).jwt : parseCookies().jwt}`,
},
});
}
I get this error: Error: Cannot return null for non-nullable field UsersPermissionsMe.username..
I am still new to GraphQL and Strapi so maybe I am missing something obvious. Everything works except uploads. I can upload from my playground, just not from my app, that is where I am at.
So after two days of this issue, I figured it out after 10 minutes of reading the apollo-upload-client documentation. So much shame. All I needed to do was move my header request from the ApolloClient options to the createUploadLink options. Final fix was
function createApolloClient(ctx) {
return new ApolloClient({
ssrMode: typeof window === "undefined",
link: createUploadLink({
uri: publicRuntimeConfig.PUBLIC_GRAPHQL_API_URL,
headers: {
Authorization: `Bearer ${ctx ? parseCookies(ctx).jwt : parseCookies().jwt}`,
},
}),
cache: new InMemoryCache(),
});
}

React Router ^6.0.0-beta.0 history prop removal - navigating outside of react context

According to the latest v6.0.0-alpha.5 release of react router, the history prop has been removed: https://github.com/ReactTraining/react-router/releases/tag/v6.0.0-alpha.5
Removed the prop and moved responsibility for setting
up/tearing down the listener (history.listen) into the wrapper
components (, , etc.). is now a
controlled component that just sets up context for the rest of the
app.
Navigating within the react context is simple with the useNavigate hook.
but now in the current V6 how the removal of history prop from BrowserRouter effect the navigation outside of the router context?
For example, if I have a Service class like that:
class Service {
constructor() {
let service = axios.create({
baseURL: BASE_URL,
headers: {
'Content-type': 'application/json; charset=utf-8'
},
timeout: 60000,
withCredentials: true
});
this.service = service;
}
handleError(path, err) {
if (axios.isCancel(err)) { // Catch axios request cancelation
console.log('caught cancel', err.message)
}
else if ( err.response && err.response.status === 401) { // Unauthorized
console.log('user unautorized');
window.location.href = '/auth/login'
}
else {
console.log(`Had Issues getting to the backend, endpoint: ${path}, with data: ${null}`);
console.dir(err);
throw err;
}
}
async get(path, params, loaderTrackerArea) {
try {
const response = await trackPromise(this.service.request({
method: 'GET',
url: path,
params
}), loaderTrackerArea);
return response.data;
} catch (err) {
this.handleError(path, err);
}
}
}
export default new Service();
Now, what I want to get is in the handleError function redirect to login page, currently I used pure javascript solution, what is the best way to achieve that with the current version of react-router-dom?
Thanks all!!!
I had the same problem (since the issue is still in the final release). While there isn't an existing interface to pass in a custom history object, it is actually quite easy to write custom wrapper around the Router component, based on the BrowserRouter implementation.
See https://github.com/remix-run/react-router/discussions/8241#discussioncomment-1677474 for code.
Edit:
As of react-router-dom v6.1.0 HistoryRouter is part of its implementation so you can just directly do:
import {HistoryRouter} from 'react-router-dom'
import {createBrowserHistory} from 'history'
const history = createBrowserHistory()
<HistoryRouter history={history} />

NextJS Redirect not work if user come from browser url bar

Already three days struggling to solve this problem so any help appreciated.
I have a simple component for check token from localStorage:
import Router from 'next/router';
const Blog = _ => {
const token = localStorage.getItem("token");
useEffect(_ => {
if(!token){
Router.push('/')
}
}, [])
}
This will work if we have a code like this:
<Link href="/blog">Blog</Link>
Then when you click blog you will be redirect to "/".
But if you type in browser url bar /blog and push enter you will not redirect to main page "/".
UPD:There is no token in localStorage
There are two problems with your code:
There is no return statement in the functional component. React throws an error if there is nothing returned.
If you type '/blog' in the browser, Nextjs throws an error, as the code is first run on the server( the server doesn't have a local storage - Error: localStorage is not defined). To resolve this you can do one of two things -
check whether the code is executing on the server or the client and then handle accordingly
move localStorage.getItem inside componentDidMount( or hooks equivalent).
import Router from "next/router";
import { useEffect } from "react";
const Blog = _ => {
useEffect(_ => {
const token = localStorage.getItem("token");
if (!token) {
Router.push("/");
}
}, []);
return <p>This is a blog</p>;
};
export default Blog;

how to Redirect to another page in next.js based on css media query?

im brand new to Next.js and i have the following situation. i want to redirect the user to the route /messages if he type route /messages/123 based on css media query so if he is mobile we will not redirect and if he in browser then redirect .
i have tried the following code
import React, { useLayoutEffect } from 'react';
import { useRouter, push } from 'next/router';
import useMediaQuery from '#material-ui/core/useMediaQuery';
import Layout from '../components/Customer/Layout/Layout';
import Chat from '../components/Customer/Chat/Chat';
const Messages = () => {
const { pathname, push } = useRouter();
const matches = useMediaQuery('(min-width:1024px)');
useLayoutEffect(() => {
console.log('I am about to render!');
if (matches && pathname === '/messages') {
console.log('match!');
push('/');
}
}, [matches, pathname, push]);
return (
<Layout currentURL={pathname}>
<Chat />
</Layout>
);
};
export default Messages;
the problem is the component render twice before redirect
But You should probably be using useEffect since you are not trying to do any DOM manipulations or calculations.
useLayoutEffect: If you need to mutate the DOM and/or DO need to perform measurements
useEffect: If you don't need to interact with the DOM at all or your DOM changes are unobservable (seriously, most of the time you should use this).
You should see immediate action.
Edit:
You can use Next JS getInitialProps to check the request headers and determine if the request if from mobile or desktop then redirect from there.
getInitialProps({ res, req }) {
if (res) {
// Test req.headers['user-agent'] to see if its mobile or not then redirect accordingly
res.writeHead(302, {
Location: '/message'
})
res.end()
}
return {}
}

How to go bact to login page from Axios interceptor?

I'm using axios for making API requests. I have implemented a centralized Interceptor which is used by all the components to fetch the data.
All the components use this interceptor for including token in request header and then to send all api requests.
Now, in case of error in response I want to redirect to login page. For that I'm trying to set a flag in Redux store named isTokenValid. Idea is I'm going to set a subscriber to the store in App.js or index.js and as soon as flag is set to false I will redirect user to login page.
The issue is I am not able to access any functions I pass via mapDispatchToProps using this.props.invalidToken().
How to solve this issue?
Is there any better approach
You can use this as quick fix
if(error.response?.status === 401){
localStorage.clear()
window.location.href = "/login";
}
try these following steps in (vue-cli) :
in axios.js
import Axios from 'axios'
import router from '../router' // use router/index.js path
import store from 'store path'
Axios.interceptors.response.use((res) => {
return Promise.resolve(res);
}, (error) => {
if (error!= null && error.status == 401) {
store.dispatch(call_to_your_store_action, pass_params_here);
router.push({ // here is the redirect component OR USE window.location.href='/login'
path: '/login',
name: 'login'
})
}
return Promise.reject(error);
})
export default Axios
router/index.js
import login from '#/components/login'
const router = new Router({
routes:[
{
path: '/login',
name: 'login',
component: login,
}
]
});
export default router
in main.js
import axios from './axios' //step 1 path

Resources