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を使用することでユーザーをルーティングさせ、アクセスを制限することが必要となります。
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
import React from "react"
export default function Auth() {
return (
<div>認証ページ</div>
)
}
以上のコーディングで、 localhost:8000/app
にアクセスすると、認証ページ、と記述されたページを出すことができます。
またGatsbyではビルドがNode.jsで実行される関係でビルド時にlocalStorage
やwindow
を使うことができません。しかし、外部認証サービスなどの中にはlocalStorageやwindowといったものにアクセスするものもあります。
なので、ビルド中に不具合を起こさないため、該当コードをラッピングする必要があります。
import app from "firebase/app"
if (typeof window !== 'undefined'){
app.initializeApp(config)
}
onCreatePage
gatsby-node.jsを編集して、/app/が制限された区画であることを定義して、必要に応じてページを作成するようにします。
onCreatePage
は、全てのページが作成された後に呼ばれます。
matchPath
で指定された部分は、build時に生成しないようになります。
exports.onCreatePage = async({ page, actions }) => {
const { createPage } = actions
if(page.path.match(/^\/app/)){
page.matchPath = "/app/*"
createPage(page)
}
}
実例
実際に、仮の認証システムをjs上で用意して、認証機能をつけてみます。
下記src/service/auth.jsで実装する機能は、本来ならばfirebaseなどが受け持ちます。
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を下記のようにします。
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となっています。
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
へ、ログインしていれば該当ページへ飛ぶ、といった制限付きのルーティングが実現します。
ログインページは下記のように実装しました。
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に飛ぶことができます。
認証後ページは、下記のようにログアウト機能もいれました。
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は見ることができません。