Files
2026-04-23 23:58:59 -05:00

72 lines
2.2 KiB
Plaintext

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());
}
}
}