1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RLSの勉強 React + Supabaseでログイン中ユーザーが所属するテナントだけのデータを表示する

1
Posted at

RLSを使うことになったが、記事などを読んでなんとくなく分かったような気になって
実際やるとしたどんな感じでやればいいかわからず、物を作りながら勉強してみた。

Supabaseで認証ユーザー準備

Addユーザーからユーザーを追加する
image.png

Confirm emailはOFFにする
image.png

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で登録したユーザーのメールアドレスを入力してください。
メールが届くので、そのメールのリンクをクリックするとログインできます。
image.png

テナントと閲覧制限

今回は、"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())
  );

これで完了。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?