華麗なるGatsby.jsの実践(認証機能をつけてみよう)

Gatsby.jsに認証機能などの動的な機能をつけるにはどうすればいいのだろう？
と思って、公式を参考にしつつ認証機能のサンプルコードを実装してみました。
なお、あくまでサンプルですので、パスやユーザーはgatsby.js内にハードコーディングされています。

リポジトリは以下になります。
https://github.com/takanokana/gatsby-practice

【参考公式ページ】
https://www.gatsbyjs.org/docs/react-hydration
https://www.gatsbyjs.org/docs/adding-app-and-website-functionality/
https://www.gatsbyjs.org/docs/client-only-routes-and-user-authentication/#implementing-client-only-routes

## React Hydration
Gatsbyは、HTMLを静的に生成する静的サイトジェネレーターとしての機能、それに加えて、
生成したHTMLを、 `React  hydration`を通してクライアントサイドで拡張し、アプリのような振る舞いを持たせます。

上記の機能とGatsbyに付属している@reach/routerを使用し、client only routes,つまり静的ページとしては吐き出さないページを作ることができます。

認証機能においては、Gatsbyが生成した静的HTMLはファイルサーバ上にあるので、制御が不可能です。（ユーザーが直接URLを入力するとアクセスできてしまう)
なので、client only routesを使用することでユーザーをルーティングさせ、アクセスを制限することが必要となります。



```react:src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Auth from "../components/Auth"
const App = () => {
  return(
    <div>
      <Router basepath="/app">
        <Auth path="/" />
      </Router>
    </div>
  )
}

export default App
```


```react:src/pages/Auth.js
import React from "react"

export default function Auth() {
  return (
    <div>認証ページ</div>
  )
}
```

以上のコーディングで、 `localhost:8000/app`にアクセスすると、認証ページ、と記述されたページを出すことができます。

またGatsbyではビルドがNode.jsで実行される関係でビルド時に`localStorage`や`window`を使うことができません。しかし、外部認証サービスなどの中にはlocalStorageやwindowといったものにアクセスするものもあります。

なので、ビルド中に不具合を起こさないため、該当コードをラッピングする必要があります。

```react
import app from "firebase/app"
if (typeof window !== 'undefined'){
  app.initializeApp(config)
 }
```

## onCreatePage
gatsby-node.jsを編集して、/app/が制限された区画であることを定義して、必要に応じてページを作成するようにします。
`onCreatePage`は、全てのページが作成された後に呼ばれます。
`matchPath`で指定された部分は、build時に生成しないようになります。

```react:gatsby-node.js
exports.onCreatePage = async({ page, actions }) => {
  const { createPage } = actions
  
  if(page.path.match(/^\/app/)){
    page.matchPath = "/app/*"
    createPage(page)
  }
}
```


## 実例
実際に、仮の認証システムをjs上で用意して、認証機能をつけてみます。
下記src/service/auth.jsで実装する機能は、本来ならばfirebaseなどが受け持ちます。

```src/service/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("gatsbyUsr", JSON.stringify(user))
 
export const handleLogin = ({ username, password }) => {
 if (username === `join` && password === `pass`){
   return setUser({
 		 username: `join`,
 		 name: `Johnny`,
 		 email: `johnny@example.com`
 	})
 }
 return false
}

export const isLoggedIn = () => {
  const user = getUser()
  
  return !!user.username
}
export const logout = callback => {
  setUser({})
  callback()
}
```
app.jsを下記のようにします。

```react:app.js
import React from "react"
import { Router } from "@reach/router"
import Auth from "../components/Auth"
import PrivateRoute from "../components/PrivateRoute"
import Secret from "../components/Secret"


const App = () => {
  return(
    <div>
      <Router basepath="/app">
        <PrivateRoute path="/secret" component={Secret} />
        <Auth path="/login" />
      </Router>
    </div>
  )
}

export default App
```

PrivateRouteは下記のようなHOCとなっています。

```react:src/components/PrivateRoute
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../service/auth"

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

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

export default PrivateRoute
```


`navigate` (https://www.gatsbyjs.org/docs/gatsby-link/) ですが、
送信後、サンクスページに移動するといった用途に使用できます。stateを渡すことなども可能です。
PrivateRouteをかますことで、ログインしていなければ `/app/login`へ、ログインしていれば該当ページへ飛ぶ、といった制限付きのルーティングが実現します。

ログインページは下記のように実装しました。

```react:src/components/Auth.js
import React, { Component } from "react"
import { handleLogin, isLoggedIn} from "../service/auth"
import { navigate, Link } from "gatsby"

export default class Auth extends Component {
  state = {
    username: ``,
    password: ``
  }


## 実際のページ

  handleUpdate(event) {
    this.setState({
      [event.target.name]: event.target.value
    })
  }
  handleSubmit(event) {
    event.preventDefault()
    handleLogin(this.state)
    navigate(`/app/secret`)
  }
  render() {
    return (
      <div>
        認証ページ
        {isLoggedIn() ?
        <Link
          to="/app/secret"
        >認証後ページへ</Link>
        :
        <>
          <dl>
            <dt>名前</dt>
            <dd>
              <input
                name="username"
                onChange={e => this.handleUpdate(e)}
              ></input>
            </dd>
          </dl>
          <dl>
            <dt>パスワード</dt>
            <dd>
              <input
                name="password"
                onChange={e => this.handleUpdate(e)}
              />
            </dd>
          </dl>
          <button
          type="submit"
          onClick={e => this.handleSubmit(e)}
        >送信</button>
        </>
        }
      </div>
    )
  }
}

```
上記により、名前とパスワードが正しい状態でログインボタンを押すと、（ここではhandleLoginで判定されている、 john/pass)認証後ページであるSecret.jsに飛ぶことができます。

認証後ページは、下記のようにログアウト機能もいれました。

```react:Secret.js
import React from "react"
import { logout } from "../service/auth"
import { navigate } from "gatsby"
export default function Auth() {
  const logoutHandler = () => {
    navigate('/')
    return
  }
  return (
    <div>認証後ページ　🎉

    <button
      type="button"
      onClick={e => logout(logoutHandler)}
    >ログアウトする</button>
    </div>
  )
}

```

## 実際の挙動
このようになります。
たとえ直接 /app/secret　と打ち込んでも、ログインされていなければsecretは見ることができません。
![ezgif-6-a9de48eb2daa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/88ca685a-2377-f9cb-3800-a03b12502076.gif)