/* * Member Portal Auth + Invitations * Links members to auth.users, adds invitation system */ -- Add user_id to members (links to Supabase Auth) ALTER TABLE public.members ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS ix_members_user ON public.members(user_id); -- Member portal invitations CREATE TABLE IF NOT EXISTS public.member_portal_invitations ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE, member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, email text NOT NULL, invite_token text NOT NULL DEFAULT gen_random_uuid()::text, status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','accepted','expired','revoked')), invited_by uuid REFERENCES auth.users(id), accepted_at timestamptz, expires_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'), created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_portal_invitations_token ON public.member_portal_invitations(invite_token); CREATE INDEX IF NOT EXISTS ix_portal_invitations_member ON public.member_portal_invitations(member_id); ALTER TABLE public.member_portal_invitations ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.member_portal_invitations FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE ON public.member_portal_invitations TO authenticated; GRANT ALL ON public.member_portal_invitations TO service_role; -- Admins can manage invitations for their account CREATE POLICY portal_invitations_admin ON public.member_portal_invitations FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); -- Anon can read invitation by token (for the accept flow) GRANT SELECT ON public.member_portal_invitations TO anon; CREATE POLICY portal_invitations_anon_read ON public.member_portal_invitations FOR SELECT TO anon USING (status = 'pending' AND expires_at > now()); -- RLS: Members can read their own portal data -- Allow authenticated users to read their own member record via user_id CREATE POLICY members_portal_self_read ON public.members FOR SELECT TO authenticated USING (user_id = auth.uid()); -- Allow members to update their own contact/gdpr fields CREATE POLICY members_portal_self_update ON public.members FOR UPDATE TO authenticated USING (user_id = auth.uid()); -- Add is_members_only flag to site_pages for member-only content ALTER TABLE public.site_pages ADD COLUMN IF NOT EXISTS is_members_only boolean NOT NULL DEFAULT false; -- Function: Link member to auth user after signup CREATE OR REPLACE FUNCTION public.link_member_to_user( p_invite_token text, p_user_id uuid ) RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_member_id uuid; v_account_id uuid; BEGIN -- Find and validate invitation SELECT member_id, account_id INTO v_member_id, v_account_id FROM public.member_portal_invitations WHERE invite_token = p_invite_token AND status = 'pending' AND expires_at > now(); IF v_member_id IS NULL THEN RAISE EXCEPTION 'Invalid or expired invitation'; END IF; -- Link member to user UPDATE public.members SET user_id = p_user_id WHERE id = v_member_id; -- Mark invitation as accepted UPDATE public.member_portal_invitations SET status = 'accepted', accepted_at = now() WHERE invite_token = p_invite_token; RETURN v_member_id; END; $$; GRANT EXECUTE ON FUNCTION public.link_member_to_user(text, uuid) TO authenticated, service_role;