fix(desktop+supabase): OAuth 로그인 완주 + RLS 재귀 픽스 (빅뱅 Phase 5 Part 1)

빅뱅 Phase 5 실증 중 발견한 3개 버그 해결:

1. implicit flow 응답 무시:
   - supabase-js가 persistSession:false 일 때 PKCE code_verifier 저장 못해
     implicit flow로 fallback, fragment(#access_token=...&refresh_token=...)로
     토큰 전달. handleDeepLink는 ?code=만 파싱하고 있어 무시됨.
   - handleDeepLink에 fragment parser 추가 (access_token + refresh_token)
   - CloudSyncService.handleAuthTokens() 신규 — setSession() 후
     기존 _onAuthenticated SSOT로 수렴
   - 에러 query/fragment(error, error_description) 상세 로깅
   - 토큰 값은 로그에 안 찍히도록 (query=yes/no, fragment=yes/no 만)

2. Supabase RLS 무한 재귀:
   - team_members_read_same_team 정책이 자기 테이블 재조회 →
     Postgres RLS 엔진 무한 재귀 탐지 에러
   - meetings / meeting_memos / meeting_documents / team_invites 정책이
     team_members 서브쿼리 경유해서 전부 같이 터짐 + Realtime TIMED_OUT
   - migration 20260411000001: SECURITY DEFINER 함수 2개
     (user_team_ids, user_admin_team_ids) 신규 — Supabase 권장 패턴
   - 영향 정책: team_members(4) + meetings(2) + meeting_memos(1) +
     meeting_documents(2) + team_invites(1) 전부 함수 기반으로 재작성
   - supabase db push 완료

3. LoginScreen stale 번들:
   - Phase 1.5에서 import 제거했는데도 vite HMR/cache 어딘가에서
     stale state 유지해서 렌더러에 계속 뜸
   - LoginScreen.tsx 파일 자체 삭제 (vite 컴파일 대상 제거)

로그인 성공 확인: yunchan8804@gmail.com 으로 Google OAuth 완주 →
users/7da3dd02-9f2f-4ee9-a9b3-1c2c24875a93/d3ro.db 생성 → Initial sync 시작.
This commit is contained in:
윤찬 2026-04-11 11:55:42 +09:00
parent 817b4a2580
commit 9498380f7f
5 changed files with 305 additions and 125 deletions

View file

@ -0,0 +1,128 @@
-- ============================================================================
-- 빅뱅 Phase 5 실증 중 발견: team_members RLS 정책이 자기 테이블을 재조회해서
-- "infinite recursion detected in policy for relation team_members" 에러.
--
-- meetings / meeting_memos / meeting_documents / team_invites 정책이
-- team_members 서브쿼리를 경유하면서 재귀가 트리거되어 pull 전부 실패 +
-- Realtime 구독 TIMED_OUT 발생.
--
-- 해결: SECURITY DEFINER 함수로 team_id 조회를 감싸 RLS 우회 + 재귀 끊기.
-- 이건 Supabase 공식 권장 패턴.
-- ============================================================================
-- ── 헬퍼 함수 ────────────────────────────────────────────────────────────
CREATE OR REPLACE FUNCTION public.user_team_ids(uid uuid)
RETURNS SETOF uuid
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT team_id FROM public.team_members WHERE user_id = uid;
$$;
CREATE OR REPLACE FUNCTION public.user_admin_team_ids(uid uuid)
RETURNS SETOF uuid
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT team_id FROM public.team_members
WHERE user_id = uid AND role IN ('owner', 'admin');
$$;
REVOKE ALL ON FUNCTION public.user_team_ids(uuid) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.user_admin_team_ids(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.user_team_ids(uuid) TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.user_admin_team_ids(uuid) TO authenticated, service_role;
-- ── team_members: 재귀 제거 ───────────────────────────────────────────────
DROP POLICY IF EXISTS "team_members_read_same_team" ON public.team_members;
CREATE POLICY "team_members_read_same_team" ON public.team_members
FOR SELECT USING (
user_id = auth.uid()
OR team_id IN (SELECT public.user_team_ids(auth.uid()))
);
DROP POLICY IF EXISTS "team_members_insert_admin" ON public.team_members;
CREATE POLICY "team_members_insert_admin" ON public.team_members
FOR INSERT WITH CHECK (
team_id IN (SELECT public.user_admin_team_ids(auth.uid()))
);
DROP POLICY IF EXISTS "team_members_update_admin" ON public.team_members;
CREATE POLICY "team_members_update_admin" ON public.team_members
FOR UPDATE USING (
team_id IN (SELECT public.user_admin_team_ids(auth.uid()))
);
DROP POLICY IF EXISTS "team_members_delete_admin_or_self" ON public.team_members;
CREATE POLICY "team_members_delete_admin_or_self" ON public.team_members
FOR DELETE USING (
user_id = auth.uid()
OR team_id IN (SELECT public.user_admin_team_ids(auth.uid()))
);
-- ── meetings: team_members 직접 참조 → 함수 호출로 교체 ───────────────────
DROP POLICY IF EXISTS "meetings_read" ON public.meetings;
CREATE POLICY "meetings_read" ON public.meetings
FOR SELECT USING (
user_id = auth.uid()
OR (team_id IS NOT NULL AND team_id IN (SELECT public.user_team_ids(auth.uid())))
);
DROP POLICY IF EXISTS "meetings_update" ON public.meetings;
CREATE POLICY "meetings_update" ON public.meetings
FOR UPDATE USING (
user_id = auth.uid()
OR (team_id IS NOT NULL AND team_id IN (SELECT public.user_admin_team_ids(auth.uid())))
);
-- ── meeting_memos: meetings RLS 경유 + insert 시 team 체크 ───────────────
DROP POLICY IF EXISTS "meeting_memos_insert" ON public.meeting_memos;
CREATE POLICY "meeting_memos_insert" ON public.meeting_memos
FOR INSERT WITH CHECK (
user_id = auth.uid()
AND meeting_id IN (
SELECT id FROM public.meetings
WHERE user_id = auth.uid()
OR (team_id IS NOT NULL AND team_id IN (SELECT public.user_team_ids(auth.uid())))
)
);
-- ── meeting_documents: 동일 패턴 ──────────────────────────────────────────
DROP POLICY IF EXISTS "meeting_documents_insert" ON public.meeting_documents;
CREATE POLICY "meeting_documents_insert" ON public.meeting_documents
FOR INSERT WITH CHECK (
user_id = auth.uid()
AND meeting_id IN (
SELECT id FROM public.meetings
WHERE user_id = auth.uid()
OR (team_id IS NOT NULL AND team_id IN (SELECT public.user_team_ids(auth.uid())))
)
);
DROP POLICY IF EXISTS "meeting_documents_update" ON public.meeting_documents;
CREATE POLICY "meeting_documents_update" ON public.meeting_documents
FOR UPDATE USING (
user_id = auth.uid()
OR meeting_id IN (
SELECT id FROM public.meetings
WHERE team_id IN (SELECT public.user_admin_team_ids(auth.uid()))
)
);
-- ── team_invites: 같은 패턴 오염 ──────────────────────────────────────────
DROP POLICY IF EXISTS "team_invites_read" ON public.team_invites;
CREATE POLICY "team_invites_read" ON public.team_invites
FOR SELECT USING (
team_id IN (SELECT public.user_team_ids(auth.uid()))
OR lower(email) = lower((SELECT email FROM auth.users WHERE id = auth.uid()))
);