🐥

Next.jsでsupabaseを使って、認証からデータベースの更新までをやってみる

2021/10/03に公開
1

Supabaseは、「The Open Source Firebase Athlternative」を謳っています。プロプライエタリなFirebaseの代わりにオープンな技術スタックで構成されてバックエンドのサービスです。

Supabaseで提供している認証とデータベースを使って、一連の基本的な機能をさわってみます。

Supabaseの新規プロジェクト作成

Supabaseのサイトより、サインアップします。

https://supabase.io/

ダッシュボードから、プロジェクトを作成しておきます。

GitHubのOauthアプリケーション作成

GitHubでこちらから新しくアプリケーションを作成します。

  • Application name
    アプリケーションの名称を入力します。
  • Homepage URL
    ホームページのURLを入力します。有効なURLを入力する必要があるので、ない場合はhttp://locahost:3000/などを入力します。
  • Authorization callback URL
    Supabaseのダッシュボードから、プロジェクトのサイドメニューの「Settings」を選びます。「API」の「Config」欄にURLがあるので、そのURLに/auth/v1/callbackを追加して入力します。

「Registration application」ボタンをクリックして、アプリケーションを作成します。

アプリケーションの詳細が表示されるので、「Client secrets」欄の「Generate a new client secret」ボタンをクリックしてシークレットキーを生成します。

Supabase側の設定

Authentication設定

作成したプロジェクトのサイドメニューの「Authentication」を選んで、「Settings」を選択します。

「External OAuth Providers」欄の「GitHub enabled」のトグルをオンにします。

そうすると、「GitHub client ID」と「GitHub secret」のテキスト入力欄が表示されます。先ほど保持したGitHubの「Client ID」と「Client secrets」をそれぞれに入力します。「Save」ボタンをクリックして、設定を保存します。

テーブル作成

ユーザーの情報を保持するテーブルを作成します。

create table users (
  id uuid references auth.users not null primary key,
  fullname text,
  avatarurl text,
  nickname text
);
alter table users enable row level security;
create policy "Can view own user data." on users for select using (auth.uid() = id);
create policy "Can update own user data." on users for update using (auth.uid() = id);

/**
 * Supabase Authでサインアップしたら自動でユーザーを作成するためのトリガー
 */
create function public.handle_new_user()
  returns trigger as $$
begin
  insert into public.users (id, fullname, avatarurl)
  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
  return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

認証のテーブルに新しくレコードが作成されたら、認証テーブルのuuidをキーにGitHubから取得した情報を付与して、このユーザーテーブルにレコードを保存するようにします。

「SQL」をサイドメニューから開いて、「Open queries」の「Query-1」を開きます。クエリーエディターが開くので、上記のSQLを入力して、「RUN」をクリックしてSQLを実行します。

Next.jsセットアップ

アプリケーション作成

TypescriptをベースにNext.jsプロジェクトを作成します。

npx create-next-app --typescript --use-npm supabase-nextjs-auth
cd supabase-nextjs-auth
npm install --save-dev typescript @types/react
touch tsconfig.json

Supabaseクライアントをインストールします。

npm install @supabase/supabase-js

Supabaseクライアント

「Settings」から「API」を選択します。

URLとパブリックキーを控えます。

.env.localというファイルを作成して、NEXT_PUBLIC_SUPABASE_ANON_KEYにパブリックキー、NEXT_PUBLIC_SUPABASE_URLにURLをセットします。

NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co

Supabaseクライアントを初期化するファイルを作成します。

lib/supabase-client.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

認証機能

まず、認証の処理をする関数を定義します。

hooks/useUser.ts
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase-client";

export default function userUser() {
  const [session, setSession] = useState();

  useEffect(() => {
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setSession(session);
      }
    );

    return () => {
      authListener.unsubscribe();
    };
  }, []);

  function signInWithGithub() {
    supabase.auth.signIn({ provider: "github" });
  }

  function signOut() {
    supabase.auth.signOut();
  }

  return {
    session,
    signInWithGithub,
    signOut,
  };
}

アプリケーション作成時にpages/index.jsというファイルがに作成されるので、そのファイルを削除します。

新しくpages/index.tsxというファイルを下記の内容で作成して、先ほどの関数をコールするようにします。

pages/index.tsx
import useUser from "../hooks/useUser";

export default function Home() {
  const { session, signOut, signInWithGithub } = useUser();

  return (
    <>
      {session ? (
        <button onClick={() => signOut()}>サインアウト</button>
      ) : (
        <button onClick={() => signInWithGithub()}>GitHubでログイン</button>
      )}
    </>
  );
}

「GitHubでログイン」ボタンだけがある簡単なページになります。ボタンをクリックしたらSupabaseの認証します。

Next.jsの開発サーバーを起動します。

npm run dev

動作確認

ブラウザーで、http://localhost:3000を開きます。

「GitHubでログイン」ボタンをクリックすると、GitHub側の認証処理が実行されます。

GitHubアプリケーションでのログインを許可するか確認されるので、「Authorize xxx」(xxxはそれぞれのGitHubユーザーや組織名がはいります)ボタンをクリックします。
これで、Supabaseで認証処理が実行され、ユーザーテーブルにレコードが作成されます。

supabaseで、「Authentication」の「Users」で確認します。

「Tables」のusersテーブルにもレコードができているのを確認できます。

ユーザー情報

ユーザーテーブルからユーザー情報を取得して、更新できるようにします。

取得

ユーザー情報をステートにセットします。

hooks/useUser.ts
 
 export default function userUser() {
   const [session, setSession] = useState();
+  const [user, setUser] = useState(null);
 
   useEffect(() => {
     const { data: authListener } = supabase.auth.onAuthStateChange(

セッションがはれたら、ユーザー情報を取得するためのuseEffectを追加します。

hooks/useUser.ts
     };
   }, []);
 
+  useEffect(() => {
+    const setupUser = async () => {
+      if (session?.user.id) {
+        const { data: user } = await supabase
+          .from("users")
+          .select("*")
+          .eq("id", session.user.id)
+          .single();
+        setUser(user);
+      }
+    };
+    setupUser();
+  }, [session]);

   function signInWithGithub() {
     supabase.auth.signIn({ provider: "github" });
   }

userを参照できるようにreturnします。

hooks/useUser.ts
   return {
     session,
+    user,
     signInWithGithub,
     signOut,
   };

ファイル全体

hooks/useUser.ts
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase-client";

export default function userUser() {
  const [session, setSession] = useState();
  const [user, setUser] = useState(null);

  useEffect(() => {
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setSession(session);
      }
    );

    return () => {
      authListener.unsubscribe();
    };
  }, []);

  useEffect(() => {
    const setupUser = async () => {
      if (session?.user.id) {
        const { data: user } = await supabase
          .from("users")
          .select("*")
          .eq("id", session.user.id)
          .single();
        setUser(user);
      }
    };
    setupUser();
  }, [session]);

  function signInWithGithub() {
    supabase.auth.signIn({ provider: "github" });
  }

  function signOut() {
    supabase.auth.signOut();
  }

  return {
    session,
    signInWithGithub,
    signOut,
  };
}

次に、取得したユーザー情報をページに表示します。

pages/index.tsx
 import useUser from "../hooks/useUser";
 
 export default function Home() {
-  const { session, signOut, signInWithGithub } = useUser();
+  const { session, user, signOut, signInWithGithub } = useUser();
 
   return (
     <>
       {session ? (
-        <button onClick={() => signOut()}>サインアウト</button>
+        <div>
+          <p>Hello, {user && user.fullname}</p>
+          <button onClick={() => signOut()}>サインアウト</button>
+        </div>
       ) : (
         <button onClick={() => signInWithGithub()}>GitHubでログイン</button>
       )}

ファイル全体

pages/index.tsx
import useUser from "../hooks/useUser";

export default function Home() {
  const { session, user, signOut, signInWithGithub } = useUser();

  return (
    <>
      {session ? (
        <div>
          <p>Hello, {user && user.full_name}</p>
          <button onClick={() => signOut()}>サインアウト</button>
        </div>
      ) : (
        <button onClick={() => signInWithGithub()}>GitHubでログイン</button>
      )}
    </>
  );
}

更新

次にユーザーのニックネームを更新できるようにします。

Supabaseでレコードを更新する関数を追加して、関数をreturnに含めます。

hooks/useUser.ts
     supabase.auth.signOut();
   }
 
+  async function updateNickname(nickname) {
+    const { data: newUser } = await supabase
+      .from("users")
+      .update({ nickname })
+      .match({ id: user.id })
+      .single();
+    setUser(newUser);
+  }
+
   return {
     session,
     user,
     signInWithGithub,
     signOut,
+    updateNickname,
   };
 }

ファイル全体

pages/index.tsx
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase-client";

export default function userUser() {
  const [session, setSession] = useState();
  const [user, setUser] = useState(null);

  useEffect(() => {
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setSession(session);
      }
    );

    return () => {
      authListener.unsubscribe();
    };
  }, []);

  useEffect(() => {
    const setupUser = async () => {
      if (session?.user.id) {
        const { data: user } = await supabase
          .from("users")
          .select("*")
          .eq("id", session.user.id)
          .single();
        setUser(user);
      }
    };
    setupUser();
  }, [session]);

  function signInWithGithub() {
    supabase.auth.signIn({ provider: "github" });
  }

  function signOut() {
    supabase.auth.signOut();
  }

  async function updateNickname(nickname) {
    const { data: newUser } = await supabase
      .from("users")
      .update({ nickname })
      .match({ id: user.id })
      .single();
    setUser(newUser);
  }

  return {
    session,
    user,
    signInWithGithub,
    signOut,
    updateNickname,
  };
}

ページにフォームを追加して、そのサブミットした内容を処理するための記述を追加します。

hooks/useUser.ts
+import { useEffect, useState, useRef } from "react";
 import useUser from "../hooks/useUser";
 
 export default function Home() {
-  const { session, user, signOut, signInWithGithub } = useUser();
+  const { session, user, signOut, signInWithGithub, updateNickname } =
+    useUser();
+  const [editingNickname, setEditingNickname] = useState(false);
+  const newNickname = useRef();
+
+  async function setNickname(evt) {
+    evt.preventDefault();
+
+    try {
+      updateNickname(newNickname.current.value);
+      setEditingNickname(false);
+      newNickname.current.value = "";
+    } catch (err) {
+      console.log("エラーが発生しました");
+    }
+  }
 
   return (
     <>
       {session ? (
         <div>
-          <p>Hello, {user && user.full_name}</p>
+          <p>Hello, {user?.nickname ? user.nickname : user?.full_name}</p>
+          <div>
+            {editingNickname ? (
+              <form onSubmit={setNickname}>
+                <input
+                  type="text"
+                  required
+                  ref={newNickname}
+                  placeholder="新しいニックネーム"
+                />
+                <button type="submit">設定</button>
+              </form>
+            ) : (
+              <div>
+                <button onClick={() => setEditingNickname(true)}>
+                  ニックネームを更新
+                </button>
+              </div>
+            )}
+          </div>
           <button onClick={() => signOut()}>サインアウト</button>
         </div>
       ) : (

ファイル全体

pages/index.tsx
import { useEffect, useState, useRef } from "react";
import useUser from "../hooks/useUser";

export default function Home() {
  const { session, user, signOut, signInWithGithub, updateNickname } =
    useUser();
  const [editingNickname, setEditingNickname] = useState(false);
  const newNickname = useRef();

  async function setNickname(evt) {
    evt.preventDefault();

    try {
      updateNickname(newNickname.current.value);
      setEditingNickname(false);
      newNickname.current.value = "";
    } catch (err) {
      console.log("エラーが発生しました");
    }
  }

  return (
    <>
      {session ? (
        <div>
          <p>Hello, {user?.nickname ? user.nickname : user?.full_name}</p>
          <div>
            {editingNickname ? (
              <form onSubmit={setNickname}>
                <input
                  type="text"
                  required
                  ref={newNickname}
                  placeholder="新しいニックネーム"
                />
                <button type="submit">設定</button>
              </form>
            ) : (
              <div>
                <button onClick={() => setEditingNickname(true)}>
                  ニックネームを更新
                </button>
              </div>
            )}
          </div>
          <button onClick={() => signOut()}>サインアウト</button>
        </div>
      ) : (
        <button onClick={() => signInWithGithub()}>GitHubでログイン</button>
      )}
    </>
  );
}

「ニックネームを更新」ボタンをクリックして、ニックネームを入力して「設定」ボタンをクリックすることでニックネームを更新できます。

おわりに

様々なクラウドのサービスをバックエンドに使ってきましたが、SQLでいろいろできるのはなんか新鮮な感じがしました。

NoSQL系のデータベースは最初馴染みやすく、さくっとつくるにはいいのですが、設計技法をちゃんとしていないとスケールに苦労することが多かったです。

Supabaseの場合、PostgreSQLベースなので、技術がこなれているので安心して使えます。

公式で提供しているJavascriptのライブラリもジョイン文など簡潔に書けるので、よりフロントエンドの開発に集中できる感じがしました。

参考

Supbaseの知識を深めるために、ドキュメントの翻訳に取り組んでいます。

Next.jsのクイックスタートも参考にしてください。認証から、Storageを使って、プロフィール画像まで登録できます。

https://www.supabase.jp/docs/guides/with-nextjs

GitHubで編集を提案

Discussion

masaishimasaishi

大変わかりやすい記事を書いて頂きありがとうございました。とても参考になりました。
現在、supabase-js のバージョンが2に上がったようで、supabase.auth.signIn({ provider: "github" }); が supabase.auth.signInWithOAuth({ provider: "github" }); に変わっているそうです。
僕の環境だとその変更をしたところ動いたので、時間があるときに編集していただけるとありがたいです。