2021年2月22日
プログラミング
Hasura Cloud × Auth0 × React でお手軽にTodoアプリを作ってみた!
はじめに
Hasura Cloud はフルスタックフレームワークである Hasura を GUI で操作できるツールです。
API が簡単に実装でき、Auth0 と一緒に使用することでユーザー認証も実装できます。
今回は、Hasura Cloud と Auth0 と React を使ってデモアプリを作ってみたので、この実装について解説していきます!
デモアプリ : https://dscc4er6cxbk4.cloudfront.net/
対象読者
・なるべくコードを書かずバックエンドを実装したいフロントエンドエンジニアの方
この記事で主に説明するもの
・Hasura Cloud を使用した API の実装
・Auth0 を使用したユーザー認証の実装
・React と上記2つとの連携
この記事で説明を省いているもの
・Hasura 公式チュートリアルで完結できる実装
(かなり分かりやすいのでご一読されることをオススメします!)
・React / GraphQL / Apollo の概要
1 : Hasura Cloud のプロジェクト設定
1-1 : 基本情報設定
・https://hasura.io/cloud/ にアクセスして「START FREE」をクリック
・「New Project」をクリック
・リージョン(大体オハイオ)やプロジェクト名を設定
1-2 : データベース設定 (Heroku)
・「Try with Heroku」をクリック
ログインすると自動でデータベースを作成し接続まで行われます
注意事項
・Free and Hobby プランの方は、アプリ上限に気をつけましょう!(1敗)
5つ以上は作成することができません
1-2 : データベース設定 (RDS)
・チュートリアルをご参照ください
Postgres : https://hasura.io/docs/1.0/graphql/cloud/getting-started/cloud-databases/aws-postgres.html
Aurora : https://hasura.io/docs/1.0/graphql/cloud/getting-started/cloud-databases/aws-aurora.html
注意事項
・パスワード自動生成した方は、DB作成中に表示される「認証情報の詳細の表示」を必ず確認しましょう!(1敗)
確認を忘れると AWS Console から再設定できません
・Admin Secret は適当で良いので必ず設定しましょう!(2敗)
何も設定しないと Auth0 との連携で使用する環境変数が設定できません
2 : Hasura Cloud の API 設定
2-1 : テーブル作成
・該当プロジェクトの「Launch Console」をクリック
・ページ上部の「DATA」「Create Table」をクリック
・下記のように users テーブルを作成
Table Name :
users
Columns :
id – Text
name – Text
created_at – Timestamp – now()
Primary Key :
id
・下記のように todos テーブルを作成
Table Name :
todos
Columns :
id – Integer (auto-increment)
title – Text
content – Text – – Nullable
is_done – Boolean – false
created_at – Timestamp – now()
user_id – Text
Primary Key :
id
2-2 : パーミッション設定
・ページ左部の「todos」「Permissions」をクリック
・下記のように user ロールを作成
Role :
user
・下記のように Insert を編集
Row insert permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}
Column select permissions :
title / content
Column presets :
user_id – from session variable – X-Hasura-User-Id
・下記のように Select を編集
Row select permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}
Column select permissions :
All
・下記のように Update を編集
Row update permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}
Column select permissions :
is_done / title / content
・下記のように Delete を編集
Row delete permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}
注意事項
・insert の Column presets は必ず設定しましょう!(2敗)
認証情報から user_id を設定してくれる便利な機能です
3 : Auth0 のアプリ設定
・チュートリアルをご参照ください
https://hasura.io/learn/graphql/hasura/authentication/
3-1 : Create Auth0 App
・Aut0 アプリを作成
・JWTアクセストークンを作成するために Auth0 API を作成
3-2 : Rules for Custom JWT Claims
・ユーザーが Hasura に対して何ができるかできないかを決定するために認証ルールを設定
3-3 : Connect Hasura with Auth0
・Auth0 の公開鍵を Hasura に設定してお互いを接続
3-4 : Sync Users with Rules
・ログインごとに呼び出すことができるクエリを使用しユーザーデータを設定
// rule-test.js
function (user, context, callback) {
const userId = user.user_id;
const nickname = user.nickname;
const admin_secret = "ここに Hasura の admin secret";
const url = "ここに Hasura の GraphQL API のエンドポイント";
const query = `mutation($userId: String!, $nickname: String) {
insert_users(objects: [{
id: $userId, name: $nickname
}], on_conflict: {constraint: users_pkey, update_columns: [name]}
) {
affected_rows
}
}`;
const variables = { "userId": userId, "nickname": nickname };
request.post({
url: url,
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': admin_secret},
body: JSON.stringify({
query: query,
variables: variables
})
}, function(error, response, body){
console.log(body);
callback(null, user, context);
});
}
注意事項
・コード5,6行目の admin_secret
と url
は Hasura で設定されているものに変更しましょう!
・コード10行目の [last_seen, name]
は [name]
に変更しましょう!(1敗)
last_seenはチュートリアル用アプリのカラムなので、そのままにしておくとエラーが発生します
3-5 : Test with Auth0 Token
・上記の設定が問題がないか検証
問題なければ users テーブルににテストしたユーザーが追加されます
注意事項
・アプリ設定の Allowed Callback URLs に下記URLを追加しましょう!(1敗)
デバッガーのコールバックURL
開発用のローカルURL
本番URL
・アプリ設定の Allowed Logout URLs / Allowed Web Origins / Allowed Origins (CORS) に下記URLを追加しましょう!
開発用のローカルURL
本番URL
・表示されたアクセストークンは「4 : React でUI作成」で使うのでメモしておきましょう
4 : React でUI作成
4-1 : プロジェクトディレクトリを作成
yarn create react-app my-app --template typescript
4-2 : モジュールのインストール (Auth0 / GraphQL / Apollo など)
yarn add @apollo/react-hooks @auth0/auth0-react apollo-cache-inmemory apollo-client apollo-link-http apollo-link-ws graphql graphql-tag subscriptions-transport-ws node-sass@4.14.1 react-icons
yarn add -D @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
4-3 : Auth0 と接続
・Auth0Provider
でアプリ全体を囲む
// src/App.tsx
import React from 'react';
import { Auth0Provider } from '@auth0/auth0-react';
import './App.scss';
const App = () => {
return (
<auth0provider
domain="ここに Auth0 アプリの domain"
clientid="ここに Auth0 アプリの clientID"
redirecturi="{window.location.origin}">
</auth0provider>
);
};
export default App;
・useAuth0
という Auth0 の hooks を使用してログインボタンとログアウトボタンを作成
// src/components/Auth/Login.tsx
import React, { FC } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
export const Login: FC = () => {
const { loginWithPopup } = useAuth0();
return (
<button classname="auth-button" onclick="{()" ==""> loginWithPopup()}>Log in</button>
);
};
// src/components/Auth/Logout.tsx
import React, { FC } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
export const Logout: FC = () => {
const { logout } = useAuth0();
return (
<button classname="auth-button" onclick="{()" ==""> logout()}>Log out</button>
);
};
4-4 : Auth0 からの認証情報を受け取り Hasura Cloud の API と接続
// src/components/Apollo/WithApolloProvider.tsx
import React, { FC, useEffect, useState} from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { ApolloProvider } from '@apollo/react-hooks';
import { Login, Logout } from '../Auth';
const createApolloClient = (authToken: string) => {
return new ApolloClient({
link: new WebSocketLink({
uri: "ここに GraphQL API のエンドポイント(https を wss に変更)",
options: {
reconnect: true,
connectionParams: {
headers: {
Authorization: `Bearer ${authToken}`
}
}
}
}),
cache: new InMemoryCache(),
});
};
export const WithApolloProvider: FC = ({ children }) => {
const [client, setClient] = useState<any|null>(null);
const { isAuthenticated, getAccessTokenSilently } = useAuth0(); // getIdTokenClaims</any|null>
const fetchIdTokenClaims = async () => {
const authToken = await getAccessTokenSilently();
const newApolloClient = createApolloClient(authToken);
setClient(newApolloClient);
};
useEffect(() => {
if (isAuthenticated) fetchIdTokenClaims();
}, [isAuthenticated]);
if (!isAuthenticated) {
return (
<div classname="apollo">
<login>
</login></div>
);
};
if (!client) {
return (
<div classname="apollo">
<logout>
</logout></div>
);
};
return (
<apolloprovider client="{client}">
{children}
<logout>
</logout></apolloprovider>
)
};
// src/App.tsx
import React from 'react';
import { Auth0Provider } from '@auth0/auth0-react';
import { WithApolloProvider } from './components/Apollo';
import './App.scss';
const App = () => {
return (
<auth0provider
domain="ここに Auth0 アプリのdomain"
clientid="ここに Auth0 アプリの clientID"
redirecturi="{window.location.origin}">
<withapolloprovider>
</withapolloprovider>
</auth0provider>
);
};
export default App;
注意事項
・GraphQL の subscription を使用するので WebSocketLink
で接続します
4-5 : Hasura Cloud の API から型定義ファイルを生成
・codegen の設定ファイルを作成
// codegen.js
module.exports = {
schema: [
{
"ここに Hasura の GraphQL API エンドポイント": {
headers: {
Authorization: 'Bearer ' + process.env.AUTH_TOKEN,
},
},
},
],
documents: ['./src/**/*.graphql', './src/**/*.tsx', './src/**/*.ts'],
overwrite: true,
generates: {
'./src/graphql/types.tsx': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
skipTypename: false,
withHooks: true,
withHOC: false,
withComponent: false,
},
},
'./graphql.schema.json': {
plugins: ['introspection'],
},
},
};
・package.json の scripts を編集
// package.json
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"generate": "graphql-codegen --config codegen.js"
}
・Hasura Cloud の graphiql を参考に API を定義
// src/graphql/mutations.graphql
mutation insertTodo($title: String!, $content: String) {
insert_todos(objects: {title: $title, content: $content}) {
affected_rows
}
}
mutation updateTodoIsDone($id: Int!, $is_done: Boolean) {
update_todos(where: {id: {_eq: $id}}, _set: {is_done: $is_done}) {
affected_rows
}
}
mutation deleteTodo($id: Int) {
delete_todos(where: {id: {_eq: $id}}) {
affected_rows
}
}
// src/graphql/subscriptions.graphql
subscription fetchNewTodos {
todos(order_by: {created_at: desc}) {
content
created_at
id
is_done
title
user_id
}
}
・コマンドを実行
AUTH_TOKEN=ここにAuth0のバリデーションで表示されたアクセストークン yarn generate --watch
注意事項
・一部だけでも良いので garaphiql を参考にAPIを定義しておきましょう!(1敗)
コードが存在しないとエラーになり型定義ファイルが生成されません
・AUTH_TOKENはJWTによって期限が設定されています!
テキスト
// src/components/Todo/TodoList.tsx
import React, { FC, useState } from 'react';
import { Todos, useInsertTodoMutation, useUpdateTodoIsDoneMutation, useDeleteTodoMutation, useFetchNewTodosSubscription } from '../../graphql/types';
import { RiAddLine, RiCheckLine, RiDeleteBinLine } from "react-icons/ri";
export const TodoList: FC = () => {
const [title, setTitle] = useState<string>('');
const [content, setContent] = useState<string>('');</string></string>
const { loading, error, data } = useFetchNewTodosSubscription();
const [insertTodo] = useInsertTodoMutation();
const [updateTodoIsDone] = useUpdateTodoIsDoneMutation();
const [deleteTodo] = useDeleteTodoMutation();
const onSubmitInsert = (eve: React.FormEvent<htmlformelement>) => {
eve.preventDefault();
if (!title) return;
insertTodo({ variables: { title, content } });
setTitle('');
setContent('');
};
const onClickUpdate = (todo: Todos) => {
updateTodoIsDone({ variables: { id: todo.id, is_done: !todo.is_done } });
};</htmlformelement>
const onClickDelete = (id: number) => {
deleteTodo({ variables: { id } });
};
if (loading) {
return (
<div classname="todo">
<div>Loading...</div>
</div>
);
};
if (error) {
return (
<div classname="todo">
<div>Error...</div>
</div>
);
};
return (
<div classname="todo">
<form classname="todo-form" onsubmit="{(eve)" ==""> onSubmitInsert(eve)}>
<input type="text" value="{title}" placeholder="Enter the title" onchange="{(eve)" ==""> setTitle(eve.target.value)} />
<input type="text" value="{content}" placeholder="Enter the content" onchange="{(eve)" ==""> setContent(eve.target.value)} />
<input type="submit" value="">
<span><riaddline></riaddline></span>
</form>
<ul classname="todo-list">
{data?.todos.map((todo) => (
<li classname="todo-item" key="{todo.id}">
<div classname="todo-item__head">
<div classname="{`todo-item__head__check" ${todo.is_done="" &&="" 'is-done'}`}="" onclick="{()" ==""> onClickUpdate(todo)}><richeckline></richeckline></div>
</div>
<div classname="todo-item__body">
<input type="text" name="title" classname="todo-item__body__title" defaultvalue="{todo.title}">
{todo?.content && <input type="text" name="content" classname="todo-item__body__content" defaultvalue="{todo.content}">}</div>
<div classname="todo-item__foot">
<button onclick="{()" ==""> onClickDelete(todo.id)}><rideletebinline></rideletebinline></button></div></li>
))}</ul>
</div>
);
};
// src/App.tsx
import React from 'react';
import { Auth0Provider } from '@auth0/auth0-react';
import { WithApolloProvider } from './components/Apollo';
import { TodoList } from './components/Todo';
import './App.scss';
const App = () => {
return (
<auth0provider
domain="ここに Auth0 アプリの domain"
clientid="ここに Auth0 アプリの clientID"
redirecturi="{window.location.origin}">
<withapolloprovider>
<todolist>
</todolist></withapolloprovider>
</auth0provider>
);
};
export default App;
これでアプリの完成です!
注意事項
・リアルタイムチャットアプリでない限り subscription でfetchすることは好ましくありません!
1つの例として実装しています
おわりに
Hasura Cloud と Auth0 と React を組み合わせることでバックエンドのコードを最小限にアプリを作成することができました!
もし、このアーキテクチャに魅力を感じていただけたら、試していただけると嬉しいです!
また、Hasura には doccker イメージがあるので次回はその解説ができたら良いなと考えています!
ご覧いただきありがとうございました!
Reactのお仕事に関するご相談
Bageleeの運営会社、palanではReactに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。
この記事は
参考になりましたか?
0
2
関連記事
2022年5月13日
Reactでオセロゲームを作る
2021年4月9日
Slack OAuth x React で Slack ユーザー認証を作ってみた!
2021年3月1日
Reactで使えるバリデーションライブラリを紹介!
2021年2月22日
Hasura Cloud × Auth0 × React でお手軽にTodoアプリを作ってみた!
2020年12月12日
Blitz.jsとTailwind CSSでメモ帳アプリの作成【第2弾】
2020年12月8日
コンポーネントを実装するときに意識すること
簡単に自分で作れるWebAR
「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。
palanARへpalanでは一緒に働く仲間を募集しています
正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。
運営メンバー
Eishi Saito 総務
SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。
sasakki デザイナー
アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。
やまかわたかし デザイナー
フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。
Sayaka Osanai デザイナー
Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。
はらた エンジニア
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
こぼり ともろう エンジニア
サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。
ささい エンジニア
フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。
Damien
WebAR/VRの企画・開発をやっています。森に住んでいます。
ゲスト bagelee
かっきー
まりな
suzuki
miyagi
ogawa
雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。
いわもと
デザイナーをしています。 好きな食べ物はラーメンです。
taishi kobari
フロントエンドの開発を主に担当してます。Blitz.js好きです。
kubota shogo
サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!
nishi tomoya
aihara
グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。
nagao
SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。
Kainuma
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
sugimoto
asama
ando
iwasawa ayane
oshimo
異業界からやってきたデザイナー。 palanARのUIをメインに担当してます。 これからたくさん吸収していきます!