supabase + GraphQL + Next.js で認証とリソースの権限を設定する
※ 書きかけです
supabase と GraphQL と認証
supabase はテーブルの構造に合わせて GraphQL スキーマが自動生成され、それを利用してプログラムを組むことができるようになりました。ただしユーザーの作成や認証機能は GraphQL を通しては実装されておらず、その部分だけは RestAPI を用いる必要があります。また、GraphQL がアクセス可能なデータベーススキーマは public のみのため、auth 配下にあるユーザ情報は GraphQL 上では関連付けることができません。これらの問題にうまく対処していく必要があります。
ローカル開発で用意するもの
https://github.com/supabase/cli
ローカル開発で必要となります。
supabase-cli の初期設定
1supabase init2supabase start
これで supabase が起動できます。supabase-cli を使う場合の注意点があります。外部からのアクセス可能な環境では絶対に使用しないでください。jwt シークレットがsuper-secret-jwt-token-with-at-least-32-characters-longという内容で決め打ちになっているので、外に出したら一瞬でクラックされます。
用意を推奨するファイル
supabase initでsupabaseディレクトリが作成されます。その中に用意しておくと良いファイルです。
supabase/seed.sql
1drop extension if exists pg_graphql;2create extension if not exists pg_graphql;3select graphql.rebuild_schema();
本来初期データを入れるためのファイルですが、マイグレーション後に GraphQL の機能を有効にするために必要です。supabase startやsupabase db resetでデータベースを作成した際に、一見 pg_graphql の拡張機能が有効になっているように見えるのですが、再起動しないと使えません。また、graphql.rebuild_schema()はデーブルの構造を変更するたびに必要になります。
最初に用意しておくユーザ管理用マイグレーションファイル
以下、ユーザ管理用のテーブルを作成するためのマイグレーションファイルです
supabase/migrations/20220418113839_create_user.sql
1CREATE OR REPLACE FUNCTION public.handle_users_update()2 RETURNS trigger3 LANGUAGE 'plpgsql'4 COST 1005 VOLATILE NOT LEAKPROOF SECURITY DEFINER6AS $BODY$7 begin8 IF (TG_OP = 'DELETE') THEN9 delete from public."User" where id=old.id;10 return old;11 ELSEIF (TG_OP = 'UPDATE') THEN12 update public."User"13 set email=NEW.email,raw_user_meta_data=NEW.raw_user_meta_data where id=old.id;14 return new;15 ELSEIF (TG_OP = 'INSERT') THEN16 insert into public."User"(id, email,raw_user_meta_data) values(NEW.id,NEW.email,NEW.raw_user_meta_data);17 return new;18 END IF;19 return NULL;20 end;21$BODY$;2223ALTER FUNCTION public.handle_users_update()24 OWNER TO postgres;2526GRANT EXECUTE ON FUNCTION public.handle_users_update() TO authenticated;2728GRANT EXECUTE ON FUNCTION public.handle_users_update() TO postgres;2930GRANT EXECUTE ON FUNCTION public.handle_users_update() TO PUBLIC;3132GRANT EXECUTE ON FUNCTION public.handle_users_update() TO anon;3334GRANT EXECUTE ON FUNCTION public.handle_users_update() TO service_role;3536CREATE TABLE IF NOT EXISTS public."User"37(38 id uuid NOT NULL,39 email character varying(255) COLLATE pg_catalog."default",40 raw_user_meta_data text,41 CONSTRAINT "User_pkey" PRIMARY KEY (id)42)4344TABLESPACE pg_default;4546ALTER TABLE IF EXISTS public."User"47 OWNER to postgres;4849ALTER TABLE IF EXISTS public."User"50 ENABLE ROW LEVEL SECURITY;5152GRANT ALL ON TABLE public."User" TO anon;5354GRANT ALL ON TABLE public."User" TO authenticated;5556GRANT ALL ON TABLE public."User" TO postgres;5758GRANT ALL ON TABLE public."User" TO service_role;59CREATE POLICY "Enable access to all users"60 ON public."User"61 AS PERMISSIVE62 FOR SELECT63 TO public64 USING (true);6566CREATE trigger on_auth_user_update67 AFTER INSERT OR UPDATE OR DELETE ON auth.users68 for each row execute procedure public.handle_users_update();
auth.usersへの操作をフックして、public.Userに必要なデータを書き込みます。こうしておくと、GraphQL のスキーマーがユーザ情報へアクセスできるようになります。
Todo アプリ用のテーブル
タイトル、説明、日時、非公開属性、ユーザ情報を記憶します。ユーザ情報は public.User にリレーションを張ります。auth.users では無いので注意してください。
1CREATE TABLE IF NOT EXISTS public."Todo"2(3 id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),4 created_at timestamp with time zone DEFAULT now(),5 user_id uuid NOT NULL DEFAULT auth.uid(),6 title text COLLATE pg_catalog."default",7 published boolean NOT NULL DEFAULT false,8 description text COLLATE pg_catalog."default",9 CONSTRAINT "Todo_pkey" PRIMARY KEY (id),10 CONSTRAINT "Todo_user_id_fkey" FOREIGN KEY (user_id)11 REFERENCES public."User" (id) MATCH SIMPLE12 ON UPDATE NO ACTION13 ON DELETE NO ACTION14)1516TABLESPACE pg_default;1718ALTER TABLE IF EXISTS public."Todo"19 OWNER to postgres;2021ALTER TABLE IF EXISTS public."Todo"22 ENABLE ROW LEVEL SECURITY;2324GRANT ALL ON TABLE public."Todo" TO anon;2526GRANT ALL ON TABLE public."Todo" TO authenticated;2728GRANT ALL ON TABLE public."Todo" TO postgres;2930GRANT ALL ON TABLE public."Todo" TO service_role;31CREATE POLICY "Enable access to all users"32 ON public."Todo"33 AS PERMISSIVE34 FOR SELECT35 TO public36 USING (published or auth.uid() = user_id);37CREATE POLICY "Enable INSERT for authenticated users only"38 ON public."Todo"39 AS PERMISSIVE40 FOR INSERT41 TO public42 WITH CHECK ((auth.role() = 'authenticated'::text) and auth.uid() = user_id);43CREATE POLICY "Enable DELETE/UPDATE for users based on user_id"44 ON public."Todo"45 AS PERMISSIVE46 FOR ALL47 TO public48 USING ((auth.uid() = user_id));
Todo テーブルはCREATE POLICYで PostgreSQL の RLS(行レベルセキュリティ)を作っています。
Enable access to all users
select の制限で private が設定されている場合は、ユーザーが一致しないとデータを返さないようにしています。この設定を入れておくと非公開データを作ることができますEnable INSERT for authenticated users only
認証ユーザのみ書き込むことができます。また書き込んだユーザが詐称できないように、実際のユーザと書き込まれる id が一致しているか検査していますEnable DELETE/UPDATE for users based on user_id
ユーザが一致した場合のみ、書き換えと削除を許可しています
このあたりの設定は慣れが必要です。
実験用 User の用意
認証に必要なユーザを作成します。cli では用意されていないので自分で作ります。ユーザの作成には service_role の方のキーを使います。テストユーザをさくっと作りたいので招待機能は使いません。
.env.local
1NEXT_PUBLIC_SUPABASE_URL=http://localhost:543212NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs3SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
bin/create-user.ts
1import { createClient } from "@supabase/supabase-js";2import { config } from "dotenv";34const { parsed } = config({ path: ".env.local" });56const endpoint = parsed?.NEXT_PUBLIC_SUPABASE_URL;7const key = parsed?.SUPABASE_KEY;89const createUser = async ({10 email,11 password,12}: {13 email: string;14 password: string;15}) => {16 const supabase = createClient(endpoint!, key!);17 const result = await supabase.auth.api.createUser({18 email,19 password,20 email_confirm: true,21 user_metadata: { name: email },22 });23 return result;24};2526(async () => {27 if (!endpoint || !key || process.argv.length < 4) {28 console.log("create-user [email] [password]");29 } else {30 let result;31 for (let i = 0; i < 3; i++) {32 result = await createUser({33 email: process.argv[2],34 password: process.argv[3],35 });36 if (result.error?.status !== 500) break;37 }38 console.log(result);39 }40})();
ユーザ作成
yarn ts-node -s bin/create-user a@example.com a
yarn ts-node -s bin/create-user b@example.com a
たまに 500 エラーを返す時があるので 3 回リトライするようにしています。とりあえずテスト用ユーザを二人作っておきます。
作成したユーザの認証と token の受け取りは GraphQL ではできないので RestAPI を直にたたくかsupabase.auth.signInを使うことになります。