How can I access the react-admin store in the dataprovider? [React Admin 4.x] - react-admin

I'm trying to make use of a user-set variable in the react admin store when making API calls.
Specifically, I am storing a workspace ID in the store, which the user can set through a switcher. I want to be able to access this ID when making API calls, so that I can send over the workspace ID as a url parameter in the API request.
One soluion is to try to get the data directly from localstorage, but that seems hacky. Is there a btter way?
I'm using the latest version of react admin

In recent versions of React-admin, pass parameters like this:
"React-admin v4 introduced the meta option in dataProvider queries, letting you add extra parameters to any API call.
Starting with react-admin v4.2, you can now specify a custom meta in <List>, <Show>, <Edit> and <Create> components,
via the queryOptions and mutationOptions props."
https://marmelab.com/blog/2022/09/05/react-admin-septempter-2022-updates.html#set-query-code-classlanguage-textmetacode-in-page-components
import { Edit, SimpleForm } from 'react-admin'
const PostEdit = () => (
<Edit mutationOptions={{ meta: { foo: 'bar' } }}>
<SimpleForm>...</SimpleForm>
</Edit>
)

#MaxAlex approach works well, but I went with a localStorage route, by setting a header in the fetchHTTP client defined with the dataprovider. This way I didn't have to modify each and every route.
const httpClient = (url: any, options: any) => {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const token = inMemoryJWT.getToken();
const organization = localStorage.getItem('RaStore.organization');
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
} else {
inMemoryJWT.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set(
'Authorization',
`Bearer ${inMemoryJWT.getToken()}`,
);
}
});
}
if (organization) {
options.headers.set('Organization-ID', organization);
}
return fetchUtils.fetchJson(url, options);
};
I also looked into the react admin internals, and the store provider is one level below the dataprovider. This means there isn't an easy way to access the store without refactoring the entire Admin provider stack.

Related

Update Next.js to React 18 breaks my API calls using next-auth

This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233

Apollo server multiple 3rd party Apis

Looking at using next js and the new api routes with Apollo server. However Iā€™m not using one source of data. I will be using contentful and shopify, however could be more 3rd party APIs.
How do you consume multiple 3rd party APIs and access all the data from through a custom end point?
Any examples?
You can combine multiple GraphQL APIs into a unified endpoint using a technique called GraphQL schema stitching. Apollo has an extensive guide on the topic.
I've also written a two part guide how to stitch the Contentful API together with other APIs: Part 1, Part 2.
All of these examples are build around Apollo Server but the same approach works with Apollo Client as well. So you can choose to do the stitching either as part of your API (adding your GraphQL API, Shopify and Contentful to a unified endpoint) or doing it client side. These have both advantages and disadvantages. Server side will generally result less requests to be made from your client while doing it client side will remove the single point of failure that is your stitching proxy.
A basic set-up, using Apollo Server, would be the following:
const {ApolloServer} = require('apollo-server');
const {HttpLink} = require('apollo-link-http');
const {setContext} = require('apollo-link-context');
const {
introspectSchema,
makeRemoteExecutableSchema,
mergeSchemas
} = require('graphql-tools');
const fetch = require('node-fetch');
const {GITHUB_TOKEN, CONTENTFUL_SPACE, CONTENTFUL_TOKEN} = require('./config.json');
const gitHubLink = setContext((request) => ({
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
}
})).concat(new HttpLink({uri: 'https://api.github.com/graphql', fetch}));
const contentfulLink = setContext((request) => ({
headers: {
'Authorization': `Bearer ${CONTENTFUL_TOKEN}`,
}
})).concat(new HttpLink({uri: `https://graphql.contentful.com/content/v1/spaces/${CONTENTFUL_SPACE}`, fetch}));
async function startServer() {
const gitHubRemoteSchema = await introspectSchema(gitHubLink);
const gitHubSchema = makeRemoteExecutableSchema({
schema: gitHubRemoteSchema,
link: gitHubLink,
});
const contentfulRemoteSchema = await introspectSchema(contentfulLink);
const contentfulSchema = makeRemoteExecutableSchema({
schema: contentfulRemoteSchema,
link: contentfulLink,
});
const schema = mergeSchemas({
schemas: [
gitHubSchema,
contentfulSchema
]
});
const server = new ApolloServer({schema});
return await server.listen();
}
startServer().then(({ url }) => {
console.log(`šŸš€ Server ready at ${url}`);
});
Note that this will just merge the two GraphQL schemas. You might have conflicts if two APIs have the same name for a type or root field.
Two make sure you don't have any conflicts I suggest you transform all schema to give the types and root fields unique prefixes. This could look this:
async function getContentfulSchema() {
const contentfulRemoteSchema = await introspectSchema(contentfulLink);
const contentfulSchema = makeRemoteExecutableSchema({
schema: contentfulRemoteSchema,
link: contentfulLink,
});
return transformSchema(contentfulSchema, [
new RenameTypes((name) => {
if (name.includes('Repository')) {
return name.replace('Repository', 'RepositoryMetadata');
}
return name;
}),
new RenameRootFields((operation, fieldName) => {
if (fieldName.includes('repository')) {
return fieldName.replace('repository', 'repositoryMetadata');
}
return fieldName;
})
]);
}
This will let you use all the apis in a unified manner but you can't query across the different APIs. To do that, you'll have to stitch together fields. I suggest you dive into the links above, they explain this in more depth.

Nuxt: Fetching data only on server side

I am using Github's API to fetch the list of my pinned repositories, and I put the call in the AsyncData method so that I have the list on the first render. But I just learnt that AsyncData is called once on ServerSide, then everytime the page is loaded on the client. That means that the client no longer has the token to make API calls, and anyways, I wouldn't let my Github token in the client.
And when I switch page (from another page to the page with the list) the data is not there I just have the default empty array
I can't figure out what is the best way to be sure that my data is always loaded on server side ?
export default defineComponent({
name: 'Index',
components: { GithubProject, Socials },
asyncData(context: Context) {
return context.$axios.$post<Query<UserPinnedRepositoriesQuery>>('https://api.github.com/graphql', {
query,
}, {
headers: {
// Token is defined on the server, but not on the client
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
},
})
.then((data) => ({ projects: data.data.user.pinnedItems.nodes }))
.catch(() => {});
},
setup() {
const projects = ref<Repository[]>([]);
return {
projects,
};
},
});
Wrap your request in if(process.server) within the asyncData method of the page.
If you absolutely require the server-side to call and cannot do it from the client side, then you can just manipulate the location.href to force the page to do a full load.
You should use Vuex with nuxtServerInit.
nuxtServerInit will fire always on first page load no matter on what page you are. So you should navigate first to store/index.js.
After that you create an state:
export const state = () => ({
data: []
})
Now you create the action that is always being executed whenever you refresh the page. Nuxt have access to the store even if its on the server side.
Now you need to get the data from the store in your component:
export const actions = {
async nuxtServerInit ({ state }, { req }) {
let response = await axios.get("some/path/...");
state.data = response.data;
}
}
You can store your token in an cookie. Cookies are on the client side but the nuxtServerInit has an second argument. The request req. With that you are able to access the headers and there is your cookie aswell.
let cookie = req.headers.cookie;

Middleware executing before Vuex Store restore from localstorage

In nuxtjs project, I created an auth middleware to protect page.
and using vuex-persistedstate (also tried vuex-persist and nuxt-vuex-persist) to persist vuex store.
Everything is working fine when navigating from page to page, but when i refresh page or directly land to protected route, it redirect me to login page.
localStorage plugin
import createPersistedState from 'vuex-persistedstate'
export default ({ store }) => {
createPersistedState({
key: 'store-key'
})(store)
}
auth middleware
export default function ({ req, store, redirect, route }) {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect(`/auth/login?redirect=${route.fullPath}`)
}
return Promise.resolve()
}
I solved this problem by using this plugin vuex-persistedstate instead of the vuex-persist plugin. It seems there's some bug (or probably design architecture) in vuex-persist that's causing it.
With the Current approach, we will always fail.
Actual Problem is Vuex Store can never be sync with server side Vuex store.
The fact is we only need data string to be sync with client and server (token).
We can achieve this synchronization with Cookies. because cookies automatically pass to every request from browser. So we don't need to set to any request. Either you just hit the URL from browser address bar or through navigation.
I recommend using module 'cookie-universal-nuxt' for set and remove of cookies.
For Setting cookie after login
this.$cookies.set('token', 'Bearer '+response.tokens.access_token, { path: '/', maxAge: 60 * 60 * 12 })
For Removing cookie on logout
this.$cookies.remove('token')
Please go through the docs for better understanding.
Also I'm using #nuxt/http module for api request.
Now nuxt has a function called nuxtServerInit() in vuex store index file. You should use it to retrieve the token from request and set to http module headers.
async nuxtServerInit ({dispatch, commit}, {app, $http, req}) {
return new Promise((resolve, reject) => {
let token = app.$cookies.get('token')
if(!!token) {
$http.setToken(token, 'Bearer')
}
return resolve(true)
})
},
Below is my nuxt page level middleware
export default function ({app, req, store, redirect, route, context }) {
if(process.server) {
let token = app.$cookies.get('token')
if(!token) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Not Provided'}})
} else if(!isTokenValid(token.slice(7))) { // slice(7) used to trim Bearer(space)
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
}
return Promise.resolve()
}
else {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath}})
// return redirect(`/auth/login?redirect=${route.fullPath}`)
} else if (!isTokenValid(store.state.auth.tokens.access_token)) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
// return redirect(`/auth/login?redirect=${route.fullPath}&message=Token Expired`)
} else if (isTokenValid(store.state.auth.tokens.refresh_token)) {
return redirect(`/auth/refresh`)
} else if (store.state.auth.user.role !== 'admin')
return redirect(`/403?message=Not having sufficient permission`)
return Promise.resolve()
}
}
I have write different condition for with different source of token, as in code. On Server Process i'm getting token from cookies and on client getting token store. (Here we can also get from cookies)
After this you may get Some hydration issue because of store data binding in layout. To overcome this issue use <no-ssr></no-ssr> wrapping for such type of template code.

Managing Vuex state using with NuxtJs on 'universal' mode

I get used to using localStorage with Vuex but as you know on SSR we can not use localeStorage. So I searched a solution to keep Vuex state on page refresh and run into vuex-persistedstate using with js-cookie many times.
My question is that do I need really vuex-persistedstate? I mean that can I just use js-cookie just like the following:
import Cookie from 'js-cookie'
const state = {
firstName: Cookie.get('firstName)
}
const mutations = {
setFirstName(state, name) {
state.firstName = name;
Cookie.set('firstName', name)
}
etc.
Following your comment question. Your example in the question is ok at least for setting the cookie. I set mine in an action first and then commit the mutation to set the state only. As for getting your cookies, you might need a bit more than just setting the state. As I said i use 'cookie' (see here) for getting my cookies and I get them through nuxtServerInit, like this:
nuxtServerInit({dispatch}, context) {
return Promise.all([
dispatch('get_cookies', context),
//other actions
]);
},
and the action itself as follows:
get_cookies (vuexContext, context) {
return new Promise((resolve, reject) => {
const cookieConst = cookie.parse(context.req.headers.cookie || '')
if (cookieConst.hasOwnProperty('YourCookieName')) {
//commit mutations here
}
else {
resolve(false)
}
})
},
Obviously you need to import:
import cookies from 'js-cookie'
import cookie from 'cookie'
You can put a fair bit of stuff into cookies, I set user details and shopping cart. I also store an access token in my cookies and validate it in my backend so I can persist an auth state.