Apollo Websocket - Unable to initiate with new connection with latest JWT token - javascript

I have a vue application that connects to a graphQL server using vue apollo. The vue-apollo.js code is as follows
const httpEndpoint = process.env.XXX;
const wsEndpoint = process.env.XXX;
let baseLink = new ApolloLink((operation, forward) => {
let isTokenExpired = isJwtExpired(token)
console.log('isExpired is:',isTokenExpired) ;
if (isTokenExpired) {
Auth.currentSession().then((data) => {
token = data.idToken.jwtToken
localStorage.setItem('JoT', JSON.stringify(token));
const headerData = token? {authorization: `Bearer ${token}`,}: {};
operation.setContext({headers: headerData})
return forward(operation)
})
} else {
token = JSON.parse(token);
const headerData = token? {authorization: `Bearer ${token}`,}: {};
operation.setContext({headers: headerData})
return forward(operation)
}
})
let observerLink = new ApolloLink((operation, forward) => {
return forward(operation)
})
const baseAndObserverLink = baseLink.concat(observerLink)
const errorLink = onError(error => {
console.log(JSON.stringify(error))
if (error.graphQLErrors) {
console.log("GraphQL Error detected")
if (error.graphQLErrors.extensions && error.graphQLErrors.extensions.code && error.graphQLErrors.extensions.code === 'invalid-jwt') {
console.log("JWT EXPIRED")
}
} else if (error.networkError && error.networkError.extensions && error.networkError.extensions.code && error.networkError.extensions.code === 'start-failed') {
console.log("GraphQL Error detected type 2")
console.log("Unable to Connect")
}else if (error.networkError && error.networkError.extensions && error.networkError.extensions.code && error.networkError.extensions.code === 'validation-failed') {
console.log("GraphQL Error detected type 3")
console.log("Validation Error")
}
else {
console.log(JSON.stringify(error))
}
})
var wsLink = new WebSocketLink({
uri: wsEndpoint,
options: {
reconnect: true,
lazy: true,
timeout: 30000,
connectionParams: async () => {
const token = JSON.parse(localStorage.getItem('JWT'));
return {
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
}
},
},
});
export var filesRoot =
process.env.VUE_APP_FILES_ROOT ||
httpEndpoint.substr(0, httpEndpoint.indexOf('/graphql'));
Vue.prototype.$filesRoot = filesRoot;
var httpLink = new HttpLink({
uri: httpEndpoint,
});
var splitLLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
export var defaultOptions = {
httpEndpoint,
wsEndpoint,
tokenName: 'JWT',
persisting: false,
websocketsOnly: true,
ssr: false,
link:baseAndObserverLink.concat(errorLink).concat(splitLLink),
};
export function createProvider(options = {}) {
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
});
apolloClient.wsClient = wsClient;
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
async errorHandler(error) {
},
});
return apolloProvider;
}
This code when we first login works well. When I refetch a new token the query part gets updated with new context and everything works fine. However the websocket connection seems to keep using the old token to reinitialise connection. So far I couldn't find any suitable solution except for do a forced reload which reinitilises the entire app and wss conection can work as we have latest token being passed. Is there another way

Related

Reactive Native TypeError: undefined is not an object (evaluating 'response.success')

I am trying to get an app working that was built using React Native. I get the error as per the screen shot. Can somebody please help me figure out what it means and how it can be fixed?
enter image description here
See below the code that is related to this error
tatic async func(route, params, verb) {
// console.warn(JSON.stringify(params));
let { orderStore } = Store;
const url = `${host}/${route}`
let options = Object.assign({ method: verb }, params ? { body: JSON.stringify(params) } : null);
options.headers = Api.headers()
const data = await LocalDb.getUserProfile();
if (params != null && params.type === 'social') {
const username = params.email;
const password = '1234';
const hash = new Buffer(`${username}:${password}`).toString('base64');
options.headers['Authorization'] = `Basic ${hash}`;
}
else
if (data != null) {
const username = data.email;
const password = data.password;
const hash = new Buffer(`${username}:${password}`).toString('base64');
options.headers['Authorization'] = `Basic ${hash}`;
}
return fetch(url, options).then(resp => {
// console.log('Api response is ------------->>>>>>', resp);
let json = resp.json();
if (resp.ok) {
return json
}
return json.then(err => { throw err });
}).catch(json => {
console.log('error is ', json)
// console.log('Api response is ------------->>>>>>', json);
Api.showAlert();
return;
});
}
tatic showAlert() {
// Works on both iOS and Android
Alert.alert(
'Network Error!',
'Click Ok To Restart App.',
[
{ text: 'OK', onPress: () => RNRestart.Restart() },
],
{ cancelable: false },
);
}

TypeError: Cannot read property 'then' of undefined, React app error when trying to save a playlist

Can't seem to resolve this error.
TypeError
For the function to work the user is supposed to be able to add songs to a 'New Playlist' and then save that playlist.
The app seems to work fine until you click the button to save the playlist at which point all you see is this error.
Trying to access this component:
const clientId = "783dbc97776940e28f307dfc902ad41b";
const redirectUri = "http//localhost:3000/";
let accessToken;
const Spotify = {
getAccessToken() {
if (accessToken) {
return accessToken;
}
// check for access token match
const accessTokenMatch = window.location.href.match(/access_token=([^&]*)/);
const expiresInMatch = window.location.href.match(/expires_in=([^&]*)/);
if (accessTokenMatch && expiresInMatch) {
accessToken = accessTokenMatch[1];
const expiresIn = Number(expiresInMatch[1]);
// This clears the parameters, allowing us to grab a new access token when it expires.
window.setTimeout(() => (accessToken = ""), expiresIn * 1000);
window.history.pushState("Access Token", null, "/");
return accessToken;
} else {
const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
window.location = accessUrl;
}
},
search(term) {
const accessToken = Spotify.getAccessToken();
return fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
.then(response => {
return response.json();
})
.then(jsonResponse => {
if (!jsonResponse.tracks) {
return [];
}
return jsonResponse.tracks.items.map(track => ({
id: track.id,
name: track.name,
artist: track.artists[0].name,
album: track.album.name,
uri: track.uri
}));
});
},
savePlaylist(name, trackUris) {
if (!name || !trackUris.length) {
return;
}
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}` };
let userId;
return fetch("https://api.spotify.com/v1/me", { headers: headers })
.then(response => response.json())
.then(jsonResponse => {
userId = jsonResponse.id;
return fetch(`https://api.spotify.com/v1/users/${userId}/playlists`, {
headers: headers,
method: "POST",
body: JSON.stringify({ name: name })
})
.then(response => response.json())
.then(jsonResponse => {
const playlistId = jsonResponse.id;
return fetch(
`https://api.spotify.com/v1/users/${userId}/playlists/${playlistId}/tracks`,
{
headers: headers,
method: "POST",
body: JSON.stringify({ uris: trackUris })
}
);
});
});
}
};
export default Spotify;
For use here:
import React from 'react';
import './App.css';
import SearchBar from '../SearchBar/SearchBar';
import SearchResults from '../SearchResults/SearchResults';
import Playlist from '../Playlist/Playlist';
import Spotify from '../../util/Spotify';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
playlistName: 'My Playlist',
playlistTracks: []
}
this.addTrack = this.addTrack.bind(this);
this.removeTrack = this.removeTrack.bind(this);
this.updatePlaylistName = this.updatePlaylistName.bind(this);
this.savePlaylist = this.savePlaylist.bind(this);
this.search = this.search.bind(this);
}
addTrack(track) {
if (this.state.playlistTracks.find(savedTrack => savedTrack.id === track.id)) {
return;
}
this.state.playlistTracks.push(track);
this.setState({playlistTracks: this.state.playlistTracks})
}
removeTrack(track) {
this.setState.playlistTracks = this.state.playlistTracks.filter(currentTrack => currentTrack.id !== track.id)
this.setState({playlistTracks: this.state.playlistTracks})
}
updatePlaylistName(name) {
this.setState({playlistName: name});
}
savePlaylist() {
const trackUris = this.state.playlistTracks.map(track => track.uri);
Spotify.savePlaylist(this.state.playlistName, trackUris).then(() => {
this.setState({
playlistName: 'New Playlist',
playlistTracks: []
})
})
}
search(term) {
Spotify.search(term).then(searchResults => {
this.setState({searchResults: searchResults})
})
}
render() {
return (
<div>
<h1>Ja<span className="highlight">mmm</span>ing</h1>
<div className="App">
<SearchBar onSearch={this.search} />
<div className="App-playlist">
<SearchResults searchResults={this.state.searchResults} onAdd={this.addTrack} />
<Playlist playlistName={this.state.playlistName} playlistTracks={this.state.playlistTracks} onRemove={this.removeTrack} onNameChange={this.updatePlaylistName} onSave={this.savePlaylist} />
</div>
</div>
</div>
)
}
}
// if(typeof(App.savePlaylist) == 'undefined') {
// console.log('whoops')
// }
export default App;
Any ideas why?
I'm very new to this and am completely lost as to why this is happening.
You need to return a Promise for when the input is not right:
if (!name || !trackUris.length) {
return Promise.resolve();
};
Also, don't change variables of the state object. Don't do this:
this.state.playlistTracks.push(track);
this.setState({playlistTracks: this.state.playlistTracks})
Instead, do this:
this.setState({playlistTracks: [...this.state.playlistTracks, track]})
In you code when the following if condition is true, you're not returning a promise.
savePlaylist(name, trackUris) {
if (!name || !trackUris.length) {
return;
// What you can do instead
// return Promise.reject();
};
....
...
Simply returning return; will return undefined and trying to access then on it will throw error.

How to store, manage REST API JWT authentication token in vue?

I am a noob, using vue.js and a node auth api, the api works fine and provides the jwt token in the response, my question is how can i use the token in all the requests that follows (using axios), and any best practices for handling the token in the front end is also appreciated.
Thanks
You can use something like that for Your scenario in your vuejs app.
import axios from 'axios'
const API_URL = 'http://localhost:3000'
const securedAxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
const plainAxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
securedAxiosInstance.interceptors.request.use(config => {
const method = config.method.toUpperCase()
if (method !== 'OPTIONS' && method !== 'GET') {
config.headers = {
...config.headers,
'X-CSRF-TOKEN': localStorage.csrf
}
}
return config
})
securedAxiosInstance.interceptors.response.use(null, error => {
if (
error.response &&
error.response.config &&
error.response.status === 401
) {
return plainAxiosInstance
.post('/refresh', {}, { headers: { 'X-CSRF-TOKEN': localStorage.csrf } })
.then(response => {
localStorage.csrf = response.data.csrf
localStorage.signedIn = true
let retryConfig = error.response.config
retryConfig.headers['X-CSRF-TOKEN'] = localStorage.csrf
return plainAxiosInstance.request(retryConfig)
})
.catch(error => {
delete localStorage.csrf
delete localStorage.signedIn
location.replace('/')
return Promise.reject(error)
})
} else {
return Promise.reject(error)
}
})
export { securedAxiosInstance, plainAxiosInstance }
And in your component you use this to process your request with api
Products.vue
export default {
name: 'products',
data () {
return {
products: [],
newProduct: [],
error: '',
editedProduct: ''
}
},
created () {
if (!localStorage.signedIn) {
this.$router.replace('/')
} else {
this.$http.secured.get('/api/v1/products')
.then(response => { this.products = response.data })
.catch(error => this.setError(error, 'Something went wrong'))
}
},
methods: {
setError (error, text) {
this.error = (error.response && error.response.data && error.response.data.error) || text
},
addProduct () {
const value = this.newProduct
if (!value) {
return
}
this.$http.secured.post('/api/v1/products/', { product: { name: this.newProduct.name } })
.then(response => {
this.products.push(response.data)
this.newProduct = ''
})
.catch(error => this.setError(error, 'Cannot create product'))
},
removeProduct (product) {
this.$http.secured.delete(`/api/v1/products/${product.id}`)
.then(response => {
this.products.splice(this.products.indexOf(product), 1)
})
.catch(error => this.setError(error, 'Cannot delete product'))
},
editProduct (product) {
this.editedproduct = product
},
updateProduct (product) {
this.editedProduct = ''
this.$http.secured.patch(`/api/v1/products/${product.id}`, { product: { title: product.name } })
.catch(error => this.setError(error, 'Cannot update product'))
}
}
}
You can find here a lot of good patterns which I personally use on my projects and how also JWT token handling.
For saving token in a brower, you can use cookie, sessionStorage or localStorate, last one is the most popular now (short explination here).
In a few words, you can create an axion instance and add a token before request sent.
const http = axios.create({
baseURL: process.env.VUE_APP_SERVER_API,
// here you can specify other params
})
http.interceptors.request.use(request => {
// Do something before request is sent
request.headers['Authorization'] = `JWT ${TOKEN_HERE}`
// some logic what to do if toke invalid, etc ...
return request
}, function (error) {
// Do something with request error
return Promise.reject(error)
})

Axios multiple request on interceptor

I'm using the library axios in my react app.
I'm having a problem with the interceptor.
My question is let say I have three requests happening concurrently and I don't have the token, the interceptor calling the getUserRandomToken three time, I want the interceptor will wait until I'm getting the token from the first request and then continue to the others.
P.S. the token he is with an expiration date so I also checking for it and if the expiration date is not valid I need to create a new token.
This is the interceptor:
axios.interceptors.request.use(
config => {
/*I'm getting the token from the local storage
If there is any add it to the header for each request*/
if (tokenExist()) {
config.headers.common["token"] = "...";
return config;
}
/*If there is no token i need to generate it
every time create a random token, this is a axios get request*/
getUserRandomToken()
.then(res => {
/*add the token to the header*/
config.headers.common["token"] = res;
return config;
})
.catch(err => {
console.log(err);
});
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);
How about singleton object that will handle the token generations? something similar to this:
const tokenGenerator ={
getTokenPromise: null,
token: null,
getToken(){
if (!this.getTokenPromise){
this.getTokenPromise = new Promise(resolve=>{
/*supposed to be a http request*/
if (!this.token){
setTimeout(()=>{
this.token = 'generated';
resolve(this.token);
},0)
}else{
resolve(this.token);
}
})
}
return this.getTokenPromise;
}
you can reference this same object from the interceptors.
see example: JS FIddle
reference: reference
You can return a Promise from interceptor callback to "wait" until promise fullfiles (this will fit your case). Check out this example:
function axiosCall () {
return new Promise((resolve, reject) => {
Axios.post(URL, {apiKey}).then((response) => {
resolve(response.data.message);
}).catch((error) => {
reject(error);
});
});
}
instance.interceptors.request.use((config) => {
return axiosCall().then((tokenResponse) => {
setWebCreds(tokenResponse);
config.headers.Authorization = `Bearer ${tokenResponse}`;
return Promise.resolve(config)
}).catch(error => {
// decide what to do if you can't get your token
})
}, (error) => {
return Promise.reject(error);
});
More details here: https://github.com/axios/axios/issues/754
Following code doing certain tasks:
Update Token on 401
Make a queue of failed requests while the token is refreshing.
Restore the original request after token refreshing.
Once the peculiar request is given 200, remove it from the queue.
Config.js
import axios from 'axios';
import { AsyncStorage } from 'react-native';
import { stateFunctions } from '../../src/sharedcomponent/static';
const APIKit = axios.create({
baseURL: '',
timeout: 10000,
withCredentials: true,
});
const requestArray = [];
// Interceptor for Request
export const setClientToken = token => {
APIKit.interceptors.request.use(
async config => {
console.log('Interceptor calling');
let userToken = await AsyncStorage.getItem('userToken');
userToken = JSON.parse(userToken);
config.headers = {
'Authorization': `Bearer ${userToken}`,
'Accept': 'application/json',
"Content-Type": "application/json",
"Cache-Control": "no-cache",
}
// console.log('caling ' , config)
return config;
},
error => {
Promise.reject(error)
});
};
// Interceptor for Response
APIKit.interceptors.response.use(
function (response) {
if (requestArray.length != 0) {
requestArray.forEach(function (x, i) {
if (response.config.url == x.url) {
requestArray.splice(i, 1);
}
});
}
return response;
},
function (error) {
const originalRequest = error.config;
requestArray.push(originalRequest);
let reqData = "username=" + number + "&password=" + pin + "&grant_type=password" + "&AppType=2" + "&FcmToken=null";
// console.log('error ' , error);
if (error.message === "Request failed with status code 401" || error.statuscode === 401) {
if (!originalRequest._retry) {
originalRequest._retry = true;
return axios({
method: 'post',
url: '/api/login',
data: reqData,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cache-Control": "no-cache",
}
})
.then(res => {
let response = res.data;
console.log('successfull Login', response)
if (res.data.StatusCode == 200) {
AsyncStorage.setItem('userToken', JSON.stringify(response.access_token));
stateFunctions.UserId = response.UserId;
stateFunctions.CustomerContactID = response.CustomerContactID;
let obj = {
access_token: response.access_token,
token_type: response.token_type,
expires_in: response.expires_in,
UserId: response.UserId,
CustomerContactID: response.CustomerContactID,
Mobile: response.Mobile,
StatusCode: response.StatusCode
}
AsyncStorage.setItem('logindetail', JSON.stringify(obj));
if (requestArray.length != 0) {
requestArray.forEach(x => {
try {
console.log(x, "request Url");
x.headers.Authorization = `Bearer ${response.access_token}`;
x.headers["Content-Type"] = "application/x-www-form-urlencoded";
APIKit.defaults.headers.common["Authorization"] = `Bearer${response.access_token}`;
APIKit(x)
} catch (e) {
console.log(e)
}
});
}
return APIKit(originalRequest);
}
})
.catch(err => {
console.log(err);
});
}
}
return Promise.reject(error);
}
);
export default APIKit;
Home.js
gettingToken = async () => {
let userToken = await AsyncStorage.getItem('userToken');
userToken = JSON.parse(userToken);
await setClientToken(userToken);
}

Handling errors for Apollo Client when using ApolloLink.split

I have a simple code:
import { split } from 'apollo-link';
import { WebSocketLink } from 'apollo-link-ws'
import { HttpLink } from 'apollo-link-http'
import ApolloClient from 'apollo-client'
import { onError } from 'apollo-link-error'
const wsLink = new WebSocketLink({
uri: hasura.wsUrl,
options: {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: {
'Authorization': `Bearer ${this.token}`
}
}
}
})
const httpLink = new HttpLink({
uri: hasura.httpUrl,
headers: {
'Authorization': `Bearer ${this.token}`
}
})
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink
)
const errorLink = onError(({graphQLErrors, networkError}) => {
// this callback is never called
console.log('graphQLErrors', graphQLErrors)
console.log('networkError', networkError)
})
this.client = new ApolloClient({
link: errorLink.concat(link),
cache: new InMemoryCache()
})
How I can a handling errors for the "split" links? For this example catching errors doesn't works. If I use links without "split" function errors catching works.
let link = ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
}
if (networkError) console.error(`[Network error]: ${networkError}`, networkError.stack);
}),
ApolloLink.split(
operation => operation.getContext().important === true,
httpLink, // don't batch important
batchHttpLink
),
]);

Categories

Resources