rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // --- HELPERS --- function isSignedIn() { return request.auth != null; } function isAdmin() { return isSignedIn() && exists(/databases/$(database)/documents/users/$(request.auth.uid)) && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin'; } // Toggle like on social_posts: only likedBy + likes may change; count must match list length. function isSocialLikeReactionUpdate() { return isSignedIn() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['likedBy', 'likes']) && request.resource.data.likedBy is list && request.resource.data.likes == request.resource.data.likedBy.size(); } // --- USERS --- match /users/{userId} { allow get: if true; allow list: if isAdmin(); allow create: if isSignedIn() && request.auth.uid == userId; allow update: if isAdmin() || ( isSignedIn() && request.auth.uid == userId && !request.resource.data.diff(resource.data).affectedKeys() .hasAny(['rankLevel', 'role', 'isVerified', 'isBanned']) ); match /{allPaths=**} { allow read, write: if isSignedIn() && (request.auth.uid == userId || isAdmin()); } } // --- SOCIAL --- match /social_posts/{postId} { allow read: if true; allow create: if isSignedIn() && request.resource.data.authorId == request.auth.uid; allow update: if isAdmin() || ( isSignedIn() && resource.data.authorId == request.auth.uid && request.resource.data.authorId == resource.data.authorId ) || isSocialLikeReactionUpdate(); allow delete: if isAdmin() || (isSignedIn() && resource.data.authorId == request.auth.uid); match /comments/{commentId} { allow read: if true; allow create: if isSignedIn() && request.resource.data.uid == request.auth.uid; allow update, delete: if isAdmin() || (isSignedIn() && resource.data.uid == request.auth.uid); } } // --- UPCOMING (Soon tab; artists manage own rows) --- match /upcoming/{announcementId} { allow read: if true; allow create: if isSignedIn() && request.resource.data.authorId == request.auth.uid && request.resource.data.keys().hasAll([ 'kind', 'authorId', 'authorName', 'seriesTitle' ]) && request.resource.data.kind in ['new_series', 'chapter_drop'] && ( request.resource.data.dateTbd == true || request.resource.data.targetDate is timestamp ); allow update: if isAdmin() || ( isSignedIn() && resource.data.authorId == request.auth.uid && request.resource.data.authorId == resource.data.authorId ); allow delete: if isAdmin() || (isSignedIn() && resource.data.authorId == request.auth.uid); } // --- MANGA & CHAPTERS --- match /manga/{mangaId} { allow read: if true; // Anyone signed in can bump read counts; series author can edit portfolio fields. allow update: if isAdmin() || (isSignedIn() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['reads'])) || (isSignedIn() && resource.data.keys().hasAll(['authorId']) && resource.data.authorId == request.auth.uid && request.resource.data.authorId == resource.data.authorId && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['coverUrl', 'bannerUrl', 'synopsis', 'aboutArtist', 'socials'])); allow write: if isAdmin(); match /chapters/{chapterId} { allow read: if true; allow create, update: if isAdmin(); allow delete: if isAdmin() || (isSignedIn() && exists(/databases/$(database)/documents/manga/$(mangaId)) && get(/databases/$(database)/documents/manga/$(mangaId)).data.authorId == request.auth.uid); match /page_interactions/{pageId} { allow read: if true; allow create, update: if isSignedIn(); match /comments/{commentId} { allow read: if true; allow create: if isSignedIn() && request.resource.data.uid == request.auth.uid; allow update: if isAdmin() || (isSignedIn() && resource.data.uid == request.auth.uid); allow delete: if isAdmin() || (isSignedIn() && resource.data.uid == request.auth.uid); } } } } // --- MARKETPLACE --- match /marketplace/{itemId} { allow read: if true; allow write: if isAdmin(); } // Sign-up validates a known code via get(); listing all codes is admin-only. match /invite_codes/{code} { allow get: if true; allow list: if isAdmin(); allow create, update, delete: if isAdmin(); } // --- SYSTEM BROADCAST --- match /system/announcement { allow read: if true; allow write: if isAdmin(); } } }