Skip to content

Conversation

@johnclary
Copy link
Member

@johnclary johnclary commented Dec 16, 2025

Associated issues

This PR adds a new route to our API to support victim photo uploads. Since we don't have a UI to test this with I went ahead and created pytest tests.

The setup and testing should be pretty straightforward, with the one gotcha that your auth token expires every 5 minutes. This was pretty annoying to dev around! I went just a little bit down the rabbit hole of configuring auth0 so that we could request JWTs programmatically, but Auth0 has some big scary warnings about how risky this is. So I opted to stick with copy-pasta JWTs. One maybe reasonable thing we could do would be to extend the TTL of our JWTs in staging—but considering that our staging data tends to be quite real and sensitive this doesn't seem like a great idea to me.

Testing

URL to test: Local

  1. From the root of the repo, rebuild the api docker image
docker compose build
  1. In the ./api directory, create your .env file based on the template file. All secrets are available in the DEVELOPMENT section of the 1pass item called Vision Zero (VZ) User & CR3 API Secrets. If you already had an .env file, note that some values have been added and removed (see diff).

  2. From the root of the repo, start your local hasura setup including the CR3 api:

 ./vision-zero local-stack-up
  1. In your ./editor directory, check your .env.local file to make sure that NEXT_PUBLIC_CR3_API_DOMAIN is set to http://localhost:8085. Then start your VZE: npm run dev

  2. From the ./database directory, apply migrations and reload metadata

hasura migrate apply
hasura metadata apply
  1. To run the API tests, follow the instructions in the ./api/tests/README.

  2. Optionally, if you'd like to explore how the API works interactively you can spin up a container to manually execute some python commands.

# starts a container with everything you need to interact with the API
docker run -it --rm --network vision-zero_default cr3-user-api /bin/bash
import os
import pytest
import requests
from PIL import Image
import io

# set up
test_person_id = 102580  # must be a person record ID available in your local DB
token = "Bearer ...." # copy from VZE local env
api_url = f"http://cr3-user-api:5000/images/person/{test_person_id}"
headers = {"Authorization": token}

# create image
img = Image.new("RGB", (500, 500), color="blue")
image_file = io.BytesIO()
img.save(image_file, format="JPEG")
image_file.seek(0)
image_file.name = "test.jpg"

# prep / do request
files = {"file": image_file}
data = {"image_source": "test_source"}
res = requests.post(api_url, files=files, headers=headers, data=data)
assert res.status_code == 201

# checkpoint: you can inspect the person record in the DB and find the uploaded image in S3 at  /dev/images/person/102580/.jpg. here's some sql 👇

"""SELECT
   image_s3_object_key,
   image_source,
   image_original_filename
FROM
   people
WHERE
   image_source IS NOT NULL;
"""


# Get presigned URL to this image
res = requests.get(api_url, headers=headers)
assert res.status_code == 200
presigned_url = res.json()["url"]

# checkpoint: you can open presigned_url in your browser

# cleanup - delete image
res = requests.delete(api_url, headers=headers)
assert res.status_code == 200

# checkpoint: inspect DB and s3 and see that that image has been deleted
  1. Test that the user management API works normally. Login to your local VZE as an admin and create a user with the email address <yourfirstname>.<yourlastname>[email protected]. Refresh the page and delete this user. Confirm the user was deleted successfully.

  2. Test that the VZE CR3 download works normally—visit crash ID 13625639 and download the CR3.


Ship list

  • Check migrations for any conflicts with latest migrations in main branch
  • Confirm Hasura role permissions for necessary access
  • Code reviewed
  • Product manager approved

@netlify
Copy link

netlify bot commented Dec 16, 2025

Deploy Preview for atd-vze-staging canceled.

Name Link
🔨 Latest commit c51ebcd
🔍 Latest deploy log https://app.netlify.com/projects/atd-vze-staging/deploys/695dccd0cf78e900086b2662

"""
import datetime
import json
import os
Copy link
Member Author

Choose a reason for hiding this comment

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

mostly just shuffling around imports to be closer to PEP8

AWS_S3_BUCKET_ENV = getenv("AWS_S3_BUCKET_ENV", "")
AWS_S3_CR3_LOCATION = f"{AWS_S3_BUCKET_ENV}/cr3s/pdfs"
AWS_S3_PERSON_IMAGE_LOCATION = f"{AWS_S3_BUCKET_ENV}/images/person"
AWS_S3_BUCKET = getenv("AWS_S3_BUCKET", "")
Copy link
Member Author

Choose a reason for hiding this comment

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

i reworked a few of these env vars and updated our 1pass entry accordingly. we're going to need to bring the ECS environments in sync with these changes

{
"code": "invalid_header",
"description": f"{e}: Unable to parse authentication token.",
"description": f"Unable to parse authentication token.",
Copy link
Member Author

Choose a reason for hiding this comment

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

i ran the auth route through an LLM and it wisely suggested to not dump exception messages into the API response 🔒

if not safe_person_id:
return jsonify(error="Missing or invalid person_id"), 400

if request.method == "GET":
Copy link
Member Author

@johnclary johnclary Dec 16, 2025

Choose a reason for hiding this comment

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

For the GET endpoint, it seemed fine to continue the CR3 download pattern of returning a pre-signed URL from S3 (as opposed to streaming the image from this API).

In the VZE, we will need to create an image component that will:

  • Ask this API for an image of person ID 12345
    The API returns a presigned URL to the VZE (if one exists)
  • The VZE passes the presigned URL into an <image> element

Copy link
Contributor

@mddilley mddilley Dec 30, 2025

Choose a reason for hiding this comment

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

fwiw when I recently chatted with Claude about CloudFront@Edge serving profile pics in Moped, it recommended pre-signed urls (or streaming from the API as a next alternative) instead of CF@Edge. so, this sounds great to me, and it is nice to both images and CR3s working the same way! 🚀

api/server.py Outdated
)
return jsonify(url=url)

elif request.method == "POST":
Copy link
Member Author

@johnclary johnclary Dec 16, 2025

Choose a reason for hiding this comment

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

The POST endpoint allows the client to upsert an image for a given person ID. So long as the image passes our validation, we store it in S3 and save the object key and original uploaded filename in our DB. The filename of the file saved in S3 will follow the format <person_id>.<png | jpg>.

@johnclary
Copy link
Member Author

Folks, I just reworked the docker compose setup a bit so that we have a separate compose file for testing. Charlie encountered that the entire local stack setup was previously broken until you created an ./api/.env.test file. That is no longer the case.

Copy link
Contributor

@Charlie-Henry Charlie-Henry left a comment

Choose a reason for hiding this comment

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

I got all of the tests to work! A couple stumbling blocks for me to get things tested

  • you might need to create a blankapi/.env.test file to run ./vision-zero local-stack-up the first time
  • In step 6 I had to use --network atd-vz-data_default because my local repo is still in the old name

Love the pytest cases, easy to understand!



def _handle_image_upload(person_id, file, s3, old_image_obj_key=None):
"""Uploads an image to S3 after validating the image and removing EXIF data
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice I would've completely forgot about EXIF data

Copy link
Member

@frankhereford frankhereford left a comment

Choose a reason for hiding this comment

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

You got a lot work done in this PR, John! It worked great, and the local testing experience was 💯. I love the pytest pattern you're establishing, and all your code is clear and concise. There's a lot of great stuff in here.

Copy link
Member

Choose a reason for hiding this comment

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

So much 🧹 🧼 🧹 going on in this file -- thank you!

Copy link
Contributor

@mddilley mddilley left a comment

Choose a reason for hiding this comment

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

Looks great! This was a really fun one to review since I haven't looked at an API in a while. All the tests passed, and I ran through the Python commands as well and saw the expected changes in S3 and my local DB. 🚢 🙌

Copy link
Contributor

@mddilley mddilley Dec 30, 2025

Choose a reason for hiding this comment

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

awesome! i've been keeping the pytest coverage for the Moped API that we shelved in the back of my mind, and this will be a nice reference for a future version.

are you thinking about including this in the release testing if the API code changes? Just thinking about how dusty the Moped tests became - we've come a long way on maintaining this kind of thing and mostly wondering what you have in mind. What a level up! 🙌

from PIL import Image
import io

TEST_PERSON_ID = 102580 # must be a person record ID available in your local DB
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe this could be another env var? I've lost context on how likely it would be for this person to be removed from the data though...

Copy link
Member Author

Choose a reason for hiding this comment

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

I went back and forth on this too. This person ID should be in the DB for a long time to come, but I like your suggestion. I'll set this as a default person ID that also supports passing an optional override in the env 🙏

@pytest.fixture
def test_image_jpg():
"""Create a 500x500 JPEG test image."""
img = Image.new("RGB", (500, 500), color="blue")
Copy link
Contributor

@mddilley mddilley Dec 30, 2025

Choose a reason for hiding this comment

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

PIL is a TIL 😄 Sure was cool seeing that blue square come up with the test presigned url!

return img


def strip_exif(img):
Copy link
Contributor

Choose a reason for hiding this comment

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

nice detail! this makes me think we should do the same in Moped. i'll test it out but I don't think that we are doing this for image uploads.



@APP.route("/cr3/download/<crash_id>")
@app.route("/cr3/download/<int:crash_id>")
Copy link
Contributor

Choose a reason for hiding this comment

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

this int converter is a TIL 🙏

if not safe_person_id:
return jsonify(error="Missing or invalid person_id"), 400

if request.method == "GET":
Copy link
Contributor

@mddilley mddilley Dec 30, 2025

Choose a reason for hiding this comment

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

fwiw when I recently chatted with Claude about CloudFront@Edge serving profile pics in Moped, it recommended pre-signed urls (or streaming from the API as a next alternative) instead of CF@Edge. so, this sounds great to me, and it is nice to both images and CR3s working the same way! 🚀

Copy link
Member

@chiaberry chiaberry left a comment

Choose a reason for hiding this comment

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

nice to get a refresher on the api and test via pytest. I am trying to wrap my head around what needs to be done when this is merged and I am really fuzzy on how this gets deployed.

@johnclary
Copy link
Member Author

johnclary commented Jan 6, 2026

I am trying to wrap my head around what needs to be done when this is merged and I am really fuzzy on how this gets deployed.

@chiaberry despite being intentional about updating docs on this topic it is not well documented in the repo or gitbook. we can expect the API to redeploy as the last step in the image build action.

I will update this README to make that more clear 🙏

EDIT: And before we merge we'll need to figure out the most graceful way to handle the env var changes. This one might need some manual finessing of the ECS config.

Copy link
Contributor

@mddilley mddilley left a comment

Choose a reason for hiding this comment

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

I reviewed the latest commits and ran the test suite which passed without issue - 14 passed in 7.83s. 😎 The API code in the last commit makes sense to me too. 🚢 🚀

mateoclarke

This comment was marked as duplicate.

Copy link
Contributor

@mateoclarke mateoclarke left a comment

Choose a reason for hiding this comment

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

14 passed in 14.27s 🚢

Copy link
Member

@chiaberry chiaberry left a comment

Choose a reason for hiding this comment

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

tests pass, I can still download cr3s too. I didnt test creating a new user, but dont see why that would change with the latest updates

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.

6 participants