SupabaseのRLSの設定

作成日
更新日

背景

ちゃんとしないとやばい。何故か private key が漏れったっぽく無限にアクセスが来ていた。

手順

  • 認証ユーザーを作る
  • RLSを設定する
  • 環境変数に追加する
    • SUPABASE_URL
    • SUPABASE_KEY
    • SUPABASE_IMAGE_BUCKET
    • SUPABASE_USER_EMAIL
    • SUPABASE_USER_PASSWORD
  • APIを使う時に user_id を含める

認証ユーザーを作る

SupabaseのRLSの設定-1743984180436

RLSを設定する

認証ユーザーのみがアクセスできるように user_id をいれる列を作っておく。

alter table articles add column if not exists user_id uuid;

カテゴリも。

alter table categories add column if not exists user_id uuid;

RLSを設定する。

-- RLS 有効化
alter table articles enable row level security;

-- INSERT: 自分の user_id しか入れられない
create policy "Allow insert for owner only"
  on articles
  for insert
  with check (user_id = auth.uid());

-- UPDATE: 自分の投稿だけ編集可
create policy "Allow update for owner only"
  on articles
  for update
  using (user_id = auth.uid())
  with check (user_id = auth.uid());

-- SELECT: 自分の投稿だけ見える(必要に応じて)
create policy "Allow read own articles"
  on articles
  for select
  using (user_id = auth.uid());
-- RLS 有効化
alter table categories enable row level security;

-- INSERT: 自分の user_id しか入れられない
create policy "Allow insert for owner"
  on categories
  for insert
  with check (user_id = auth.uid());

-- UPDATE: 自分のカテゴリだけ編集可
create policy "Allow update for owner"
  on categories
  for update
  using (user_id = auth.uid())
  with check (user_id = auth.uid());

-- SELECT: 自分のカテゴリだけ見える(必要に応じて)
create policy "Allow read own categories"
  on categories
  for select
  using (user_id = auth.uid());

ストレージも。

-- RLS 有効化
alter table storage.objects enable row level security;

-- INSERT: アップロード(=insert)には `with check` を使う!
create policy "Allow insert for image owner"
  on storage.objects
  for insert
  to authenticated
  with check (
    auth.uid() is not null
    and bucket_id = 'blog-images'
  );

-- SELECT: signed URL 発行など
create policy "Allow read for image owner"
  on storage.objects
  for select
  to authenticated
  using (
    auth.uid() is not null
    and bucket_id = 'blog-images'
  );

Anon キーと JWT を使う

upload_to_supabase.ts
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_KEY")!;
const SUPABASE_EMAIL = Deno.env.get("SUPABASE_USER_EMAIL")!;
const SUPABASE_PASSWORD = Deno.env.get("SUPABASE_USER_PASSWORD")!;

async function getFreshToken() {
  const tempClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

  const { data, error } = await tempClient.auth.signInWithPassword({
    email: SUPABASE_EMAIL,
    password: SUPABASE_PASSWORD,
  });

  if (error || !data.session) {
    throw new Error(`Auth failed: ${error?.message}`);
  }

  return {
    access_token: data.session.access_token,
    refresh_token: data.session.refresh_token,
    user: data.session.user,
  };
}

// supabase を access_token で再作成
async function createAuthedClient() {
  const tokenInfo = await getFreshToken();

  // 必要なら .env 書き換え保存などもここで
  return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
    global: {
      headers: {
        Authorization: `Bearer ${tokenInfo.access_token}`,
      },
    },
  });
}

// アクセス時の共通関数ラッパー(エラー時リトライ)
async function withAuthRetry<T>(fn: () => Promise<T>): Promise<T> {
  let result: T;
  try {
    result = await fn();
  } catch (err) {
    const message = (err as Error).message;
    if (message.includes("JWT expired") || message.includes("401")) {
      console.warn("🔁 トークン期限切れのため再認証中...");
      supabase = await createAuthedClient();
      result = await fn(); // retry
    } else {
      throw err;
    }
  }
  return result;
}
// グローバル Supabase クライアント(認証付き)
let supabase = await createAuthedClient();
const { data: { user } } = await supabase.auth.getUser();
// 実行
await uploadArticles();

withAuthRetry関数でトークン切れの時は再度取得し直す。

async function uploadCategory(category: string) {
  await withAuthRetry(async () => {
    const { error } = await supabase
      .from("category")
      .upsert([{ name: category, user_id: user?.id }], { onConflict: "name" });

    if (error) {
      console.error(`Error uploading category ${category}:`, error.message);
    } else {
      console.log(`Uploaded category: ${category}`);
    }
  });
}

閲覧

閲覧は誰でも可能にする

-- 誰でも(匿名含む)読める公開記事
create policy "Allow read public articles"
  on articles
  for select
  using (
    private = false
  );
-- 誰でも読めるカテゴリ
create policy "Allow read categories"
  on category
  for select
  using (true);
-- 非ログインユーザーでも CDN 経由で画像取得OK
create policy "Allow public read of blog-images"
  on storage.objects
  for select
  using (
    bucket_id = 'blog-images'
  );

認証処理は追加しなくておk

おまけでこれもやっておいた。

サイトアイコン
公開日
更新日