Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.
This repository was archived by the owner on Aug 11, 2026. It is now read-only.

feat(storage): add direct-to-R2 presigned upload flow for news images #634

Description

@ahliweb

Depends on: #631, #632, #633

Context

Full-online R2-only news portal mode requires news image uploads to go to Cloudflare R2 without writing files to the application server filesystem. The application should create controlled upload sessions, generate server-owned object keys, and verify uploaded objects before attaching them to content.

Objective

Add a secure direct-to-R2 presigned upload flow for news images.

Scope

Add API routes such as:

POST /api/v1/media/news-images/upload-sessions
POST /api/v1/media/news-images/upload-sessions/{id}/finalize
POST /api/v1/media/news-images/upload-sessions/{id}/cancel

Upload session flow:

  1. Authenticated admin requests an upload session.
  2. Server validates tenant context, ABAC, file metadata, MIME, extension, size, and target resource.
  3. Server creates a pending_upload media object metadata row.
  4. Server generates an R2 presigned PUT URL or equivalent upload credential.
  5. Browser uploads directly to R2.
  6. Browser calls finalize.
  7. Server verifies object existence and metadata via R2 HEAD/metadata.
  8. Server marks media object as verified.

Suggested permissions:

media_objects.news_images.upload
media_objects.news_images.read
media_objects.news_images.attach
media_objects.news_images.delete

Required validations

  • Allowed MIME types from config.
  • File extension allowlist.
  • Maximum file size.
  • Optional checksum SHA-256.
  • Server-generated object key only.
  • Tenant/module prefix in object key.
  • Short-lived upload session.
  • Short-lived presigned URL.
  • No request body, cookie, token, auth header, or secret stored in metadata.

Out of scope

  • Image transformation service.
  • Local temp-file uploads.
  • Local storage fallback.
  • Importing legacy files.
  • Public anonymous uploads.

Acceptance criteria

  • Upload session requires authenticated admin session.
  • Upload session requires tenant context.
  • Upload session enforces ABAC default-deny.
  • Object key is generated server-side and cannot be supplied by the client.
  • Presigned upload URL is short-lived and scoped to one object key.
  • Finalize fails if object does not exist in R2.
  • Finalize fails if checksum mismatches when checksum is required.
  • Finalize fails if uploaded object metadata violates size or MIME policy.
  • Server never writes the image file to local disk.
  • R2 credentials are never exposed to browser/client code.
  • Failed/expired sessions can be marked failed or orphaned.
  • Audit events are written for session create, finalize, cancel, and failure.
  • OpenAPI is updated with schemas, auth, errors, and examples.
  • Tests cover successful upload session, ABAC deny, invalid MIME, size too large, expired session, finalize missing object, checksum mismatch, and no-local-write guarantee where feasible.
  • bun run api:spec:check passes.
  • bun run test passes.
  • bun run check passes.

Security notes

  • Never accept public_url from the browser.
  • Never accept client-provided object keys.
  • Never write upload files to /tmp, /public, /uploads, Docker volumes, or app filesystem.
  • Log only safe metadata and correlation IDs; redact secrets and presigned URLs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions