Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using client-only routes with page templates coming from Contentful

Goal

I am looking to use client-only routes for content under a certain URL (/dashboard). Some of this content will be coming from Contentful and using a page template. An example of this route would be {MYDOMAIN}/dashboard/{SLUG_FROM_CONTENTFUL}. The purpose of this is to ensure projects I have worked on at an agency are not able to be crawled/accessed and are only visible to 'employers' once logged in.

What I have tried

My pages are generated via gatsby-node.js. The way of adding authentication/client-only routes has been taken from this example. Now the basics of it have been setup and working fine, from what I can tell. But the private routes seem to only work in the following cases:

If I'm logged in and navigate to /dashboard

  • I'm shown Profile.js

If I an not logged in and go to /dashboard

  • I'm shown Login.js

So that all seems to be fine. The issue comes about when I go to /dashboard/url-from-contentful and I am not logged in. I am served the page instead of being sent to /dashboard/login.


exports.createPages = async ({graphql, actions}) => {
    const { createPage } = actions;
    
    const { data } = await graphql(`
        query {
            agency: allContentfulAgency {
                edges {
                    node {
                        slug
                    }
                }
            }
        }
    `);
    data.agency.edges.forEach(({ node }) => {
        createPage({
            path: `dashboard/${node.slug}`,
            component: path.resolve("src/templates/agency-template.js"),
            context: {
                slug: node.slug,
            },
        });
    });
}

exports.onCreatePage = async ({ page, actions }) => {
    const { createPage } = actions;
    
    if(page.path.match(/^\/dashboard/)) {
        page.matchPath = "/dashboard/*";
        
        createPage(page);
    }
};

My auth.js is setup (the username and password are basic as I am still only developing this locally):

export const isBrowser = () => typeof window !== "undefined";

export const getUser = () =>
  isBrowser() && window.localStorage.getItem("gatsbyUser")
    ? JSON.parse(window.localStorage.getItem("gatsbyUser"))
    : {};

const setUser = (user) =>
  window.localStorage.setItem("gatsbyUser", JSON.stringify(user));

export const handleLogin = ({ username, password }) => {
  if (username === `john` && password === `pass`) {
    return setUser({
      username: `john`,
      name: `Johnny`,
      email: `[email protected]`,
    });
  }

  return false;
};

export const isLoggedIn = () => {
  const user = getUser();

  return !!user.username;
};

export const logout = (callback) => {
  setUser({});
  call
};

PrivateRoute.js is setup the following way:

import React from "react";
import { navigate } from "gatsby";
import { isLoggedIn } from "../services/auth";

const PrivateRoute = ({ component: Component, location, ...rest }) => {
  if (!isLoggedIn() && location.pathname !== `/dashboard/login`) {
    navigate("/dashboard/login");
    return null;
  }

  return <Component {...rest} />;
};

export default PrivateRoute;

dashboard.js has the following. The line <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />, I have tried a couple of things here - Statically typing the route and using the exact prop, using route parameters such as /:id, /:path, /:slug :


import React from "react";
import { Router } from "@reach/router";
import Layout from "../components/Layout";
import Profile from "../components/Profile";
import Login from "../components/Login";
import PrivateRoute from "../components/PrivateRoute";
import Agency from "../templates/agency-template";

const App = () => (
  <Layout>
    <Router>
      <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />
      <PrivateRoute path="/dashboard/profile" component={Profile} />
      <PrivateRoute path="/dashboard" />
      <Login path="/dashboard/login" />
    </Router>
  </Layout>
);

export default App;

And finally agency-template.js


import React from "react";
import { graphql, Link } from "gatsby";
import styled from "styled-components";
import SEO from "../components/SEO";
import Layout from "../components/Layout";
import Gallery from "../components/Gallery";
import GeneralContent from "../components/GeneralContent/GeneralContent";

const agencyTemplate = ({ data }) => {
  const {
    name,
    excerpt,
    richDescription,
    richDescription: { raw },
    images,
    technology,
    website,
  } = data.agency;

  const [mainImage, ...projectImages] = images;

  return (
    <>
      <SEO title={name} description={excerpt} />
      <Layout>
        <div className="container__body">
          <GeneralContent title={name} />
          <Gallery mainImage={mainImage} />

          <GeneralContent title="Project Details" content={richDescription} />
          <div className="standard__images">
            <Gallery projectImages={projectImages} />
          </div>
          <ViewWebsite>
            <Link className="btn" to={website}>
              View the website
            </Link>
          </ViewWebsite>
        </div>
      </Layout>
    </>
  );
};

export const query = graphql`
  query ($slug: String!) {
    agency: contentfulAgency(slug: { eq: $slug }) {
      name
      excerpt
      technology
      website
      images {
        description
        gatsbyImageData(
          layout: FULL_WIDTH
          placeholder: TRACED_SVG
          formats: [AUTO, WEBP]
          quality: 90
        )
      }
      richDescription {
        raw
      }
    }
  }
`;
export default agencyTemplate;

I assume that gating content from a CMS is possible with Gatsby but I might be wrong given it is an SSG. I may be misunderstanding the fundamentals of client-only. The concepts in React and using Gatsby are still very new to me so any help or guidance in achieving the goal would be appreciated.

What I ended up doing

So the answer I marked was the one that 'got the ball rolling'. The explanation of what was happening with state and requiring either useContext or redux helped me understand where I was going wrong.

Also, the suggestion to use web tokens prompted me to find more information on using Auth0 with the application.

Once I had got out of the mindset of creating pages using Gatsby (Through a template, via gatsby-node.s), and instead doing it in a 'React way' (I know Gatsby is built with React) by handling the routing and GraphQL it became clearer. Along with the authentication, all I ended up doing was creating a new <Agency /> component and feeding the data from GraphQL into it and updating the path with my map().

return (
    <>
      <Router>
        <DashboardArea path="/dashboard/" user={user} />
        {agencyData.map(({ node }, index) =>
          node.slug ? (
            <Agency key={index} data={node} path={`/dashboard/${node.slug}`} />
          ) : null
        )}
      </Router>
    </>
  );
like image 754
Bagseye Avatar asked Aug 18 '21 11:08

Bagseye


Video Answer


1 Answers

I assume that in your PrivateRoute component, you're using the isLoggedIn check incorrectly. importing and using isLoggedIn from auth.js will run only initially and will not act as a listner. What you can do is that store the value of isLoggedin in global state variable like(useContext or redux) and make a custom hook to check for the login state. Secondly avoid accessing localStorage directly, instead use the global state managment (useContext, redux) or local state managment (useState, this.state). Note: that when ever you go to a route by directly pasting url in browser, it always refreshes the page and all your stored state is reinitialized. This may be the reason why you may be experiencing this issue. The browser does not know that you had been previously logged in and therefore it always validates once your application is mounted. What you can do is that you can store isLoggedIn state in browser's localstore. Personally I like to use redux-persist for that.

export const useGetUser = () => { //add use infront to make a custom hook
  return useSelector(state => state.gatsByUser) // access user info from redux store
};


export const handleLogin = ({ username, password }) => {
  //suggestion: don't validate password on client side or simply don't use password, 
  //instead use tokens for validation on client side

  if (username === `john` && password === `pass`) {
    dispatch(setUserInfo({
      username: `john`,
      name: `Johnny`,
      email: `[email protected]`,
      isLoggedIn: true,
    }));
    return true;
  }

  return false;
};


// adding 'use' infront to make it a custom hook
export const useIsLoggedIn = () => {
  //this will act as a listner when ever the state changes
  return useSelector(state => state.gatsByUser?.isLoggedIn ?? false);
};

export const logout = (callback) => {
  const dispatch = useDispatch(); // redux
  dispatch(clearUserInfo());
};

Now in private route do

import React from "react";
import { navigate } from "gatsby";
import { useIsLoggedIn } from "../services/auth";

const PrivateRoute = ({ component: Component, location, ...rest }) => {
  const isLoggedIn = useIsLoggedIn();

  if (!isLoggedIn) {
    return navigate("/dashboard/login");
  }

  return <Component {...rest} />;
};

export default PrivateRoute;
like image 186
Waleed Tariq Avatar answered Oct 19 '22 15:10

Waleed Tariq