rules_version = '2'; // FANUZU — Cloud Storage 보안 규칙 // // 근거 // - 정책 원장 B-44 「Community 미디어」 — *업로드 → 클라이언트 1차 검증 → // Storage 규칙 서버 검증 → 게시*. 상한은 이미지 10MB · 영상 100MB/60초. // - firestore.rules 의 `users/{uid}` — `photoUrl` 은 본인이 고칠 수 있는 필드다. // // 원칙 // 1) 클라이언트 검증은 **거를 수 있을 뿐 막지는 못한다.** 용량과 형식을 여기서 // 한 번 더 본다. 앱을 고쳐 올리는 경로가 있어도 규칙은 우회되지 않는다. // 2) 남의 경로에는 쓰지 못한다. 경로 첫 칸이 UID 이고 토큰의 UID 와 같아야 한다. // 3) 열지 않은 경로는 전부 닫는다(마지막 블록). // // 다운로드 URL 에 대하여 // `getDownloadURL()` 이 만드는 URL 은 토큰이 붙은 공개 링크이고 **본 규칙의 read // 판정을 지나지 않는다.** 프로필 사진은 프로필 카드·라운지에서 다른 사용자에게 // 보여야 하므로 그 성격이 맞다. 여기 `read` 는 SDK 로 바이트를 직접 읽는 경로만 // 가린다. service firebase.storage { match /b/{bucket}/o { function isSelf(uid) { return request.auth != null && request.auth.uid == uid; } // 프로필 사진 — `FirestoreMyRepository.uploadProfilePhoto` 가 쓰는 경로. // 사용자당 한 장으로 고정되어 있어 바꿀 때 이전 파일이 남지 않는다. match /users/{uid}/profile/{fileName} { allow read: if request.auth != null; allow write: if isSelf(uid) && request.resource.size <= 10 * 1024 * 1024 && request.resource.contentType.matches('image/(jpeg|png|webp|gif)'); // 사진을 지우는 동선이 아직 없다. 여는 순간 「되돌릴 수 없는 삭제」의 // 확인 동선까지 필요해지므로 필요해지는 회차에 함께 연다. allow delete: if false; } // 캠페인 결과보고 집행 사진 · 영수증 — 정책 원장 B-35 · FR-ADM-05 「결과보고는 Admin 수동 운영」. // 운영(master·ops)만 올린다. `fn-campaign-report-submit` 이 이 경로 접두와 파일 존재를 // 다시 본다. 결과보고는 공개 캠페인 상세에 뜨므로 읽기는 열어 둔다. 상한은 B-44 10MB. match /campaignReports/{campaignId}/{fileName} { allow read: if true; allow create: if request.auth != null && request.auth.token.get('role', '') in ['master', 'ops'] && request.resource.size <= 10 * 1024 * 1024 && request.resource.contentType.matches('image/(jpeg|png|webp|gif)'); allow update, delete: if false; } // 팬덤 라운지 글 사진 — `FirestorePlanetRepository.createPost` 가 쓰는 경로. // B-44 「이미지 5 장 10MB」의 1 장 상한 · 형식을 여기서 보고, 장수(5)는 // firestore.rules 의 `posts.imageUrls.size()` 가 본다. 팬덤 회원 판정도 글 문서 // 쪽 규칙이 한다 — 회원이 아니면 글이 거부되고 앱이 올린 사진을 지운다. // 삭제는 본인만(글 삭제 · 글 저장 실패 시 되돌리기). match /users/{uid}/lounge/{fileName} { allow read: if request.auth != null; // `resource == null` — 덮어쓰기 금지. 글의 URL 이 가리키는 내용이 바뀌면 안 된다. // 파일 이름 = 「글 ID(Firestore 자동 ID 20자)-칸 번호(0~4)」. 글 문서의 규칙이 // 칸마다 같은 경로만 받으므로 다른 이름의 파일은 어디에도 붙지 않는다. allow create: if isSelf(uid) && resource == null && fileName.matches('[A-Za-z0-9]{20}-[0-4]') && request.resource.size <= 10 * 1024 * 1024 && request.resource.contentType.matches('image/(jpeg|png|webp|gif)'); allow update: if false; allow delete: if isSelf(uid); } // 위치 Quest 방문 인증 사진 — 정책 원장 C-03 · PRD-05 「GPS · 사진 · QR 인증」. `fn-quest-verify-place` 가 // 이 경로 접두(`questProofs/{본인}/{questId}/`)와 파일 존재를 다시 본다. 운영 검토용이라 본인 · 서버만 읽는다. // 덮어쓰기 · 삭제 금지 — 제출한 증거가 바뀌면 안 된다. 상한은 B-44 이미지 10MB. match /questProofs/{uid}/{questId}/{fileName} { allow read: if isSelf(uid); allow create: if isSelf(uid) && resource == null && request.resource.size <= 10 * 1024 * 1024 && request.resource.contentType.matches('image/(jpeg|png|webp)'); allow update, delete: if false; } // 그 밖의 경로는 닫는다. 인사이드 미디어 · 라운지 영상(B-44 영상 100MB/60초)은 // 업로드 동선이 들어오는 회차에 같은 방식으로 상한과 함께 연다. match /{allPaths=**} { allow read, write: if false; } } }