-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
157 lines (133 loc) · 7.3 KB
/
firestore.rules
File metadata and controls
157 lines (133 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ── Helper: get the authenticated user's email (lowercase) ──
function userEmail() {
return request.auth.token.email.lower();
}
// ── Helper: check if user is a member of a group (email or legacy UID) ──
function isGroupMember(groupId) {
let members = get(/databases/$(database)/documents/groups/$(groupId)).data.members;
return userEmail() in members || request.auth.uid in members;
}
// ── Helper: get the group creator's email ──
function groupCreator(groupId) {
return get(/databases/$(database)/documents/groups/$(groupId)).data.createdBy;
}
// ── users/{userId} ──
// Any authenticated user can read user docs (needed for member name lookups).
// Users can only write their own document.
match /users/{userId} {
allow read: if request.auth != null;
allow create: if request.auth != null && request.auth.uid == userId;
allow update: if request.auth != null && request.auth.uid == userId;
allow delete: if request.auth != null && request.auth.uid == userId;
}
// ── groups/{groupId} ──
match /groups/{groupId} {
// Only members can read the group (supports both email and legacy UID in members array)
allow read: if request.auth != null
&& (userEmail() in resource.data.members
|| request.auth.uid in resource.data.members);
// Authenticated users can create groups (they must include themselves as a member)
allow create: if request.auth != null
&& userEmail() in request.resource.data.members
&& userEmail() == request.resource.data.createdBy
&& request.resource.data.members is list
&& request.resource.data.members.size() <= 2
&& request.resource.data.keys().hasAll(['name', 'members', 'createdBy', 'createdAt'])
&& request.resource.data.name is string
&& request.resource.data.name.size() <= 50;
// Only members can update the group; enforce member cap
allow update: if request.auth != null
&& (userEmail() in resource.data.members
|| request.auth.uid in resource.data.members)
&& request.resource.data.members.size() <= 2;
// Creator can delete, or sole remaining member can delete (for account cleanup)
allow delete: if request.auth != null
&& (userEmail() == resource.data.createdBy
|| (resource.data.members.size() == 1
&& userEmail() in resource.data.members));
// ── groups/{groupId}/bills/{billId} ──
match /bills/{billId} {
allow read: if request.auth != null && isGroupMember(groupId);
// Validate bill shape on create
allow create: if request.auth != null && isGroupMember(groupId)
&& request.resource.data.keys().hasAll(['paidBy', 'amount', 'description', 'category', 'createdAt', 'settled'])
&& request.resource.data.amount is number
&& request.resource.data.amount > 0
&& request.resource.data.amount <= 999999.99
&& request.resource.data.paidBy is string
&& request.resource.data.description is string
&& request.resource.data.settled is bool;
allow update: if request.auth != null && isGroupMember(groupId)
&& request.resource.data.amount is number
&& request.resource.data.amount > 0;
// Any group member can delete bills
allow delete: if request.auth != null && isGroupMember(groupId);
}
// ── groups/{groupId}/settlements/{settlementId} ──
match /settlements/{settlementId} {
allow read: if request.auth != null && isGroupMember(groupId);
// Validate settlement shape on create
allow create: if request.auth != null && isGroupMember(groupId)
&& request.resource.data.keys().hasAll(['from', 'to', 'amount', 'settledAt'])
&& request.resource.data.amount is number
&& request.resource.data.amount > 0
&& request.resource.data.amount <= 999999.99
&& request.resource.data.from is string
&& request.resource.data.to is string;
allow update: if request.auth != null && isGroupMember(groupId);
// Any group member can delete settlements
allow delete: if request.auth != null && isGroupMember(groupId);
}
// ── groups/{groupId}/lastSeen/{uid} ──
// Members can read any lastSeen doc; users can only write their own.
match /lastSeen/{uid} {
allow read: if request.auth != null && isGroupMember(groupId);
allow write: if request.auth != null && isGroupMember(groupId)
&& request.auth.uid == uid;
}
// ── groups/{groupId}/categories/{categoryId} ──
match /categories/{categoryId} {
allow read: if request.auth != null && isGroupMember(groupId);
allow create: if request.auth != null && isGroupMember(groupId)
&& request.resource.data.name is string
&& request.resource.data.name.size() <= 50;
allow update: if request.auth != null && isGroupMember(groupId);
allow delete: if request.auth != null && isGroupMember(groupId);
}
}
// ── invites/{inviteId} ──
match /invites/{inviteId} {
// Inviter can create invites (must be authenticated)
// Rate limit: createdAt must be server timestamp (prevents backdating)
// Shape validation: must contain required fields
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.inviterUid
&& request.resource.data.keys().hasAll(['groupId', 'groupName', 'inviterUid', 'inviterName', 'inviteeEmail', 'status', 'createdAt'])
&& request.resource.data.status == 'pending'
&& request.resource.data.inviteeEmail is string
&& request.resource.data.createdAt == request.time;
// Invitee can read their own invites; inviter can read invites they sent
allow read: if request.auth != null
&& (userEmail() == resource.data.inviteeEmail
|| request.auth.uid == resource.data.inviterUid);
// Invitee can update (accept) their invites
allow update: if request.auth != null
&& userEmail() == resource.data.inviteeEmail;
// Either party can delete the invite
allow delete: if request.auth != null
&& (userEmail() == resource.data.inviteeEmail
|| request.auth.uid == resource.data.inviterUid);
}
// ── feedback/{feedbackId} ──
match /feedback/{feedbackId} {
// Anyone authenticated can submit feedback
allow create: if request.auth != null;
// Users can only read/delete their own feedback
allow read, delete: if request.auth != null
&& userEmail() == resource.data.email;
}
}
}