Page in React renders before the clients permissions are checked - javascript

I have executed a query in Javascript to fetch the data of the current user in my application. This query returns the current user's (the clients) data which is used to check the clients access permissions for various pages in my React application using Apollo Client and GraphQL.
However, for a page that only the administrator should have access to, whilst this query is taking place the page renders so that a user without administrator permissions can temporarily view the contents of the page. Once the permissions have been checked and it is known that the user does not have access, an error page is produced.
I would like this error page to be produced immediately so that none of the content can be viewed at all by clients who don't have permission.
// This is a currentUser.js file that is imported by various React components
// which uses the query to check permissions
import gql from 'graphql-tag';
export default apolloClient => apolloClient
.query({
query: gql`
query CURRENT_USER {
name
age
gender
permissions
}
`,
})
.then(({ data }) => ({ currentUser: data }))
.catch(() =>
({ currentUser: {} }));
// This is a AdministratorPage.jsx file that shouldn't render whilst
// permissions are checked
import currentUser from '../lib/currentUser';
import React, { Component } from 'react';
import { ApolloConsumer } from 'react-apollo';
class AdministratorPage extends Component {
render() {
return (
<ApolloConsumer>
{(client) => {
currentUser(client).then((data) => { ...}
Any ideas?

You should render loading page or some sort of loading indicator until the current user data is fetched. Try something like this in your render method:
<Query query={GET_CURRENT_USER}>
{({ data, loading, error }) => {
if (loading) return <Loading />;
if (error) return <p>ERROR</p>;
return (
<AdministratorPage />
);
}}
</Query>

Related

Reloading a page in React will result in no props

I have the following code, but when the web page is displayed, this.props.items is not passed to Item. When I try to output this.props.items in console.log, nothing is displayed.
However, when the page is automatically refreshed by saving it in the code editor instead of reloading the page, this.props.items is properly passed.
Why is this happening?
I would appreciate it if you could tell me more about it.
import React, { Component } from 'react';
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { getItems } from '../../actions/items'
class Container extends Component {
static propTypes = {
items: PropTypes.array.isRequired,
getItems: PropTypes.func.isRequired,
}
componentWillMount() {
this.props.getItems();
}
render() {
return (
<Item timetable={this.props.items} />
);
}
}
const mapStateToProps = (state) => ({
items: state.items.items,
});
export default connect(mapStateToProps, { getItems })(Container);
export const getItems = () => (dispatch, getState) => {
axios.get(`${url}/items/`, tokenConfig(getState))
.then(res => {
dispatch({
type: GET_ITEMS,
payload: res.data
})
}).catch(err => console.log(err));
}
Most probably the issue would be that your backend API response time is more than what you had expected. It's always a good practice to check whether the data that you fetch from your backend API is actually present, in the sense not null/not empty. The most common counter to this is to use a Loader component and wait until the whole data is fetched from server.
class Container extends Component {
static propTypes = {
items: PropTypes.array.isRequired,
getItems: PropTypes.func.isRequired,
}
componentWillMount() {
this.props.getItems();
}
render() {
if (!this.props?.items) return (<p>Loading...</p>); // --> Use a guard clause
return (
<Item timetable={this.props.items} />
);
}
}
Please go through these references to understand more clearly the points I have elaborated:
How to handle AJAX requests - Official React Docs
Why use guard clauses?
When you save a file in your code editor, React refreshes the app but retains as much of the state as possible, so that you can keep working without needing to redo a lot of steps to get where you're working on. But when you refresh the page, the whole app is restarted from zero and all state is lost.
You can avoid this behavior and save the state you want to persist by using localStorage or sessionStorage. You can do this in the context of React and Redux using Redux Persist.

React JS How to handle authentication

I am trying to join Backend (Express JS) and Frontend (React JS). Right now I am trying to understand how to handle session management.
When a user logins using a form made with React JS, the backend returns a HS-256 JWT token with the user information as a payload.
To avoid unlogged users from fetching data I read that the frontend has to send the bearer token as header and this works fine. But I am not able to find a way of how to avoid the users of using the application in first place, I mean, when every page loads.
Right now my Login code looks like this:
import React, { useEffect, useState } from 'react';
import { useCookies } from 'react-cookie';
export default () => {
const [cookies, setCookie] = useCookies(['auth']);
const [redirect, setRedirect] = useState(false);
const handleSubmit = async e => {
e.preventDefault();
const response = await fetch('server/auth/login', options);
const {token} = await response.json();
setCookie('token', token, { path: '/' });
};
useEffect(() => {
if (cookies.auth) {
setRedirect(true);
}
}, [cookies]);
if (redirect) {
return <Redirect to="/admin" />;
}
return <form onSubmit={handleSubmit}>...</form>
};
I don't know how to use this information in each component to proper set user restrictions. What should I do in order to achieve this?
What you'd want to do is initially check if the by user is authenticated by checking for a the token in local storage / cookie. If so, allow the user to proceed to the route they have visited, if not redirect them back to the login screen ( or error screen). Typically, React users use routing libraries such as react-router-dom. On the documentation of the site, they go over how to go about authentication. Happy coding https://reacttraining.com/react-router/web/example/auth-workflow
Just store the token in local storage and in your router use protected wrappers to routes. If the token doesn't exist or is expired, return redirect to Login.js.
The best way to handle it would be to use an identity and Auth provider like Auth0 and read their documentation for react applications. it's really easy to set up and achieve what you are trying to do out of the box.
Here's an example from my application:
function PrivateRoute({ component: Component, ...rest }) {
return (
<AuthContext.Consumer>
{auth => (
<Route
{...rest}
render={props => {
if (!auth.isAuthenticated()) return auth.login();
return <Component auth={auth} {...props} />;
}}
/>
)}
</AuthContext.Consumer>
);
}
You have to save the token in local storage, so that whenever the user refreshes the page, the token would still exists.
const handleSubmit = async e => {
e.preventDefault();
const response = await fetch('server/auth/login', options);
const {token} = await response.json();
setCookie('token', token, { path: '/' });
localStorage.setItem('token', token);
};
// Check if token exists
const token = localStorage.getItem('token');
if (token && token !== '') {
return <Redirect to="/admin" />;
}
return <form onSubmit={handleSubmit}>...</form>
Note that you still have to validate that token on your server-side script before actually allowing the user to access your admin page.

Fetch global data for the whole app in Next.js on initial page load

In my Next.js application I have search filters.
Filters consist of checkboxes, and to render these checkboxes I need to fetch (GET) all possible options from the API.
Those filters are available on many pages, so regardless the page where user lands I need to fetch the data for filters immediately and put it in the local storage to avoid further excessive API calls. Putting API call in each page is not an option.
I see the one option is to put the API call in getInitialProps in _app.js, but then according to Next.js docs automatic static optimization will not work and every page in my app will be server-side rendered.
So what is the proper way to fetch such global data in Next.js?
--------UPDATE
So at this moment I've used the next solution: in _app.js I put useEffect React Hook and once the Frontend is ready I am checking whether my data for whole application is in locale storage. If it's not then fetch data from server and put in local storage for further use.
// _app.js
const AppWrapper = ({ children }) => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch({ type: FRONTEND_LOADED });
loadInitialData(dispatch);
}, [false]);
return <>{children}</>;
};
class MyApp extends App {
render() {
const { Component, router, pageProps } = this.props;
return (
<>
<AppProvider>
<AppWrapper>
<MainLayout pathname={router.pathname}>
<Component {...pageProps} />
</MainLayout>
</AppWrapper>
</AppProvider>
</>
);
}
}
// loadInitialData.js
import {
SET_DIETS_LIST,
UPDATE_FILTERS_FROM_CACHE,
} from "Store/types";
import fetch from "isomorphic-unfetch";
export default dispatch => {
const ls = JSON.parse(localStorage.getItem("filters"));
if (ls) {
const localStorageState = {
diet: {
list: ls.diet.list || [],
selected: ls.diet.selected || [],
},
...
};
dispatch({
type: UPDATE_FILTERS_FROM_CACHE,
payload: { filters: localStorageState },
});
}
if (!ls || !ls.diet.list.length) {
fetch(`${process.env.API_URL}/diets`)
.then(r => r.json())
.then(data => {
dispatch({ type: SET_DIETS_LIST, payload: { data[0] } });
});
}
...
};
It seems this filter is located on headermenu or sidebar menu?
If that is the case, I would suggest (an option other than _app.js) putting the API caller inside header/ sidebar component, and call the header/sidebar component on layout/ pages component.
Therefore, you will get the same behavior as what you've described (not invoking SSR on every pages and static optimization is still working because the concept is similar with the _app.js (just put it inside a structure).

Next.js - understanding getInitialProps

I have an app that uses next.js along with Apollo/ Graphql and i'm trying to fully understand how the getInitialProps lifecycle hook works.
The lifecycle getInitialProps in my understanding is used to set some initial props that will render server side for when the app first loads which can be used prefetch data from a database in order to help SEO or simply to enhance page load time.
My question is this:
Every time I have a query component that fetches some data in my
components across my app, do I have to use getInitialProps to be
sure that data will be rendered server side?
My understanding is also that getInitialProps will only work in the page index components (as well as in _app.js), this would mean that any component lower down in the component tree would not have access to this lifecycle and would need to get some initial props from way up at the page level and then have them passed down the component tree. (would be great if someone could confirm this assumption)
Here is my code:
_app.js (in /pages folder)
import App, { Container } from 'next/app';
import { ApolloProvider } from 'react-apollo';
class AppComponent extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
render() {
const { Component, apollo, pageProps } = this.props;
return (
<Container>
<ApolloProvider client={apollo}>
<Component client={client} {...pageProps} />
</ApolloProvider>
</Container>
);
}
}
export default AppComponent;
Index.js (in /pages/users folder)
import React, { PureComponent } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const USERS_QUERY = gql`
query USERS_QUERY {
users {
id
firstName
}
}
`;
class Index extends PureComponent {
render() {
return (
<Query query={USERS_QUERY}>
{({data}) => {
return data.map(user => <div>{user.firstName}</div>);
}}
</Query>
);
}
}
export default Index;
The answer is NO
If you use Apollo with Next JS you will not have to use getInitialProps on each page to get some initial data rendered server side. The following configuration for getInitialProps is enough for all the components to render out with their respective queries if they have <Query> components in them
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
My issue and why I wasnt seeing any server side rendering is that Heroku or Now wouldnt perform SSR with a public URL ie my-app.heroku.com. To resolve this I purchased and applied a custom URL in Heroku and it worked. Along with a custom URL I had the following configuration in my Apollo config
const request = (operation) => {
operation.setContext({
fetchOptions: {
credentials: 'include'
},
headers: { cookie: headers.cookie }
});
};
This completely resolved it and now I have SSR without the pain of having to manually set getInitialProps on each page
Hope this helps someone

Relay Modern isn't sending network request to GraphQL Server

I just started looking at Relay Modern recently and creating a simple app with a GraphQL backend (which works perfectly fine when testing with GraphIQL). However, I'm running into problems with Relay not sending network requests to retrieve any data. I'm not 100% confident about the below code but I definitely would expect it to at least send a network request to http://localhost:3000/graphql, but the devtools don't show any such request (or server logs).
environment.js
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
const store = new Store(new RecordSource());
const network = Network.create((operation, variables) =>
fetch('http://localhost:3000/graphql', {
method: 'POST',
headers: {
// Add authentication and other headers here
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: operation.text, // GraphQL text from input
variables,
}).then(res => res.json()),
})
);
const environment = new Environment({
network,
store,
});
export default environment;
App.jsx
import React, { Component } from 'react';
import { graphql, QueryRenderer } from 'react-relay';
import environment from '#utilities/environment';
class App extends Component {
render() {
console.log(this.props); // Empty object {} here
return (
<div>
Hello World!
</div>
);
}
}
const Query = graphql`
query AppQuery {
user(id: "u01") {
id
username
}
}
`;
const AppQuery = () =>
(<QueryRenderer
environment={environment}
graphql={Query}
variables={{}}
render={({ error, props }) => {
console.log(error, props); // Get (undefined, {}) here
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return <App {...props} />;
}
return <div>Loading!</div>;
}}
/>);
export default AppQuery;
Am I missing something obvious? There are no console/webpack errors and the app renders properly, such as it is, but simply no GraphQL/Relay data. Thanks!
I think your environnement is just fine.
Few things that might help : You might want to create a FragmentContainer and setup/run Relay Compiler to generate the needed graphql files in order to Relay run your queries.
You probably want declare and collocate the data requirements with App through a FragmentContainer. You need a Fragment Container because your data is masked in App hence not available through props (see more here why it's masked).
You'll need to use createFragmentContainer() like this :
App = createFragmentContainer(
App,
graphql`
fragment App_users on User { // The fragment name should follow the convention <FileName>_<propName>, so maybe you might the App component to an another file.
user(id: "u01") {
id
username
}
}
`,
);
Modify the Query to :
graphql`
viewer {
...App_users_viewer
}
`
When done, you should be able to run the Relay Compiler and have graphql generated files

Categories