From e752e684840e148d2c3a59cc698c0d3a2935d45b Mon Sep 17 00:00:00 2001 From: mustapha bouddahr Date: Tue, 17 Feb 2026 23:42:33 +0000 Subject: [PATCH 1/6] aws s3 intialization --- docker-compose.yml | 21 ++++++ pom.xml | 12 +++- .../config/S3Configuration.java | 34 +++++++++ .../nbat/listing/application/S3Service.java | 70 +++++++++++++++++++ src/main/resources/application.yml | 16 +++-- 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 docker-compose.yml create mode 100644 src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java create mode 100644 src/main/java/fr/codecake/nbat/listing/application/S3Service.java diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9472fdf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + postgres: + image: postgres:16 + container_name: nbat-postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 0000 + POSTGRES_DB: nbat_db + ports: + - "5432:5432" + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d nbat_db"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local diff --git a/pom.xml b/pom.xml index 8456b68..037287b 100644 --- a/pom.xml +++ b/pom.xml @@ -90,11 +90,17 @@ - com.stripe - stripe-java - 25.0.0 + com.stripe + stripe-java + 25.0.0 + + + software.amazon.awssdk + s3 + 2.20.26 + diff --git a/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java b/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java new file mode 100644 index 0000000..f2265bc --- /dev/null +++ b/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java @@ -0,0 +1,34 @@ +package fr.codecake.nbat.infrastructure.config; + +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; + +@Configuration +public class S3Configuration { + + @Value("${aws.credentials.access-key}") + private String accessKey; + + @Value("${aws.credentials.secret-key}") + private String secretKey; + + @Value("${aws.s3.region}") + private String region; + + @Bean + public S3Client S3Client() { + AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, accessKey); + + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials)) + .build(); + } + +} diff --git a/src/main/java/fr/codecake/nbat/listing/application/S3Service.java b/src/main/java/fr/codecake/nbat/listing/application/S3Service.java new file mode 100644 index 0000000..32ddbb8 --- /dev/null +++ b/src/main/java/fr/codecake/nbat/listing/application/S3Service.java @@ -0,0 +1,70 @@ +package fr.codecake.nbat.listing.application; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; + +import java.util.UUID; + +@Service +public class S3Service { + + private final S3Client s3Client; + + @Value("${aws.s3.bucket-name}") + private String bucketName; + + @Value("${aws.s3.region}") + private String region; + + public S3Service(S3Client s3Client) { + this.s3Client = s3Client; + } + + /** + * Uploads a file to S3 and returns the public URL where the file can be + * accessed. + * + * @param fileBytes The actual file data as a byte array + * @param contentType The MIME type of the file (e.g., "image/jpeg", + * "image/png") + * @param originalFileName The original name of the file (used to determine file + * extension) + * @return The public URL where the uploaded file can be accessed + */ + public String uploadFile(byte[] fileBytes, String contentType, String originalFileName) { + // Generate a unique filename to avoid collisions + // We use UUID to ensure each file has a unique name + String fileExtension = getFileExtension(originalFileName); + String uniqueFileName = "listings/" + UUID.randomUUID().toString() + fileExtension; + + // Build the S3 upload request + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(uniqueFileName) + .contentType(contentType) + .acl(ObjectCannedACL.PUBLIC_READ) // Make the file publicly readable + .build(); + + // Actually upload the file to S3 + s3Client.putObject(putObjectRequest, RequestBody.fromBytes(fileBytes)); + + // Construct and return the public URL + return String.format("https://%s.s3.%s.amazonaws.com/%s", + bucketName, region, uniqueFileName); + } + + /** + * Extracts the file extension from a filename. + * For example: "house.jpg" returns ".jpg" + */ + private String getFileExtension(String fileName) { + if (fileName == null || !fileName.contains(".")) { + return ""; + } + return fileName.substring(fileName.lastIndexOf(".")); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2c4d6be..97835ce 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,11 +6,11 @@ logging: spring: application: - name: nbat-clone-back + name: nbat-back docker: compose: - enabled: false + enabled: true lifecycle-management: start_only datasource: @@ -18,7 +18,7 @@ spring: username: postgres password: 1234 driver-class-name: org.postgresql.Driver - hikari: # ← ajoute ici + hikari: # ← ajoute ici keepalive-time: 60000 connection-timeout: 30000 idle-timeout: 600000 @@ -49,10 +49,16 @@ okta: # ============= STRIPE CONFIGURATION ============= stripe: api: - key: sk_test_51SzCCMCBpNRlxye2LDTsbKkd7cdP8pnayOmzOoieXMeyrRtY8e70EuLj54l1LZFTmYcdiQxAyKJ30K2K4o2XZks900Ii3nLGGq + key: sk_test_51SzCCMCBpNRlxye2LDTsbKkd7cdP8pnayOmzOoieXMeyrRtY8e70EuLj54l1LZFTmYcdiQxAyKJ30K2K4o2XZks900Ii3nLGGq webhook: secret: whsec_3c3d45265f373bc6806d3424ad00fe870860b0fdb3000b14a293526f1ff01d72 success-url: http://localhost:4200/payment-success cancel-url: http://localhost:4200/payment-cancel # ================================================ - +aws: + s3: + backet-name: nbat-file-prod + region: eu-west-3 + credentials: + access-key: AKIAUGGODHXX736JYEST + secret-key: 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 From 9add503cbf54a1dcbc19a3279e03b156c36bfcfd Mon Sep 17 00:00:00 2001 From: mustapha bouddahr Date: Wed, 18 Feb 2026 22:10:00 +0000 Subject: [PATCH 2/6] upload images to s3 instead of db --- .../config/S3Configuration.java | 2 +- .../listing/application/PictureService.java | 38 +++++++++++++++++-- .../nbat/listing/domain/ListingPicture.java | 28 ++++++++------ .../listing/mapper/ListingPictureMapper.java | 14 +++++-- src/main/resources/application.yml | 6 +-- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java b/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java index f2265bc..5bd1d42 100644 --- a/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java +++ b/src/main/java/fr/codecake/nbat/infrastructure/config/S3Configuration.java @@ -23,7 +23,7 @@ public class S3Configuration { @Bean public S3Client S3Client() { - AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, accessKey); + AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, secretKey); return S3Client.builder() .region(Region.of(region)) diff --git a/src/main/java/fr/codecake/nbat/listing/application/PictureService.java b/src/main/java/fr/codecake/nbat/listing/application/PictureService.java index 5626a44..2e744b3 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/PictureService.java +++ b/src/main/java/fr/codecake/nbat/listing/application/PictureService.java @@ -14,18 +14,25 @@ public class PictureService { private final ListingPictureRepository listingPictureRepository; - private final ListingPictureMapper listingPictureMapper; + // NEW: Inject the S3Service so we can upload files + private final S3Service s3Service; - public PictureService(ListingPictureRepository listingPictureRepository, ListingPictureMapper listingPictureMapper) { + // CHANGED: Constructor now includes S3Service parameter + // Spring will automatically inject the S3Service bean + public PictureService(ListingPictureRepository listingPictureRepository, + ListingPictureMapper listingPictureMapper, + S3Service s3Service) { this.listingPictureRepository = listingPictureRepository; this.listingPictureMapper = listingPictureMapper; + this.s3Service = s3Service; } public List saveAll(List pictures, Listing listing) { if (pictures == null || pictures.isEmpty()) { return List.of(); } + Set listingPictures = listingPictureMapper.pictureDTOsToListingPictures(pictures); boolean isFirst = true; @@ -33,10 +40,35 @@ public List saveAll(List pictures, Listing listing) { for (ListingPicture listingPicture : listingPictures) { listingPicture.setCover(isFirst); listingPicture.setListing(listing); + + // REAL S3 UPLOAD - uncommented now + PictureDTO matchingDTO = findMatchingDTO(listingPicture, pictures); + if (matchingDTO != null && matchingDTO.file() != null) { + String s3Url = s3Service.uploadFile( + matchingDTO.file(), + listingPicture.getFileContentType(), + "listing-image"); + listingPicture.setFileUrl(s3Url); + } + isFirst = false; } listingPictureRepository.saveAll(listingPictures); return listingPictureMapper.listingPictureToPictureDTO(listingPictures.stream().toList()); } -} + + /** + * Helper method to find the DTO that corresponds to an entity. + */ + private PictureDTO findMatchingDTO(ListingPicture picture, List dtos) { + // Match by content type and cover status since these are copied during mapping + for (PictureDTO dto : dtos) { + if (dto.fileContentType().equals(picture.getFileContentType()) + && dto.isCover() == picture.isCover()) { + return dto; + } + } + return dtos.isEmpty() ? null : dtos.get(0); // Fallback to first DTO if no match + } +} \ No newline at end of file diff --git a/src/main/java/fr/codecake/nbat/listing/domain/ListingPicture.java b/src/main/java/fr/codecake/nbat/listing/domain/ListingPicture.java index 869bd29..5dfbffb 100644 --- a/src/main/java/fr/codecake/nbat/listing/domain/ListingPicture.java +++ b/src/main/java/fr/codecake/nbat/listing/domain/ListingPicture.java @@ -20,9 +20,9 @@ public class ListingPicture extends AbstractAuditingEntity { @JoinColumn(name = "listing_fk", referencedColumnName = "id") private Listing listing; - @Lob - @Column(name = "file", nullable = false) - private byte[] file; + // store the url of the file instead of old bytes[] + @Column(name = "file", nullable = false, length = 500) + private String fileUrl; @Column(name = "file_content_type") private String fileContentType; @@ -47,12 +47,12 @@ public void setListing(Listing listing) { this.listing = listing; } - public byte[] getFile() { - return file; + public String getFileUrl() { + return fileUrl; } - public void setFile(byte[] file) { - this.file = file; + public void setFileUrl(String fileUrl) { + this.fileUrl = fileUrl; } public String getFileContentType() { @@ -73,21 +73,25 @@ public void setCover(boolean cover) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; ListingPicture that = (ListingPicture) o; - return isCover == that.isCover && Objects.deepEquals(file, that.file) && Objects.equals(fileContentType, that.fileContentType); + return isCover == that.isCover + && Objects.equals(fileUrl, that.fileUrl) + && Objects.equals(fileContentType, that.fileContentType); } @Override public int hashCode() { - return Objects.hash(Arrays.hashCode(file), fileContentType, isCover); + return Objects.hash(fileUrl, fileContentType, isCover); } @Override public String toString() { return "ListingPicture{" + - "file=" + Arrays.toString(file) + + "fileUrl='" + fileUrl + '\'' + ", fileContentType='" + fileContentType + '\'' + ", isCover=" + isCover + '}'; diff --git a/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java b/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java index d7de15f..86de3ff 100644 --- a/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java +++ b/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java @@ -12,7 +12,6 @@ @Mapper(componentModel = "spring") public interface ListingPictureMapper { - Set pictureDTOsToListingPictures(List pictureDTOs); @Mapping(target = "id", ignore = true) @@ -20,16 +19,25 @@ public interface ListingPictureMapper { @Mapping(target = "createdDate", ignore = true) @Mapping(target = "lastModifiedDate", ignore = true) @Mapping(target = "cover", source = "isCover") + // NEW: Ignore fileUrl because PictureService will set it after uploading to S3 + // MapStruct was trying to map "file" from DTO to "fileUrl" in entity but + // they're + // different types (byte[] vs String) so we tell it to skip this field + @Mapping(target = "fileUrl", ignore = true) ListingPicture pictureDTOToListingPicture(PictureDTO pictureDTO); List listingPictureToPictureDTO(List listingPictures); @Mapping(target = "isCover", source = "cover") + // NEW: When converting entity back to DTO, we need to map the URL to the file + // field + // But wait - DTO expects byte[], not String. This creates a problem. + // We'll need to modify this mapping or change how we handle responses + @Mapping(target = "file", ignore = true) PictureDTO convertToPictureDTO(ListingPicture listingPicture); @Named("extract-cover") default PictureDTO extractCover(Set pictures) { return pictures.stream().findFirst().map(this::convertToPictureDTO).orElseThrow(); } - -} +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 97835ce..509c291 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,8 +57,8 @@ stripe: # ================================================ aws: s3: - backet-name: nbat-file-prod + bucket-name: npat-files-prod region: eu-west-3 credentials: - access-key: AKIAUGGODHXX736JYEST - secret-key: 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 + access-key: "AKIAUGGODHXX736JYEST" + secret-key: "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" From 2a121a28df62717d6b32762a7a8acfe1b2b05299 Mon Sep 17 00:00:00 2001 From: mustapha bouddahr Date: Wed, 18 Feb 2026 22:23:49 +0000 Subject: [PATCH 3/6] remove the ACL public_read from the S3Service --- .../fr/codecake/nbat/listing/application/PictureService.java | 2 +- .../java/fr/codecake/nbat/listing/application/S3Service.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/codecake/nbat/listing/application/PictureService.java b/src/main/java/fr/codecake/nbat/listing/application/PictureService.java index 2e744b3..aa2933a 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/PictureService.java +++ b/src/main/java/fr/codecake/nbat/listing/application/PictureService.java @@ -41,7 +41,7 @@ public List saveAll(List pictures, Listing listing) { listingPicture.setCover(isFirst); listingPicture.setListing(listing); - // REAL S3 UPLOAD - uncommented now + // REAL S3 UPLOAD PictureDTO matchingDTO = findMatchingDTO(listingPicture, pictures); if (matchingDTO != null && matchingDTO.file() != null) { String s3Url = s3Service.uploadFile( diff --git a/src/main/java/fr/codecake/nbat/listing/application/S3Service.java b/src/main/java/fr/codecake/nbat/listing/application/S3Service.java index 32ddbb8..e952897 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/S3Service.java +++ b/src/main/java/fr/codecake/nbat/listing/application/S3Service.java @@ -46,7 +46,7 @@ public String uploadFile(byte[] fileBytes, String contentType, String originalFi .bucket(bucketName) .key(uniqueFileName) .contentType(contentType) - .acl(ObjectCannedACL.PUBLIC_READ) // Make the file publicly readable + // .acl(ObjectCannedACL.PUBLIC_READ) // Make the file publicly readable .build(); // Actually upload the file to S3 From ca79335ce3d7144e5970f24e360763dc756bec47 Mon Sep 17 00:00:00 2001 From: mustapha bouddahr Date: Thu, 19 Feb 2026 14:03:05 +0000 Subject: [PATCH 4/6] implement the pictures s3 url in the database and beeing sent to the front instead of s3 --- .../application/dto/BookedListingDTO.java | 13 +++-- .../listing/application/LandlordService.java | 2 +- .../listing/application/PictureService.java | 50 +++++++++---------- .../nbat/listing/application/S3Service.java | 1 - .../listing/application/TenantService.java | 21 ++++---- .../dto/DisplayCardListingDTO.java | 9 ++-- .../application/dto/SaveListingDTO.java | 20 +++++--- .../application/dto/sub/PictureDTO.java | 17 +++---- .../application/dto/sub/PictureUploadDTO.java | 9 ++++ .../codecake/nbat/listing/domain/Listing.java | 28 +++++++++-- .../nbat/listing/mapper/ListingMapper.java | 10 ++-- .../listing/mapper/ListingPictureMapper.java | 26 +++++----- .../presentation/LandlordResource.java | 15 +++--- .../listing/repository/ListingRepository.java | 25 ++++------ 14 files changed, 136 insertions(+), 110 deletions(-) create mode 100644 src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureUploadDTO.java diff --git a/src/main/java/fr/codecake/nbat/booking/application/dto/BookedListingDTO.java b/src/main/java/fr/codecake/nbat/booking/application/dto/BookedListingDTO.java index c858393..cfadd27 100644 --- a/src/main/java/fr/codecake/nbat/booking/application/dto/BookedListingDTO.java +++ b/src/main/java/fr/codecake/nbat/booking/application/dto/BookedListingDTO.java @@ -1,6 +1,5 @@ package fr.codecake.nbat.booking.application.dto; -import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; import fr.codecake.nbat.listing.application.dto.vo.PriceVO; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; @@ -8,10 +7,10 @@ import java.util.UUID; -public record BookedListingDTO(@Valid PictureDTO cover, - @NotEmpty String location, - @Valid BookedDateDTO dates, - @Valid PriceVO totalPrice, - @NotNull UUID bookingPublicId, - @NotNull UUID listingPublicId) { +public record BookedListingDTO(@Valid String cover, + @NotEmpty String location, + @Valid BookedDateDTO dates, + @Valid PriceVO totalPrice, + @NotNull UUID bookingPublicId, + @NotNull UUID listingPublicId) { } diff --git a/src/main/java/fr/codecake/nbat/listing/application/LandlordService.java b/src/main/java/fr/codecake/nbat/listing/application/LandlordService.java index a95c9d7..0ec2200 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/LandlordService.java +++ b/src/main/java/fr/codecake/nbat/listing/application/LandlordService.java @@ -54,7 +54,7 @@ public CreatedListingDTO create(SaveListingDTO saveListingDTO) { @Transactional(readOnly = true) public List getAllProperties(ReadUserDTO landlord) { - List properties = listingRepository.findAllByLandlordPublicIdFetchCoverPicture(landlord.publicId()); + List properties = listingRepository.findAllByLandlordPublicId(landlord.publicId()); return listingMapper.listingToDisplayCardListingDTOs(properties); } diff --git a/src/main/java/fr/codecake/nbat/listing/application/PictureService.java b/src/main/java/fr/codecake/nbat/listing/application/PictureService.java index aa2933a..f0686e6 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/PictureService.java +++ b/src/main/java/fr/codecake/nbat/listing/application/PictureService.java @@ -1,14 +1,15 @@ package fr.codecake.nbat.listing.application; import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; +import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO; import fr.codecake.nbat.listing.domain.Listing; import fr.codecake.nbat.listing.domain.ListingPicture; import fr.codecake.nbat.listing.mapper.ListingPictureMapper; import fr.codecake.nbat.listing.repository.ListingPictureRepository; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; -import java.util.Set; @Service public class PictureService { @@ -28,47 +29,46 @@ public PictureService(ListingPictureRepository listingPictureRepository, this.s3Service = s3Service; } - public List saveAll(List pictures, Listing listing) { + // CHANGE: accept PictureUploadDTO (has bytes) instead of PictureDTO + public List saveAll(List pictures, Listing listing) { if (pictures == null || pictures.isEmpty()) { return List.of(); } - Set listingPictures = listingPictureMapper.pictureDTOsToListingPictures(pictures); - + List listingPictures = new ArrayList<>(); + String firstCoverUrl = null; // we'll use this for Change 4 boolean isFirst = true; - for (ListingPicture listingPicture : listingPictures) { + for (PictureUploadDTO pictureDTO : pictures) { + ListingPicture listingPicture = new ListingPicture(); + listingPicture.setFileContentType(pictureDTO.fileContentType()); listingPicture.setCover(isFirst); listingPicture.setListing(listing); - // REAL S3 UPLOAD - PictureDTO matchingDTO = findMatchingDTO(listingPicture, pictures); - if (matchingDTO != null && matchingDTO.file() != null) { + if (pictureDTO.file() != null) { + // NOW this compiles correctly because PictureUploadDTO has .file() String s3Url = s3Service.uploadFile( - matchingDTO.file(), - listingPicture.getFileContentType(), + pictureDTO.file(), + pictureDTO.fileContentType(), "listing-image"); listingPicture.setFileUrl(s3Url); + + // Capture the cover URL to store directly on Listing (see Change 4) + if (isFirst) { + firstCoverUrl = s3Url; + } } + listingPictures.add(listingPicture); isFirst = false; } - listingPictureRepository.saveAll(listingPictures); - return listingPictureMapper.listingPictureToPictureDTO(listingPictures.stream().toList()); - } - - /** - * Helper method to find the DTO that corresponds to an entity. - */ - private PictureDTO findMatchingDTO(ListingPicture picture, List dtos) { - // Match by content type and cover status since these are copied during mapping - for (PictureDTO dto : dtos) { - if (dto.fileContentType().equals(picture.getFileContentType()) - && dto.isCover() == picture.isCover()) { - return dto; - } + // Store the cover URL directly on the Listing entity (Change 4) + if (firstCoverUrl != null) { + listing.setCoverUrl(firstCoverUrl); } - return dtos.isEmpty() ? null : dtos.get(0); // Fallback to first DTO if no match + + listingPictureRepository.saveAll(listingPictures); + return listingPictureMapper.listingPictureToPictureDTO(listingPictures); } } \ No newline at end of file diff --git a/src/main/java/fr/codecake/nbat/listing/application/S3Service.java b/src/main/java/fr/codecake/nbat/listing/application/S3Service.java index e952897..0ea3f94 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/S3Service.java +++ b/src/main/java/fr/codecake/nbat/listing/application/S3Service.java @@ -46,7 +46,6 @@ public String uploadFile(byte[] fileBytes, String contentType, String originalFi .bucket(bucketName) .key(uniqueFileName) .contentType(contentType) - // .acl(ObjectCannedACL.PUBLIC_READ) // Make the file publicly readable .build(); // Actually upload the file to S3 diff --git a/src/main/java/fr/codecake/nbat/listing/application/TenantService.java b/src/main/java/fr/codecake/nbat/listing/application/TenantService.java index 646947a..99a0fff 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/TenantService.java +++ b/src/main/java/fr/codecake/nbat/listing/application/TenantService.java @@ -32,8 +32,8 @@ public class TenantService { private final UserService userService; private final BookingService bookingService; - - public TenantService(ListingRepository listingRepository, ListingMapper listingMapper, UserService userService, BookingService bookingService) { + public TenantService(ListingRepository listingRepository, ListingMapper listingMapper, UserService userService, + BookingService bookingService) { this.listingRepository = listingRepository; this.listingMapper = listingMapper; this.userService = userService; @@ -43,9 +43,9 @@ public TenantService(ListingRepository listingRepository, ListingMapper listingM public Page getAllByCategory(Pageable pageable, BookingCategory category) { Page allOrBookingCategory; if (category == BookingCategory.ALL) { - allOrBookingCategory = listingRepository.findAllWithCoverOnly(pageable); + allOrBookingCategory = listingRepository.findAll(pageable); } else { - allOrBookingCategory = listingRepository.findAllByBookingCategoryWithCoverOnly(pageable, category); + allOrBookingCategory = listingRepository.findAllByBookingCategory(pageable, category); } return allOrBookingCategory.map(listingMapper::listingToDisplayCardListingDTO); @@ -62,19 +62,20 @@ public State getOne(UUID publicId) { DisplayListingDTO displayListingDTO = listingMapper.listingToDisplayListingDTO(listingByPublicIdOpt.get()); - ReadUserDTO readUserDTO = userService.getByPublicId(listingByPublicIdOpt.get().getLandlordPublicId()).orElseThrow(); + ReadUserDTO readUserDTO = userService.getByPublicId(listingByPublicIdOpt.get().getLandlordPublicId()) + .orElseThrow(); LandlordListingDTO landlordListingDTO = new LandlordListingDTO(readUserDTO.firstName(), readUserDTO.imageUrl()); displayListingDTO.setLandlord(landlordListingDTO); return State.builder().forSuccess(displayListingDTO); } - @SuppressWarnings("null") @Transactional(readOnly = true) public Page search(Pageable pageable, SearchDTO newSearch) { - Page allMatchedListings = listingRepository.findAllByLocationAndBathroomsAndBedroomsAndGuestsAndBeds(pageable, newSearch.location(), + Page allMatchedListings = listingRepository.findAllByLocationAndBathroomsAndBedroomsAndGuestsAndBeds( + pageable, newSearch.location(), newSearch.infos().baths().value(), newSearch.infos().bedrooms().value(), newSearch.infos().guests().value(), @@ -82,9 +83,11 @@ public Page search(Pageable pageable, SearchDTO newSearch List listingUUIDs = allMatchedListings.stream().map(Listing::getPublicId).toList(); - List bookingUUIDs = bookingService.getBookingMatchByListingIdsAndBookedDate(listingUUIDs, newSearch.dates()); + List bookingUUIDs = bookingService.getBookingMatchByListingIdsAndBookedDate(listingUUIDs, + newSearch.dates()); - List listingsNotBooked = allMatchedListings.stream().filter(listing -> !bookingUUIDs.contains(listing.getPublicId())) + List listingsNotBooked = allMatchedListings.stream() + .filter(listing -> !bookingUUIDs.contains(listing.getPublicId())) .map(listingMapper::listingToDisplayCardListingDTO) .toList(); diff --git a/src/main/java/fr/codecake/nbat/listing/application/dto/DisplayCardListingDTO.java b/src/main/java/fr/codecake/nbat/listing/application/dto/DisplayCardListingDTO.java index d00c974..f329f67 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/dto/DisplayCardListingDTO.java +++ b/src/main/java/fr/codecake/nbat/listing/application/dto/DisplayCardListingDTO.java @@ -1,14 +1,13 @@ package fr.codecake.nbat.listing.application.dto; -import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; import fr.codecake.nbat.listing.application.dto.vo.PriceVO; import fr.codecake.nbat.listing.domain.BookingCategory; import java.util.UUID; public record DisplayCardListingDTO(PriceVO price, - String location, - PictureDTO cover, - BookingCategory bookingCategory, - UUID publicId) { + String location, + String cover, + BookingCategory bookingCategory, + UUID publicId) { } diff --git a/src/main/java/fr/codecake/nbat/listing/application/dto/SaveListingDTO.java b/src/main/java/fr/codecake/nbat/listing/application/dto/SaveListingDTO.java index b43e0ee..215b773 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/dto/SaveListingDTO.java +++ b/src/main/java/fr/codecake/nbat/listing/application/dto/SaveListingDTO.java @@ -2,7 +2,7 @@ import fr.codecake.nbat.listing.application.dto.sub.DescriptionDTO; import fr.codecake.nbat.listing.application.dto.sub.ListingInfoDTO; -import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; +import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO; import fr.codecake.nbat.listing.application.dto.vo.PriceVO; import fr.codecake.nbat.listing.domain.BookingCategory; import jakarta.validation.Valid; @@ -15,19 +15,23 @@ public class SaveListingDTO { @NotNull BookingCategory category; - @NotNull String location; + @NotNull + String location; - @NotNull @Valid + @NotNull + @Valid ListingInfoDTO infos; - @NotNull @Valid + @NotNull + @Valid DescriptionDTO description; - @NotNull @Valid + @NotNull + @Valid PriceVO price; @NotNull - List pictures; + List pictures; public BookingCategory getCategory() { return category; @@ -69,11 +73,11 @@ public void setPrice(PriceVO price) { this.price = price; } - public List getPictures() { + public List getPictures() { return pictures; } - public void setPictures(List pictures) { + public void setPictures(List pictures) { this.pictures = pictures; } } diff --git a/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureDTO.java b/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureDTO.java index 0946b69..73c441f 100644 --- a/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureDTO.java +++ b/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureDTO.java @@ -1,18 +1,17 @@ package fr.codecake.nbat.listing.application.dto.sub; -import jakarta.validation.constraints.NotNull; - import java.util.Objects; -public record PictureDTO( - @NotNull byte[] file, - @NotNull String fileContentType, - @NotNull boolean isCover -) { +// It carries the S3 URL so Angular can display the image. +public record PictureDTO(String url, + String fileContentType, + boolean isCover) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; PictureDTO that = (PictureDTO) o; return isCover == that.isCover && Objects.equals(fileContentType, that.fileContentType); } diff --git a/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureUploadDTO.java b/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureUploadDTO.java new file mode 100644 index 0000000..94c033b --- /dev/null +++ b/src/main/java/fr/codecake/nbat/listing/application/dto/sub/PictureUploadDTO.java @@ -0,0 +1,9 @@ +package fr.codecake.nbat.listing.application.dto.sub; + +// This DTO travels INWARD — from the frontend to your service layer. +// It carries the raw bytes that need to be uploaded to S3. +public record PictureUploadDTO(byte[] file, // the actual image bytes + String fileContentType, // e.g. "image/jpeg" + boolean isCover) { + +} diff --git a/src/main/java/fr/codecake/nbat/listing/domain/Listing.java b/src/main/java/fr/codecake/nbat/listing/domain/Listing.java index de616b4..de3b28f 100644 --- a/src/main/java/fr/codecake/nbat/listing/domain/Listing.java +++ b/src/main/java/fr/codecake/nbat/listing/domain/Listing.java @@ -54,6 +54,10 @@ public class Listing extends AbstractAuditingEntity { @OneToMany(mappedBy = "listing", cascade = CascadeType.REMOVE) private Set pictures = new HashSet<>(); + // Add this field inside your Listing entity class + @Column(name = "cover_url", length = 500) + private String coverUrl; + @Override public Long getId() { return id; @@ -159,17 +163,33 @@ public void setPictures(Set pictures) { this.pictures = pictures; } + // Add getter and setter + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } + @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; Listing listing = (Listing) o; - return guests == listing.guests && bedrooms == listing.bedrooms && beds == listing.beds && bathrooms == listing.bathrooms && price == listing.price && Objects.equals(title, listing.title) && Objects.equals(description, listing.description) && bookingCategory == listing.bookingCategory && Objects.equals(location, listing.location) && Objects.equals(landlordPublicId, listing.landlordPublicId); + return guests == listing.guests && bedrooms == listing.bedrooms && beds == listing.beds + && bathrooms == listing.bathrooms && price == listing.price && Objects.equals(title, listing.title) + && Objects.equals(description, listing.description) && bookingCategory == listing.bookingCategory + && Objects.equals(location, listing.location) + && Objects.equals(landlordPublicId, listing.landlordPublicId); } @Override public int hashCode() { - return Objects.hash(title, description, guests, bedrooms, beds, bathrooms, price, bookingCategory, location, landlordPublicId); + return Objects.hash(title, description, guests, bedrooms, beds, bathrooms, price, bookingCategory, location, + landlordPublicId); } @Override diff --git a/src/main/java/fr/codecake/nbat/listing/mapper/ListingMapper.java b/src/main/java/fr/codecake/nbat/listing/mapper/ListingMapper.java index ce884c6..4764e45 100644 --- a/src/main/java/fr/codecake/nbat/listing/mapper/ListingMapper.java +++ b/src/main/java/fr/codecake/nbat/listing/mapper/ListingMapper.java @@ -8,7 +8,7 @@ import java.util.List; -@Mapper(componentModel = "spring", uses = {ListingPictureMapper.class}) +@Mapper(componentModel = "spring", uses = { ListingPictureMapper.class }) public interface ListingMapper { @Mapping(target = "landlordPublicId", ignore = true) @@ -17,6 +17,7 @@ public interface ListingMapper { @Mapping(target = "id", ignore = true) @Mapping(target = "createdDate", ignore = true) @Mapping(target = "pictures", ignore = true) + @Mapping(target = "coverUrl", ignore = true) // PictureService sets this after S3 upload @Mapping(target = "title", source = "description.title.value") @Mapping(target = "description", source = "description.description.value") @Mapping(target = "bedrooms", source = "infos.bedrooms.value") @@ -29,10 +30,11 @@ public interface ListingMapper { CreatedListingDTO listingToCreatedListingDTO(Listing listing); - @Mapping(target = "cover", source = "pictures") + // CHANGED: use coverUrl directly instead of scanning the pictures set List listingToDisplayCardListingDTOs(List listings); - @Mapping(target = "cover", source = "pictures", qualifiedByName = "extract-cover") + // CHANGED: was source = "pictures" + extract-cover, now just coverUrl + @Mapping(target = "cover", source = "coverUrl") DisplayCardListingDTO listingToDisplayCardListingDTO(Listing listing); default PriceVO mapPriceToPriceVO(int price) { @@ -52,4 +54,4 @@ default PriceVO mapPriceToPriceVO(int price) { @Mapping(target = "listingPublicId", source = "publicId") ListingCreateBookingDTO mapListingToListingCreateBookingDTO(Listing listing); -} +} \ No newline at end of file diff --git a/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java b/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java index 86de3ff..b208d2c 100644 --- a/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java +++ b/src/main/java/fr/codecake/nbat/listing/mapper/ListingPictureMapper.java @@ -1,6 +1,7 @@ package fr.codecake.nbat.listing.mapper; import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; +import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO; import fr.codecake.nbat.listing.domain.ListingPicture; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -12,32 +13,31 @@ @Mapper(componentModel = "spring") public interface ListingPictureMapper { - Set pictureDTOsToListingPictures(List pictureDTOs); + // CHANGE: input is now PictureUploadDTO (for the creation wizard) + Set pictureUploadDTOsToListingPictures(List pictureDTOs); @Mapping(target = "id", ignore = true) @Mapping(target = "listing", ignore = true) @Mapping(target = "createdDate", ignore = true) @Mapping(target = "lastModifiedDate", ignore = true) @Mapping(target = "cover", source = "isCover") - // NEW: Ignore fileUrl because PictureService will set it after uploading to S3 - // MapStruct was trying to map "file" from DTO to "fileUrl" in entity but - // they're - // different types (byte[] vs String) so we tell it to skip this field - @Mapping(target = "fileUrl", ignore = true) - ListingPicture pictureDTOToListingPicture(PictureDTO pictureDTO); + @Mapping(target = "fileUrl", ignore = true) // still set manually in PictureService + ListingPicture pictureUploadDTOToListingPicture(PictureUploadDTO pictureDTO); List listingPictureToPictureDTO(List listingPictures); + // CHANGE: map fileUrl from entity to fileUrl in display DTO @Mapping(target = "isCover", source = "cover") - // NEW: When converting entity back to DTO, we need to map the URL to the file - // field - // But wait - DTO expects byte[], not String. This creates a problem. - // We'll need to modify this mapping or change how we handle responses - @Mapping(target = "file", ignore = true) + @Mapping(target = "url", source = "fileUrl") PictureDTO convertToPictureDTO(ListingPicture listingPicture); + // This extract-cover works on the entity set and returns a display DTO @Named("extract-cover") default PictureDTO extractCover(Set pictures) { - return pictures.stream().findFirst().map(this::convertToPictureDTO).orElseThrow(); + return pictures.stream() + .filter(ListingPicture::isCover) + .findFirst() + .map(this::convertToPictureDTO) + .orElseThrow(); } } \ No newline at end of file diff --git a/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java b/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java index 943bf75..eb26c54 100644 --- a/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java +++ b/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java @@ -7,6 +7,7 @@ import fr.codecake.nbat.listing.application.dto.DisplayCardListingDTO; import fr.codecake.nbat.listing.application.dto.SaveListingDTO; import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; +import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO; import fr.codecake.nbat.sharedkernel.service.State; import fr.codecake.nbat.sharedkernel.service.StatusNotification; import fr.codecake.nbat.user.application.UserException; @@ -54,10 +55,9 @@ public LandlordResource(LandlordService landlordService, Validator validator, Us public ResponseEntity create( MultipartHttpServletRequest request, @RequestPart(name = "dto") String saveListingDTOString) throws IOException { - List pictures = request.getFileMap() - .values() + List pictures = request.getFiles("file") .stream() - .map(mapMultipartFileToPictureDTO()) + .map(mapMultipartFileToPictureUploadDTO()) .toList(); SaveListingDTO saveListingDTO = objectMapper.readValue(saveListingDTOString, SaveListingDTO.class); @@ -78,10 +78,10 @@ public ResponseEntity create( } } - private static Function mapMultipartFileToPictureDTO() { + private static Function mapMultipartFileToPictureUploadDTO() { return multipartFile -> { try { - return new PictureDTO(multipartFile.getBytes(), multipartFile.getContentType(), false); + return new PictureUploadDTO(multipartFile.getBytes(), multipartFile.getContentType(), false); } catch (IOException ioe) { throw new UserException( String.format("Cannot parse multipart file: %s", multipartFile.getOriginalFilename())); @@ -110,10 +110,9 @@ public ResponseEntity update( MultipartHttpServletRequest request, @RequestParam UUID publicId, @RequestPart(name = "dto") String saveListingDTOString) throws IOException { - List pictures = request.getFileMap() - .values() + List pictures = request.getFiles("file") .stream() - .map(mapMultipartFileToPictureDTO()) + .map(mapMultipartFileToPictureUploadDTO()) .toList(); SaveListingDTO saveListingDTO = objectMapper.readValue(saveListingDTOString, SaveListingDTO.class); diff --git a/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java b/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java index 4dbfc47..6d9cb00 100644 --- a/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java +++ b/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java @@ -13,27 +13,20 @@ public interface ListingRepository extends JpaRepository { - @Query("SELECT listing FROM Listing listing LEFT JOIN FETCH listing.pictures picture" + - " WHERE listing.landlordPublicId = :landlordPublicId AND picture.isCover = true") - List findAllByLandlordPublicIdFetchCoverPicture(UUID landlordPublicId); + List findAllByLandlordPublicId(UUID landlordPublicId); - long deleteByPublicIdAndLandlordPublicId(UUID publicId, UUID landlordPublicId); + long deleteByPublicIdAndLandlordPublicId(UUID publicId, UUID landlordPublicId); - @Query("SELECT listing from Listing listing LEFT JOIN FETCH listing.pictures picture" + - " WHERE picture.isCover = true AND listing.bookingCategory = :bookingCategory") - Page findAllByBookingCategoryWithCoverOnly(Pageable pageable, BookingCategory bookingCategory); + Page findAllByBookingCategory(Pageable pageable, BookingCategory bookingCategory); - @Query("SELECT listing from Listing listing LEFT JOIN FETCH listing.pictures picture" + - " WHERE picture.isCover = true") - Page findAllWithCoverOnly(Pageable pageable); + Page findAll(Pageable pageable); - Optional findByPublicId(UUID publicId); + Optional findByPublicId(UUID publicId); - List findAllByPublicIdIn(List allListingPublicIDs); + List findAllByPublicIdIn(List allListingPublicIDs); - Optional findOneByPublicIdAndLandlordPublicId(UUID listingPublicId, UUID landlordPublicId); + Optional findOneByPublicIdAndLandlordPublicId(UUID listingPublicId, UUID landlordPublicId); - Page findAllByLocationAndBathroomsAndBedroomsAndGuestsAndBeds( - Pageable pageable, String location, int bathrooms, int bedrooms, int guests, int beds - ); + Page findAllByLocationAndBathroomsAndBedroomsAndGuestsAndBeds( + Pageable pageable, String location, int bathrooms, int bedrooms, int guests, int beds); } From 3000e85eec5dab6338ad8a7399b214949874a809 Mon Sep 17 00:00:00 2001 From: mustapha bouddahr Date: Fri, 20 Feb 2026 10:33:01 +0000 Subject: [PATCH 5/6] remove anused imports --- .../fr/codecake/nbat/listing/presentation/LandlordResource.java | 2 -- .../fr/codecake/nbat/listing/repository/ListingRepository.java | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java b/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java index eb26c54..ee4cd3d 100644 --- a/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java +++ b/src/main/java/fr/codecake/nbat/listing/presentation/LandlordResource.java @@ -1,12 +1,10 @@ package fr.codecake.nbat.listing.presentation; import com.fasterxml.jackson.databind.ObjectMapper; -import fr.codecake.nbat.infrastructure.config.SecurityUtils; import fr.codecake.nbat.listing.application.LandlordService; import fr.codecake.nbat.listing.application.dto.CreatedListingDTO; import fr.codecake.nbat.listing.application.dto.DisplayCardListingDTO; import fr.codecake.nbat.listing.application.dto.SaveListingDTO; -import fr.codecake.nbat.listing.application.dto.sub.PictureDTO; import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO; import fr.codecake.nbat.sharedkernel.service.State; import fr.codecake.nbat.sharedkernel.service.StatusNotification; diff --git a/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java b/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java index 6d9cb00..267e2f8 100644 --- a/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java +++ b/src/main/java/fr/codecake/nbat/listing/repository/ListingRepository.java @@ -5,7 +5,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import java.util.List; import java.util.Optional; From 0d832121af59fec11bbdb09beec63053f52b5894 Mon Sep 17 00:00:00 2001 From: ihassis Date: Sat, 21 Feb 2026 09:59:31 +0000 Subject: [PATCH 6/6] my branch is up to date --- src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 509c291..4f4d382 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,13 +10,13 @@ spring: docker: compose: - enabled: true + enabled: false lifecycle-management: start_only datasource: url: jdbc:postgresql://localhost:5432/nbat_db?currentSchema=nbat_schema username: postgres - password: 1234 + password: "0000" driver-class-name: org.postgresql.Driver hikari: # ← ajoute ici keepalive-time: 60000