Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8424bc1
refactor: remove FileStorageService and its associated tests
Iyedchaabane Feb 9, 2026
76a1125
feat: add Cloudinary dependency for cloud storage integration
Iyedchaabane Feb 9, 2026
a6544bd
feat: add Cloudinary configuration for file upload integration
Iyedchaabane Feb 9, 2026
420dc76
feat: implement CloudinaryService for file upload and deletion
Iyedchaabane Feb 9, 2026
99eab97
feat: integrate CloudinaryService for book cover upload and deletion
Iyedchaabane Feb 9, 2026
98725d1
feat: update BookResponse cover field type to String and adjust BookM…
Iyedchaabane Feb 9, 2026
416a07b
feat: enhance GlobalExceptionHandler with logging for unexpected errors
Iyedchaabane Feb 9, 2026
ca94665
feat: add Cloudinary configuration for file storage in production
Iyedchaabane Feb 9, 2026
26e3ed6
feat: update application-dev.yml for Cloudinary file storage configur…
Iyedchaabane Feb 9, 2026
7481a21
feat: add comprehensive tests for CloudinaryService including upload …
Iyedchaabane Feb 9, 2026
6e9179b
feat: refactor BookServiceTest to use CloudinaryService for file uploads
Iyedchaabane Feb 9, 2026
3e0b63b
feat: update version to 1.1.0 in package.json and pom.xml
Iyedchaabane Feb 9, 2026
870ddf8
fix: specify generic type for uploadResult and remove unnecessary var…
Iyedchaabane Feb 9, 2026
28ad57d
feat: update Cloudinary dependencies and improve cover upload logic i…
Iyedchaabane Feb 9, 2026
298d5d2
refactor: simplify public ID generation in uploadFile method
Iyedchaabane Feb 9, 2026
f58ed1f
test: remove redundant original filename mock in CloudinaryServiceTest
Iyedchaabane Feb 9, 2026
ea2a262
feat: enhance version token handling in Cloudinary file upload logic
Iyedchaabane Feb 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book-network-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "book-network-ui",
"version": "1.0.0",
"version": "1.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
18 changes: 17 additions & 1 deletion book-network/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>com.ichaabane</groupId>
<artifactId>book-network</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
<name>book-network-api</name>
<description>Book-Social-Network</description>
<properties>
Expand Down Expand Up @@ -74,6 +74,22 @@
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!-- Cloudinary for cloud storage -->
<dependency>
<groupId>com.cloudinary</groupId>
<artifactId>cloudinary-http44</artifactId>
<version>1.39.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
Comment on lines +77 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

🧩 Analysis chain

🌐 Web query:

Spring Boot 3.5.3 BOM dependency management cloudinary commons-lang3 httpclient versions

πŸ’‘ Result:

For Spring Boot 3.5.3 (org.springframework.boot:spring-boot-dependencies:3.5.3) the BOM manages:

  • Apache Commons Lang (org.apache.commons:commons-lang3): 3.17.0 [1]
  • Apache HttpClient 5 (org.apache.httpcomponents.client5:httpclient5): 5.5 [1]

Cloudinary: there is no Cloudinary (com.cloudinary:*) entry/version managed by the Spring Boot 3.5.3 BOM (no cloudinary property and no managed dependency coordinates for it), so you must specify Cloudinary’s version yourself (or use Cloudinary’s own BOM, if they provide one). [1]

Sources
[1] Maven Central (Sonatype) – spring-boot-dependencies:3.5.3 POM/properties and managed dependencies list.


🌐 Web query:

CVE cloudinary-http44 1.39.0 security vulnerabilities

πŸ’‘ Result:

For com.cloudinary:cloudinary-http44:1.39.0, there don’t appear to be direct CVEs filed against the Cloudinary artifact itself, but it pulls in vulnerable transitive dependencies. [5][1]

CVEs impacting cloudinary-http44 1.39.0 via dependencies

  • CVE-2020-13956 (Apache HttpClient): cloudinary-http44:1.39.0 depends on org.apache.httpcomponents:httpclient:4.4, and HttpClient versions < 4.5.13 are affected (wrong target host selection via malformed URI authority). Mitigation: upgrade HttpClient to β‰₯ 4.5.13. [1][4]
  • CVE-2025-48924 (Apache Commons Lang): cloudinary-http44:1.39.0 depends on org.apache.commons:commons-lang3:3.1, and Commons Lang 3 versions < 3.18.0 are affected (uncontrolled recursion/StackOverflowError DoS). Mitigation: upgrade commons-lang3 to β‰₯ 3.18.0. [1][3]

What to do

  • If you must stay on cloudinary-http44, use Maven/Gradle dependency overrides to pin:
    • org.apache.httpcomponents:httpclient to 4.5.13+
    • org.apache.commons:commons-lang3 to 3.18.0+ (or newer)
      then run tests to ensure compatibility. [1][3][4]

🌐 Web query:

CVE commons-lang3 3.18.0 Apache Commons Lang security issues

πŸ’‘ Result:

For Apache Commons Lang (org.apache.commons:commons-lang3), the main published security issue relevant to 3.18.0 is:

  • CVE-2025-48924 (Uncontrolled Recursion / DoS): In ClassUtils.getClass(...), very long crafted inputs can trigger a StackOverflowError, potentially causing an application crash/denial of service. Affected versions: commons-lang3 3.0 up to (but not including) 3.18.0. Fixed in: 3.18.0. [1][2]

So, Commons Lang 3.18.0 is the patched (non-vulnerable) version for CVE-2025-48924, and upgrading to 3.18.0 or later is the recommended remediation. [1][3]

Sources:
[1] NVD CVE record (CVE-2025-48924)
[2] GitHub Advisory (GHSA-j288-q9x7-2f5v / CVE-2025-48924)
[3] SUSE security update noting update to 3.18.0 to fix CVE-2025-48924


🌐 Web query:

CVE httpclient 4.5.13 Apache HttpComponents vulnerabilities 2024 2025

πŸ’‘ Result:

For Apache HttpComponents HttpClient 4.5.13 (the 4.x line), I could not find any new CVEs from 2024 or 2025 that list 4.5.13 as an affected version.

What you will commonly see in 2024/2025 scanners is older items:

  • CVE-2020-13956 (malformed URI authority can cause β€œwrong target host” selection) affects HttpClient < 4.5.13 and is fixed in 4.5.13. So 4.5.13 is not vulnerable to this CVE. [1][2]

A 2025 CVE people sometimes confuse with 4.x is:

  • CVE-2025-27820 (PSL validation bug disabling domain checks impacting cookie/hostname verification) affects Apache HttpClient 5.4.0–5.4.2 and is fixed in 5.4.3β€”this is HttpClient 5.x, not 4.5.13. [3][4]

If you want the safest 4.x baseline anyway, the latest 4.5 line commonly recommended by vulnerability databases is 4.5.14. [5]

Sources: [1] [2] [3] [4] [5]


Version-convergence conflicts confirmed; explicit pins are necessary for CVE mitigation but require integration testing.

The pinned versions mitigate real CVEs in Cloudinary's transitive dependencies:

  • commons-lang3 3.18.0 patches CVE-2025-48924 (DoS via uncontrolled recursion)
  • httpclient 4.5.13 patches CVE-2020-13956 (malformed URI handling)

However, these pins diverge from Spring Boot 3.5.3 BOM expectations:

  • BOM manages commons-lang3 3.17.0 (PR pins 3.18.0β€”newer, acceptable but needs testing)
  • BOM manages httpclient5 5.5 (PR pins httpclient 4.xβ€”different artifact line due to Cloudinary's dependency)

Confirm your build and integration tests validate cross-module compatibility with the 4.x HttpClient line alongside Spring Boot 3.5.3's 5.x expectations.

πŸ€– Prompt for AI Agents
In `@book-network/pom.xml` around lines 77 - 92, The PR pins commons-lang3:3.18.0
and httpclient:4.5.13 to mitigate CVEs but diverges from the Spring Boot 3.5.3
BOM (commons-lang3 3.17.0 and httpclient5 5.x); run a full build and
cross-module integration test matrix to confirm the app works with
cloudinary-http44 (which pulls httpclient 4.x) alongside Spring Boot 3.5.3, and
if any incompatibility appears either align versions via dependencyManagement
(override BOM) or add exclusions and explicit replacements (use
httpclient5-compatible Cloudinary or force commons-lang3 to BOM if necessary);
focus checks on classes that use httpclient and commons-lang3 and update the pom
dependencyManagement or exclusions for artifactIds cloudinary-http44,
commons-lang3, and httpclient accordingly.

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class BookResponse {
private String isbn;
private String synopsis;
private String owner;
private byte[] cover;
private String cover;
private double rate;
private boolean archived;
private boolean shareable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public BookResponse toBookResponse(Book book) {
.shareable(book.isShareable())
.rate(book.getRate())
.owner(book.getOwner().getFullName())
.cover(FileUtils.readFileFromLocation(book.getBookCover()))
.cover(book.getBookCover())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
import com.ichaabane.book_network.application.dto.response.PageResponse;
import com.ichaabane.book_network.application.mapper.BookMapper;
import com.ichaabane.book_network.domain.exception.OperationNotPermittedException;
import com.ichaabane.book_network.application.service.FileStorageService;
import com.ichaabane.book_network.domain.model.Book;
import com.ichaabane.book_network.domain.model.BookTransactionHistory;
import com.ichaabane.book_network.domain.repository.BookRepository;
import com.ichaabane.book_network.domain.repository.BookTransactionHistoryRepository;
import com.ichaabane.book_network.application.service.NotificationService;
import com.ichaabane.book_network.domain.model.BookReservation;
import com.ichaabane.book_network.domain.repository.BookReservationRepository;
import com.ichaabane.book_network.domain.model.User;
Expand Down Expand Up @@ -46,7 +44,7 @@ public class BookService {
private final BookTransactionHistoryRepository transactionHistoryRepository;
private final BookMapper bookMapper;
private final BookTransactionHistoryRepository bookTransactionHistoryRepository;
private final FileStorageService fileStorageService;
private final CloudinaryService cloudinaryService;
private final NotificationService notificationService;
private final BookReservationRepository reservationRepository;

Expand Down Expand Up @@ -276,11 +274,25 @@ public void uploadCover(MultipartFile file, Authentication connectedUser, Intege
if (file == null || file.isEmpty()) return;

Book book = bookRepository.findById(bookId)
.orElseThrow(() -> new EntityNotFoundException(NO_BOOK_FOUND_PREFIX + bookId));
.orElseThrow(() -> new EntityNotFoundException(NO_BOOK_FOUND_PREFIX + bookId));
User user = ((User) connectedUser.getPrincipal());
var bookCover = fileStorageService.saveFile(file, user.getId());
log.info(bookCover);
book.setBookCover(bookCover);

// Upload new cover to Cloudinary first
String coverUrl = cloudinaryService.uploadUserFile(file, user.getId());

// Verify upload was successful
if (coverUrl == null || coverUrl.isEmpty()) {
throw new OperationNotPermittedException("Failed to upload book cover");
}

log.info("New cover uploaded: {}", coverUrl);

// Only delete old cover after successful upload
if (book.getBookCover() != null) {
cloudinaryService.deleteFile(book.getBookCover());
}

book.setBookCover(coverUrl);
bookRepository.save(book);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.ichaabane.book_network.application.service;

import com.cloudinary.Cloudinary;
import com.cloudinary.utils.ObjectUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Map;
import java.util.UUID;

@Service
@Slf4j
@RequiredArgsConstructor
public class CloudinaryService {

private final Cloudinary cloudinary;

/**
* Upload file to Cloudinary
*
* @param file The file to upload
* @param folder The folder name in Cloudinary (e.g., "book-covers")
* @return The secure URL of the uploaded file
*/
public String uploadFile(MultipartFile file, String folder) {
try {
// Generate unique public ID
String publicId = generatePublicId();

// Upload to Cloudinary
Map<String, Object> uploadResult = cloudinary.uploader().upload(
file.getBytes(),
ObjectUtils.asMap(
"folder", folder,
"public_id", publicId,
"resource_type", "auto",
"transformation", new com.cloudinary.Transformation()
.width(500)
.height(700)
.crop("limit")
.quality("auto:good")
.fetchFormat("auto")
)
);

String secureUrl = (String) uploadResult.get("secure_url");
log.info("File uploaded successfully to Cloudinary: {}", secureUrl);
return secureUrl;

} catch (IOException e) {
log.error("Failed to upload file to Cloudinary: {}", e.getMessage());
return null;
}
}

/**
* Upload file for a specific user
*
* @param file The file to upload
* @param userId The user ID
* @return The secure URL of the uploaded file
*/
public String uploadUserFile(
MultipartFile file,
Integer userId) {
String folder = "book-network/users/" + userId;
return uploadFile(file, folder);
}

/**
* Delete file from Cloudinary
*
* @param imageUrl The full URL of the image to delete
*/
public void deleteFile(String imageUrl) {
if (imageUrl == null || imageUrl.isEmpty()) {
return;
}

try {
// Extract public ID from URL
String publicId = extractPublicId(imageUrl);
if (publicId != null) {
cloudinary.uploader().destroy(publicId, ObjectUtils.emptyMap());
log.info("File deleted from Cloudinary: {}", publicId);
}
} catch (IOException e) {
log.error("Failed to delete file from Cloudinary: {}", e.getMessage());
}
}

/**
* Generate unique public ID for file
* Note: No extension is appended as Cloudinary handles file types automatically with "resource_type":"auto"
*/
private String generatePublicId() {
return UUID.randomUUID().toString() + "-" + System.currentTimeMillis();
}

/**
* Extract public ID from Cloudinary URL
* Example URL: https://res.cloudinary.com/demo/image/upload/v1234567890/folder/file.jpg
* Public ID: folder/file
*/
private String extractPublicId(String imageUrl) {
try {
// Split by "upload/"
String[] parts = imageUrl.split("/upload/");
if (parts.length < 2) {
return null;
}

// Get the part after "upload/"
String afterUpload = parts[1];

// Find version token pattern: v followed by digits (e.g., v1234567890)
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("v\\d+");
java.util.regex.Matcher matcher = pattern.matcher(afterUpload);

if (matcher.find()) {
// Extract everything after the version token
int versionEnd = matcher.end();
if (versionEnd < afterUpload.length()) {
afterUpload = afterUpload.substring(versionEnd);

// Strip leading "/"
if (afterUpload.startsWith("/")) {
afterUpload = afterUpload.substring(1);
}
} else {
return null; // No content after version token
}
} else {
return null; // No version token found
}

// Remove file extension
int lastDot = afterUpload.lastIndexOf(".");
if (lastDot > 0) {
afterUpload = afterUpload.substring(0, lastDot);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return afterUpload;
} catch (Exception e) {
log.error("Failed to extract public ID from URL: {}", imageUrl);
return null;
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.ichaabane.book_network.infrastructure.config;

import com.cloudinary.Cloudinary;
import com.cloudinary.utils.ObjectUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CloudinaryConfig {

@Value("${cloudinary.cloud-name}")
private String cloudName;

@Value("${cloudinary.api-key}")
private String apiKey;

@Value("${cloudinary.api-secret}")
private String apiSecret;

@Bean
public Cloudinary cloudinary() {
return new Cloudinary(ObjectUtils.asMap(
"cloud_name", cloudName,
"api_key", apiKey,
"api_secret", apiSecret,
"secure", true
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import com.ichaabane.book_network.domain.exception.*;
import jakarta.mail.MessagingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
Expand All @@ -18,6 +19,7 @@
import static com.ichaabane.book_network.presentation.handler.BusinessErrorCodes.ACCOUNT_LOCKED;
import static org.springframework.http.HttpStatus.*;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

Expand Down Expand Up @@ -115,7 +117,8 @@ public ResponseEntity<ExceptionResponse> handleMethodArgumentNotValidException(M

@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionResponse> handleException(Exception exp) {
exp.printStackTrace();
// Use proper logging instead of printStackTrace for production
log.error("Unexpected error occurred", exp);
return ResponseEntity
.status(INTERNAL_SERVER_ERROR)
.body(
Expand Down
Loading
Loading