Skip to content

Release: Phase 16 — product images on S3 (LocalStack dev) (#57) - #77

Merged
Poojithvsc merged 3 commits into
mainfrom
dev
Jun 5, 2026
Merged

Release: Phase 16 — product images on S3 (LocalStack dev) (#57)#77
Poojithvsc merged 3 commits into
mainfrom
dev

Conversation

@Poojithvsc

Copy link
Copy Markdown
Owner

Phase 16: an admin can attach an image to a product; shoppers see it via a short-lived presigned URL from a private bucket. Bytes live in S3; the DB stores only a key. Built so the same binary runs on LocalStack in dev and real S3 in cloud by changing config — the S3 mirror of the Phase-11 RDS seam.

What shipped

  • ProductImageStorage deep module — upload(productId, bytes, contentType) → key, presignedRead(key, ttl) → URL; hides the S3 SDK, bucket, key scheme, presigning. Catalog code never sees an S3 type.
  • Config seam shopsphere.storage.s3.endpoint — LocalStack URL in dev, blank in cloud (SDK resolves real S3); creds via explicit keys or the default AWS chain; path-style forced.
  • Admin upload POST /api/v1/admin/products/{id}/image (multipart), under the existing hasRole('ADMIN') guard — closes the image-upload guard deferred in ADR-0017. Unsupported type → 400, unknown product → 404. Flyway V15 adds nullable image_key.
  • Read exposureProductDto.imageUrl is a 5-minute presigned URL (null if no image); ProductMapper is now a component. Presigning is local (no S3 round-trip), so per-row minting while paging is cheap.
  • LocalStack in docker-compose (S3 + bucket-init script); the full-profile app wired to it.
  • Cloud Terraform terraform/s3/ — private bucket (public access blocked, ACLs off, SSE-S3), authored, apply deferred to a lab (ADR-0013 posture).
  • ADR-0016, article stub.

Testing (lab-free)

LocalStack via Testcontainers in SharedContainers (signature validation on, so expiry is enforced). ProductImageStorageIT (upload→presigned GET→bytes; expired→403) and AdminImageUploadIT (USER→403, admin→200, unsupported→400, unknown→404, end-to-end presigned read, null-when-absent). mvn verify green — 62 ITs + units, full suite.

Acceptance criteria (#57)

  • ProductImageStorage deep module (upload/presignedRead)
  • Dev compose localstack on :4566 + init creates shopsphere-product-images
  • Cloud Terraform — private bucket + public-access block (authored; apply deferred, ADR-0016)
  • POST /api/v1/admin/products/{id}/image multipart, stored under <productId>.<ext>and admin-guarded (closes ADR-0017 deferral)
  • ProductDto.imageUrl = 5-min presigned URL on reads, null if none
  • Testcontainers LocalStack IT: upload → presigned read → bytes; expired → 403
  • mvn verify green (LocalStack IT in the default suite)
  • ADR-0016 written, books cited; article stub
  • [~] Known limit (ADR-0016): under the fully-containerised full profile, presigned host is localstack:4566 (resolves in-network, not host browser); the host-run dev loop mints host-openable URLs

Closes #57.

🤖 Generated with Claude Code

Poojithvsc and others added 3 commits June 5, 2026 20:53
A PaymentProvider-style deep module for product images: upload(productId,
bytes, contentType) -> key and presignedRead(key, ttl) -> URL, hiding the S3
SDK, bucket, key scheme, and presigning behind a two-method interface. The
dev↔cloud seam is a single config knob — shopsphere.storage.s3.endpoint —
mirroring the ${DB_HOST} RDS pattern: a LocalStack URL in dev, blank in cloud
so the SDK resolves real S3. Path-style + default-or-static credentials make
the same binary work against both.

Tested against a real S3 API (LocalStack via Testcontainers, shared in
SharedContainers with signature validation on so expiry is enforced):
upload -> presigned GET returns the bytes; after the TTL the URL is rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ds (Phase 16)

POST /api/v1/admin/products/{id}/image (multipart) stores the bytes via
ProductImageStorage and records the returned key on the product (Flyway V15
adds the nullable image_key column). The endpoint inherits the controller's
@PreAuthorize("hasRole('ADMIN')") — closing the image-upload guard deferred in
Phase 17 / ADR-0017. Unsupported content types are rejected 400, unknown
products 404, and an edit (PUT) now carries the image key across.

ProductDto gains imageUrl; ProductMapper becomes a component that mints a
5-minute presigned read URL when a product has an image (null otherwise), so
the private bucket is reachable only through a short-lived URL. Presigning is a
local signature computation, so per-row minting while paging stays cheap.
AdminImageUploadIT covers USER->403, admin->200, unsupported->400, unknown->404,
the end-to-end presigned read, and null-when-absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…016 (Phase 16)

docker-compose grows a localstack service (S3) with a ready.d init script that
creates the product-image bucket, and the full-profile app gains the S3_* seam
env. terraform/s3/ authors a PRIVATE bucket (public access blocked, ACLs off,
SSE-S3) — apply deferred to a lab like the secrets terraform (ADR-0013); the app
doesn't depend on it. ADR-0016 records the deep-module + LocalStack-Service-Stub
design, the dev↔cloud config seam, and two honest limits (deferred cloud apply;
presigned-URL host under the fully-containerised profile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Poojithvsc
Poojithvsc merged commit 4459245 into main Jun 5, 2026
2 checks passed
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.

Phase 16: S3 product images + presigned URLs (LocalStack dev)

1 participant