From 0ac0be342d003530fba6693df9603a1affacc6ba Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Sat, 11 Oct 2025 12:39:07 -1000 Subject: [PATCH] Implement supabase custom claims --- src/types/db.ts | 28 +++ .../20251011223628_remote_schema.sql | 186 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 supabase/migrations/20251011223628_remote_schema.sql diff --git a/src/types/db.ts b/src/types/db.ts index a7377ce..efc91c8 100644 --- a/src/types/db.ts +++ b/src/types/db.ts @@ -279,6 +279,26 @@ export type Database = { [_ in never]: never } Functions: { + delete_claim: { + Args: { claim: string; uid: string } + Returns: string + } + get_claim: { + Args: { claim: string; uid: string } + Returns: Json + } + get_claims: { + Args: { uid: string } + Returns: Json + } + get_my_claim: { + Args: { claim: string } + Returns: Json + } + get_my_claims: { + Args: Record + Returns: Json + } gtrgm_compress: { Args: { "": unknown } Returns: unknown @@ -303,6 +323,14 @@ export type Database = { Args: Record Returns: boolean } + is_claims_admin: { + Args: Record + Returns: boolean + } + set_claim: { + Args: { claim: string; uid: string; value: Json } + Returns: string + } set_limit: { Args: { "": number } Returns: number diff --git a/supabase/migrations/20251011223628_remote_schema.sql b/supabase/migrations/20251011223628_remote_schema.sql new file mode 100644 index 0000000..a62bfab --- /dev/null +++ b/supabase/migrations/20251011223628_remote_schema.sql @@ -0,0 +1,186 @@ +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.delete_claim(uid uuid, claim text) + RETURNS text + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ + BEGIN + IF NOT is_claims_admin() THEN + RETURN 'error: access denied'; + ELSE + update auth.users set raw_app_meta_data = + raw_app_meta_data - claim where id = uid; + return 'OK'; + END IF; + END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.get_claim(uid uuid, claim text) + RETURNS jsonb + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ + DECLARE retval jsonb; + BEGIN + IF NOT is_claims_admin() THEN + RETURN '{"error":"access denied"}'::jsonb; + ELSE + select coalesce(raw_app_meta_data->claim, null) from auth.users into retval where id = uid::uuid; + return retval; + END IF; + END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.get_claims(uid uuid) + RETURNS jsonb + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ + DECLARE retval jsonb; + BEGIN + IF NOT is_claims_admin() THEN + RETURN '{"error":"access denied"}'::jsonb; + ELSE + select raw_app_meta_data from auth.users into retval where id = uid::uuid; + return retval; + END IF; + END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.get_my_claim(claim text) + RETURNS jsonb + LANGUAGE sql + STABLE +AS $function$ + select + coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb -> 'app_metadata' -> claim, null) +$function$ +; + +CREATE OR REPLACE FUNCTION public.get_my_claims() + RETURNS jsonb + LANGUAGE sql + STABLE +AS $function$ + select + coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb -> 'app_metadata', '{}'::jsonb)::jsonb +$function$ +; + +CREATE OR REPLACE FUNCTION public.is_claims_admin() + RETURNS boolean + LANGUAGE plpgsql +AS $function$ + BEGIN + IF session_user = 'authenticator' THEN + -------------------------------------------- + -- To disallow any authenticated app users + -- from editing claims, delete the following + -- block of code and replace it with: + -- RETURN FALSE; + -------------------------------------------- + IF extract(epoch from now()) > coalesce((current_setting('request.jwt.claims', true)::jsonb)->>'exp', '0')::numeric THEN + return false; -- jwt expired + END IF; + If current_setting('request.jwt.claims', true)::jsonb->>'role' = 'service_role' THEN + RETURN true; -- service role users have admin rights + END IF; + IF coalesce((current_setting('request.jwt.claims', true)::jsonb)->'app_metadata'->'claims_admin', 'false')::bool THEN + return true; -- user has claims_admin set to true + ELSE + return false; -- user does NOT have claims_admin set to true + END IF; + -------------------------------------------- + -- End of block + -------------------------------------------- + ELSE -- not a user session, probably being called from a trigger or something + return true; + END IF; + END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.set_claim(uid uuid, claim text, value jsonb) + RETURNS text + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ + BEGIN + IF NOT is_claims_admin() THEN + RETURN 'error: access denied'; + ELSE + update auth.users set raw_app_meta_data = + raw_app_meta_data || + json_build_object(claim, value)::jsonb where id = uid; + return 'OK'; + END IF; + END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.hacks_update_guard() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +begin + if not public.is_admin() then + if new.slug is distinct from old.slug then + raise exception 'non-admins cannot change slug'; + end if; + if new.created_by is distinct from old.created_by then + raise exception 'non-admins cannot change created_by'; + end if; + if new.approved is distinct from old.approved then + raise exception 'non-admins cannot change approved'; + end if; + end if; + return new; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION public.handle_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +begin + insert into public.profiles (id, full_name, avatar_url) + values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url'); + return new; +end; +$function$ +; + +CREATE OR REPLACE FUNCTION public.is_admin() + RETURNS boolean + LANGUAGE sql + STABLE +AS $function$ + select + coalesce(get_my_claim('claims_admin')::bool,false) +$function$ +; + +CREATE OR REPLACE FUNCTION public.set_updated_at() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +begin + new.updated_at = now(); + return new; +end; +$function$ +; + + +