feat(V2-2): Supabase 인프라 — 스키마 + RLS + Edge Functions 스캐폴딩
server/supabase/ 신규 디렉토리:
config.toml — Supabase CLI 설정
- 프로젝트 ID, DB 포트, auth providers (Google/GitHub/Apple),
edge_runtime, functions.*.verify_jwt 설정
migrations/ (PostgreSQL DDL, 시간순):
- 20260409000001_initial_schema.sql
12개 테이블: profiles, teams, team_members, meetings,
meeting_memos, meeting_documents, transcripts, history,
dictionary, memo_tags, daily_usage, subscriptions
+ 인덱스 + FK CASCADE + transcripts Realtime publication
- 20260409000002_rls_policies.sql
개인 전용(history/dictionary): user_id = auth.uid()
팀 공유(meetings 등): 본인 OR team_members 조회 subquery
daily_usage: 읽기만, 쓰기는 service_role RPC
- 20260409000003_auth_triggers.sql
handle_new_user — auth.users INSERT → profiles+subscriptions 자동 생성
moddatetime — 8개 테이블 updated_at 자동 갱신
increment_daily_usage — service_role 전용 쿼터 RPC
- 20260409000004_storage_buckets.sql
audio/exports/avatars 3개 버킷 + 경로 기반 접근 정책
(파일 경로가 {user_id}/...로 시작해야 쓰기 허용)
functions/ (Deno/TypeScript Edge Functions):
- _shared/cors.ts — CORS 헤더 + preflight 핸들러
- _shared/auth.ts — requireUser (JWT 검증 + User 반환)
- _shared/quota.ts — 티어별 쿼터 체크 + consume + service role client
- stt-proxy/index.ts — Google Cloud STT 래퍼 스캐폴딩
placeholder 응답, 실제 API 호출 코드는 주석으로 포함
- llm-proxy/index.ts — Anthropic Messages API 래퍼 스캐폴딩
티어별 허용 모델 정책 (free=Haiku, pro=Sonnet, team=Opus)
설계 문서:
- docs/v2/phase-V2-2.md — 상세 설계 (스키마/RLS/Edge Functions/Realtime)
- docs/v2/phase-V2-2-setup.md — 사용자 액션 가이드 (Supabase 계정/
OAuth 등록/CLI/배포/검증)
apps/desktop typecheck 통과 (server/는 Deno 런타임이라 별도).
실제 Supabase 프로젝트 배포는 사용자가 phase-V2-2-setup.md 따라 수행.
This commit is contained in:
parent
3524e958ba
commit
97eb886ec3
16 changed files with 1675 additions and 8 deletions
205
server/supabase/migrations/20260409000002_rls_policies.sql
Normal file
205
server/supabase/migrations/20260409000002_rls_policies.sql
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
-- ============================================================================
|
||||
-- Phase V2-2: Row Level Security 정책
|
||||
-- 패턴:
|
||||
-- - 개인 전용 테이블: user_id = auth.uid()
|
||||
-- - 팀 공유 테이블: 본인 소유 OR 소속 팀의 리소스
|
||||
-- ============================================================================
|
||||
|
||||
-- RLS 활성화 ------------------------------------------------------------------
|
||||
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.teams ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.team_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.meetings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.meeting_memos ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.meeting_documents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.transcripts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.history ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.dictionary ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.memo_tags ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.daily_usage ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============================================================================
|
||||
-- profiles: 본인 프로필만 읽기/수정
|
||||
-- ============================================================================
|
||||
CREATE POLICY "profiles_read_own" ON public.profiles
|
||||
FOR SELECT USING (id = auth.uid());
|
||||
|
||||
CREATE POLICY "profiles_update_own" ON public.profiles
|
||||
FOR UPDATE USING (id = auth.uid()) WITH CHECK (id = auth.uid());
|
||||
|
||||
-- ============================================================================
|
||||
-- teams: 본인 소유 OR 멤버인 팀만 조회, 본인만 생성/수정/삭제
|
||||
-- ============================================================================
|
||||
CREATE POLICY "teams_read_member" ON public.teams
|
||||
FOR SELECT USING (
|
||||
owner_id = auth.uid()
|
||||
OR id IN (SELECT team_id FROM public.team_members WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "teams_insert_own" ON public.teams
|
||||
FOR INSERT WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
CREATE POLICY "teams_update_owner" ON public.teams
|
||||
FOR UPDATE USING (owner_id = auth.uid()) WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
CREATE POLICY "teams_delete_owner" ON public.teams
|
||||
FOR DELETE USING (owner_id = auth.uid());
|
||||
|
||||
-- ============================================================================
|
||||
-- team_members: 같은 팀 멤버는 서로 조회 가능, 관리는 owner/admin
|
||||
-- ============================================================================
|
||||
CREATE POLICY "team_members_read_same_team" ON public.team_members
|
||||
FOR SELECT USING (
|
||||
team_id IN (SELECT team_id FROM public.team_members WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "team_members_insert_admin" ON public.team_members
|
||||
FOR INSERT WITH CHECK (
|
||||
team_id IN (
|
||||
SELECT team_id FROM public.team_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "team_members_update_admin" ON public.team_members
|
||||
FOR UPDATE USING (
|
||||
team_id IN (
|
||||
SELECT team_id FROM public.team_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "team_members_delete_admin_or_self" ON public.team_members
|
||||
FOR DELETE USING (
|
||||
user_id = auth.uid() -- 본인 탈퇴
|
||||
OR team_id IN (
|
||||
SELECT team_id FROM public.team_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- 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 team_id FROM public.team_members WHERE user_id = auth.uid()
|
||||
))
|
||||
);
|
||||
|
||||
CREATE POLICY "meetings_insert" ON public.meetings
|
||||
FOR INSERT WITH CHECK (user_id = auth.uid());
|
||||
|
||||
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 team_id FROM public.team_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
||||
))
|
||||
);
|
||||
|
||||
CREATE POLICY "meetings_delete" ON public.meetings
|
||||
FOR DELETE USING (user_id = auth.uid());
|
||||
|
||||
-- ============================================================================
|
||||
-- meeting_memos: 회의와 동일한 권한 (meeting RLS 경유)
|
||||
-- ============================================================================
|
||||
CREATE POLICY "meeting_memos_read" ON public.meeting_memos
|
||||
FOR SELECT USING (
|
||||
meeting_id IN (SELECT id FROM public.meetings)
|
||||
);
|
||||
|
||||
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 IN (
|
||||
SELECT team_id FROM public.team_members WHERE user_id = auth.uid()
|
||||
))
|
||||
);
|
||||
|
||||
CREATE POLICY "meeting_memos_update_own" ON public.meeting_memos
|
||||
FOR UPDATE USING (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "meeting_memos_delete_own" ON public.meeting_memos
|
||||
FOR DELETE USING (user_id = auth.uid());
|
||||
|
||||
-- ============================================================================
|
||||
-- meeting_documents: 동일 패턴
|
||||
-- ============================================================================
|
||||
CREATE POLICY "meeting_documents_read" ON public.meeting_documents
|
||||
FOR SELECT USING (
|
||||
meeting_id IN (SELECT id FROM public.meetings)
|
||||
);
|
||||
|
||||
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 IN (
|
||||
SELECT team_id FROM public.team_members WHERE user_id = auth.uid()
|
||||
))
|
||||
);
|
||||
|
||||
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 team_id FROM public.team_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "meeting_documents_delete_own" ON public.meeting_documents
|
||||
FOR DELETE USING (user_id = auth.uid());
|
||||
|
||||
-- ============================================================================
|
||||
-- transcripts: 회의 RLS 경유
|
||||
-- ============================================================================
|
||||
CREATE POLICY "transcripts_read" ON public.transcripts
|
||||
FOR SELECT USING (
|
||||
meeting_id IN (SELECT id FROM public.meetings)
|
||||
);
|
||||
|
||||
CREATE POLICY "transcripts_insert" ON public.transcripts
|
||||
FOR INSERT WITH CHECK (
|
||||
meeting_id IN (SELECT id FROM public.meetings WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "transcripts_update" ON public.transcripts
|
||||
FOR UPDATE USING (
|
||||
meeting_id IN (SELECT id FROM public.meetings WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "transcripts_delete" ON public.transcripts
|
||||
FOR DELETE USING (
|
||||
meeting_id IN (SELECT id FROM public.meetings WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- history, dictionary, memo_tags, daily_usage: 본인 전용
|
||||
-- ============================================================================
|
||||
CREATE POLICY "history_own" ON public.history
|
||||
FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "dictionary_own" ON public.dictionary
|
||||
FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "memo_tags_own" ON public.memo_tags
|
||||
FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "daily_usage_read_own" ON public.daily_usage
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
|
||||
-- daily_usage INSERT/UPDATE는 Edge Function(service role)만 수행
|
||||
-- 일반 유저는 읽기만 가능
|
||||
|
||||
-- ============================================================================
|
||||
-- subscriptions: 본인 읽기만. Stripe webhook이 service role로 쓰기
|
||||
-- ============================================================================
|
||||
CREATE POLICY "subscriptions_read_own" ON public.subscriptions
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
Loading…
Add table
Add a link
Reference in a new issue