【问题标题】:Authorization on a per-route basis (Protected Routes) with Apollo for local state management使用 Apollo 对每条路线(受保护的路线)进行授权以进行本地状态管理
【发布时间】:2022-01-14 18:54:42
【问题描述】:

我正在使用 react-router-dom、typescript、react 和 Apollo graphql-generator & client。

我希望处理 4 个场景:

  1. 路由对登录和注销的用户开放
  2. 路由只对登录用户开放
  3. 路由只对注销的用户开放
  4. 路由对存储在 db 上的组策略成员的用户开放

我不想通过 props 管理状态,而是使用类似 Redux 的方法来管理状态,使用 Apollo Client 中的某些东西。

到目前为止我得到的最接近的是通过反应变量(见下面的代码)。

但是,我宁愿避免使用它们,并坚持使用 Apollo 查询。

我们在 GraphQL 中有一个返回当前登录用户的查询,但是,我似乎无法在登录时运行和更新查询,因此它可以用于检查路由。那是除非我在 App 文件中创建一个状态,并将其注入到 Login 组件中以进行更新。然后,当 Login 重定向到新的路由时,App 文件中的组件,使用刚刚更新的 userState,可以检查 userState 是否授权 Login 重定向到的路由。

不过,正如我上面所说,我想避免通过 props 传递状态。

目前的实现是基于这个:https://v5.reactrouter.com/web/example/auth-workflow

import React, { useState } from 'react'
import ReactDOM from 'react-dom'
import { HashRouter, Redirect, Route, Switch, useHistory } from 'react-router-dom'

import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  makeVar,
} from '@apollo/client'

// -------------------------- client.js -------------------------------------------------
const cache = new InMemoryCache();

// set userVar initially to null, so if !null then logged in
export const userVar = makeVar(null)

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache
});


// --------------------------- routes.js ------------------------------------------------
const ROUTES = {
  HOME: '/',          // Only accessible by logged-in users
  LOGIN: '/login',    // Only accessible by users NOT logged-in
  ABOUT: '/about',    // Accessible by all logged-in / and not logged-in users
  NOTFOUND: '/notFound',
}

const { PUBLIC, AUTH, GUEST } = {
  PUBLIC: 0,
  AUTH: 1,
  GUEST: 2,
}

const AuthRoute = props => {
  const { path, restrictTo, redirectPath, ...routeProps } = props
  console.log("Inside AuthRoute")
  console.table({path, restrictTo, redirectPath, ...routeProps})
  const isAuthorized = to => {
    const authOnly = !!(userVar() ?? false)
    console.log(`authOnly = ${ authOnly }`)
    console.log(`to = ${ to }`)

    const allowAll = true

    switch (to) {
      case PUBLIC:
        console.log(`PUBLIC --> isAuthorized --> allowAll = ${ allowAll }`)
        return allowAll
      case AUTH:
        console.log(`AUTH --> isAuthorized --> authOnly = ${ authOnly }`)
        return authOnly
      case GUEST:
        console.log(`GUEST --> isAuthorized --> !authOnly = ${ !authOnly }`)
        return !authOnly
    }
  }

  if (isAuthorized(restrictTo)) {
    console.log(`Authorized -- Routing to ${ path }`)
    console.log(`Authorized -- routeProps = `)
    console.table({...routeProps})

    return <Route {...routeProps} />
  } else {
    console.log(`--> NOT Authorized -- Redirecting to ${ redirectPath }`)
    return <Redirect to={ redirectPath } />
  }
}


// ------------------------   home.js  -----------------------------------------
const Home = () => {
  const history = useHistory()
  const signOut = () => {
    // Do auth reset here
    userVar(null) //reset global state to logged-out
    history.push(ROUTES.LOGIN)
  }
  return (
    <div>
      <h1>Home - Private Page</h1>
      <button  onClick={ signOut }>Sign Out</button>
    </div>
  )
}


// ------------------------   about.js  -----------------------------------------
const About = () => {
  return (
    <div>
      <h1>About - Public Page</h1>
    </div>
  )
}


// ------------------------   notfound.js  -----------------------------------------
const NotFound = () => {
  return (
    <div>
      <h1>404 - Public Page</h1>
    </div>
  )
}


// ------------------------   login.js  -----------------------------------------
const Login = ({onSubmit}) => {
  console.log(`--> Inside Login`)
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const history = useHistory()

  const onLogin = e => {
    e.preventDefault()
    //Do email/password auth here
    userVar(email) //Set global state to logged-in
    history.push(ROUTES.HOME)
  }

  return (
    <div>
      <h1>LOGIN</h1>
      <form onSubmit={ onLogin }>
        <label for="uemail"><b>Email</b></label>
        <input
          type="text"
          placeholder="Enter Email"
          name="uemail"
          value={ email }
          onChange={ (e) => setEmail( e.target.value ) }
          required
        />
        <label for="upassword"><b>Password</b></label>
        <input
          type="password"
          placeholder="Enter Password"
          name="upassword"
          value={ password }
          onChange={ (e) => setPassword( e.target.value ) }
          required
        />
        <button type="submit">Login</button>
      </form>
    </div>
  )
}


// ------------------------   index.js   ---------------------------------------------
ReactDOM.render(
  <React.StrictMode>
    <HashRouter>
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>
    </HashRouter>
  </React.StrictMode>,
  document.getElementById("root"),
)


// ------------------------   App.js   ---------------------------------------------
function App() {
  return (
    <Switch>
      <AuthRoute exact
          path={ROUTES.HOME}
          restrictTo={AUTH}
          redirectPath={ROUTES.LOGIN}
      >
        <Home />
      </AuthRoute>

      <AuthRoute
        path={ROUTES.LOGIN}
        restrictTo={GUEST}
        redirectPath={ROUTES.HOME}
      >
        <Login />
      </AuthRoute>

      <AuthRoute
        path={ROUTES.ABOUT}
        restrictTo={PUBLIC}
        redirectPath={ROUTES.ABOUT}
      >
        <About />
      </AuthRoute>

      <AuthRoute
        path={ROUTES.NOTFOUND}
        restrictTo={PUBLIC}
        redirectPath={ROUTES.NOTFOUND}
      >
        <NotFound />
      </AuthRoute>

      // Catch-all Route -- could send to 404 if you want
      <Route>
        <Redirect to={ROUTES.NOTFOUND} />
      </Route>
    </Switch>
  )
}
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@apollo/client@3.3.2/apollo-client.cjs.min.js"></script>
<script src="https://unpkg.com/react-router-dom@5.2.0/umd/react-router-dom.min.js"></script>
<script src="https://unpkg.com/react-router@5.2.0/umd/react-router.min.js"></script>

<div id="root"></div>

【问题讨论】:

  • 我知道这是一个自我回答,但您的问题过于宽泛,缺少minimal, complete, and reproducible code example,并且缺少正式声明和任何具体问题的详细信息。它更像是一个代码编写服务请求,这是 Stackoverflow 所没有的,而且非常离题。
  • 这是一个中肯的评论。如果这不是一个自我回答,我会重写。但是,由于我的答案中的代码提供了我想要实现的全部细节,我觉得它可以保持原样。我什至发布的唯一原因是看看是否有人可以改进我的解决方案。
  • 如果你正在编写代码并且你正在寻找更多的代码审查,那么我建议codereview.stackexchange.com
  • 谢谢。我不知道 codereview.stack... 存在。
  • 我已经修正了问题和答案,以更好地匹配所提供的指南。感谢您的建设性反馈。

标签: reactjs typescript react-router apollo apollo-client


【解决方案1】:

这个问题的答案是使用来自 Apollo 的 LazyQuery,这是一个实际上可以用于回调(包括钩子等)的查询。

我们正在为 GraphQL 使用 Apollo code-gen,在深入研究生成的代码后,我确实注意到它同时生成了 getCurrentUserQuery 和 getCurrentUserLazyQuery。

另外,虽然这个解决方案确实传递了一个 prop,但它是一个不可变的使用,它只会深入一层,并且不会再进一步​​传递,所以我觉得这是一个不错的折衷方案,因为我可以调用其他地方当前用户的 Apollo 查询,当我需要它时。

因此,我去掉了整个 AuthRoute / ProtectedRoute 组件,只是做了如下(仅相关代码):

// In App.tsx

// other code is around the code contained here... this is just a snippet

const [getUser, { data }] = useGetCurrentUserLazyQuery({})

  let location = useLocation()

  useAsyncEffect(     // Also works with useEffect, but Async cuts down on calls
    async isMounted => {
      await getUser()
      if (!isMounted()) return
    },
    [location]
  )
  
  return (
    <div className="App">
      <AppRoutes email={data?.getCurrentUser?.email} />
    </div>
  )
  
  
  
  // In AppRoutes.tsx
  // other code is around the code contained here... this is just a snippet
  // email prop type declaration is ... email: string | undefined
  const { email } = props
  const isLoggedIn = !!(email ?? false)
  
  return (
      <Switch>
        <Route path={ROUTES.LOGIN}>
          {isLoggedIn ? <Redirect to={ROUTES.HOME} /> : <Login />}
        </Route>
        <Route path={ROUTES.HOME}>
          {!isLoggedIn ? (
            <Redirect to={ROUTES.LOGIN} />
          ) : (
            <Redirect to={DEFAULT_PATH} />
          )}
        </Route>
        )

【讨论】:

    猜你喜欢
    • 2018-08-01
    • 2020-08-30
    • 2020-01-28
    • 1970-01-01
    • 2020-11-24
    • 1970-01-01
    • 2022-01-24
    • 2018-09-07
    • 1970-01-01
    相关资源
    最近更新 更多