How can I do refresh auth token logiс with axios for multiple requests? - axios

I was trying to do it like this, but for three requests it sends three refresh requests:
1, 2, 3 fails with 401
refresh success, 1 success, 2, 3 fails
refresh success, 2 success, 3 fails
refresh success 3 success
I can't put that much load on a mobile device (even if there was only 3 refresh without "refail")
Here is my code:
function requestRefreshToken(refreshToken, accessToken) {
return axios
.create({
baseURL: apiUrl + Endpoints.AUTH.REFRESH,
skipAuthRefresh: true,
headers: {
'Accept-Language': 'ru',
'User-Agent': `${Platform.OS} ${packageJson.version}`,
Authorization: accessToken,
},
})
.post(
'',
{
grant_type: GrantTypes.REFRESH_TOKEN,
refresh_token: refreshToken,
client_id: Client.id,
client_secret: Client.secret,
},
{ validateStatus },
);
}
const refreshAuthLogic = async failedRequest =>
Keychain.getCredentials()
.then(old => requestRefreshToken(old.refreshToken, old.accessToken))
.then(({ data: credentials }) => {
failedRequest.config.headers.Authorization = `${credentials.token_type} ${
credentials.access_token
}`;
return Keychain.setCredentials(credentials);
});
createAuthRefreshInterceptor(axios, refreshAuthLogic, {
retryInstance: axios,
skipWhileRefreshing: true,
onRetry: function(config) {
return Keychain.getCredentials().then(({ accessToken }) =>
axios({
...config,
header: { ...config.headers, Authorization: accessToken },
}),
);
},
});
axios.interceptors.response.use(
r => r,
request => {
if (request.response.status === 401) {
return Keychain.getCredentials().then(({ accessToken }) =>
axios({
...request.config,
header: { ...request.config.headers, Authorization: accessToken },
}),
);
}
},
);

Solved with this! Seems there is no way solving it without failed request queue( https://gist.github.com/mkjiau/650013a99c341c9f23ca00ccb213db1c

Here's a quick way I implemented it.
I found it a bit simpler to reason about the code.
I've adapted it from this solution:
HERE
let refreshTokenPromise: null | Promise < any > ;
instance.interceptors.response.use(r => {
const {
data
} = r;
if (data.errors && data.errors[0].message === "AUTH_EXPIRED") {
if (!refreshTokenPromise) {
refreshTokenPromise = fetchRefreshToken().then(data => {
refreshTokenPromise = null;
return data;
});
}
return refreshTokenPromise.then(token => {
if (r.config.headers) r.config.headers["Authorization"] = token;
return instance.request(r.config);
});
}
return r;
});

Related

SWR optimistic mutation

I am building a small app to sign up users to sing a karaoke music. I am using SWR to fetch info from a mongoDB and due to performance issues I am trying to implement optimist UI. Although, I can't seem to make it work...
When a song is played/singed is marked as such with the code below:
const { mutate } = useSWRConfig();
const isSongPlayed = async (song: Song, bool: boolean) => {
try {
const isTheSongPlayed = await fetch(`/api/songs/${song?._id}`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ ...song, played: bool }),
});
const playedSong = await isTheSongPlayed.json();
if (!playedSong.success || !playedSong.data) setLoading(true);
// the information to be changed is fetch is this endpoint
mutate(`/api/events/${id}`);
} catch (error) {
console.log(error);
}
};
// this function is called from a button
const onSetAsPlayed = (song: Song) => {
song.played ? isSongPlayed(song, false) : isSongPlayed(song, true);
};
Everything works pretty well locally, although the deployed version is very slow because it's counting on revalidation (call to the server) to update the UI. The code above isn't optimistic.
The flow is following:
singleEvent page:
displays a component to all requests;
displays a component to all moments:
Each moment display the songs list.
The object that is passed look like this:
{
eventTitle: '',
moments: [
{
momentTitle: '',
songs: [
{
title: '',
artist: '',
played: boolean,
requests: [user object]
},
],
},
],
};
What I have been doing is:
const isSongPlayed = async (song: Song, bool: boolean) => {
/* all code necessary to update the object above
which is basically marked the song as played and
pass it to the object again */
mutate(`/api/events/${id}`, newObjectUpdated, false);
try {
const isTheSongPlayed = await fetch(`/api/songs/${song?._id}`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ ...song, played: bool }),
});
const playedSong = await isTheSongPlayed.json();
if (!playedSong.success || !playedSong.data) setLoading(true);
mutate(`/api/events/${id}`, newObjectUpdated, true);
} catch (error) {
console.log(error);
}
};
The code above works poorly, basically the updated song disappears and appears again with the changes which is a really bad UI.
Thanks in advance.
So I managed to fix it:
const isSongPlayed = async (song: Song, bool: boolean) => {
await fetch(`/api/songs/${song?._id}`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ ...song, played: bool }),
});
const filterMoments = singleEvent.moments.filter((moment: any) => {
return moment._id !== momentData._id;
});
const filterSongs = momentData.songs.filter((s: any) => {
return s._id !== song._id;
});
const songUpdated = [...filterSongs, { ...song, played: bool }].sort(
(a, b) => {
if (a.createdAt < b.createdAt) {
return -1;
}
if (a.createdAt > b.createdAt) {
return 1;
}
return 0;
}
);
const momentsUpdated = [
...filterMoments,
{ ...momentData, songs: songUpdated },
].sort((a, b) => {
return a.index - b.index;
});
const eventUpdated = { ...singleEvent, moments: momentsUpdated };
mutate(`/api/events/${momentData.event}`, eventUpdated, false);
};
What happened is that I was revalidating the data and the event moments and songs wasn't sorted and originally, after a few time I figured out the problem. Not sure if it's the best solution, but seems serving the porpuse right.

UI Doesn't update after SWR data does NEXTJS

I have a simple project going on and it is smoothly working but I have failed to add a item delete button. My post request to add items is perfectly working but my delete items doesn't work. I chose to go with post instead of delete because of my api structure.
Repo: https://github.com/berkaydagdeviren/rl-revenue-calculator
const handleClick = async (e) => {
e.preventDefault();
console.log(totalCredit, 'total credit before')
setTotalCredit([...totalCredit, credit])
console.log(ref, 'refFFFFFFF')
const currentDate = ref.current
const options = {
method: "PUT",
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(
{ date: currentDate, credits: credit }
)
}
var responseClone;
fetch(`api/hello?date=${ref.current}`, options)
.then(res => {
responseClone = res.clone();
return res.json()
}).then(data => {
console.log(data, 'data')
}).then(function (data) {
// Do something with data
}, function (rejectionReason) { // 3
console.log('Error parsing JSON from response:', rejectionReason, responseClone); // 4
responseClone.text() // 5
.then(function (bodyText) {
console.log('Received the following instead of valid JSON:', bodyText); // 6
});
});
setCredit(0)
}
This is working perfectly fine but this does not;
const handleItemDelete = async itemToBeDeleted => {
console.log(itemToBeDeleted, "itemTobeDeleted")
const options = {
method: "PUT",
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(
{ date: ref.current, index: itemToBeDeleted }
)
}
var responseClone;
await fetch(`api/hello?date=${ref.current}`, options)
.then(async res => {
responseClone = res.clone();
console.log(res, "res")
return await res.json()
// I used this to read server's response when it was giving parsing JSON error
}).then(data => {
console.log(data, 'data')
}).then(function (data) {
// Do something with data
}, function (rejectionReason) { // 3
console.log('Error parsing JSON from response:', rejectionReason, responseClone); // 4
responseClone.text() // 5
.then(function (bodyText) {
console.log('Received the following instead of valid JSON:', bodyText); // 6
});
});
const newTotalCredit = await data.find(item => item.date == ref.current).credits
setTotalCredit(newTotalCredit)
console.log("STATE UPDATED BEFORE DATA")
}
This is where I reference handleItemDelete to;
credit.map((item, index) => {
return (
item > 0 ?
React.Children.toArray(
<div>
<span style={{ color: 'green' }}> +{item}C </span>
<button onClick={() =>handleItemDelete(index)}>
X
</button>
</div>
)
:
null
)
})
}
And this is how I handle put request, again I can see that mongodb is updated after refresh but because ui didn't totalCredits' indexes are messed up and results in either no deletion or false deletion.
handler.put(async (req, res) => {
let data = req.body
console.log(typeof(data))
if (data.index) {
let {date, index} = req.body
console.log(data.index, "data.index")
await req.db.collection('credits').update({date: date}, {$unset: {["credits."+ index] : 1}})
await req.db.collection('credits').update({date: date}, {$pullAll: {credits: [null]}})
}
await req.db.collection('credits').updateOne({date: data.date}, {$push: {credits: data.credits}})
res.json(data)
})
I use SWR right in the index.js Home component
export default function Home()
{
const [totalCredit, setTotalCredit] = useState([])
const [credit, setCredit] = useState('')
const ref = useRef(null);
const [date, setDate] = useState(null);
const { data } = useSWR('/api/hello', async (url) => {const response = await axios.get(url);
return response.data; },
{ refreshInterval: 1000, revalidateOnMount: true });
Sorry if I'm not clear or providing wrong pieces of code please let me know. Thank you in advance!
your options in handleDeleteItem:
const options = {
method: "PUT",
headers: {
'Content-type': 'application/json'
},
Should not method be DELETE? You are sending PUT request instead of DELETE

react-admin authentication flow not working

Working on authentication react-admin using JWT token and storing in memory as closure variable.
Creating AuthToken from Django works fine, it's emitting the Token with user info as JSON.
Here is inMemoryJWT Manager
File: inMemoryJwt.js
// https://github.com/marmelab/ra-in-memory-jwt
const inMemoryJWTManager = () => {
let inMemoryJWT = null;
let isRefreshing = null;
// Token code goes here
const getToken = () => inMemoryJWT;
const setToken = (token) => {
inMemoryJWT = token;
return true;
};
const ereaseToken = () => {
inMemoryJWT = null;
return true;
}
return {
ereaseToken,
getToken,
setToken,
}
};
export default inMemoryJWTManager();
File: AuthProvider.js
oryJWTManager';
const authProvider = {
login: ({ username, password }) => {
const request = new Request('http://127.0.0.1:8000/api/token-auth/', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
return fetch(request)
.then(response => {
console.log('Response status ' + response.status )
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
console.log('response.json is ');
console.log(response.json());
return response.json();
})
.then(({ token }) => {
console.log(' about to set token in storage .......... ');
JwtManager.setToken(token);
const decodedToken = decodeJwt(token);
# Its not setting in localstorage
localStorage.setItem('token', token);
console.log('Token from lS : -------------',localStorage.getIdentity('token'));
localStorage.setItem('permissions', decodedToken.permissions);
})
.then(auth => {
localStorage.setItem('auth', JSON.stringify(auth));
})
.catch(response => {
console.log('Catch : ' );
console.log(response.json());
throw new Error('Network errorkkkkkkkkkkkkk')
});
},
logout: () => {
localStorage.setItem('not_authenticated', true);
localStorage.removeItem('auth');
localStorage.removeItem('role');
localStorage.removeItem('login');
localStorage.removeItem('user');
localStorage.removeItem('avatar');
inMemoryJWT.ereaseToken();
return Promise.resolve();
},
checkError: ({ status }) => {
if (status === 401 || status === 403) {
inMemoryJWT.ereaseToken();
return Promise.reject();
}
return Promise.resolve();
// return status === 401 || status === 403
// ? Promise.reject( { redirectTo: '/login' , logoutUser: false} )
// : Promise.resolve();
},
checkAuth: () => {
return inMemoryJWT.getToken() ? Promise.resolve() : Promise.reject({ redirectTo: '/no-access', message: 'login.required' });
// localStorage.getItem('auth')
// ? Promise.resolve()
// : Promise.reject({ redirectTo: '/no-access', message: 'login.required' }),
},
getPermissions: () => {
return inMemoryJWT.getToken() ? Promise.resolve() : Promise.reject();
// const role = localStorage.getItem('permissions');
// return role ? Promise.resolve(role) : Promise.reject();
},
getIdentity: () => {
try {
const { id, fullName, avatar } = JSON.parse(localStorage.getItem('auth'));
return Promise.resolve({ id, fullName, avatar });
} catch (error) {
return Promise.reject(error);
}
// return {
// id: localStorage.getItem('login'),
// fullName: localStorage.getItem('user'),
// avatar: localStorage.getItem('avatar'),
// };
},
}
export default authProvider;
App.js
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const { token } = JSON.parse(localStorage.getItem('auth'));
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
};
const dataProvider = drfProvider('http://127.0.0.1:8000/api/token-auth/', httpClient);
const App = () => (
<Admin title=""
loginPage={Login}
catchAll={NotFound}
logoutButton={PfsLogoutButton}
authProvider={authProvider}
dataProvider={dataProvider}
catchAll={NotFound}
dashboard={Dashboard} disableTelemetry>
The problem
The problem I am facing is to, It's not setting in-memory or local storage, also I get the message in the footer on the login button clicked. I am breaking my head with it for quite some time. Let me know if you need any other info.
Login required error for every page
HTTP Response goes here

How to code Multipart-form POST REQUEST using apollo-datasource-rest

I want to code the multipart-form POST REQUEST below using apollo-datasource-rest
My attempt to code this leads to a BAD REQUEST error
const { RESTDataSource } = require('apollo-datasource-rest');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
class SalesforceApi extends RESTDataSource {
constructor() {
super();
this.initialize({});
this.getAccessToken()
.then((accessToken) => {
this.headers = {
Authorization: `Bearer ${accessToken}`,
};
});
}
async getAccessToken() {
console.log('Getting Salesforce access token');
try {
const response = await this.post(
'https://test.salesforce.com/services/oauth2/token',
{
username: 'FILTERED#FILTERED',
password: `${'FILTERED'}`,
grant_type: 'password',
client_id: 'FILTERED',
client_secret: 'FILTERED',
},
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
);
const { accessToken } = response;
console.log(`ChangeGear sessionId: ${accessToken}`);
return accessToken;
} catch (error) {
console.log(`${error}`);
}
return 'No access token!!!';
}
module.exports = SalesforceApi;
[server:salesforce:local] POST https://test.salesforce.com/services/oauth2/token (343ms)
[server:salesforce:local] Error: 400: Bad Request
If memory serves correctly, form data is serialized slightly differently hence why the FormData interface exists. And the apollo-datasource-rest's this.post method is just a wrapper around fetch, so something like the below should work.
Instead of passing the body as a JSON object, try something like this
const formData = new FormData();
formData.append('username', 'FILTERED#FILTERED');
// ... more append lines for your data
const response = await this.post(
'https://test.salesforce.com/services/oauth2/token',
formData
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
);

Cypress how to return JWT token from localstorage and use it in another api call

Hi What I am trying to do is to save the localStorage in to variable somewhere , So i can refer to it in different test scenario but I am not sure if that is possible as my token1 variable is always empty.
this is my support/command.js file
Cypress.Commands.add('postTokenLogin', () => {
cy.request({
method: 'POST',
url: '***/people/sign_in',
body: {
"login": "test#test.com",
"password":"***",
},
headers:{
'content-type': 'application/json'
}
}).then((response) => {
expect(response.body).have.property('firstName')
expect(response.body.token).have.property('authorization')
cy.setLocalStorage('token',response.body.token.authorization )
})
})
now in my test case I wanna be able to use that token inside my header
import "cypress-localstorage-commands";
let token1 = '';
describe('HTTP Example', ()=>{
before(() => {
cy.postTokenLogin();
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
it("the value of JWT Token should exist in localStorage", () => {
cy.getLocalStorage('token').then(token => {
cy.log("the token", token); // I get JWT Token in here
});
});
it('GET List ', ()=>{
cy.getLocalStorage('token').then((token) => {
token1 = token;
})
cy.log('Let Tokennn is ===>' , token1) // Always Empty
cy.request({
method: 'GET',
url: '***/peopleList',
headers:{
'content-type': 'application/json',
'Authorization': token1 // ===> this is also empty
}
}).then((response) => {
expect(response.body).have.property('firstName')
expect(response.body).have.property('lastname')
})
})
})
Can I have another it('GET colours ', ()=>{}) and just pass the token1?
You are working in a async code, so if you need to use the token instead of validation, you should nest the code like below
import "cypress-localstorage-commands";
let token1 = '';
describe('HTTP Example', () => {
before(() => {
cy.postTokenLogin();
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
it("the value of JWT Token should exist in localStorage", () => {
cy.getLocalStorage('token').then(token => {
cy.log("the token", token); // I get JWT Token in here
});
});
it('GET List ', () => {
cy.getLocalStorage('token').then((token) => {
token1 = token;
cy.log('Let Tokennn is ===>', token1) // Always Empty
cy.request({
method: 'GET',
url: '***/peopleList',
headers: {
'content-type': 'application/json',
'Authorization': token1 // ===> this is also empty
}
}).then((response) => {
expect(response.body).have.property('firstName')
expect(response.body).have.property('lastname')
})
})
})