Skip to content

Commit 4fc1bb7

Browse files
committed
Implement Firestore security rules and add user authentication functions
1 parent ea96ddb commit 4fc1bb7

File tree

3 files changed

+222
-71
lines changed

3 files changed

+222
-71
lines changed

firestore.rules

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,37 @@ rules_version = '2';
22

33
service cloud.firestore {
44
match /databases/{database}/documents {
5-
6-
// This rule allows anyone with your Firestore database reference to view, edit,
7-
// and delete all data in your Firestore database. It is useful for getting
8-
// started, but it is configured to expire after 30 days because it
9-
// leaves your app open to attackers. At that time, all client
10-
// requests to your Firestore database will be denied.
11-
//
12-
// Make sure to write security rules for your app before that time, or else
13-
// all client requests to your Firestore database will be denied until you Update
14-
// your rules
5+
// Helper functions
6+
function isAuthenticated() {
7+
return request.auth != null;
8+
}
9+
10+
function isOwner(userId) {
11+
return isAuthenticated() && request.auth.uid == userId;
12+
}
13+
14+
// Users collection
15+
match /users/{userId} {
16+
// Users can read and update their own profiles
17+
allow read, update: if isOwner(userId);
18+
// Only allow creation through Cloud Functions (triggered by Auth)
19+
allow create: if false;
20+
// Only allow deletion through Cloud Functions
21+
allow delete: if false;
22+
}
23+
24+
// Courses collection - will be implemented later
25+
match /courses/{courseId} {
26+
// Users can read courses they're enrolled in
27+
allow read: if isAuthenticated() &&
28+
exists(/databases/$(database)/documents/users/$(request.auth.uid)/courses/$(courseId));
29+
// Write operations will be handled by Cloud Functions
30+
allow write: if false;
31+
}
32+
33+
// Default deny all
1534
match /{document=**} {
16-
allow read, write: if request.time < timestamp.date(2025, 3, 19);
35+
allow read, write: if false;
1736
}
1837
}
19-
}
38+
}

functions/auth.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from firebase_functions import https_fn
2+
from firebase_admin import firestore, auth
3+
import google.cloud.firestore
4+
import json
5+
6+
@https_fn.on_call()
7+
def create_user_profile(req: https_fn.CallableRequest) -> dict:
8+
"""
9+
Callable function to create a user profile.
10+
Called after user registration.
11+
"""
12+
if not req.auth:
13+
raise https_fn.HttpsError(
14+
code=https_fn.FunctionsErrorCode.UNAUTHENTICATED,
15+
message="User must be authenticated"
16+
)
17+
18+
try:
19+
# Get user data from Firebase Auth
20+
user = auth.get_user(req.auth.uid)
21+
22+
# Create user profile in Firestore
23+
firestore_client: google.cloud.firestore.Client = firestore.client()
24+
user_ref = firestore_client.collection('users').document(user.uid)
25+
26+
user_data = {
27+
'displayName': user.display_name or '',
28+
'email': user.email,
29+
'photoURL': user.photo_url or '',
30+
'createdAt': firestore.SERVER_TIMESTAMP,
31+
'lastLogin': firestore.SERVER_TIMESTAMP,
32+
'role': 'student', # Default role
33+
'courses': [] # Empty list of courses initially
34+
}
35+
36+
user_ref.set(user_data)
37+
38+
return {"success": True, "message": "User profile created successfully"}
39+
40+
except Exception as e:
41+
raise https_fn.HttpsError(
42+
code=https_fn.FunctionsErrorCode.INTERNAL,
43+
message=f"Error creating user profile: {str(e)}"
44+
)
45+
46+
@https_fn.on_call()
47+
def delete_user_data(req: https_fn.CallableRequest) -> dict:
48+
"""
49+
Callable function to delete a user's data.
50+
Called before deleting a user account.
51+
"""
52+
if not req.auth:
53+
raise https_fn.HttpsError(
54+
code=https_fn.FunctionsErrorCode.UNAUTHENTICATED,
55+
message="User must be authenticated"
56+
)
57+
58+
try:
59+
# Delete user profile from Firestore
60+
firestore_client: google.cloud.firestore.Client = firestore.client()
61+
user_ref = firestore_client.collection('users').document(req.auth.uid)
62+
63+
# Delete the user document
64+
user_ref.delete()
65+
66+
return {"success": True, "message": "User data deleted successfully"}
67+
68+
except Exception as e:
69+
raise https_fn.HttpsError(
70+
code=https_fn.FunctionsErrorCode.INTERNAL,
71+
message=f"Error deleting user data: {str(e)}"
72+
)
73+
74+
@https_fn.on_request()
75+
def get_user_profile(req: https_fn.Request) -> https_fn.Response:
76+
"""
77+
Get a user's profile data from Firestore.
78+
Requires authentication token in Authorization header.
79+
"""
80+
# Check if request has authorization header
81+
if not req.headers.get('Authorization'):
82+
return https_fn.Response(
83+
json.dumps({'error': 'No authorization token provided'}),
84+
status=401,
85+
headers={'Content-Type': 'application/json'}
86+
)
87+
88+
try:
89+
# Verify the Firebase ID token
90+
id_token = req.headers['Authorization'].split('Bearer ')[1]
91+
decoded_token = auth.verify_id_token(id_token)
92+
uid = decoded_token['uid']
93+
94+
# Get user profile from Firestore
95+
firestore_client: google.cloud.firestore.Client = firestore.client()
96+
user_doc = firestore_client.collection('users').document(uid).get()
97+
98+
if not user_doc.exists:
99+
return https_fn.Response(
100+
json.dumps({'error': 'User profile not found'}),
101+
status=404,
102+
headers={'Content-Type': 'application/json'}
103+
)
104+
105+
# Update last login time
106+
user_doc.reference.update({
107+
'lastLogin': firestore.SERVER_TIMESTAMP
108+
})
109+
110+
# Return user profile data
111+
return https_fn.Response(
112+
json.dumps(user_doc.to_dict(), default=str),
113+
headers={'Content-Type': 'application/json'}
114+
)
115+
116+
except Exception as e:
117+
return https_fn.Response(
118+
json.dumps({'error': str(e)}),
119+
status=400,
120+
headers={'Content-Type': 'application/json'}
121+
)
122+
123+
@https_fn.on_request()
124+
def update_user_profile(req: https_fn.Request) -> https_fn.Response:
125+
"""
126+
Update a user's profile data in Firestore.
127+
Requires authentication token in Authorization header.
128+
"""
129+
# Check if request has authorization header
130+
if not req.headers.get('Authorization'):
131+
return https_fn.Response(
132+
json.dumps({'error': 'No authorization token provided'}),
133+
status=401,
134+
headers={'Content-Type': 'application/json'}
135+
)
136+
137+
try:
138+
# Verify the Firebase ID token
139+
id_token = req.headers['Authorization'].split('Bearer ')[1]
140+
decoded_token = auth.verify_id_token(id_token)
141+
uid = decoded_token['uid']
142+
143+
# Get update data from request body
144+
try:
145+
update_data = json.loads(req.data.decode())
146+
except json.JSONDecodeError:
147+
return https_fn.Response(
148+
json.dumps({'error': 'Invalid JSON in request body'}),
149+
status=400,
150+
headers={'Content-Type': 'application/json'}
151+
)
152+
153+
# Remove any fields that shouldn't be updated by users
154+
protected_fields = ['email', 'createdAt', 'role']
155+
for field in protected_fields:
156+
update_data.pop(field, None)
157+
158+
# Update user profile in Firestore
159+
firestore_client: google.cloud.firestore.Client = firestore.client()
160+
user_ref = firestore_client.collection('users').document(uid)
161+
user_ref.update(update_data)
162+
163+
# Get and return updated profile
164+
updated_profile = user_ref.get()
165+
return https_fn.Response(
166+
json.dumps(updated_profile.to_dict(), default=str),
167+
headers={'Content-Type': 'application/json'}
168+
)
169+
170+
except Exception as e:
171+
return https_fn.Response(
172+
json.dumps({'error': str(e)}),
173+
status=400,
174+
headers={'Content-Type': 'application/json'}
175+
)

functions/main.py

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,19 @@
1-
from firebase_functions import https_fn, firestore_fn
2-
from firebase_admin import initialize_app, firestore
3-
import google.cloud.firestore
1+
from firebase_functions import https_fn
2+
from firebase_admin import initialize_app
3+
from auth import (
4+
create_user_profile,
5+
delete_user_data,
6+
get_user_profile,
7+
update_user_profile
8+
)
49

10+
# Initialize Firebase app
511
initialize_app()
612

7-
@https_fn.on_request()
8-
def store_numbers(req: https_fn.Request) -> https_fn.Response:
9-
"""Store two numbers in Firestore"""
10-
# Get the numbers from query parameters
11-
num1 = req.args.get("num1")
12-
num2 = req.args.get("num2")
13-
14-
# Validate input
15-
if not num1 or not num2:
16-
return https_fn.Response("Please provide both num1 and num2 as query parameters", status=400)
17-
18-
try:
19-
num1 = float(num1)
20-
num2 = float(num2)
21-
except ValueError:
22-
return https_fn.Response("Numbers must be valid numeric values", status=400)
23-
24-
# Store in Firestore
25-
firestore_client: google.cloud.firestore.Client = firestore.client()
26-
_, doc_ref = firestore_client.collection("number_pairs").add({
27-
"num1": num1,
28-
"num2": num2,
29-
"timestamp": firestore.SERVER_TIMESTAMP
30-
})
31-
32-
return https_fn.Response(f"Numbers stored with ID: {doc_ref.id}")
33-
34-
@https_fn.on_request()
35-
def add_latest(req: https_fn.Request) -> https_fn.Response:
36-
"""Add the most recently stored number pair"""
37-
firestore_client: google.cloud.firestore.Client = firestore.client()
38-
39-
# Get the most recent number pair
40-
docs = firestore_client.collection("number_pairs")\
41-
.order_by("timestamp", direction=firestore.Query.DESCENDING)\
42-
.limit(1)\
43-
.get()
44-
45-
# Check if we have any documents
46-
if not docs:
47-
return https_fn.Response("No numbers found in database", status=404)
48-
49-
# Get the first (most recent) document
50-
doc = docs[0]
51-
num1 = doc.get("num1")
52-
num2 = doc.get("num2")
53-
54-
# Calculate sum
55-
sum_result = num1 + num2
56-
57-
# Store the result
58-
doc.reference.update({
59-
"sum": sum_result
60-
})
61-
62-
return https_fn.Response(f"Sum of {num1} and {num2} is {sum_result}")
13+
# Re-export authentication functions
14+
__all__ = [
15+
'create_user_profile',
16+
'delete_user_data',
17+
'get_user_profile',
18+
'update_user_profile'
19+
]

0 commit comments

Comments
 (0)