Release: Phase 16 — product images on S3 (LocalStack dev) (#57) - #77
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ProductImageStoragedeep 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.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.POST /api/v1/admin/products/{id}/image(multipart), under the existinghasRole('ADMIN')guard — closes the image-upload guard deferred in ADR-0017. Unsupported type → 400, unknown product → 404. FlywayV15adds nullableimage_key.ProductDto.imageUrlis a 5-minute presigned URL (null if no image);ProductMapperis now a component. Presigning is local (no S3 round-trip), so per-row minting while paging is cheap.docker-compose(S3 + bucket-init script); thefull-profile app wired to it.terraform/s3/— private bucket (public access blocked, ACLs off, SSE-S3), authored, apply deferred to a lab (ADR-0013 posture).Testing (lab-free)
LocalStack via Testcontainers in
SharedContainers(signature validation on, so expiry is enforced).ProductImageStorageIT(upload→presigned GET→bytes; expired→403) andAdminImageUploadIT(USER→403, admin→200, unsupported→400, unknown→404, end-to-end presigned read, null-when-absent).mvn verifygreen — 62 ITs + units, full suite.Acceptance criteria (#57)
ProductImageStoragedeep module (upload/presignedRead)localstackon :4566 + init createsshopsphere-product-imagesPOST /api/v1/admin/products/{id}/imagemultipart, stored under<productId>.<ext>— and admin-guarded (closes ADR-0017 deferral)ProductDto.imageUrl= 5-min presigned URL on reads, null if nonemvn verifygreen (LocalStack IT in the default suite)fullprofile, presigned host islocalstack:4566(resolves in-network, not host browser); the host-run dev loop mints host-openable URLsCloses #57.
🤖 Generated with Claude Code