react firebase authentication with apollo graphql - reactjs

I have found a great article adding authentication into react.
Article: https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/
This article finishes the firebase setup (before redux) with a HOC component that I can put into the app and can access with context.
My issue is how do i put this into the apollo client which is outside of the app component so even with the context I cant set it. I have this same problem with redux. Only one I have found is use local storage but I would like to avoid that.
This is my apollo client in the main app component.
const client = new ApolloClient({
uri: clientUrl,
request: async operation => {
const token = How_do_i_set_this <-- ???
console.log('token in request', token)
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
});
}
});
const App = () => (
<DashAppHolder>
<ApolloProvider client={client}>
<Provider store={store}>
<PublicRoutes history={history} />
</Provider>
</ApolloProvider>
</DashAppHolder>
);

So this my not be the best way but this is how I have it setup at the moment.
this is my apollo client setup
import Firebase from './helpers/firebase';
import ApolloClient from "apollo-boost";
const client = new ApolloClient({
uri: clientUrl,
request: async operation => {
const fireToken = await Firebase.token();
console.log('fire token', fireToken);
operation.setContext({
headers: {
authorization: fireToken ? `Bearer ${fireToken}` : 'token bad'
}
});
}
});
This is my firebase helper class I import
import firebase from 'firebase';
import 'firebase/firestore';
import { firebaseConfig } from '../../settings';
const valid = firebaseConfig && firebaseConfig.apiKey && firebaseConfig.projectId;
const firebaseApp = firebase.initializeApp(firebaseConfig);
const firebaseAuth = firebase.auth;
class FirebaseHelper {
isValid = valid;
EMAIL = 'email';
FACEBOOK = 'facebook';
GOOGLE = 'google';
GITHUB = 'github';
TWITTER = 'twitter';
constructor() {
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.signup = this.signup.bind(this);
this.resetPassword = this.resetPassword.bind(this);
this.doPasswordUpdate = this.doPasswordUpdate.bind(this);
this.auth = this.auth.bind(this);
this.database = firebase.firestore();
}
token = async() => {
const user = this.user()
if (user) {
return await user.getIdToken().then(token => { return token });
} else {
return null;
}
}
user() {
return firebaseAuth().currentUser;
}
login(provider, info) {
switch (provider) {
case this.EMAIL:
return firebaseAuth().signInWithEmailAndPassword(
info.email,
info.password
);
case this.GOOGLE:
var googleProvider = new firebase.auth.GoogleAuthProvider();
return firebaseAuth().signInWithPopup(googleProvider);
default:
}
}
signup(provider, info) {
switch (provider) {
case this.EMAIL:
return firebaseAuth().createUserWithEmailAndPassword(
info.email,
info.password
);
case this.FACEBOOK:
return firebaseAuth().FacebookAuthProvider();
case this.GOOGLE:
return firebaseAuth().GoogleAuthProvider();
case this.GITHUB:
return firebaseAuth().GithubAuthProvider();
case this.TWITTER:
return firebaseAuth().TwitterAuthProvider();
default:
alert('defaulted');
}
}
logout() {
return firebaseAuth().signOut();
}
auth = () => {
return firebaseAuth()
}
resetPassword(email) {
return firebaseAuth().sendPasswordResetEmail(email);
}
doPasswordUpdate(password) {
firebaseAuth().currentUser.updatePassword(password);
}
createNewRef() {
return firebase
.database()
.ref()
.push().key;
}
}
And lastly on the server here is my graphql setup relevent information I use
const admin = require('firebase-admin');
const getUid = async (request) => {
let idToken = (request.headers && request.headers.authorization) ? request.headers.authorization : null;
if (!idToken) {
console.log('no token found');
return null;
}
console.log('raw token', idToken);
var newToken = idToken.replace("Bearer ", "");
console.log('pure token', newToken)
let uid = await admin.auth().verifyIdToken(newToken)
.then(decodedToken => {
var uid = decodedToken.uid;
return uid;
}).catch((error) => {
// Handle error
console.log('uid failed', error);
return null;
});
console.log('uid found', uid);
return uid;
}
app.use(
'/graphql',
cors(),
express.json(),
graphqlExpress(async (request) => ({
schema: schema,
context: {
request: request,
uid: await getUid(request),
accountLoader: new Dataloader(keys => batchAccounts(keys, db)),
dealLoader: new Dataloader(keys => batchDeals(keys, db)),
}
})),
);
This answer works and gets me the UID of the user authorized through firebase and looks like there is no delay at all. This might not be the best answer because I Put this together from several documents when I was just learning everything and have been meaning to go back and revisit when I have time but again, working.

Related

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?

How do I get the data from an API call, in a different file, in React

I'm trying to update some code, taking into account new sdk versions. I have the new api call in one file:
import { CognitoIdentityProviderClient, ListUsersCommand } from "#aws-sdk/client-cognito-identity-provider";
import awsmobile from "../../aws-exports";
import { Auth } from "aws-amplify";
export default async function ListUsers() {
await Auth.currentCredentials().then((data) => {
const client = new CognitoIdentityProviderClient({
region: awsmobile.aws_project_region,
credentials: data
});
const params = {
UserPoolId: awsmobile.aws_user_pools_id
};
const command = new ListUsersCommand(params);
client.send(command).then(
(data) => {
return data
},
(error) => {
console.log(error)
}
);
});
}
I'm trying to retrive the data in another file:
import ListUsers from "../../../API/cognito/ListUsers";
import ListUsersInGroup from "../../../API/cognito/ListUsersInGroup";
import { useState, useEffect, useRef } from "react";
import PortalUsersTable from "../../../components/tables/PortalUsersTable";
export default function ManageUsers() {
const [userDetails, setUserDetails] = useState("");
const refUsers = useRef();
const refUsersExec = useRef();
const refUsersAdmin = useRef();
const refUsersGroups = useRef();
useEffect(() => {
function getUsers() {
refUsers.current = ListUsers();
refUsersExec.current = ListUsersInGroup("usersAdmin");
refUsersAdmin.current = ListUsersInGroup("usersExec");
//setUsersTloOfficers(apiTloOfficers);
refUsersGroups.current = ListUsersInGroup("usersGroups");
let userData = [];
let arrUsersExec = [];
for (let a in refUsersExec.current.Users) {
arrUsersExec.push(refUsersExec.current.Users[a].Username);
}
let arrUsersAdmin = [];
for (let b in refUsersAdmin.current.Users) {
arrUsersAdmin.push(refUsersAdmin.current.Users[b].Username);
}
let arrUsersGroups = [];
for (let b in refUsersNtigGroups.current.Users) {
arrUsersGroups.push(refUsersGroups.current.Users[b].Username);
}
for (let i in refUsers.current.Users) {
let email = null;
for (let x in refUsers.current.Users[i].Attributes) {
if (refUsers.current.Users[i].Attributes[x].Name === "email") {
email = refUsers.current.Users[i].Attributes[x].Value;
break;
}
}
let memberExec = arrUsersExec.includes(refUsers.current.Users[i].Username);
let memberAdmin = arrUsersAdmin.includes(refUsers.current.Users[i].Username);
let memberGroups = arrUsersGroups.includes(refUsers.current.Users[i].Username);
userData.push({
id: i,
Username: refUsers.current.Users[i].Username,
AccountStatus: refUsers.current.Users[i].UserStatus,
Email: email,
Users: memberGroups,
Exec: memberExec,
Admin: memberAdmin,
});
}
setUserDetails(userData);
}
getUsers();
}, []);
return (
<>
<h2>Manage Portal Users</h2>
<PortalUsersTable userDetails={userDetails} />
</>
);
}
The logic to handle the API data is sound.
This is the old API call:
import AWS from "aws-sdk";
import awsmobile from "../../aws-exports";
import { Auth } from "aws-amplify";
export default async function ListUsers() {
let idToken = "";
await Auth.currentAuthenticatedUser().then((user) => {
idToken = user.signInUserSession.idToken.getJwtToken();
});
AWS.config.region = awsmobile.aws_cognito_region;
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: awsmobile.aws_cognito_identity_pool_id,
RoleArn: "arn:aws:iam::xxxxxxxxx:role/xxxxxxxxxxxxx",
Logins: { "xxxxxxxxxxxxxxxxxxxx": idToken }
});
let cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
let params = {
UserPoolId: awsmobile.aws_user_pools_id,
AttributesToGet: ["email"]
};
return new Promise((resolve, reject) => {
cognitoidentityserviceprovider.listUsers(params, function (err, result) {
if (err) {
console.log(err);
//onError(err);
reject(err);
return;
}
if (result) {
resolve(result);
}
});
});
}
I can see the new API call is returning the correct data in the console. I think I'm not passing the data between files correctly.
I've tried various ways of changing the API call function, reading the cognito sdk description but it's not the API call that is incorrect.
How can I use the API call data in the separate file?
Even if your API call if correct, it looks like you are not returning anything from your function ListUsers. You are mixing async/await pattern with the then. I assume you have added a console.log right before the return data. Refactoring your function using async/await would look like this :
export default async function ListUsers() {
try {
const data = await Auth.currentCredentials();
const client = new CognitoIdentityProviderClient({
region: awsmobile.aws_project_region,
credentials: data,
});
const params = {
UserPoolId: awsmobile.aws_user_pools_id,
};
const command = new ListUsersCommand(params);
const commandData = await client.send(command);
return commandData;
} catch (error) {
console.log(error);
}
}

Correct implementation of refreshtoken with ApolloClient

I am trying to refresh the authentication token when it is near the expiration time, however I end up with an endless loop. The expectation is that the code that checks the expiration time in App.js should work and reset the auth token and the refresh token, however is starts looping endlessly.
I have the following Auth helper functions:
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { AuthenticationError } = require("apollo-server-express");
const config = require("config");
const jwtSecret = config.get("jwt_secret");
const jwtRefreshTokenSecret = config.get("jwt_refresh_token");
const authFunctions = {
checkSignedIn: async (req, requireAuth = true) => {
const header = req.headers.authorization;
if (header) {
const token = header.replace("Bearer ", "");
const decoded = jwt.verify(token, jwtSecret);
console.log(decoded);
let user = await User.findById(decoded.id);
if (!user) {
throw new AuthenticationError("Invalid user credentials.");
}
return user;
}
if (requireAuth) {
throw new AuthenticationError("You must be logged in.");
}
return null;
},
issueToken: async (user) => {
let token = "Bearer " + (await authFunctions.createToken(user));
let refreshToken = await authFunctions.createToken(user, 100);
return { token, refreshToken };
},
issueNewToken: async (req) => {
try {
const token = req.headers.refreshtoken;
if (token) {
const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
let user = await User.findById(decoded.id);
console.log(user);
if (!user) {
throw new AuthenticationError("No user found.");
}
let tokens = await authFunctions.issueToken(user);
return { ...tokens, user };
}
} catch (err) {
throw new AuthenticationError("Invalid Refresh Token.");
}
},
createToken: async ({ id, address }, expiresIn = 60) => {
let secret = expiresIn === 60 ? jwtSecret : jwtRefreshTokenSecret;
return await jwt.sign({ id, address }, secret, { expiresIn });
},
};
module.exports = authFunctions;
The schema:
const { gql } = require("apollo-server");
const typeDefs = gql`
scalar Date
scalar MongoId
type User {
_id: MongoId!
address: String!
createdAt: Date
}
type Auth {
user: User!
token: String!
refreshToken: String!
}
input LoginInput {
address: String!
}
type Query {
refreshTokens: Auth!
}
type Mutation {
createOrGetUser(loginInput: LoginInput): Auth!
}
`;
module.exports = typeDefs;
The resolvers:
const User = require("../../models/User");
const {
issueToken,
issueNewToken,
checkSignedIn,
} = require("../../middleware/Auth");
const resolvers = {
Query: {
refreshTokens: async (root, args, { req }, info) =>
await issueNewToken(req),
},
Mutation: {
createOrGetUser: async (root, args, { req }, info) => {
try {
const existingUser = await User.findOne({
address: args.loginInput.address,
})
.populate({
path: "orders",
model: Order,
})
.exec();
if (existingUser) {
let tokens = await issueToken(existingUser);
return {
user: existingUser,
...tokens,
};
}
const user = await new User({
address: args.loginInput.address,
});
const result = await user.save();
let tokens = await issueToken(result);
return {
user,
...tokens,
};
} catch (err) {
throw err;
}
},
},
};
module.exports = resolvers;
And the App.js
import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import {
ApolloClient,
ApolloLink,
createHttpLink,
useReactiveVar,
concat,
} from "#apollo/client";
import { ApolloProvider } from "#apollo/client/react";
import { accessTokenVar, isLoggedInVar } from "./cache";
import cache from "./cache.js";
import jwtDecode from "jwt-decode";
// Styling
import "./styles/App.css";
// Components
import Routes from "./components/routing/Routes";
import Navbar from "./components/navbar/Navbar";
import { REFRESH_TOKENS } from "./queries/User";
const httpLink = createHttpLink({
uri: "/graphql",
});
const refreshTokens = () => {
return client.query({ query: REFRESH_TOKENS }).then((response) => {
console.log(response);
// Need to set the token to cache here, but the query doesn't get executed it seems
//accessTokenVar(response.data)
return;
});
};
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
let token = accessTokenVar();
if (token) {
token = token.replace("Bearer ", "");
const { exp } = jwtDecode(token);
console.log(exp);
// Refresh the token a minute early to avoid latency issues
const expirationTime = exp - 30;
if (Date.now() / 1000 >= expirationTime) {
refreshTokens(); // this keeps going on a loop
}
}
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? token : "",
},
}));
return forward(operation);
});
const client = new ApolloClient({
cache,
link: concat(authMiddleware, httpLink),
connectToDevTools: true,
credentials: "include",
});
const App = () => {
const accessToken = useReactiveVar(accessTokenVar);
const isLoggedIn = useReactiveVar(isLoggedInVar);
//let isLoggedIn;
accessToken ? isLoggedInVar(true) : isLoggedInVar(false);
return (
<ApolloProvider client={client}>
<Router>
<Fragment>
<Navbar isLoggedIn={isLoggedIn} />
</Fragment>
</Router>
</ApolloProvider>
);
};
export default App;
Note: When createOrGetUser is executed the first time, the flow works. Then, I wait for 30 seconds and send a protected query, after which it gets in an endless loop.
Overall, I feel this flow is broken somehow, but I can't figure out what exactly. Would appreciate any help with this!

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,
}),
});

Laravel lighthouse current user is null via next apollo

I have a fresh copy of laravel with sanctum and lighthouse. When I do the login route via axios, everything works as expected. After logging in via axios, I added a lazyquery to attempt to query some guarded fields but I get unauthenticated. I am not sure why and it has been three days I've been dealing with this. I'd really appreciate your help.
This works
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log(`logged in ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", { withCredentials: true })
.then(function (resolve) {
console.log(`gated ${resolve.data}`);
axios
.get("http://api.newods.test/api/logout", {
withCredentials: true,
})
.then(function (resolve) {
console.log(`logged out ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", {
withCredentials: true,
})
.then(function (resolve) {
console.log(
`trying to get to gated after logging out ${resolve.data}`
);
});
});
});
});
});
}, []);
But when I cut it short and change to this, I get unauthenticated
const HELLO = gql\`
query hello {
hello
}
`;
function Home() {
const [hello, { loading, data }] = useLazyQuery(HELLO);
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log('logged in');
});
});
}, []);
return (
<div className="container">
<div>Index</div>
<button onClick={() => hello()}>
Click to hello world
</button>
<p>{data && data.hello || ''}</p>
</div>
);
}
export default withApollo(Home);
And that returns unauthenticated when I add the #guard directive and I see the token from the axios login request is in the headers... I am not sure what I am missing here I'd greatly appreciate your help.
schema.graphql
type Query {
users: [User!]! #paginate(defaultCount: 10)
user(id: ID #eq): User #find
hello: String! #guard
me: User #auth
}
.env
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_DOMAIN=.newods.test
SANCTUM_STATEFUL_DOMAINS=newods.test:3000
config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'graphql'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
config/lighthouse
'route' => [
/*
* The URI the endpoint responds to, e.g. mydomain.com/graphql.
*/
'uri' => '/graphql',
/*
* Lighthouse creates a named route for convenient URL generation and redirects.
*/
'name' => 'graphql',
/*
* Beware that middleware defined here runs before the GraphQL execution phase,
* make sure to return spec-compliant responses in case an error is thrown.
*/
'middleware' => [
\Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
// Logs in a user if they are authenticated. In contrast to Laravel's 'auth'
// middleware, this delegates auth and permission checks to the field level.
\Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class,
],
/*
* The `prefix` and `domain` configuration options are optional.
*/
//'prefix' => '',
//'domain' => '',
],
In my next app with apollo
create.js
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import Cookies from 'js-cookie';
import { serverUrl } from '../config';
export default function createApolloClient(initialState, ctx) {
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = Cookies.get("XSRF-TOKEN");
// console.log(`token is ${token}`);
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
"Access-Control-Allow-Credentials": true,
...(token ? { authorization: `X-XSRF-TOKEN=${token}` } : {}),
},
};
});
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'same-origin',
});
return new ApolloClient({
ssrMode: Boolean(ctx),
link: authLink.concat(httpLink),
connectToDevTools: true,
cache: new InMemoryCache().restore(initialState),
});
}
withApollo.js
import React from "react";
import Head from "next/head";
import { ApolloProvider } from "#apollo/react-hooks";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";
import createApolloClient from './create';
let apolloClient = null;
/**
* Creates and provides the apolloContext
* to a next.js PageTree. Use it by wrapping
* your PageComponent via HOC pattern.
* #param {Function|Class} PageComponent
* #param {Object} [config]
* #param {Boolean} [config.ssr=true]
*/
export function withApollo(PageComponent, { ssr = true } = {}) {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState);
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
);
};
// Set the correct displayName in development
if (process.env.NODE_ENV !== "production") {
const displayName =
PageComponent.displayName || PageComponent.name || "Component";
if (displayName === "App") {
console.warn("This withApollo HOC only works with PageComponents.");
}
WithApollo.displayName = `withApollo(${displayName})`;
}
if (ssr || PageComponent.getInitialProps) {
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx;
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(
{},
ctx.req.headers.cookie
));
// Run wrapped getInitialProps methods
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
}
// Only on the server:
if (typeof window === "undefined") {
// When redirecting, the response is finished.
// No point in continuing to render
if (ctx.res && ctx.res.finished) {
return pageProps;
}
// Only if ssr is enabled
if (ssr) {
try {
// Run all GraphQL queries
const { getDataFromTree } = await import("#apollo/react-ssr");
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient,
}}
/>
);
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
console.error("Error while running `getDataFromTree`", error);
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind();
}
}
// Extract query data from the Apollo store
// #ts-ignore
const apolloState = apolloClient.cache.extract();
return {
...pageProps,
apolloState,
};
};
}
return WithApollo;
}
/**
* Always creates a new apollo client on the server
* Creates or reuses apollo client in the browser.
* #param {Object} initialState
*/
function initApolloClient(initialState = {}, cookie = "") {
// Make sure to create a new client for every server-side request so that data
// isn"t shared between connections (which would be bad)
if (typeof window === "undefined") {
return createApolloClient(initialState, cookie);
}
// Reuse client on the client-side
if (!apolloClient) {
// #ts-ignore
apolloClient = createApolloClient(initialState);
}
return apolloClient;
}
I have a very similar architecture, but using Vue. From comparing your code against my working implementation, I think the majority of your problems are in create.js.
I don't know much about js-cookie, but this is how I get the XSRF-TOKEN, and decode it.
let token = RegExp('XSRF-TOKEN[^;]+').exec(document.cookie)
token = decodeURIComponent(token ? token.toString().replace(/^[^=]+./, '') : '')
Then, in your setContext, you need to set the header as follows.
return {
headers: {
...headers,
'X-XSRF-TOKEN': token,
}
}
Also, I had trouble with credentials: 'same-origin' even though I'm using a subdomain. Therefore I would suggest:
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'include',
})

Resources