RLSを使うことになったが、記事などを読んでなんとくなく分かったような気になって
実際やるとしたどんな感じでやればいいかわからず、物を作りながら勉強してみた。
Supabaseで認証ユーザー準備
ReactのビルドとSupabase連携
Reactビルド
npm create vite@latest my-app -- --template react
supabase連携
npm install @supabase/supabase-js
App.jsxを編集
App.jsx
//import './index.css'
import { useState, useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
)
export default function App() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const [claims, setClaims] = useState(null)
// Check URL params on initial render
const params = new URLSearchParams(window.location.search)
const hasTokenHash = params.get('token_hash')
const [verifying, setVerifying] = useState(!!hasTokenHash)
const [authError, setAuthError] = useState(null)
const [authSuccess, setAuthSuccess] = useState(false)
const [report,setReport] = useState([])
useEffect(() => {
getReport()
}, [])
async function getReport() {
const { data, error } = await supabase.from('report').select()
if (error) {
console.error(error)
return
}
setReport(data)
console.log(data)
}
useEffect(() => {
// Check if we have token_hash in URL (magic link callback)
const params = new URLSearchParams(window.location.search)
const token_hash = params.get('token_hash')
const type = params.get('type')
if (token_hash) {
// Verify the OTP token
supabase.auth
.verifyOtp({
token_hash,
type: type || 'email',
})
.then(({ error }) => {
if (error) {
setAuthError(error.message)
} else {
setAuthSuccess(true)
// Clear URL params
window.history.replaceState({}, document.title, '/')
}
setVerifying(false)
})
}
// Check for existing session using getClaims
supabase.auth.getClaims().then(({ data: { claims } }) => {
setClaims(claims)
})
// Listen for auth changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
supabase.auth.getClaims().then(({ data: { claims } }) => {
setClaims(claims)
})
})
return () => subscription.unsubscribe()
}, [])
const handleLogin = async (event) => {
event.preventDefault()
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.origin,
},
})
if (error) {
alert(error.error_description || error.message)
} else {
alert('Check your email for the login link!')
}
setLoading(false)
}
const handleLogout = async () => {
await supabase.auth.signOut()
setClaims(null)
}
// Show verification state
if (verifying) {
return (
<div>
<h1>Authentication</h1>
<p>Confirming your magic link...</p>
<p>Loading...</p>
</div>
)
}
// Show auth error
if (authError) {
return (
<div>
<h1>Authentication</h1>
<p>✗ Authentication failed</p>
<p>{authError}</p>
<button
onClick={() => {
setAuthError(null)
window.history.replaceState({}, document.title, '/')
}}
>
Return to login
</button>
</div>
)
}
// Show auth success (briefly before claims load)
if (authSuccess && !claims) {
return (
<div>
<h1>Authentication</h1>
<p>✓ Authentication successful!</p>
<p>Loading your account...</p>
</div>
)
}
// If user is logged in, show welcome screen
if (claims) {
return (
<div>
<h1>Welcome!</h1>
<p>You are logged in as: {claims.email}</p>
<button onClick={handleLogout}>Sign Out</button>
<ul>
{report.map((r) => (
<li key={r.title}>{r.title}</li>
))}
</ul>
</div>
)
}
// Show login form
return (
<div>
<h1>Supabase + React</h1>
<p>Sign in via magic link with your email below</p>
<p>メールが届くので、メールからログインしてください</p>
<form onSubmit={handleLogin}>
<input
type="email"
placeholder="Your email"
value={email}
required={true}
onChange={(e) => setEmail(e.target.value)}
/>
<button disabled={loading}>
{loading ? <span>Loading</span> : <span>Send magic link</span>}
</button>
</form>
</div>
)
}
ログインを試す
supabaseで登録したユーザーのメールアドレスを入力してください。
メールが届くので、そのメールのリンクをクリックするとログインできます。

テナントと閲覧制限
今回は、"report"というテーブルに対して、テナントごとで閲覧制限をかけます。
テナントに企業A,企業B,企業Cとあった際に、自分が企業Aの社員であれば企業Aのreportしか
見ることができないようにsuoabaseで制限をかけるイメージ。
reportテーブル、テナントテーブルとテナントとユーザー情報を結びつける中間テーブル(※)を作成
※supabaseのユーザーテーブルにカラム追加できないようなので中間テーブルを作成する方式とした
-- レポートテーブル作成
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL
);
-- テナントテーブル作成
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
content TEXT NOT NULL,
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
);
-- テナントとユーザーを紐付ける中間テーブル作成
CREATE TABLE public.tenant_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
UNIQUE (tenant_id, user_id) -- 同じユーザーが同じテナントに重複登録されるのを防ぐ
);
RLS の有効化
RLSを有効化して制限をかける
また、中間テーブルの情報からログイン中のユーザーがどのテナントに所属するかを片別する関数を作り、
所属テナントの情報を閲覧可能なポリシーを作成する
-- RLS の有効化
ALTER TABLE public.report ENABLE ROW LEVEL SECURITY;
-- ログイン中ユーザーの所属テナントIDを取得する関数を作成(定義)する
CREATE OR REPLACE FUNCTION get_my_tenant_ids()
RETURNS UUID[] AS $$
BEGIN
RETURN ARRAY(
SELECT tenant_id
FROM tenant_members
WHERE user_id = auth.uid()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 関数が作られたら、もう一度RLSポリシーを作成する
CREATE POLICY "同じテナントのレポートのみ参照可能" ON public.report
FOR SELECT USING (
tenant_id = ANY(get_my_tenant_ids())
);
これで完了。

