-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
216 lines (179 loc) · 10.4 KB
/
Copy pathfirestore.rules
File metadata and controls
216 lines (179 loc) · 10.4 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
rules_version = '2';
// ─────────────────────────────────────────────────────────────────────────────
// Wing Zone – Production Firestore Security Rules
// Last updated: 2026-02-20
//
// Admin check strategy:
// PRIMARY – custom Auth claim (request.auth.token.admin == true)
// Zero extra Firestore reads; set via Admin SDK:
// admin.auth().setCustomUserClaims(uid, { admin: true })
// FALLBACK – Firestore field (users/{uid}.role == 'admin')
// Used while custom claims are not yet set up.
// ─────────────────────────────────────────────────────────────────────────────
service cloud.firestore {
match /databases/{database}/documents {
// ── Helper functions ──────────────────────────────────────────────────────
// Is the caller authenticated at all?
function isAuthenticated() {
return request.auth != null;
}
// Is the caller an admin?
// Checks custom claim first (fast, no Firestore read), then the users doc.
function isAdmin() {
return isAuthenticated() && (
request.auth.token.admin == true ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin'
);
}
// Is the caller the owner of the document? (pass the stored userId field)
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// Validate that an incoming write only touches the listed fields
// (prevents privilege escalation by adding unexpected fields)
function onlyFields(fields) {
return request.resource.data.keys().hasOnly(fields);
}
// ── users ─────────────────────────────────────────────────────────────────
// Users can only read/write their own document.
// Admins can read and write everything.
// Role and sensitive fields can only be set by admins.
match /users/{userId} {
allow read: if isOwner(userId) || isAdmin();
// Self-registration: the new doc's ID must match the caller's UID.
// Role defaults to 'user'; only admins may set role to 'admin'.
allow create: if isOwner(userId)
&& request.resource.data.get('role', 'user') == 'user';
allow update: if (
// Owner updating their own non-privileged fields
(isOwner(userId) && !('role' in request.resource.data.diff(resource.data).affectedKeys()))
|| isAdmin()
);
allow delete: if isAdmin();
}
// ── menuItems ─────────────────────────────────────────────────────────────
// Public read (guests can browse the menu).
// Only admins may create, update, or delete items.
match /menuItems/{itemId} {
allow read: if true;
allow create, update, delete: if isAdmin();
}
// ── categories ────────────────────────────────────────────────────────────
match /categories/{catId} {
allow read: if true;
allow create, update, delete: if isAdmin();
}
// ── homeBanners ───────────────────────────────────────────────────────────
match /homeBanners/{bannerId} {
allow read: if true;
allow create, update, delete: if isAdmin();
}
// ── restaurantLocations ───────────────────────────────────────────────────
match /restaurantLocations/{locationId} {
allow read: if true;
allow create, update, delete: if isAdmin();
}
// ── settings ──────────────────────────────────────────────────────────────
match /settings/{document} {
allow read: if true;
allow create, update, delete: if isAdmin();
}
// ── appSettings ───────────────────────────────────────────────────────────
// Admin dashboard uses 'appSettings' collection for preferences
match /appSettings/{document} {
allow read: if true;
allow create, update, delete: if isAdmin();
}
// ── orders ────────────────────────────────────────────────────────────────
// Authenticated users can create their own orders.
// Users can read / update only orders they own.
// Admins can read, update (status changes), and delete all orders.
match /orders/{orderId} {
allow create: if isAuthenticated()
&& request.resource.data.userId == request.auth.uid;
allow read: if isOwner(resource.data.userId) || isAdmin();
// Owner may update limited fields (e.g. special instructions);
// Admins may update any field (e.g. status → 'Preparing', 'Delivered').
allow update: if isOwner(resource.data.userId) || isAdmin();
allow delete: if isAdmin();
}
// ── groupOrders ───────────────────────────────────────────────────────────
// Authenticated users can create group orders.
// Only the host or a member may read.
// Host and members may update (add items, update status).
// Only host or admin may delete.
match /groupOrders/{groupOrderId} {
// Helper: is the caller the host of this group order?
function isGroupOrderHost() {
return isAuthenticated() && resource.data.hostId == request.auth.uid;
}
allow create: if isAuthenticated()
&& request.resource.data.hostId == request.auth.uid;
// Read is allowed to authenticated users — deep member-map checks are
// not supported in rules; access is narrowed by Auth in the app layer.
allow read: if isAuthenticated() || isAdmin();
allow update: if isGroupOrderHost() || isAdmin();
allow delete: if isGroupOrderHost() || isAdmin();
}
// ── lobbies ───────────────────────────────────────────────────────────────
// Authenticated users can create lobbies (hostUserId must match UID).
// Any authenticated user may read (needed to join via code).
// Host can update anything; other authenticated users may append themselves
// to the members array but cannot change hostUserId or status directly.
// Only host or admin may delete.
match /lobbies/{lobbyId} {
function isLobbyHost() {
return isAuthenticated() && resource.data.hostUserId == request.auth.uid;
}
allow create: if isAuthenticated()
&& request.resource.data.hostUserId == request.auth.uid
&& request.resource.data.status == 'active';
allow read: if isAuthenticated();
allow update: if (
// Host can change any field
isLobbyHost()
// Members can update (join/add items) but cannot change hostUserId or status
|| (isAuthenticated()
&& !('hostUserId' in request.resource.data.diff(resource.data).affectedKeys())
&& !('status' in request.resource.data.diff(resource.data).affectedKeys())
)
|| isAdmin()
);
allow delete: if isLobbyHost() || isAdmin();
}
// ── reviews ───────────────────────────────────────────────────────────────
// Anyone can read published+approved reviews.
// Authenticated users can read their own review regardless of status.
// Authenticated users can create a review for their own orderId;
// new reviews always start in 'pending' moderation.
// Only admins can update (approve/reject/publish) or delete reviews.
match /reviews/{reviewId} {
allow read: if (
// Public: only enabled and approved reviews
(resource.data.isEnabled == true && resource.data.moderationStatus == 'approved')
// Owner: can always read their own review
|| isOwner(resource.data.userId)
|| isAdmin()
);
allow create: if isAuthenticated()
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.moderationStatus == 'pending';
allow update: if isAdmin();
allow delete: if isAdmin();
}
// ── notifications (admin dashboard – flat top-level collection) ─────────
// The admin-dashboard NotificationService writes to collection('notifications')
// directly (not a subcollection), so we need a flat-collection rule here.
match /notifications/{notifId} {
allow read, write: if isAdmin();
}
// ── notifications (user-scoped subcollection) ─────────────────────────────
match /notifications/{userId}/{notifId} {
allow read, update: if isOwner(userId) || isAdmin();
allow create: if isAdmin(); // Created by Cloud Functions / admin
allow delete: if isOwner(userId) || isAdmin();
}
// ── Deny everything else ─────────────────────────────────────────────────
// No wildcard catch-all — unmatched paths are denied by default in v2.
}
}