Skip to content

Comments

[U1] User Service and REST APIs#1

Open
1jeanpaul1 wants to merge 9 commits intodevfrom
U1_JP
Open

[U1] User Service and REST APIs#1
1jeanpaul1 wants to merge 9 commits intodevfrom
U1_JP

Conversation

@1jeanpaul1
Copy link
Collaborator

@1jeanpaul1 1jeanpaul1 commented Dec 11, 2025

User Model and APIs

API docs: https://fridgefinder.github.io/CFM_User/

@1jeanpaul1 1jeanpaul1 changed the title U1 User Service and REST APIs [U1] User Service and REST APIs Dec 11, 2025
@@ -0,0 +1,41 @@
name: Deploy API Docs to GitHub Pages
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,22 @@
version: "3.9"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can ignore - this is for local testing

@@ -0,0 +1,41 @@
{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can ignore - for local testing

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can ignore - for local testing

return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'


class UserType(str, Enum):
Copy link
Collaborator Author

@1jeanpaul1 1jeanpaul1 Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserTypes

Really only need Neighbor and Volunteer right now, but in the future ORGANIZER or HOST might have some special privileges

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when signing out we need to set this to None

we should also consider allowing for multiple devices to receive notifications

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Collaborator Author

@1jeanpaul1 1jeanpaul1 Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Api Gateway Validates JWT tokens

@1jeanpaul1
Copy link
Collaborator Author

1jeanpaul1 commented Dec 11, 2025

User Service API Endpoints

1. Create User

POST /v1/users

  • Auth: Required (Firebase JWT)
  • Description: Create a new user account. Users are automatically assigned the Neighbor type.

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 User

GET /v1/users/{userId}

  • Auth: Required (Firebase JWT)
  • Description: Retrieve user profile. Users can only access their own profile.
  • Success Response (200 OK):
{
  "user": {
    "userId": "firebase-uid-here",
    ...
  }
}

3. Update User

PATCH /v1/users/{userId}

  • Auth: Required (Firebase JWT)
  • Description: Partially update user profile. Only provided fields will be updated.

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 User

DELETE /v1/users/{userId}

  • Auth: Required (Firebase JWT)
  • Description: Permanently delete user account. Users can only delete their own account.

Success Response (204 No Content):

(Empty response body)

5. Check Username Availability

GET /v1/users/check-username/{username}

  • Auth: Not required (Public endpoint)
  • Description: Check if a username is available (not taken by another user).

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
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"
    }
}

API Base URLs

  • Development: https://users-api-dev.communityfridgefinder.com

Authentication

All endpoints (except username check) require Firebase JWT in the Authorization header:

Authorization: Bearer <your-firebase-jwt-token>

Documentation

📖 Interactive API Docs: https://fridgefinder.github.io/CFM_User/

- '*'

# DynamoDB Table for Users
UsersTable:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hash/PrimaryKey - userId
Indexed - username

parameter_overrides = [
"Environment=dev",
"DeploymentTarget=aws",
"FirebaseProjectId=YOUR_STAGING_FIREBASE_PROJECT_ID",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: User Model, confirm fields and structure

Copy link

@mansoorsiddiqui mansoorsiddiqui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_EXISTS while the notification PR uses NOT_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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason settings is a Dict[str, bool] instead of a proper nested Pydantic model? The notification PR uses typed nested models for ContactTypePreferencesFridgePreferences, 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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants