빅뱅 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 시작.
128 lines
5.2 KiB
PL/PgSQL
128 lines
5.2 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 빅뱅 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()))
|
|
);
|