🐱

Supabase Authを使ったNext.jsアプリの認証と認可(2/2)

2022/03/03に公開約6,700字

前回の記事でSupabase Authを使ってマジック・リンクによる認証を実装しながら、処理の内容を詳しくみてきました。

https://zenn.dev/hrtk/articles/supabase-nextjs-database-authentication

今回は、ログインしたユーザーが自身のデータを取り扱うための認可部分をみていきます。

テーブルに関して

Supbaseでアプリケーションを構築する際、使用するテーブルは特に指定しない限りpublic(公開)スキーマに作成されます。名前の通り、デフォルトではだれでも参照でき、操作できる状態になっています。

そこで、テーブルに対して、行単位セキュリティー(Row Level Security - RLS)を有効にすることで、anonキーを使ったアクセスでの参照や操作をできなくします。

その上で、ポリシーを作成することにより、特定のユーザーがデータを参照したり操作することを認可します。

行単位セキュリティーに関しては、こちらも参考にしくてださい。

https://zenn.dev/hrtk/articles/2a1492c9c2cc15

テーブルを作成

それでは、実際にテーブルを作成して、ログインしたユーザーが自身のプロフィールを登録できるようにします。

プロジェクトのダッシュボードから、SQL Editorを開きます。

「+ New query」ボタンをクリックすると、「SQL snippets」の下に「New Query」が作成されます。

ユーザーのプロフィールを保持するprofilesテーブルを作成するために、次のSQLを入力して、「Run」ボタンをクリックして実行します。

-- publicにprofilesテーブルを作成
create table profiles (
  id uuid references auth.users not null,
  updated_at timestamp with time zone,
  name text,

  primary key (id)
);

nameというカラムに名前を保持します。ここで、authを指定することで、authスキーマのテーブルを参照できます。

次に認可を設定します。下のSQLを入力して実行します。

alter table profiles enable row level security;

create policy "パブリックなプロフィールはだれでも参照(select)できる。"
  on profiles for select
  using ( true );

create policy "自身のプロフィールを更新(update)できる。"
  on profiles for update
  using ( auth.uid() = id );

create policy "自身のプロフィールを追加(insert)できる。"
  on profiles for insert
  with check ( auth.uid() = id );

alter句を使って、行単位セキュリティーを有効にします。create句で、それぞれの操作に応じたポリシーを作成します。

auth.uid()はSupabaseが用意している関数で、呼びだすことでauthスキーマのusersテーブルのidを参照できるようになっています。

ここで、updateではusinginsertではwith checkを使用して検証しています。

usingの場合は、既存のテーブルの行に対して照合します。まずauth.uid()で認証されていることを確認して、認証ユーザーのIDと一致する行に対してupdateを許可します。

with checkでも、まずauth.uid()で認証されているユーザーを確認します。その上で、認証ユーザーのIDと追加しようとしているIDが一致している場合のみinsertを許可します。

これにより、認証していないユーザーは、一切そのテーブルに対してinsertはできません。さらに、認証されているユーザーでも自分のIDではないIDを指定してinsertすることを防ぐことができます。

Next.jsアプリを実装

セットアップ

前回の記事からのコードをベースに実装をします。

もし、ここからはじめる場合は、degitを使ってコードを取得できます。

npx degit hirotaka/examples/supabase-nextjs-database#atlanta-0.0.2 supabase-nextjs-database

こちらを参照にセットアップしてください。

https://zenn.dev/hrtk/articles/supabase-nextjs-database-authentication#next.jsアプリをセットアップ

プロフィール・コンポーネント

プロフィールを更新するためのコンポーネントを作成します。

components/profile.tsx
import { useState } from 'react'
import { useUser } from '@/contexts/user'
import { supabase } from '@/lib/supabase-client'

export default function Profile() {
  const { user, setUser } = useUser()
  const [loading, setLoading] = useState()
  const [name, setName] = useState(user.profile && user.profile.name)

  async function updateProfile({ name }) {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      const updates = {
        id: user.id,
        name,
        updated_at: new Date(),
      }

      const { data: profile, error } = await supabase
        .from('profiles')
        .upsert(updates)
        .single()

      if (error) {
        throw error
      } else {
        alert('プロフィールを更新しました!')
        setUser({ ...user, profile })
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="mt-6">
      <label className="block">名前</label>
      <input
        id="name"
        className="w-full mt-2"
        type="text"
        value={name || ''}
        onChange={(e) => setName(e.target.value)}
      />
      <button
        className="primary mt-4 mr-4"
        onClick={() => updateProfile({ name })}
        disabled={loading}
      >
        {loading ? '読み込み中...' : '更新'}
      </button>
    </div>
  )
}

まず、auth.user()メソッドでセッション情報を取得します。ユーザーIDをセットして、Supabaseにupsert()メソッドで問い合わせます。その際に、IDで照合して存在すれば、該当する行をupdateで更新し、存在しなければ新しくinsertで追加します。

更新が成功すると、更新後の値がSupbaseから返ってきます。それらを、ユーザーのステートにprofileという属性を加えてセットします。

contexts/user.tsxでもログインした際、ユーザーのステートにセットします。

contexts/user.tsx
@@ -8,12 +8,38 @@ const Provider = ({ children }) => {
   const [loading, setLoading] = useState(false)
 
   useEffect(() => {
-    setUser(supabase.auth.user())
+    setProfile(supabase.auth.user())
     supabase.auth.onAuthStateChange(() => {
-      setUser(supabase.auth.user())
+      setProfile(supabase.auth.user())
     })
   }, [])
 
+  const setProfile = async (user) => {
+    if (!user) {
+      setUser(null)
+      return
+    }
+
+    try {
+      setLoading(true)
+      const { data, error } = await supabase.from('profiles').select('*')
+
+      if (error) {
+        throw error
+      } else {
+        if (data.length > 0) {
+          setUser({ ...user, profile: data[0] })
+        } else {
+          setUser({ ...user })
+        }
+      }
+    } catch (error) {
+      console.error(error.message)
+    } finally {
+      setLoading(false)
+    }
+  }
+
   const login = async (email) => {
     try {
       setLoading(true)

setProfileメソッドでは、次のケースの対応が必要です。

  • ログインをしていない
  • ログインをしている
    • プロフィールを作成していない
    • プロフィールを作成している

まず、「ログインをしていない」ケースでは、userオブジェクトを判定します。オブジェクトを特定できない場合、ユーザーのステートにnullをセットします。

「ログインをしている」ケースでは、「プロフィールを作成していない」か、「プロフィールを作成してる」かで処理を別けます。

上記で更新の際にはsingle()メソッドで1つのオブジェクトを取得しましたが、これだと「プロフィールを作成していない」状態だとエラーになってしまいます。

そのため、Supbaseからは、いったん配列で取得します。その配列のlengthで「プロフィールを作成していない」か「プロフィールを作成している」かを判断し、ユーザーのステートにセットする内容を決定します。

次にダッシュボードでコンポーネントを読み込みます。

components/dashboard.tsx
@@ -1,4 +1,5 @@
 import { useUser } from '@/contexts/user'
+import Profile from '@/components/profile'
 
 export default function Dashboard() {
   const { logout, user } = useUser()
@@ -9,6 +10,7 @@ export default function Dashboard() {
       <div className="mt-6">
         <p>Hello, {user.email}!</p>
       </div>
+      <Profile />
       <div className="mt-6">
         <button className="secondary" onClick={() => logout()}>
           ログアウト

ここまでのコード全体はこちらで確認できます。

https://github.com/hirotaka/examples/blob/atlanta-0.0.3/supabase-nextjs-database/

動作を確認

ログインして、ダッシュボードを開きます。

名前を入力して、更新ボタンをクリックします。

更新が成功すると、アラートにプロフィールが更新された旨を表示します。

Supabaseのダッシュボードで、「Table editor」から「profiles」テーブルを開きます。

新しくレコードが追加されていることが確認できます。

おわりに

Postgresは結構使っていましたが、アクセス制御はミドルウェアで実装する機会がほとんどでした。Supababaseを触るようになって、行単位セキュリティーとポリシーを使いはじめました。

最初は、なかなか馴染めなかったですが、一度コツさえ掴めればすごくわかりやすいし、柔軟で強力な仕組みですね。

参考

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

Supabase Authに関しては、こちらも参考にしくてださい。

https://www.supabase.jp/docs/guides/auth

作成したのをベースに、次回以降で何回かに分けてデータベースの便利な機能を試していきます。
その上で、何回かに分けてデータベースの便利な機能も試していきます。

GitHubで編集を提案

Discussion

ログインするとコメントできます