Supabase Authを使ったNext.jsアプリの認証と認可(2/2)
前回の記事でSupabase Authを使ってマジック・リンクによる認証を実装しながら、処理の内容を詳しくみてきました。
今回は、ログインしたユーザーが自身のデータを取り扱うための認可部分をみていきます。
テーブルに関して
Supbaseでアプリケーションを構築する際、使用するテーブルは特に指定しない限りpublic
(公開)スキーマに作成されます。名前の通り、デフォルトではだれでも参照でき、操作できる状態になっています。
そこで、テーブルに対して、行単位セキュリティー(Row Level Security - RLS)を有効にすることで、anonキーを使ったアクセスでの参照や操作をできなくします。
その上で、ポリシーを作成することにより、特定のユーザーがデータを参照したり操作することを認可します。
行単位セキュリティーに関しては、こちらも参考にしくてださい。
テーブルを作成
それでは、実際にテーブルを作成して、ログインしたユーザーが自身のプロフィールを登録できるようにします。
プロジェクトのダッシュボードから、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
ではusing
、insert
では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
こちらを参照にセットアップしてください。
プロフィール・コンポーネント
プロフィールを更新するためのコンポーネントを作成します。
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
でもログインした際、ユーザーのステートにセットします。
@@ -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
で「プロフィールを作成していない」か「プロフィールを作成している」かを判断し、ユーザーのステートにセットする内容を決定します。
次にダッシュボードでコンポーネントを読み込みます。
@@ -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()}>
ログアウト
ここまでのコード全体はこちらで確認できます。
動作を確認
ログインして、ダッシュボードを開きます。
名前を入力して、更新ボタンをクリックします。
更新が成功すると、アラートにプロフィールが更新された旨を表示します。
Supabaseのダッシュボードで、「Table editor」から「profiles」テーブルを開きます。
新しくレコードが追加されていることが確認できます。
おわりに
Postgresは結構使っていましたが、アクセス制御はミドルウェアで実装する機会がほとんどでした。Supababaseを触るようになって、行単位セキュリティーとポリシーを使いはじめました。
最初は、なかなか馴染めなかったですが、一度コツさえ掴めればすごくわかりやすいし、柔軟で強力な仕組みですね。
参考
Supbaseの知識を深めるために、ドキュメントの翻訳に取り組んでいます。
Supabase Authに関しては、こちらも参考にしくてださい。
作成したのをベースに、次回以降で何回かに分けてデータベースの便利な機能を試していきます。
その上で、何回かに分けてデータベースの便利な機能も試していきます。
Discussion