Skip to content

File Uploads — download endpoint + upload records #501

Description

@adityapat24

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

  • 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.
  • Both endpoints are gated by requireUser().

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions