MotorSignal-App/firestore.rules

199 lines
7.1 KiB
Plaintext

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function ownsUser(userId) {
return signedIn() && request.auth.uid == userId;
}
function validUserCreate(userId) {
return ownsUser(userId)
&& request.resource.data.id == userId
&& request.resource.data.keys().hasOnly([
'id', 'username', 'displayName', 'profilePhotoUrl', 'avatarPreset', 'bio',
'location', 'createdAt', 'updatedAt', 'privacySettings', 'preferences'
]);
}
function validUserUpdate(userId) {
return ownsUser(userId)
&& request.resource.data.id == resource.data.id
&& request.resource.data.createdAt == resource.data.createdAt
&& request.resource.data.keys().hasOnly([
'id', 'username', 'displayName', 'profilePhotoUrl', 'avatarPreset', 'bio',
'location', 'createdAt', 'updatedAt', 'privacySettings', 'preferences'
]);
}
function validPublicProfileCreate(userId) {
return ownsUser(userId)
&& request.resource.data.id == userId
&& request.resource.data.keys().hasOnly([
'id', 'username', 'displayName', 'profilePhotoUrl', 'avatarPreset', 'bio',
'location', 'isSearchableByHandle', 'isProfilePublic', 'allowFriendRequests',
'updatedAt'
]);
}
function validPublicProfileUpdate(userId) {
return ownsUser(userId)
&& request.resource.data.id == resource.data.id
&& request.resource.data.keys().hasOnly([
'id', 'username', 'displayName', 'profilePhotoUrl', 'avatarPreset', 'bio',
'location', 'isSearchableByHandle', 'isProfilePublic', 'allowFriendRequests',
'updatedAt'
]);
}
function validRideCreate(userId, rideId) {
return ownsUser(userId)
&& request.resource.data.ownerId == userId
&& request.resource.data.id == rideId;
}
function validRideUpdate(userId) {
return ownsUser(userId)
&& resource.data.ownerId == userId
&& request.resource.data.ownerId == resource.data.ownerId
&& request.resource.data.id == resource.data.id;
}
function isBlockedBy(sourceUid, targetUid) {
return exists(/databases/$(database)/documents/users/$(targetUid)/blocks/$(sourceUid));
}
function areMutuallyBlocked(userA, userB) {
return isBlockedBy(userA, userB) || isBlockedBy(userB, userA);
}
// --- HANDLE RESERVATIONS ---
// Internal uniqueness index. Read access is guarded to prevent
// turning handle lookup into an open user enumeration directory.
match /handles/{handle} {
allow list: if false;
allow get: if signedIn() && (
resource == null ||
resource.data.uid == request.auth.uid || (
!areMutuallyBlocked(request.auth.uid, resource.data.uid) &&
get(/databases/$(database)/documents/public_profiles/$(resource.data.uid)).data.isSearchableByHandle == true
)
);
allow create: if signedIn()
&& request.resource.data.uid == request.auth.uid
&& request.resource.data.handle == handle;
allow delete: if signedIn() && resource.data.uid == request.auth.uid;
allow update: if false;
}
// --- PUBLIC PROFILES ---
// Contains ONLY safe public fields. No email, tokens, rides, or billing.
match /public_profiles/{userId} {
allow list: if false;
allow get: if signedIn() && (
ownsUser(userId) || (
!areMutuallyBlocked(request.auth.uid, userId) &&
(
resource.data.isSearchableByHandle == true ||
exists(/databases/$(database)/documents/users/$(userId)/friends/$(request.auth.uid))
)
)
);
allow create: if validPublicProfileCreate(userId);
allow update: if validPublicProfileUpdate(userId);
allow delete: if ownsUser(userId);
}
// --- PRIVATE USER ACCOUNTS ---
// Strictly private to the account owner.
match /users/{userId} {
allow create: if validUserCreate(userId);
allow read: if ownsUser(userId);
allow update: if validUserUpdate(userId);
allow delete: if ownsUser(userId);
match /rides/{rideId} {
allow create: if validRideCreate(userId, rideId);
allow update: if validRideUpdate(userId);
allow list: if ownsUser(userId);
allow get, delete: if ownsUser(userId) && resource.data.ownerId == userId;
match /events/{eventId} {
allow read, write: if ownsUser(userId);
}
match /performance/{runId} {
allow read, write: if ownsUser(userId);
}
}
match /motorcycles/{motorcycleId} {
allow create, update: if ownsUser(userId)
&& request.resource.data.ownerId == userId;
allow read, delete: if ownsUser(userId);
}
match /trips/{tripId} {
allow read, write: if ownsUser(userId);
}
match /events/{eventId} {
allow read, write: if ownsUser(userId);
}
// --- SOCIAL SUBCOLLECTIONS ---
match /friends/{friendId} {
allow read: if ownsUser(userId);
allow create: if signedIn()
&& (request.auth.uid == userId || request.auth.uid == friendId)
&& userId != friendId
&& !areMutuallyBlocked(userId, friendId)
&& (
(request.auth.uid == userId && exists(/databases/$(database)/documents/users/$(userId)/friend_requests_incoming/$(friendId))) ||
(request.auth.uid == friendId && exists(/databases/$(database)/documents/users/$(friendId)/friend_requests_incoming/$(userId)))
);
allow delete: if signedIn() && (request.auth.uid == userId || request.auth.uid == friendId);
allow update: if false;
}
match /friend_requests_incoming/{senderId} {
allow read: if ownsUser(userId);
allow create: if signedIn()
&& request.auth.uid == senderId
&& senderId != userId
&& request.resource.data.receiverUid == userId
&& request.resource.data.senderUid == senderId
&& !areMutuallyBlocked(userId, senderId)
&& !exists(/databases/$(database)/documents/users/$(userId)/friends/$(senderId))
&& get(/databases/$(database)/documents/public_profiles/$(userId)).data.allowFriendRequests == true;
allow delete: if signedIn() && (request.auth.uid == userId || request.auth.uid == senderId);
allow update: if false;
}
match /friend_requests_outgoing/{receiverId} {
allow read: if ownsUser(userId);
allow create: if ownsUser(userId)
&& receiverId != userId
&& request.resource.data.receiverUid == receiverId
&& request.resource.data.senderUid == userId
&& !areMutuallyBlocked(userId, receiverId);
allow delete: if signedIn() && (request.auth.uid == userId || request.auth.uid == receiverId);
allow update: if false;
}
match /blocks/{blockedId} {
allow read, write: if ownsUser(userId);
}
}
match /groupRides/{groupRideId} {
allow read, write: if false;
}
match /{document=**} {
allow read, write: if false;
}
}
}