-- Production-like user control: application-level user table (public.profiles) -- auto-provisioned from auth.users, with admin-managed control fields. -- 1. Control columns (inherit existing table-wide grants automatically) ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS role text NOT NULL DEFAULT 'user', ADD COLUMN IF NOT EXISTS is_banned boolean NOT NULL DEFAULT false; -- 2. Backfill profiles for existing auth users (e.g. the current test accounts) INSERT INTO public.profiles (id, email) SELECT id, email FROM auth.users ON CONFLICT (id) DO NOTHING; -- 3. Auto-provision a profile row whenever an auth user is created (signup/admin) CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ BEGIN INSERT INTO public.profiles (id, email) VALUES (new.id, new.email) ON CONFLICT (id) DO NOTHING; RETURN new; END; $$; -- 4. Keep profile email in sync with auth.users email changes CREATE OR REPLACE FUNCTION public.handle_user_email_change() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ BEGIN IF new.email IS DISTINCT FROM old.email THEN UPDATE public.profiles SET email = new.email WHERE id = new.id; END IF; RETURN new; END; $$; DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); DROP TRIGGER IF EXISTS on_auth_user_email_change ON auth.users; CREATE TRIGGER on_auth_user_email_change AFTER UPDATE OF email ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_user_email_change(); -- 5. Users may update their own profile, but must NOT be able to escalate -- their own role or unban themselves. -- NOTE: a column-level REVOKE does NOT override a broad TABLE-level grant. -- The earlier grant migration gave INSERT,UPDATE,DELETE on profiles to -- anon/authenticated (table-wide => every column). So undo the table-level -- write grants for user-facing roles, then re-grant UPDATE only on the -- editable columns. Profiles are created via the trigger, so users need no -- INSERT or DELETE. service_role (backend/admin) keeps full access and -- bypasses RLS. REVOKE INSERT, UPDATE, DELETE ON public.profiles FROM authenticated; REVOKE INSERT, UPDATE, DELETE ON public.profiles FROM anon; GRANT SELECT ON public.profiles TO anon, authenticated; GRANT UPDATE (display_name, avatar_url) ON public.profiles TO authenticated;