You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Last sprint shipped the signed upload URL endpoint (POST /api/v1/uploads/sign). What's missing is the download side, persistent tracking of uploads in Mongo, and ownership enforcement.
Tasks
Upload records collection
Create a new Mongo collection uploads (or upload_records — pick a name and stick with it). Document shape: { _id: uploadId, userId, filename, mime, size, gcsPath, createdAt }.
What this accomplishes: Tracks what files exist, who owns them, and where they live in GCS. Without this, files exist in GCS but can't be looked up by ID from the app.
Add a Mongo index on userId for fast "give me all this user's uploads."
What this accomplishes: When an admin views an applicant, showing their resume requires finding the upload record by userId. Indexed lookup is essential at scale.
recordUpload service function
Implement recordUpload({ uploadId, userId, filename, mime, size, gcsPath }) in src/lib/uploads/service.ts. Inserts a document into the uploads collection.
What this accomplishes: The write side of the upload record. Called after a successful signing (or after successful upload confirmation from the client).
Decide when to call it: either at sign time (before the actual upload) or after the client confirms upload. Recommend at sign time, since it's simpler and orphan records (signed but never uploaded) are cheap to clean up later with a batch job.
What this accomplishes: Trades some database noise for simplicity. Alternative (confirm-based) requires a second endpoint the client hits after upload succeeds.
Modify createSignedUploadUrl to call recordUpload internally.
What this accomplishes: Sign requests now also persist the record, so a subsequent download request can look up the record by uploadId.
Download endpoint
Implement createSignedDownloadUrl(uploadId, requester) in src/lib/uploads/service.ts. Looks up the upload record by ID. If the requester is the owner (requester.userId === record.userId) or is admin (requester.isAdmin), returns a signed download URL from GCS. Otherwise returns null (which the endpoint converts to a 403).
What this accomplishes: Ownership check is centralized. Non-owners can never fish for other people's uploads by guessing IDs; admins can always download to review applications.
Wire GET /api/v1/uploads/[id] to call it. Returns { url, expiresAt } on success, 404 if the upload ID doesn't exist, 403 if the requester isn't allowed.
What this accomplishes: The endpoint that the applicant form (to show "your uploaded resume") and the admin applicant detail page (to review resumes) both call.
Signed download URLs expire in 15 minutes.
What this accomplishes: Matches the upload URL expiry. Short enough to limit damage from a leaked URL, long enough for normal use.
Auth gating
Wire requireUser() into both POST /api/v1/uploads/sign and GET /api/v1/uploads/[id].
What this accomplishes: Anonymous users can't request upload URLs or download files. Every request is tied to a specific user via session.
Remove the placeholder userId header/query-param handling from the sign endpoint. Use session.user.id from requireUser().
What this accomplishes: Same cleanup pattern as the other tickets.
End-to-end verification
Sign in, upload a file via /uploads-demo, note the returned uploadId, hit GET /api/v1/uploads/[id] with that ID, download the file at the returned URL, verify bytes match.
What this accomplishes: Proves the full round trip works with real auth.
Sign in as a different user, try to download the first user's uploadId, confirm 403.
What this accomplishes: Proves the ownership check actually blocks cross-user access.
Sign in as an admin, download the first user's file, confirm it works.
What this accomplishes: Proves admin override works — critical for application review.
Definition of done
Every signed upload writes a corresponding record in the uploads collection.
GET /api/v1/uploads/[id] returns a working signed download URL for the owner or an admin, 403 for anyone else, 404 for missing IDs.
Context
Last sprint shipped the signed upload URL endpoint (
POST /api/v1/uploads/sign). What's missing is the download side, persistent tracking of uploads in Mongo, and ownership enforcement.Tasks
Upload records collection
uploads(orupload_records— pick a name and stick with it). Document shape:{ _id: uploadId, userId, filename, mime, size, gcsPath, createdAt }.userIdfor fast "give me all this user's uploads."recordUpload service function
recordUpload({ uploadId, userId, filename, mime, size, gcsPath })insrc/lib/uploads/service.ts. Inserts a document into the uploads collection.createSignedUploadUrlto callrecordUploadinternally.Download endpoint
createSignedDownloadUrl(uploadId, requester)insrc/lib/uploads/service.ts. Looks up the upload record by ID. If the requester is the owner (requester.userId === record.userId) or is admin (requester.isAdmin), returns a signed download URL from GCS. Otherwise returns null (which the endpoint converts to a 403).GET /api/v1/uploads/[id]to call it. Returns{ url, expiresAt }on success, 404 if the upload ID doesn't exist, 403 if the requester isn't allowed.Auth gating
requireUser()into bothPOST /api/v1/uploads/signandGET /api/v1/uploads/[id].userIdheader/query-param handling from the sign endpoint. Usesession.user.idfromrequireUser().End-to-end verification
/uploads-demo, note the returneduploadId, hitGET /api/v1/uploads/[id]with that ID, download the file at the returned URL, verify bytes match.uploadId, confirm 403.Definition of done
uploadscollection.GET /api/v1/uploads/[id]returns a working signed download URL for the owner or an admin, 403 for anyone else, 404 for missing IDs.requireUser().