Share a wishlist by link
Java · Spring Boot · beginner · modification
Customers can now share a wishlist by link, for example with family before a birthday. `POST /api/wishlists/{id}/share` gives the wishlist a random share token (or returns the one it already has), `DELETE` on the same path clears it, and `GET /api/public/wishlists/{token}` shows the list to anyone holding the link. The token is stored in the new `share_token` column on `Wishlist`. `WishlistShareService` holds the logic; the owner endpoints load the wishlist with a row lock, scoped to the signed-in customer.
Shop backend on Spring Boot 3.3.5 (Java 21), Spring Data JPA with Hibernate 6.5 on PostgreSQL 16 at its default isolation (read committed); Hibernate takes a row write lock for `@Lock(PESSIMISTIC_WRITE)` (`SELECT ... FOR NO KEY UPDATE` on PostgreSQL), which a second locking read of the same row waits on. The existing, unchanged security config requires a signed-in session on every `/api/**` route except `/api/public/**` and the `/error` page, which it lets anonymous visitors call (so a 404 from the public route keeps its status through the error dispatch), and it handles CSRF for the session routes. `CurrentCustomer.id()` returns the signed-in customer's id and is never null on a signed-in route. Existing and unchanged: the rest of `Wishlist`; `WishlistItem` (wishlist id, product id, product name, `addedAt`); `WishlistItemRepository.findByWishlistIdOrderByAddedAtAsc`; the `WishlistItemView` record (`productId`, `productName`) with `WishlistItemView.from(item)`; `WishlistNotFoundException`, annotated `@ResponseStatus(NOT_FOUND)`; and `WishlistController` (`GET /api/wishlists` and `GET /api/wishlists/{id}`), whose owner endpoints return `WishlistView` records, never the entity. Errors use Spring Boot's default error body with `server.error.include-message` at its default (`never`). A migration that ships separately and is already applied added the nullable `share_token varchar(43)` column to `wishlists` with a unique constraint. Deleting a wishlist (existing, unchanged) deletes its row and its items.
Requirements
- New `POST /api/wishlists/{id}/share` (signed in) turns on sharing for one of the customer's own wishlists and answers 200 with `{"token": "<token>"}`; the web app builds the link `https://shop.example/w/<token>` from it. If the wishlist is already shared, the call answers the existing token, so a link already sent keeps working. Two calls at the same moment get the same token.
- The token must be unguessable and fit in a link: at least 128 bits from a cryptographically secure random source, using only letters, digits, `-` and `_`.
- New `DELETE /api/wishlists/{id}/share` (signed in) turns sharing off and answers 204; from then on the old link answers 404. Turning it off when it is already off also answers 204. Sharing again later gives a new token, so the old link stays dead.
- Both owner endpoints answer 404 and change nothing for a wishlist that does not exist or belongs to another customer. A non-numeric `id` answers 400.
- New `GET /api/public/wishlists/{token}` needs no login. For a live token it answers 200 with the wishlist's `name` and its `items` (each `productId` and `productName`) in the order they were added; items added at the same instant may come back in either order. An unknown or revoked token answers 404. The response never contains the owner's id or email.
Files touched
- src/main/java/com/acme/shop/wishlist/Wishlist.java
- src/main/java/com/acme/shop/wishlist/WishlistRepository.java
- src/main/java/com/acme/shop/wishlist/WishlistShareService.java
- src/main/java/com/acme/shop/wishlist/WishlistShareController.java
--- src/main/java/com/acme/shop/wishlist/Wishlist.java
@Column(name = "created_at", nullable = false)
private Instant createdAt;
+
+ @Column(name = "share_token", unique = true, length = 43)
+ private String shareToken;
protected Wishlist() {
return createdAt;
}
+
+ public String getShareToken() {
+ return shareToken;
+ }
+
+ public void setShareToken(String shareToken) {
+ this.shareToken = shareToken;
+ }
}
--- src/main/java/com/acme/shop/wishlist/WishlistRepository.java
package com.acme.shop.wishlist;
+import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
public interface WishlistRepository extends JpaRepository<Wishlist, Long> {