How do I pull data out of response or cache for apollo client 3 - http-headers

I am trying to pull a jwt out of a loginUser mutation and store it in a variable then use the apollo-link to setContext of the header via "Authorization: Bearer ${token} for authentification as all my other mutations and queries require the token. I have been slamming the docs for days on Apollo Client(React) -v 3.3.20. I have been through all the docs and they show all these examples of client.readQuery & writeQuery which frankly seem to just refetch data? I don't understand how you actually pull the data out of the response and store it in a variable.
The response is being stored in the cache and I have no idea how to take that data and store it in a token variable as I stated above. Which remote queries I can just access the returned data via the data object from the useQuery hook, however on the useMutation hook data returns undefined. The only thing I could find on this on stack overflow was the my data type may be custom or non-traditional type but not sure if that is the problem.
[Cache in apollo dev tools][1]
[Mutation in apollo dev tools][2]
[Response in network tab][3]
Here is my ApolloClient config:
const httpLink = createHttpLink({ uri: 'http://localhost:4000/',
// credentials: 'same-origin'
});
const authMiddleware = new ApolloLink((operation, forward) => {
const token = localStorage.getItem('token');
// add the authorization to the headers
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: `Bearer ${token}` || null,
}
}));
return forward(operation);
})
const client = new ApolloClient({
cache: new InMemoryCache(),
link: concat(authMiddleware, httpLink),
});
The header works obviously I just can't grab the token to pass so the header just sends Authorization: Bearer.
For the login I have this:
const LOGIN_USER = gql`
mutation($data:LoginUserInput!) {
loginUser(
data: $data
) {
user {
id
name
}
token
}
}
`;
const [loginUser, { data, loading, error }] = useMutation(LOGIN_USER);
if (loading) return 'Submitting...';
if (error) return `Submission error! ${error.message}`;
Originally I was just calling
onClick={loginUser( { variables })}
For the login but onComplete never works and everywhere I look I see lots of posts about it with no solutions. So I tried slamming everything into a function that I then called with loginUser inside it:
const submit = async () => {
loginUser({ variables})
// const { user } = await client.readQuery({
// query: ACCESS_TOKEN,
// })
// console.log(`User : ${JSON.stringify(user)}`)
const token = 'token';
const userId = 'userId';
// console.log(user);
// localStorage.setItem(token, 'helpme');
// console.log({data});
}
At this point I was just spending hours upon hours just trying mindless stuff to potentially get some clue on where to go.
But seriously, what does that { data } in useMutation even do if it's undefined. Works perfectly fine for me to call data.foo from useQuery but useMutation it is undefined.
Any help is greatly appreciated.
[1]: https://i.stack.imgur.com/bGcYj.png
[2]: https://i.stack.imgur.com/DlzJ1.png
[3]: https://i.stack.imgur.com/D0hb3.png

Related

How to get data correctly using the Spotify API with React

I have the following problem when I request data from the Spotify API, at first I get it, but when I reload the page or try to write this state using useState, an error 400 or 401 occurs. The code I use to get the data:
`
import axios from 'axios';
const BASE_URL = 'https://api.spotify.com/v1';
export const fetchFromAPI = async (url: string, token: string) => {
const { data } = await axios.get((`${BASE_URL}/${url}`), {
method: 'GET',
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
});
return data;
}
`
Next, I use the fetchFromAPI function:
`
const { token } = useContext(Context);
const [albums, setAlbums] = useState<string[]>([]);
useEffect(() => {
fetchFromAPI('browse/new-releases', token)
.then((data) => console.log(data));
}, [token])
`
I've tried logging out of my account and back in, I've also tried other links to get data but it's always the same problem. I also checked if the token is present before requesting the data and it is
Ok, I managed to find and solve this error myself.
The error was that I didn't have a user token yet, but useEffect was already starting to receive data.
useEffect(() => {
if (token) {
fetchNewReleases();
fetchFeaturedPlaylists();
fetchCategories();
fetchRecommendations();
} else {
console.log('error');
}}, [token])
For example, this piece of code will print an error twice, and only after that I receive a token and can receive data from the API.
To be honest, I didn't know how to run useEffect only when I have a token, so I solved it in a simpler way, but I don't know if it's completely correct, I have the following condition Object.values(state).length) !== 0 and if it is true, only then will I display the data from the API

Get localStorage in NextJs getInitialProps

I working with localStorage token in my next.js application. I tried to get the localStorage on page getInitialProps but, it returns undefined.
Here is an example,
Dashboard.getInitialProps = async () => {
const token = localStorage.getItem('auth');
const res = await fetch(`${process.env.API_URL}/pages/about`, {
headers: { 'Authorization': token }
});
const data = await res.json();
return { page: data };
}
For the initial page load, getInitialProps will run on the server
only. getInitialProps will then run on the client when navigating to a
different route via the next/link component or by using next/router. Docs
This means you will not be able to access localStorage(client-side-only) all the time and will have to handle it:
Dashboard.getInitialProps = async ({ req }) => {
let token;
if (req) {
// server
return { page: {} };
} else {
// client
const token = localStorage.getItem("auth");
const res = await fetch(`${process.env.API_URL}/pages/about`, {
headers: { Authorization: token },
});
const data = await res.json();
return { page: data };
}
};
If you want to get the user's token for the initial page load, you have to store the token in cookies instead of localStorage which #alejandro also mentioned in the comment.

Firestore cloud functions apollo graphql authentication

I need help getting my Firebase Apollo/GraphQL Cloud Function to authenticate and receive query requests.
I implemented an Apollo/GraphQL server as a Cloud Function in
Firebase/Firestore using this repository from this post.
I set permissions for the cloud function to
allAuthenticatedUsers and I am using Firebase Phone
Authentication to authenticate.
I used code from this stackoverflow answer to help structure the
authentication portion not included in the initial repository.
The Apollo/GraphQL function works fine (tested with playground) when permissions are set to allUsers. After setting permissions to allAuthenticatedUsers and attempting to send authenticated queries I am receiving the following error response:
Bearer error="invalid_token" error_description="The access token could not be verified"
I believe I am making a mistake with the request sent by the client, and or the handling of the verification and "context" of the ApolloServer. I have confirmed the initial user token is correct. My current theory is that I am sending the wrong header, or messing up the syntax somehow at either the client or server level.
To explain what I believe the appropriate flow of the request should be:
Token generated in client
Query sent from client with token as header
ApolloServer cloud function receives request
Token is verified by Firebase, provides new verified header token
Server accepts query with new verified header token and returns data
If anyone can explain how to send valid authenticated client queries to a Firebase Apollo/GraphQL Cloud Function the help would be greatly appreciated. Code for server and client below.
Server.js (ApolloServer)
/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "[db-url]",
});
/* Async verification with user token */
const verify = async (idToken) => {
var newToken = idToken.replace("Bearer ", "");
let header = await admin.auth().verifyIdToken(newToken)
.then(function(decodedToken) {
let uid = decodedToken.uid;
// Not sure if I should be using the .uid from above as the token?
// Also, not sure if returning the below object is acceptable, or
// if this is even the correct header to send to firebase from Apollo
return {
"Authorization": `Bearer ${decodedToken}`
}
}).catch(function(error) {
// Handle error
return null
});
return header
}
/* Server */
function gqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: schema,
resolvers,
context: async ({ req, res }) => {
const verified = await verify(req.headers.Authorization)
console.log('log verified', verified)
return {
headers: verified ? verified: '',
req,
res,
}
},
// Enable graphiql gui
introspection: true,
playground: true
});
apolloServer.applyMiddleware({app, path: '/', cors: true});
return app;
}
export default gqlServer;
Client.js (ApolloClient)
Client query constructed using these instructions.
/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
const userToken = await user.getIdToken();
/* Client creation */
const client = new ApolloClient({
uri: '[Firebase Cloud Function URL]',
headers: {
Authorization: userToken ? `Bearer ${userToken}` : ''
},
cache: new InMemoryCache(),
});
/* Query test */
client.query({
query: gql`
{
hello
}
`
}).then(
(result) => console.log('log query result', result)
).catch(
(error) => console.log('query error', error)
)
})
UPDATE 05/03/20
I may have found the source of the error. I won't post an answer until I confirm, but here's the update. Looks like allAuthenticatedUsers is a role specific to Google accounts and not Firebase. See this part of the google docs and this stackoverflow answer.
I will do some testing but the solution may be to change the permissions to allUsers which may still require authentication. If I can get it working I will update with an answer.
I was able to get things working. Working requests required the following changes:
Change cloud function "invoker" role to include allUsers instead of allAuthenticatedUsers. This because the allUsers role makes the function available to http requests (you can still require authentication through sdk verification)
Adjusting the code for the server and client as shown below. Minor change to header string construction.
Server.js (ApolloServer)
/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "[db-url]",
});
/* Async verification with user token */
const verify = async (idToken) => {
if (idToken) {
var newToken = idToken.replace("Bearer ", "");
// var newToken = idToken
let header = await admin.auth().verifyIdToken(newToken)
.then(function(decodedToken) {
// ...
return {
"Authorization": 'Bearer ' + decodedToken
}
}).catch(function(error) {
// Handle error
return null
});
return header
} else {
throw 'No Access'
}
}
/* Server */
function gqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: schema,
resolvers,
context: async ({ req, res }) => {
// headers: req.headers,
const verified = await verify(req.headers.authorization)
console.log('log verified', verified)
return {
headers: verified ? verified: '',
req,
res,
}
},
// Enable graphiql gui
introspection: true,
playground: true
});
apolloServer.applyMiddleware({app, path: '/', cors: true});
return app;
}
export default gqlServer;
Client.js (ApolloClient)
/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
const userToken = await user.getIdToken();
/* Client creation */
const userToken = await user.getIdToken();
const client = new ApolloClient({
uri: '[Firebase Cloud Function URL]',
headers: {
"Authorization": userToken ? 'Bearer ' + userToken : ''
},
cache: new InMemoryCache(),
});
client.query({
query: gql`
{
hello
}
`
}).then(
(result) => console.log('log query result', result)
).catch(
(error) => console.log('query error', error)
)
})

Automatically log out user when token is invalidated

I have a SPA that is built on vuejs. When a user is logged in via API, the token is stored in local storage.
I need a global solution which will logout and prompt the user when the token is no longer valid. At the moment, I get "invalid token" error when accessing private API endpoints.
How do I rig axios so that ALL response of invalid tokens will trigger the logout/prompt code?
Here is an simple example with axios. It use a Bearer token for authentification.
import axios from "axios";
import { useUserStore } from "#/store/userStore";
const apiClient = axios.create({
baseURL: ""http://127.0.0.1:8001",
headers: {},
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const config = error?.config;
if (error?.response?.status === 401) {
const result = await refreshToken();
if (result) {
config.headers = {
...config.headers,
authorization: `Bearer ${result?.token}`,
};
}
return axios(config);
}
);
const refreshToken = async () => {
/* do stuff for refresh token */
// if refresh token failed
try {
useUserStore().actionLogout();
} catch (error) {
console.log(error);
} finally {
loacalStorage.clear();
}
};
you can write a function that clears your local storage after some time and logout user

Apollo Client Delaying the Authorization Header

I am using Apollo (with Graph Cool), redux, and Auth0 in a React-Native app. I am trying to delay the queries and mutations until the header is set.
The idToken is stored in Async Storage, and is therefore a promise. I can't use redux to pass the token, because that would create a circular dependency.
When the user logins in for the first time or the token has expired, the queries are sent before header is set, which means I get the error Error: GraphQL error: Insufficient Permissions
How can I delay the queries until the token is found and added to the header? I have been searching three main solutions:
Add forceFetch: true; This seems to be part of an earlier implementation of the Apollo client. Even if I find the equivalent, the app still fails on the first attempt to fetch.
Reset the store (rehydrate?) upon logging in. This is still asynchronous so I don't see how this could affect the outcome.
Remove all mutations and queries from login itself, but due to the progress of the app, this is not feasible.
Some snippets:
const token = AsyncStorage.getItem('token');
const networkInterface = createNetworkInterface({ uri:XXXX})
//adds the token in the header
networkInterface.use([{
applyMiddleware(req, next) {
if(!req.options.headers) {
req.options.headers = {}
}
if(token) {
token
.then(myToken => {
req.options.headers.authorization = `Bearer ${myToken}`;
})
.catch(err => console.log(err));
}
next(); // middleware so needs to allow the endpoint functions to run;
},
}]);
// create the apollo client;
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
and
const store = createStore(
combineReducers({
token: tokenReducer,
profile: profileReducer,
path: pathReducer,
apollo: client.reducer(),
}),
{}, // initial state
compose(
applyMiddleware(thunk, client.middleware(), logger),
)
);
I'm not certain this will work without a reproduction app, mostly because I don't have an app of your structure set up, but you're hitting this race condition because you are calling next() outside of your async chain.
Calling next() where it is currently will tell the client to continue on with the request, even if your token isn't set. Instead, let's wait until the token comes back and the header gets set before continuing on.
networkInterface.use([{
applyMiddleware(req, next) {
if(!req.options.headers) {
req.options.headers = {}
}
AsyncStorage.getItem('token')
.then(myToken => {
req.options.headers.authorization = `Bearer ${myToken}`;
})
.then(next) // call next() after authorization header is set.
.catch(err => console.log(err));
}
}]);