将仅限客户端的路由与来自 Contentful 的页面模板一起使用

Using client-only routes with page templates coming from Contentful

目标

我希望对特定 URL (/dashboard) 下的内容使用仅限客户端的路由。其中一些内容将来自 Contentful 并使用页面模板。这条路线的一个例子是 {MYDOMAIN}/dashboard/{SLUG_FROM_CONTENTFUL}。这样做的目的是确保我在代理机构工作的项目无法 crawled/accessed 并且只有 'employers' 登录后才能看到。

我试过的

我的页面是通过 gatsby-node.js 生成的。添加authentication/client-only 路由的方法已从this example 中获取。据我所知,现在它的基础已经设置好并且工作正常。但是私有路由似乎只适用于以下情况:

如果我登录并导航到 /dashboard

如果我没有登录并转到 /dashboard

所以一切似乎都很好。当我转到 /dashboard/url-from-contentful 但我没有登录时,问题就出现了。我收到了页面,而不是被发送到 /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);
    }
};

我的 auth.js 已设置(用户名和密码是基本的,因为我仍然只在本地开发):

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: `johnny@example.org`,
    });
  }

  return false;
};

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

  return !!user.username;
};

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

PrivateRoute.js 设置如下:

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 有以下内容。 <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} /> 行,我在这里尝试了一些东西——静态输入路由并使用 exact 属性,使用路由参数,例如 /: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;

最后 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;

我假设 Gatsby 可以从 CMS 门控内容,但鉴于它是 SSG,我可能错了。我可能误解了仅限客户的基本原理。 React 中的概念和使用 Gatsby 对我来说仍然很新,因此在实现目标方面的任何帮助或指导将不胜感激。

我最后做了什么

所以我标记的答案是'got the ball rolling'。对 state 发生了什么以及需要 useContext 或 redux 的解释帮助我理解了我哪里出错了。

此外,使用网络令牌的建议促使我查找有关在应用程序中使用 Auth0 的更多信息。

一旦我摆脱了使用 Gatsby 创建页面的心态(通过模板,通过 gatsby-node.s),而是在 'React way' 中创建页面(我知道 Gatsby 是用 React 构建的) 通过处理路由和 GraphQL 它变得更加清晰。除了身份验证,我最后所做的就是创建一个新的 <Agency /> 组件并将来自 GraphQL 的数据输入其中并使用我的 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>
    </>
  );

我假设您在 PrivateRoute 组件中错误地使用了 isLoggedIn 检查。从 auth.js 导入和使用 isLoggedIn 只会 运行 一开始不会充当监听器。您可以做的是将 isLoggedin 的值存储在全局状态变量(如(useContext 或 redux))中,并创建一个自定义挂钩来检查登录状态。其次避免直接访问localStorage,而是使用全局状态管理(useContext,redux)或本地状态管理(useState,this.state)。 注意:当你通过在浏览器中直接粘贴 url 进入路由时,它总是刷新页面并且你所有存储的状态都被重新初始化。这可能是您遇到此问题的原因。浏览器不知道您之前已经登录,因此它总是在您的应用程序安装后进行验证。您可以做的是将 isLoggedIn 状态存储在浏览器的本地存储中。我个人喜欢为此使用 redux-persist。

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: `johnny@example.org`,
      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());
};

现在在私人路线做

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;

您似乎在 gatsby-node.js/createPages() 中进行服务器端渲染 dashboard/[url]? IIRC 这些路由的优先级高于动态路由(您在 dashboard.js 中用 @reach/router 指定)。

另外,这些路线的内容目前是公开的。如果你想让它们真正保密,你应该直接在客户端查询 Contentful graphql API(通过 fetch() 或使用 apollo 客户端,urql 等),而不是依赖 Gatsby 的 graphql 服务器。

我会做以下事情:

  • 删除 gatsby 中的 dashboard/[url] 部分-node.js
  • 配置您的虚拟主机,以便所有匹配“/dashboard/*”的路由都将重定向到“/dashboard”

如果你碰巧在 Netlify 上托管你的静态站点,你会用这个创建一个 _redirects,假设你配置 Gatsby 来创建 nice url:

# /static/_redirect

/dashboard/*   /dashboard   200

匹配您当前设置的一种可能更简单的方法是在虚拟主机级别控制内容。您可以配置 nginx 以使用基本身份验证保护 /dasboard/*。然而,maintaining/updating 密码是一个痛苦的问题,现代托管解决方案实际上不允许用户配置它。

Netlify 提供了自己的身份验证解决方案,您可以研究一下。

我之前遇到过同样的问题,我无法使用专用路由获得确切的功能。

在我的例子中,我为 Public 和私有路由创建了两个单独的布局,并为私有布局构建了身份验证。登录用户数据链接到 redux 存储(首先我使用 Context,然后移至 Redux)。在使用 Private Layout 的 Private routes 中,它将访客用户重定向到登录页面,并在登录后将他们重定向到同一页面。

私有布局是这样的:

import React from "react";
import { navigate } from "gatsby";
import { useSelector } from "react-redux";

const PrivateLayout = ({children}) => {
    const isLoggedIn = useSelector(state => state.user.isLoggedIn);

    useEffect(() => {
        if (!isLoggedIn) {
            // redirect the user to login page.
            // I'm sending the current page's URL as the redirect URL
            // so that I can take the user back to this page after logging in.
        }
    }, [isLoggedIn])

    if (!isLoggedIn) return null;

    return <>
        {...header}
        {children}
        {...footer}
    </>
}

export default PrivateLayout;

不确定此解决方法是否适合您。如果是的话,我可以给你更多信息。