diff --git a/app/lib/screens/study/onboarding/study_selection.dart b/app/lib/screens/study/onboarding/study_selection.dart index 0f60e8ad7..6da27176e 100644 --- a/app/lib/screens/study/onboarding/study_selection.dart +++ b/app/lib/screens/study/onboarding/study_selection.dart @@ -300,6 +300,7 @@ class _InviteCodeDialogState extends State { .from('study_invite') .select('preselected_intervention_ids') .eq('code', _controller.text) + .eq('study_id', study.id) .maybeSingle(); if (!context.mounted) return; if (inviteResult != null && diff --git a/database/migration/20260506_scope_invite_codes_to_study.sql b/database/migration/20260506_scope_invite_codes_to_study.sql new file mode 100644 index 000000000..5a375951f --- /dev/null +++ b/database/migration/20260506_scope_invite_codes_to_study.sql @@ -0,0 +1,61 @@ +BEGIN; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'study_invite_study_id_code_unique' + AND conrelid = 'public.study_invite'::regclass + ) THEN + ALTER TABLE public.study_invite + ADD CONSTRAINT study_invite_study_id_code_unique UNIQUE (study_id, code); + END IF; +END $$; + +ALTER TABLE public.study_subject + DROP CONSTRAINT IF EXISTS "study_subject_loginCode_fkey"; + +ALTER TABLE public.study_subject + DROP CONSTRAINT IF EXISTS study_subject_study_invite_fkey; + +ALTER TABLE public.study_subject + ADD CONSTRAINT study_subject_study_invite_fkey + FOREIGN KEY (study_id, invite_code) + REFERENCES public.study_invite(study_id, code) + ON DELETE CASCADE; + +CREATE OR REPLACE FUNCTION public.is_invite_code_for_study( + study_id uuid, + invite_code text +) +RETURNS boolean +LANGUAGE sql +STABLE +SECURITY DEFINER +SET search_path = '' +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.study_invite + WHERE study_invite.study_id = is_invite_code_for_study.study_id + AND study_invite.code = is_invite_code_for_study.invite_code + ); +$$; + +DROP POLICY IF EXISTS "Invite code must match study_id" ON public.study_subject; + +CREATE POLICY "Invite code must match study_id" +ON public.study_subject +AS RESTRICTIVE +FOR INSERT +TO authenticated +WITH CHECK ( + invite_code IS NULL + OR public.is_invite_code_for_study(study_id, invite_code) +); + +REVOKE EXECUTE ON FUNCTION public.is_invite_code_for_study(uuid, text) +FROM public, anon; + +COMMIT; diff --git a/database/studyu-schema.sql b/database/studyu-schema.sql index 90612a30b..7aaa7a395 100644 --- a/database/studyu-schema.sql +++ b/database/studyu-schema.sql @@ -212,6 +212,22 @@ $$; ALTER FUNCTION "public"."get_study_record_from_invite"("invite_code" "text") OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."is_invite_code_for_study"("study_id" "uuid", "invite_code" "text") RETURNS boolean + LANGUAGE "sql" STABLE SECURITY DEFINER + SET "search_path" TO '' + AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.study_invite + WHERE study_invite.study_id = is_invite_code_for_study.study_id + AND study_invite.code = is_invite_code_for_study.invite_code + ); +$$; + + +ALTER FUNCTION "public"."is_invite_code_for_study"("study_id" "uuid", "invite_code" "text") OWNER TO "postgres"; + + CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER SET "search_path" TO '' @@ -611,6 +627,11 @@ ALTER TABLE ONLY "public"."study_invite" +ALTER TABLE ONLY "public"."study_invite" + ADD CONSTRAINT "study_invite_study_id_code_unique" UNIQUE ("study_id", "code"); + + + ALTER TABLE ONLY "public"."study" ADD CONSTRAINT "study_pkey" PRIMARY KEY ("id"); @@ -660,7 +681,7 @@ ALTER TABLE ONLY "public"."study_invite" ALTER TABLE ONLY "public"."study_subject" - ADD CONSTRAINT "study_subject_loginCode_fkey" FOREIGN KEY ("invite_code") REFERENCES "public"."study_invite"("code") ON DELETE CASCADE; + ADD CONSTRAINT "study_subject_study_invite_fkey" FOREIGN KEY ("study_id", "invite_code") REFERENCES "public"."study_invite"("study_id", "code") ON DELETE CASCADE; @@ -738,7 +759,7 @@ CREATE POLICY "Enable read access for study participants for fitbit credential" -CREATE POLICY "Invite code must match study_id" ON "public"."study_subject" AS RESTRICTIVE FOR INSERT TO "authenticated" WITH CHECK ((("invite_code" IS NULL) OR ("study_id" IN ( SELECT ("public"."get_study_record_from_invite"("study_subject"."invite_code"))."id" AS "id")))); +CREATE POLICY "Invite code must match study_id" ON "public"."study_subject" AS RESTRICTIVE FOR INSERT TO "authenticated" WITH CHECK ((("invite_code" IS NULL) OR "public"."is_invite_code_for_study"("study_id", "invite_code"))); @@ -848,6 +869,7 @@ REVOKE EXECUTE ON FUNCTION public.last_completed_task(uuid) FROM public, anon; -- RPC/API functions REVOKE EXECUTE ON FUNCTION public.get_study_record_from_invite(text) FROM public, anon; +REVOKE EXECUTE ON FUNCTION public.is_invite_code_for_study(uuid, text) FROM public, anon; -- Trigger functions REVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM public, anon;