-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
186 lines (152 loc) · 7.57 KB
/
firestore.rules
File metadata and controls
186 lines (152 loc) · 7.57 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
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ============================================
// HELPER FUNCTIONS
// ============================================
// Check if the user is authenticated
function isAuthenticated() {
return request.auth != null;
}
// Check if the user owns the resource
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// Validate page count (10, 15, or 20)
function isValidPageCount(pageCount) {
return pageCount in [10, 15, 20];
}
// Validate image style
function isValidImageStyle(style) {
return style in ['coloring', 'cartoon', 'anime', 'watercolor'];
}
// Validate book status (including 'completed')
function isValidBookStatus(status) {
return status in ['draft', 'generating', 'completed', 'ready', 'error'];
}
// Validate tone (allow both formats)
function isValidTone(tone) {
return tone == null || tone in ['warm', 'joyful', 'calm', 'warm_simple', 'calm_reflective'];
}
// Validate reading level (allow both formats)
function isValidReadingLevel(level) {
return level == null || level in ['very-simple', 'standard', 'very_simple'];
}
// Validate image category
function isValidImageCategory(category) {
return category in ['profile_reference', 'childhood', 'teen', 'adult', 'laterLife'];
}
// Validate section ID
function isValidSectionId(sectionId) {
return sectionId in ['childhood', 'teen', 'adult', 'laterLife'];
}
// ============================================
// USERS COLLECTION
// ============================================
match /users/{userId} {
// Users can only read and write their own document
allow read: if isOwner(userId);
allow create: if isOwner(userId) &&
request.resource.data.keys().hasAll(['displayName', 'email', 'createdAt']) &&
request.resource.data.displayName is string &&
request.resource.data.email is string;
allow update: if isOwner(userId);
allow delete: if isOwner(userId);
}
// ============================================
// MEMORY BOOKS COLLECTION
// ============================================
match /memoryBooks/{bookId} {
// Read: authenticated users can read their own books
allow read: if isAuthenticated() &&
resource.data.ownerId == request.auth.uid;
// Create: authenticated users can create books
allow create: if isAuthenticated() &&
request.resource.data.ownerId == request.auth.uid &&
request.resource.data.keys().hasAll(['ownerId', 'title', 'pageCount', 'imageStyle', 'status', 'createdAt', 'updatedAt']) &&
request.resource.data.title is string &&
request.resource.data.title.size() > 0 &&
isValidPageCount(request.resource.data.pageCount) &&
isValidImageStyle(request.resource.data.imageStyle) &&
isValidBookStatus(request.resource.data.status) &&
isValidTone(request.resource.data.get('tone', null)) &&
isValidReadingLevel(request.resource.data.get('readingLevel', null));
// Update: owner only, cannot change ownerId
allow update: if isAuthenticated() &&
resource.data.ownerId == request.auth.uid &&
request.resource.data.ownerId == resource.data.ownerId &&
isValidBookStatus(request.resource.data.status);
// Delete: owner only
allow delete: if isAuthenticated() &&
resource.data.ownerId == request.auth.uid;
// ----------------------------------------
// SECTIONS SUBCOLLECTION
// ----------------------------------------
match /sections/{sectionId} {
allow read: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow create, update: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow delete: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
}
// ----------------------------------------
// IMAGES SUBCOLLECTION
// ----------------------------------------
match /images/{imageId} {
allow read: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow create: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow update: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow delete: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
}
// ----------------------------------------
// PAGES SUBCOLLECTION
// ----------------------------------------
match /pages/{pageId} {
allow read: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow create: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow update: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow delete: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
}
// ----------------------------------------
// GENERATION JOBS SUBCOLLECTION
// ----------------------------------------
match /generationJobs/{jobId} {
allow read: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow create: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow update: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
// Owner can delete generation jobs (for book cleanup)
allow delete: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
// Persisted book images subcollection
match /images/{imageId} {
allow read: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
allow create, update: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
// Owner can delete images (for book cleanup)
allow delete: if isAuthenticated() &&
get(/databases/$(database)/documents/memoryBooks/$(bookId)).data.ownerId == request.auth.uid;
}
}
}
// ============================================
// DENY ALL OTHER ACCESS
// ============================================
// Prevent listing all documents in root collections
match /{document=**} {
allow read, write: if false;
}
}
}