diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index f347d25..6ba0e35 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -186,15 +186,42 @@ export type Database = { } Relationships: [] } + user_roles: { + Row: { + created_at: string + id: string + role: Database["public"]["Enums"]["app_role"] + user_id: string + } + Insert: { + created_at?: string + id?: string + role: Database["public"]["Enums"]["app_role"] + user_id: string + } + Update: { + created_at?: string + id?: string + role?: Database["public"]["Enums"]["app_role"] + user_id?: string + } + Relationships: [] + } } Views: { [_ in never]: never } Functions: { - [_ in never]: never + has_role: { + Args: { + _role: Database["public"]["Enums"]["app_role"] + _user_id: string + } + Returns: boolean + } } Enums: { - [_ in never]: never + app_role: "admin" | "user" } CompositeTypes: { [_ in never]: never @@ -321,6 +348,8 @@ export type CompositeTypes< export const Constants = { public: { - Enums: {}, + Enums: { + app_role: ["admin", "user"], + }, }, } as const diff --git a/supabase/migrations/20260711135235_dc1842ae-a480-4bfe-8b1e-f88b475b421e.sql b/supabase/migrations/20260711135235_dc1842ae-a480-4bfe-8b1e-f88b475b421e.sql new file mode 100644 index 0000000..23dd2f5 --- /dev/null +++ b/supabase/migrations/20260711135235_dc1842ae-a480-4bfe-8b1e-f88b475b421e.sql @@ -0,0 +1,62 @@ + +-- Role enum +DO $$ BEGIN + CREATE TYPE public.app_role AS ENUM ('admin', 'user'); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- user_roles table +CREATE TABLE IF NOT EXISTS public.user_roles ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + role public.app_role NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (user_id, role) +); + +GRANT SELECT ON public.user_roles TO authenticated; +GRANT ALL ON public.user_roles TO service_role; + +ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Users can view own roles" ON public.user_roles; +CREATE POLICY "Users can view own roles" + ON public.user_roles FOR SELECT + TO authenticated + USING (auth.uid() = user_id); + +-- Security definer role check +CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role) +RETURNS boolean +LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 FROM public.user_roles + WHERE user_id = _user_id AND role = _role + ) +$$; + +-- Backfill admin for existing owner +INSERT INTO public.user_roles (user_id, role) +SELECT id, 'admin'::public.app_role +FROM auth.users +WHERE lower(email) = 'tiffanysvson@gmail.com' +ON CONFLICT (user_id, role) DO NOTHING; + +-- Auto-grant admin on future signup with owner email +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS trigger +LANGUAGE plpgsql SECURITY DEFINER SET search_path = public +AS $$ +BEGIN + INSERT INTO public.profiles (user_id) VALUES (NEW.id) + ON CONFLICT DO NOTHING; + + IF lower(NEW.email) = 'tiffanysvson@gmail.com' THEN + INSERT INTO public.user_roles (user_id, role) + VALUES (NEW.id, 'admin') + ON CONFLICT (user_id, role) DO NOTHING; + END IF; + + RETURN NEW; +END; +$$;