From f53a03277ff8ba5d81aed035026def567ad68be3 Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Tue, 30 Jun 2026 19:12:59 +0200 Subject: [PATCH 1/5] added product with details --- .../hackyourfuture/hyfshop/product/Product.java | 2 ++ .../hyfshop/product/ProductRepository.java | 16 +++++++++++++++- .../hyfshop/product/dto/ProductResponse.java | 7 +++++-- src/main/resources/application.yaml | 4 ++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java index ccc6204..a3108b9 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java @@ -6,6 +6,7 @@ import lombok.Setter; import java.math.BigDecimal; +import java.util.Map; @AllArgsConstructor @NoArgsConstructor @@ -17,4 +18,5 @@ public class Product { private BigDecimal price; private String category; private String imageUrl; + private Map details; } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 2766734..096fdbb 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -4,12 +4,17 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + import java.util.List; +import java.util.Map; @Repository @AllArgsConstructor public class ProductRepository { private final JdbcClient jdbcClient; + private static final ObjectMapper objectMapper = new ObjectMapper(); public static final RowMapper PRODUCT_ROW_MAPPER = (rs, _) -> { var product = new Product(); @@ -18,12 +23,21 @@ public class ProductRepository { product.setPrice(rs.getBigDecimal("price")); product.setCategory(rs.getString("category")); product.setImageUrl(rs.getString("image_url")); + try { + String json = rs.getString("details"); + if (json != null) { + product.setDetails(objectMapper.readValue(json, + new TypeReference>() {})); + } + } catch (Exception e) { + throw new RuntimeException(e); + } return product; }; public List getAllProducts() { return jdbcClient - .sql("SELECT id, title, price, category, image_url FROM products") + .sql("SELECT * FROM products") .query(PRODUCT_ROW_MAPPER) .list(); diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java b/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java index 0c35ef1..2e4a45b 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java @@ -3,13 +3,15 @@ import net.hackyourfuture.hyfshop.product.Product; import java.math.BigDecimal; +import java.util.Map; public record ProductResponse ( int id, String title, BigDecimal price, String category, - String imageUrl + String imageUrl, + Map details ){ public static ProductResponse from(Product product) { return new ProductResponse( @@ -17,7 +19,8 @@ public static ProductResponse from(Product product) { product.getTitle(), product.getPrice(), product.getCategory(), - product.getImageUrl() + product.getImageUrl(), + product.getDetails() ); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 05b7788..be78240 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -7,3 +7,7 @@ spring: password: '${DB_PASSWORD}' server: port: '8080' +sql: + init: + # runs schema.sql automatically every time the app starts + mode: always \ No newline at end of file From 9150a61a00907e0fc2cbe333aa72fbce34d6b42a Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Tue, 30 Jun 2026 19:34:38 +0200 Subject: [PATCH 2/5] fix search by color --- .../hackyourfuture/hyfshop/product/ProductRepository.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 096fdbb..ac7eaf5 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -63,8 +63,11 @@ public void setImageUrl(int id, String imageUrl) { } public List findByColor(String color) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + return jdbcClient + .sql("SELECT * FROM products WHERE details @> CAST(? AS jsonb)") + .param("{\"color\": \"" + color + "\"}") + .query(PRODUCT_ROW_MAPPER) + .list(); } public Product setSize(int id, String size) { From 93051539b0d2d937cda21a35906263caeef2a7ae Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Tue, 30 Jun 2026 20:10:53 +0200 Subject: [PATCH 3/5] updata size for product --- .../hyfshop/product/ProductRepository.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index ac7eaf5..2215695 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -1,6 +1,7 @@ package net.hackyourfuture.hyfshop.product; import lombok.AllArgsConstructor; +import net.hackyourfuture.hyfshop.product.dto.ProductResponse; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; @@ -45,7 +46,7 @@ public List getAllProducts() { public Product findById(int id) { return jdbcClient - .sql("SELECT id, title, price, category, image_url FROM products WHERE id = :id") + .sql("SELECT * FROM products WHERE id = :id") .param("id", id) .query(PRODUCT_ROW_MAPPER) .single(); @@ -70,8 +71,11 @@ public List findByColor(String color) { .list(); } - public Product setSize(int id, String size) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + public void setSize(int id, String size) { + jdbcClient. + sql("UPDATE products SET details = jsonb_set(details, '{size}', ?::jsonb) WHERE id = ?") + .param("\"" + size + "\"") + .param(id) + .update(); } } From 0b87ebab050a4d9ff1d26dea9d1a5991569aabfa Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Wed, 1 Jul 2026 21:04:52 +0200 Subject: [PATCH 4/5] initial file upload --- pom.xml | 7 +++ .../hyfshop/configuration/B2Config.java | 41 +++++++++++++ .../hyfshop/file/FileService.java | 59 +++++++++++++++++++ src/main/resources/application.yaml | 9 ++- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java create mode 100644 src/main/java/net/hackyourfuture/hyfshop/file/FileService.java diff --git a/pom.xml b/pom.xml index bbdda38..ed8adf6 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,13 @@ jackson-core 3.2.0 + + + software.amazon.awssdk + s3 + 2.25.0 + + diff --git a/src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java b/src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java new file mode 100644 index 0000000..5fc2688 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java @@ -0,0 +1,41 @@ +package net.hackyourfuture.hyfshop.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +import java.net.URI; + +@Configuration +public class B2Config { + + @Value("${b2.endpoint}") private String endpoint; + @Value("${b2.region}") private String region; + @Value("${b2.access-key}") private String accessKey; + @Value("${b2.secret-key}") private String secretKey; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .endpointOverride(URI.create(endpoint)) + .region(Region.of(region)) + .build(); + } + + @Bean + public S3Presigner s3Presigner() { + return S3Presigner.builder() + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .endpointOverride(URI.create(endpoint)) + .region(Region.of(region)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java b/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java new file mode 100644 index 0000000..0786cb0 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java @@ -0,0 +1,59 @@ +package net.hackyourfuture.hyfshop.file; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; + +import java.time.Duration; +import java.util.UUID; + +@Service +public class FileService { + + private final S3Client s3Client; + private final S3Presigner s3Presigner; + + @Value("${b2.bucket}") + private String bucket; + + public FileService(S3Client s3Client, S3Presigner s3Presigner) { + this.s3Client = s3Client; + this.s3Presigner = s3Presigner; + } + + // Upload — returns the key to save in your database + public String upload(MultipartFile file) throws Exception { + String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket).key(key) + .contentType(file.getContentType()).build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + return key; + } + + // Presigned URL — expires after given minutes + public String getLink(String key, int minutes) { + return s3Presigner.presignGetObject( + GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(minutes)) + .getObjectRequest(r -> r.bucket(bucket).key(key)) + .build() + ).url().toString(); + } + + // Delete + public void delete(String key) { + s3Client.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucket).key(key).build() + ); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index be78240..f0b4310 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -10,4 +10,11 @@ server: sql: init: # runs schema.sql automatically every time the app starts - mode: always \ No newline at end of file + mode: always + +b2: + endpoint: https://s3.eu-central-003.backblazeb2.com + region: eu-central-003 + access-key: ${B2_ACCESS_KEY} + secret-key: ${B2_SECRET_KEY} + bucket: ${B2_BUCKET} \ No newline at end of file From 37171676a7bdcd1dfd7758ddf49bcc3afb5094d1 Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Wed, 1 Jul 2026 23:06:04 +0200 Subject: [PATCH 5/5] file service --- .../hyfshop/file/FileService.java | 41 ++++++++++--------- .../hyfshop/product/ProductService.java | 32 ++++++++++++--- src/main/resources/application.yaml | 6 +-- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java b/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java index 0786cb0..9fd321c 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java +++ b/src/main/java/net/hackyourfuture/hyfshop/file/FileService.java @@ -8,9 +8,7 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; -import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; -import java.time.Duration; import java.util.UUID; @Service @@ -22,38 +20,43 @@ public class FileService { @Value("${b2.bucket}") private String bucket; + @Value("${b2.endpoint}") + private String endpoint; + public FileService(S3Client s3Client, S3Presigner s3Presigner) { this.s3Client = s3Client; this.s3Presigner = s3Presigner; } - // Upload — returns the key to save in your database + public String upload(MultipartFile file) throws Exception { - String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); + String key = UUID.randomUUID().toString() + "-" + file.getOriginalFilename(); + s3Client.putObject( PutObjectRequest.builder() - .bucket(bucket).key(key) - .contentType(file.getContentType()).build(), + .bucket(bucket) + .key(key) + .contentType(file.getContentType()) + .build(), RequestBody.fromInputStream(file.getInputStream(), file.getSize()) ); - return key; - } - // Presigned URL — expires after given minutes - public String getLink(String key, int minutes) { - return s3Presigner.presignGetObject( - GetObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(minutes)) - .getObjectRequest(r -> r.bucket(bucket).key(key)) - .build() - ).url().toString(); + return endpoint + "/file/" + bucket + "/" + key; } - // Delete - public void delete(String key) { + + public void deleteByUrl(String imageUrl) { + if (imageUrl == null || imageUrl.isEmpty()) { + return; + } + + String key = imageUrl.substring(imageUrl.lastIndexOf("/") + 1); + s3Client.deleteObject( DeleteObjectRequest.builder() - .bucket(bucket).key(key).build() + .bucket(bucket) + .key(key) + .build() ); } } \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java index 779d3b2..0d69ad8 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java @@ -1,6 +1,7 @@ package net.hackyourfuture.hyfshop.product; import lombok.RequiredArgsConstructor; +import net.hackyourfuture.hyfshop.file.FileService; import net.hackyourfuture.hyfshop.product.dto.ProductResponse; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -11,6 +12,7 @@ @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; + private final FileService fileService; public List getAllProducts() { return productRepository.getAllProducts().stream().map(ProductResponse::from).toList(); @@ -26,14 +28,32 @@ public ProductResponse setProductSize(int id, String size) { } public ProductResponse setProductImage(int id, MultipartFile file) { - // TODO: Implement - // call ProductRepository.setImageUrl() afterwards with the new URL - throw new UnsupportedOperationException("Not implemented yet"); + try { + Product product = productRepository.findById(id); + if (product.getImageUrl() != null) { + fileService.deleteByUrl(product.getImageUrl()); + } + String imageUrl = fileService.upload(file); + productRepository.setImageUrl(id, imageUrl); + + Product updatedProduct = productRepository.findById(id); + return ProductResponse.from(updatedProduct); + + } catch (Exception e) { + throw new RuntimeException("Failed to upload product image", e); + } } public ProductResponse deleteProductImage(int id) { - // TODO: Implement - // call ProductRepository.setImageUrl() to set the image url to null - throw new UnsupportedOperationException("Not implemented yet"); + Product product = productRepository.findById(id); + + if (product.getImageUrl() != null) { + fileService.deleteByUrl(product.getImageUrl()); + } + + productRepository.setImageUrl(id, null); + + Product updatedProduct = productRepository.findById(id); + return ProductResponse.from(updatedProduct); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f0b4310..9c7d2bf 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,6 +15,6 @@ sql: b2: endpoint: https://s3.eu-central-003.backblazeb2.com region: eu-central-003 - access-key: ${B2_ACCESS_KEY} - secret-key: ${B2_SECRET_KEY} - bucket: ${B2_BUCKET} \ No newline at end of file + access-key: B2_ACCESS_KEY + secret-key: B2_SECRET_KEY + bucket: hyfshop \ No newline at end of file