Conversation
| @@ -0,0 +1,41 @@ | |||
| name: Deploy API Docs to GitHub Pages | |||
There was a problem hiding this comment.
| @@ -0,0 +1,22 @@ | |||
| version: "3.9" | |||
There was a problem hiding this comment.
can ignore - this is for local testing
| @@ -0,0 +1,41 @@ | |||
| { | |||
There was a problem hiding this comment.
can ignore - for local testing
| @@ -0,0 +1,29 @@ | |||
| #!/usr/bin/env python3 | |||
There was a problem hiding this comment.
can ignore - for local testing
| return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' | ||
|
|
||
|
|
||
| class UserType(str, Enum): |
There was a problem hiding this comment.
UserTypes
Really only need Neighbor and Volunteer right now, but in the future ORGANIZER or HOST might have some special privileges
There was a problem hiding this comment.
Mobile App would have to change check from isVolunteer to user.userType == 'Volunteer'
| email: Optional[str] = None | ||
| phoneNumber: Optional[str] = None | ||
| zipcode: Optional[str] = None | ||
| fcmToken: Optional[str] = None #TODO: Consider making this a set to allow for multiple devices to receive notifications |
There was a problem hiding this comment.
when signing out we need to set this to None
we should also consider allowing for multiple devices to receive notifications
There was a problem hiding this comment.
I think fcmToken should be a list of tokens to allow for multiple devices
| } | ||
|
|
||
|
|
||
| def error_response(status_code: int, code: ErrorCode, message: str, field: Optional[str] = None, |
There was a problem hiding this comment.
API errors are returned in this format:
Status Code: 400-500
Headers: X-Request-Id - to query Cloudwatch
{
"error": {
"code": "INTERNAL_ERROR_CODE",
"message": "MESSAGE"
}
}
example:
{
"error": {
"code": "FORBIDDEN",
"message": "User can only update their own user data"
}
}
| Authorizers: | ||
| FirebaseAuthorizer: | ||
| JwtConfiguration: | ||
| issuer: !Sub "https://securetoken.google.com/${FirebaseProjectId}" |
There was a problem hiding this comment.
Api Gateway Validates JWT tokens
User Service API Endpoints1. Create UserPOST
Request Body: {
"userId": "firebase-uid-here",
"username": "mariamekaba",
"email": "mariame@example.com",
"phoneNumber": "+1234567890",
"zipcode": "12345",
"fcmToken": "fcm-token-here",
"settings": {
"pushNotificationEnabled": true,
"emailNotificationEnabled": true,
"geofenceEnabled": false
},
"userType": "Neighbor"
}Success Response (201 Created): {
"user": {
"userId": "firebase-uid-here",
...,
"createdAt": "2024-03-20T14:22:00.000Z",
"lastUpdated": "2024-03-20T14:22:00.000Z",
"lastLoginAt": "2024-03-20T14:22:00.000Z"
}
}2. Get UserGET
{
"user": {
"userId": "firebase-uid-here",
...
}
}3. Update UserPATCH
Request Body (example - update email and settings): {
"email": "newemail@example.com",
"settings": {
"pushNotificationEnabled": false
}
}Success Response (200 OK): {
"user": {
"userId": "firebase-uid-here",
"email": "newemail@example.com",
"settings": {
"pushNotificationEnabled": false,
"emailNotificationEnabled": true,
"geofenceEnabled": false
},
"lastUpdated": "2024-03-20T15:30:00.000Z",
...,
}
}4. Delete UserDELETE
Success Response (204 No Content): 5. Check Username AvailabilityGET
Success Response (200 OK) - Available: {
"available": true
}Success Response (200 OK) - Taken: {
"available": false
}API errors are returned in this format:Status Code: 400-500 example: API Base URLs
AuthenticationAll endpoints (except username check) require Firebase JWT in the Documentation📖 Interactive API Docs: https://fridgefinder.github.io/CFM_User/ |
| - '*' | ||
|
|
||
| # DynamoDB Table for Users | ||
| UsersTable: |
There was a problem hiding this comment.
Hash/PrimaryKey - userId
Indexed - username
| parameter_overrides = [ | ||
| "Environment=dev", | ||
| "DeploymentTarget=aws", | ||
| "FirebaseProjectId=YOUR_STAGING_FIREBASE_PROJECT_ID", |
There was a problem hiding this comment.
TOOD: create firebase project on fridgefinder google account for: Dev and Prod
| # Fields that can be updated (excluding userType - handled above) | ||
| allowed_fields = { | ||
| 'email', 'phoneNumber', 'username', 'points', | ||
| 'zipcode', 'fcmToken', 'lastLoginAt', 'settings' |
There was a problem hiding this comment.
lastLoginAt - I think there should be an api that updates this instead of the frontend sending the lastLoginAt field
| GEOFENCE_ENABLED = "geofenceEnabled" | ||
|
|
||
|
|
||
| class User(BaseModel): |
There was a problem hiding this comment.
Reviewer: User Model, confirm fields and structure
mansoorsiddiqui
left a comment
There was a problem hiding this comment.
Solid work overall, looks clean. Most of my comments are around a few edge cases and making sure we're aligned on the cross-service contract with the notification PR.
Cross-PR concerns:
- EventBridge contract gap: The notification PR subscribes to
"User Deleted"events from"user-service", but this PR never publishes that event. Notification cleanup won't happen on account deletion. - Error code naming: This PR uses
USER_NOT_FOUND,USER_ALREADY_EXISTSwhile the notification PR usesNOT_FOUND,ALREADY_EXISTS. The Flutter app will need to parse errors from both — should we align on a convention? - Error response format: Both PRs use
{"error": {"code": "...", "message": "..."}}which is good, but the OpenAPI spec here documents it differently. Let's make sure the specs match the implementations.
| return error_response(404, ErrorCode.USER_NOT_FOUND, f"User not found") | ||
|
|
||
| # Delete from database | ||
| self.repository.delete_user(user_id) |
There was a problem hiding this comment.
Hey, so I see the delete just removes from DynamoDB and returns 204 — but over in the notification PR, there's a UserDeletionHandlerFunction that's listening for a "User Deleted" EventBridge event from "user-service". If we don't publish that event here, notification cleanup will just silently never happen when someone deletes their account. Were you planning to add the EventBridge put_events() call in a follow-up, or should it be in this PR? We'd also need the events:PutEvents IAM permission in the template.
| Args: | ||
| user_data: Complete user data to save | ||
| """ | ||
| self.dynamodb_client.put_item( |
There was a problem hiding this comment.
This does a full put_item with no condition expression. Since the service layer does a get → mutate in memory → put, two concurrent updates to the same user would just last-write-wins with no error. The old notification code actually had optimistic locking on updated_at — did you intentionally leave that out here? Might be worth adding a ConditionExpression on lastUpdated so we at least detect conflicts.
| Returns: | ||
| True if user exists, False otherwise | ||
| """ | ||
| return self.get_user_by_id(user_id) is not None |
There was a problem hiding this comment.
This calls get_user_by_id() which fetches the entire item and deserializes it through Pydantic just to return a boolean. Could we just do a get_item with ProjectionExpression='userId' and check if 'Item' is in the result? Would save a bunch of unnecessary work.
| phoneNumber: Optional[str] = None | ||
| zipcode: Optional[str] = None | ||
| fcmToken: Optional[str] = None #TODO: Consider making this a set to allow for multiple devices to receive notifications | ||
| settings: Dict[str, bool] = Field(default_factory=lambda: { |
There was a problem hiding this comment.
Any reason settings is a Dict[str, bool] instead of a proper nested Pydantic model? The notification PR uses typed nested models for ContactTypePreferences → FridgePreferences, which gives you validation for free. With a raw dict, someone could theoretically pass {"pushNotificationEnabled": "yes"} and it might not get caught the way you'd expect.
| Uses Pydantic's model_dump with enum values as strings | ||
| Excludes None values to save space | ||
| """ | ||
| return self.model_dump(mode='json', exclude_none=True) |
There was a problem hiding this comment.
Quick sanity check — to_dict() uses exclude_none=True, and then update_user does a full put_item which replaces the entire DynamoDB item. So if a user clears a field (sets it to None), the old value in DynamoDB would get dropped because put_item replaces everything. Is that the intended behavior? Just want to make sure we're aligned on that.
| request_id = context.aws_request_id if context else 'unknown' | ||
| # Extract username from path | ||
| path_params = event.get('pathParameters', {}) | ||
| username = path_params.get('username') |
There was a problem hiding this comment.
The username goes straight from the path parameter into the DynamoDB query with no validation. The OpenAPI spec says minLength: 3, maxLength: 30, pattern: '^[a-zA-Z0-9_-]+$' — but API Gateway v2 doesn't enforce OpenAPI path parameter schemas. So someone could hit this endpoint with a 10,000 character string or special characters. Worth adding a quick length/format check before the query?
| push: | ||
| branches: | ||
| - main | ||
| - U1_JP #TODO: remove this branch after testing |
There was a problem hiding this comment.
U1_JP branch is still in the trigger list with a #TODO: remove this branch after testing. Don't forget to pull this out before merging to main.
|
|
||
| assert ret["statusCode"] == 200 | ||
| assert "message" in ret["body"] | ||
| assert data["message"] == "hello world" |
There was a problem hiding this comment.
This asserts data["message"] == "hello world" but the actual hello_world/app.py returns "hello user service". This test will fail.
| Returns: | ||
| Formatted API Gateway response with error details | ||
| """ | ||
| error_body = { |
There was a problem hiding this comment.
Minor thing — the error response body format is {"error": {"code": "...", "message": "..."}} but the OpenAPI spec defines it as {"error": "string", "errorCode": "string"} (flat structure). Which one are we going with? The nested format is better IMO, but the docs should match the implementation.
| parameter_overrides = [ | ||
| "Environment=dev", | ||
| "DeploymentTarget=aws", | ||
| "FirebaseProjectId=YOUR_STAGING_FIREBASE_PROJECT_ID", |
There was a problem hiding this comment.
There are a bunch of YOUR_STAGING_FIREBASE_PROJECT_ID and YOUR_HOSTED_ZONE_ID placeholders checked in. Could we either use env variable references or add a note in the README about what to replace? Just worried someone copies this for a deploy and accidentally uses the placeholder values.
User Model and APIs
API docs: https://fridgefinder.github.io/CFM_User/