2021年2月22日

プログラミング

Hasura Cloud × Auth0 × React でお手軽にTodoアプリを作ってみた!

目次

  1. はじめに
  2. 1 : Hasura Cloud のプロジェクト設定
  3. 2 : Hasura Cloud の API 設定
  4. 3 : Auth0 のアプリ設定
  5. 4 : React でUI作成
  6. 終わりに

はじめに

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」をクリック

・リージョン(大体オハイオ)やプロジェクト名を設定

Frame 11.png (79.9 kB)

1-2 : データベース設定 (Heroku)

・「Try with Heroku」をクリック
ログインすると自動でデータベースを作成し接続まで行われます

Frame 13.png (74.0 kB)

注意事項

・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」をクリック

Frame 2.png (79.6 kB)

・下記のように users テーブルを作成

Table Name :
users

Columns :
id – Text
name – Text
created_at – Timestamp – now()

Primary Key :
id

Frame 3.png (82.7 kB)

・下記のように 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

Frame 4.png (85.6 kB)

2-2 : パーミッション設定

・ページ左部の「todos」「Permissions」をクリック

・下記のように user ロールを作成

Role :
user

Frame 6.png (62.7 kB)

・下記のように 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

Frame 7.png (105.5 kB)

・下記のように Select を編集

Row select permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}

Column select permissions :
All

Frame 8.png (92.4 kB)

・下記のように Update を編集

Row update permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}

Column select permissions :
is_done / title / content

Frame 9.png (110.5 kB)

・下記のように Delete を編集

Row delete permissions :
{"user_id":{"_eq":"X-Hasura-User-Id"}}

Frame 10.png (96.0 kB)

注意事項

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_secreturl は 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) =&gt; {
  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 }) =&gt; {
  const [client, setClient] = useState<any|null>(null);
  const { isAuthenticated, getAccessTokenSilently } = useAuth0(); // getIdTokenClaims</any|null>

  const fetchIdTokenClaims = async () =&gt; {
  const authToken = await getAccessTokenSilently();
  const newApolloClient = createApolloClient(authToken);
  setClient(newApolloClient);
};

  useEffect(() =&gt; {
    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 = () =&gt; {
  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 = () =&gt; {
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>) =&gt; {
eve.preventDefault();
if (!title) return;
insertTodo({ variables: { title, content } });
setTitle('');
setContent('');
};
const onClickUpdate = (todo: Todos) =&gt; {
updateTodoIsDone({ variables: { id: todo.id, is_done: !todo.is_done } });
};</htmlformelement>

const onClickDelete = (id: number) =&gt; {
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)}&gt;
<input type="text" value="{title}" placeholder="Enter the title" onchange="{(eve)" ==""> setTitle(eve.target.value)} /&gt;
<input type="text" value="{content}" placeholder="Enter the content" onchange="{(eve)" ==""> setContent(eve.target.value)} /&gt;
<input type="submit" value="">
<span><riaddline></riaddline></span>
</form>
<ul classname="todo-list">
        {data?.todos.map((todo) =&gt; (
    <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)}&gt;<richeckline></richeckline></div>
</div>
<div classname="todo-item__body">
              <input type="text" name="title" classname="todo-item__body__title" defaultvalue="{todo.title}">
{todo?.content &amp;&amp; <input type="text" name="content" classname="todo-item__body__content" defaultvalue="{todo.content}">}</div>
<div classname="todo-item__foot">
              <button onclick="{()" ==""> onClickDelete(todo.id)}&gt;<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 = () =&gt; {
  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

1

AUTHOR

sasai

ささい エンジニア

フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。

アプリでもっと便利に!気になる記事をチェック!

記事のお気に入り登録やランキングが表示される昨日に対応!毎日の情報収集や調べ物にもっと身近なメディアになりました。

簡単に自分で作れるWebAR

「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。

palanARへ
palanar

palanはWebARの開発を
行っています

弊社では企画からサービスの公開終了まで一緒に関わらせていただきます。 企画からシステム開発、3DCG、デザインまで一貫して承ります。

webar_waterpark

palanでは一緒に働く仲間を募集しています

正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。

話を聞いてみたい

運営メンバー

eishis

Eishi Saito 総務

SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。

sasakki デザイナー

アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。

yamakawa

やまかわたかし デザイナー

フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。

Sayaka Osanai デザイナー

Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。

はらた

はらた エンジニア

サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています

kobori

こぼり ともろう エンジニア

サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。

sasai

ささい エンジニア

フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。

damien

Damien

WebAR/VRを中心に企画・マークアップ・開発をやっています。森に住んでいます。

ゲスト bagelee

ゲスト bagelee

かっきー

かっきー

まりな

まりな

suzuki

suzuki

taro

taro

xR界隈のビズをやっています。新しいガジェットとか使うのが好きです。あとお寿司は玉子のお寿司が好きです。

miyagi

ogawa

ogawa

雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。

いわもと

いわもと

kobari

taishi kobari

フロントエンドの開発を主に担当してます。Blitz.js好きです。

shogokubota

kubota shogo

サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!

nishi tomoya

aihara

aihara

グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。

nagao

SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。

CONTACT PAGE TOP