iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0

有時候,我們在做專案時,總會有會員的需求,大概上就是指有些內容只有登入過的使用者能夠瀏覽,今天我們就要來實作這樣的流程。

在 Gatsby 中,我們透過 API 的方式去與後端交換資訊,例如說註冊、登入、忘記密碼等等,今天的文章主要是實作一個流程,針對 API 部分不會有太多著墨,不過有蠻多相關的資源可以協助我們做身份驗證,例如 Auth0、Firebase、Passport.js 等等,筆者會在參考資料中附上這些服務的相關資訊,有興趣的讀者可以在自行閱讀。

本篇文章預設的模板是 gatsby-starter-hello-world

我們就用以下指令快速起一個專案吧!

gatsby new ithelp-auth-tutorial gatsbyjs/gatsby-starter-hello-world

建立完專案後,就讓我們開始今天的章節吧!

建置畫面

因為目前的模板只有提供我們簡易的 Hello World 首頁,我們必須先快速的建立幾個頁面來應付之後的需求。

首先,我們就先建立 NavBar 元件吧!

我們在 src 目錄下建立一個 components 資料夾,並在裡面新增一支 NavBar.js,我們會再檔案中放入一下程式碼,程式碼中我們快速了建立幾個連到首頁的 NavItem,之後我們會再來微調導轉連結。

import React from "react"
import { Link } from "gatsby"

export default () => (
  <div
    style={{
      display: "flex",
      flex: "1",
      justifyContent: "space-between",
      borderBottom: "1px solid #d1c1e0",
    }}
  >
    <span>You are not logged in</span>

    <nav>
      <Link to="/">Home</Link>
      {` `}
      <Link to="/">Profile</Link>
      {` `}
      <Link to="/">Logout</Link>
    </nav>
  </div>
)

完成 NavBar 後,我們來做一個通用的 Layout,所以我們繼續在 Components 目錄下新增一支 Layout.js,裡面的程式碼會放入以下程式碼,非常單純的結構,只是將 NavBar 引入後,放到頁面中,並與 Props.children 一起顯示在畫面上。

import React from "react"
import NavBar from "./nav-bar"

const Layout = ({ children }) => (
  <>
    <NavBar />
    {children}
  </>
)

export default Layout

完成 Layout 元件後,我們改寫一下 pages 目錄下的 index.js,調整的內容只是將我們剛剛完成 Layout 元件做引入,並放置 JSX 中即可

import React from "react"

import Layout from "../components/layout"

export default () => (
  <Layout>
    <h1>Hello world!</h1>
  </Layout>
)

完成上述步驟後,我們開啟開發伺服器 ( gatsby develop ) 來看看吧!

應該會有一個抖擻的 Hello World 與顯示目前狀態與三個連結的 NavBar。

https://ithelp.ithome.com.tw/upload/images/20201010/20109495qQDDFypwLy.png

實作認證

再完成畫面後,我們要來做一些跟會員相關的基礎功能,例如判斷是否已經有會員資訊,或者用我們 hard code 寫入的帳號密碼來判斷是否成功登入等等,所以我們先在 src 下創建一個 services 目錄,裡面新增一支 auth.js 。

我們在這支檔案中會做幾個功能

  1. 登入
  2. 登出
  3. 判斷是否已經登入
  4. 取得用戶資訊
  5. 設定用戶資訊

詳細程式碼與註解如下,讀者可以直接將以下程式碼貼入到 Auth.js 當中。

// 判斷是否藉由瀏覽器瀏覽
export const isBrowser = () => typeof window !== "undefined"

// 從 LocalStorage 中取得使用者資訊,有的話就 Parse 這筆資訊。
export const getUser = () =>
  isBrowser() && window.localStorage.getItem("gatsbyUser")
    ? JSON.parse(window.localStorage.getItem("gatsbyUser"))
    : {}

// 用來登入成功後,設定資訊在 LocalStorage 中。
const setUser = user =>
  window.localStorage.setItem("gatsbyUser", JSON.stringify(user))


// 處理登入,這邊使用 Hard Code 的方式驗證,此方式極度不安全,只是為了示範登入流程才用此方式,正式環境上會打 API 去跟後端確認資料是否正確,並等回傳結果後才會進行下一步
export const handleLogin = ({ username, password }) => {
  if (username === `ithelp` && password === `123456`) {
    return setUser({
      username: `reynold`,
      name: `Reynold`,
      email: `reynold@example.org`,
    })
  }

  return false
}

// 判斷使用者是否已經登入,如以登入就直接從 LocalStorage 中撈取使用者資料
export const isLoggedIn = () => {
  const user = getUser()

  return !!user.username
}

// 登出,直接將使用者資料清空
export const logout = callback => {
  setUser({})
  callback()
}

創建私人頁面

功能完成後,我們要來建立私人路由,也就是指登入過後的使用者才能瀏覽的頁面,我們同樣會用 createPage API 來動態創建頁面,我們會在 Gatsby-node.js 設定所有 APP 開頭的頁面都需要權限才可瀏覽。

所以我們會在 Gatsby-node.js 放置以下程式碼

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

這邊的邏輯,Gatsby 也有相關套件已經幫我們處理,詳細請參考 gatsby-plugin-create-client-paths

現在我們要來創建一個通用的頁面,裡面會用來放需要權限的頁面,
我們在 src 的 pages 目錄中建立一支 app.js,而裡面的內容是

import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/Profile"
import Login from "../components/Login"

const App = () => (
  <Layout>
    <Router>
      <Profile path="/app/profile" />
      <Login path="/app/login" />
    </Router>
  </Layout>
)

export default App

我們引入待會會製作的登入與個人資料頁,而聰明的讀者們應該都猜得到,個人資料頁就是需要權限的頁面,而登入頁就會拿來用於取得權限,接著我們來製作個人資料頁,個人資料頁的程式碼會如下方一般,非常單純,只是做資料的呈現。

import React from "react"

const Profile = () => (
  <>
    <h1>我的個人簡介</h1>
    <ul>
      <li>名稱: Your name will appear here</li>
      <li>信箱: And here goes the mail</li>
    </ul>
  </>
)

export default Profile

再來,我們要製作登入頁,裡面邏輯會稍微複雜,會做一些表單的處理,並引入我們先前做好的函式來使用,最後使用我們引入的 navigate 來協助使用者導轉到正確的頁面。

import React, { useState } from "react"
import { navigate } from "gatsby"
import { handleLogin, isLoggedIn } from "../services/auth"

const Login = () => {
  const [userAccount, setUserAccount] = useState({
    username: '',
    password: ''
  });

  const handleUpdate = event => {
    setUserAccount({...userAccount, [event.target.name]: event.target.value})
  }

  const handleSubmit = event => {
    event.preventDefault()
    handleLogin(userAccount)
  }

  if (isLoggedIn()) {
    navigate(`/app/profile`)
  }
  
  return (
    <>
      <h1>登入</h1>
      <form
        method="post"
        onSubmit={event => {
          handleSubmit(event)
          navigate(`/app/profile`)
        }}
      >
        <label>
          Username
          <input type="text" name="username" onChange={handleUpdate} />
        </label>
        <label>
          Password
          <input
            type="password"
            name="password"
            onChange={handleUpdate}
          />
        </label>
        <input type="submit" value="登入" />
      </form>
    </>
  )
}

export default Login

完成登入頁面後,我們可以重啟開發伺服器看看,此時應該 /app/login 與 /app/profile 都可以暢行無阻的瀏覽,所以接下來我們要製作一個 HOC ( High Order Component ) ,HOC 簡單來說是一個函數,我們可以將元件作為參數代入,經由處理後,會回傳一個新的元件,而使用 HOC 的目的在於我們將通用的邏輯都放在其中,使程式碼更容易維護且更簡潔 。

所以我們在 src/components 目錄下新增一支 privateRoute.js ,並在裡面放入以下程式碼,我們在程式碼中做了是否登入的檢查,如果沒登入就導轉回 Login 頁面

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

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

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

export default PrivateRoute

若對 HOC 有興趣的讀者,可以閱讀此篇文章 React.js: Higher-Order Components

完成 PrivateRoute 後,我們回到 app.js 去將原本的 Profile 元件更改為我們剛剛製作好的 PrivateRoute,並把連結跟顯示的元件用 Props 的方式傳給 HOC 作處理,所以程式碼會調整成以下

import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import PrivateRoute from "../components/privateRoute"
import Profile from "../components/Profile"
import Login from "../components/Login"

const App = () => (
  <Layout>
    <Router>
      <PrivateRoute path="/app/profile" component={Profile} />
      <Login path="/app/login" />
    </Router>
  </Layout>
)

export default App

還記得我們之前先寫好的 NavBar 嗎?
在相關頁面與邏輯都處理的差不多了,我們現在要回頭去稍作調整,
首先我們打開 components 目錄下的 NavBar.js,
並把我們做好的登出函示放到對應的位置上,還有判斷如果會員已經登入,就顯示對應的資料,並把登出按鈕做隱藏。

import React from 'react'
import { Link, navigate } from "gatsby"
import { getUser, isLoggedIn, logout } from "../services/auth"

const NavBar = () => {
  const content = { message: "", login: true }
  if (isLoggedIn()) {
    content.message = `歡迎回來, ${getUser().name}`
  } else {
    content.message = "還沒登入唷。"
  }

  return (
    <div
      style={{
        display: "flex",
        flex: "1",
        justifyContent: "space-between",
        borderBottom: "1px solid #d1c1e0",
      }}
    >
      <span>{content.message}</span>

      <nav>
        <Link to="/">Home</Link>
        {` `}
        <Link to="/app/profile">Profile</Link>
        {` `}
        {isLoggedIn() ? (
          <a
            href="/"
            onClick={event => {
              event.preventDefault()
              logout(() => navigate(`/app/login`))
            }}
          >
            Logout
          </a>
        ) : null}
      </nav>
    </div>
  )
}

export default NavBar

再來,我們更改首頁,同樣的判斷使用者是否已經登入,並且顯示對應的資訊

import React from "react"
import { Link } from "gatsby"
import { getUser, isLoggedIn } from "../services/auth"
import Layout from "../components/Layout"

export default () => (
  <Layout>
    <h1>嗨, {isLoggedIn() ? getUser().name : "你還沒註冊唷"}!</h1>
    <p>
      {isLoggedIn() ? (
        <>
          你已經是會員,請確認你的{" "}
          <Link to="/app/profile">個人資訊</Link>
        </>
      ) : (
        <>
          你應該先進行<Link to="/app/login">登入</Link>來閱讀更多的內容。
        </>
      )}
    </p>
  </Layout>
)

再堅持一下,最後一步,我們修改 Profile 頁面,裡面會顯示會員的資訊

import React from "react"
import { getUser } from "../services/auth"

const Profile = () => (
  <>
    <h1>我的個人簡介</h1>
    <ul>
      <li>名稱: {getUser().name}</li>
      <li>信箱: {getUser().email}</li>
    </ul>
  </>
)

export default Profile

完成後,各位讀者可以試著玩玩看,輸入帳號密碼後,是否能正確登入,
對應的頁面有沒有顯示正確的資料,如果都正常的話,
恭喜!各位讀者已經擁有一個可以登入並且限制權限頁面的網站囉!

參考資料

Gatsby - Making a Site with User Authentication


上一篇
[Day 24] - 動態導覽列 ( Dynamic Navigation )
下一篇
[Day 26] - Gatsby feat. WordPress
系列文
雖然你不是木村拓哉,但它也可以讓你變很行 - Gatsby.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言