rules_version = '2'; service firebase.storage { match /b/{bucket}/o { function isSignedIn() { return request.auth != null; } // Single Firestore read — Storage rules allow at most 2 Firestore document accesses // per evaluation; pairing exists()+get() on users then on upcoming/manga exceeds that. function isAdmin() { return isSignedIn() && firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.role == 'admin'; } function isReasonableImage() { return request.resource != null && request.resource.size < 15 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); } // fileName like "{upcomingDocId}.jpg" — Firestore row must exist; get() fails if missing. function ownsUpcomingCover(fileName) { return isSignedIn() && fileName.matches('.*\\.jpg') && fileName.split('.').size() == 2 && firestore.get(/databases/(default)/documents/upcoming/$(fileName.split('.')[0])).data.authorId == request.auth.uid; } function mangaAuthorFromJpg(fileName) { return isSignedIn() && fileName.split('.').size() == 2 && fileName.split('.')[1] == 'jpg' && firestore.get(/databases/(default)/documents/manga/$(fileName.split('.')[0])).data.authorId == request.auth.uid; } match /{allPaths=**} { allow read: if true; } match /avatars/{fileName} { allow write: if isSignedIn() && isReasonableImage() && fileName == request.auth.uid + '.jpg'; } match /social/{fileName} { allow write: if isSignedIn() && isReasonableImage(); } match /social_images/{fileName} { allow write: if isSignedIn() && isReasonableImage(); } match /series_covers/{fileName} { allow write: if isReasonableImage() && (mangaAuthorFromJpg(fileName) || isAdmin()); } match /series_banners/{fileName} { allow write: if isReasonableImage() && (mangaAuthorFromJpg(fileName) || isAdmin()); } match /upcoming_covers/{fileName} { allow write: if isReasonableImage() && (ownsUpcomingCover(fileName) || isAdmin()); } } }