From 174d25ab2d309220d9adb787e9ea8bdd3bda31f1 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Sun, 12 Jan 2025 21:03:44 +0100 Subject: [PATCH 01/12] migration script to add user photo --- .../resources/db/migration/V1.0.9__add_userPhoto_to_users.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql diff --git a/server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql b/server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql new file mode 100644 index 00000000..672b72af --- /dev/null +++ b/server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN user_photo varchar(255); \ No newline at end of file From f62e49510057b1dcf61c69987696243392797760 Mon Sep 17 00:00:00 2001 From: kinci24 Date: Mon, 13 Jan 2025 02:14:22 +0100 Subject: [PATCH 02/12] Rename V1.0.9__add_userPhoto_to_users.sql to V1.0.10__add_userPhoto_to_users.sql --- .../resources/db/migration/V1.0.10__add_userPhoto_to_users.sql | 2 ++ .../resources/db/migration/V1.0.9__add_userPhoto_to_users.sql | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 server/src/main/resources/db/migration/V1.0.10__add_userPhoto_to_users.sql delete mode 100644 server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql diff --git a/server/src/main/resources/db/migration/V1.0.10__add_userPhoto_to_users.sql b/server/src/main/resources/db/migration/V1.0.10__add_userPhoto_to_users.sql new file mode 100644 index 00000000..84f72561 --- /dev/null +++ b/server/src/main/resources/db/migration/V1.0.10__add_userPhoto_to_users.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN user_photo varchar(255); diff --git a/server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql b/server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql deleted file mode 100644 index 672b72af..00000000 --- a/server/src/main/resources/db/migration/V1.0.9__add_userPhoto_to_users.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE users -ADD COLUMN user_photo varchar(255); \ No newline at end of file From 6dabc9fd55945e6ceabc84805000c554e787e86f Mon Sep 17 00:00:00 2001 From: kinci24 Date: Mon, 13 Jan 2025 02:49:59 +0100 Subject: [PATCH 03/12] Rename V1.0.10__add_userPhoto_to_users.sql to V2__add_userPhoto_to_users.sql --- ..._add_userPhoto_to_users.sql => V2__add_userPhoto_to_users.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/src/main/resources/db/migration/{V1.0.10__add_userPhoto_to_users.sql => V2__add_userPhoto_to_users.sql} (100%) diff --git a/server/src/main/resources/db/migration/V1.0.10__add_userPhoto_to_users.sql b/server/src/main/resources/db/migration/V2__add_userPhoto_to_users.sql similarity index 100% rename from server/src/main/resources/db/migration/V1.0.10__add_userPhoto_to_users.sql rename to server/src/main/resources/db/migration/V2__add_userPhoto_to_users.sql From 398ab14e497a643e63c393d77494824e8bbfbb49 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Mon, 13 Jan 2025 06:22:55 +0100 Subject: [PATCH 04/12] added atribute userPhoto to User and created method for changing userPhoto --- .../main/java/dev/findfirst/users/model/user/User.java | 3 +++ .../findfirst/users/service/UserManagementService.java | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/server/src/main/java/dev/findfirst/users/model/user/User.java b/server/src/main/java/dev/findfirst/users/model/user/User.java index 06f3a042..0ebeba59 100644 --- a/server/src/main/java/dev/findfirst/users/model/user/User.java +++ b/server/src/main/java/dev/findfirst/users/model/user/User.java @@ -43,4 +43,7 @@ public User(SignupRequest signup, String encodedPasswd) { @Column("role_role_id") private AggregateReference role; + @Column(name = "user_photo") + private String userPhoto; + } diff --git a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java index 9199533a..2efa5d6c 100644 --- a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java +++ b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java @@ -81,6 +81,16 @@ public void changeUserPassword(User user, String password) { saveUser(user); } + public void changeUserPhoto(User user, String userPhoto) { + user.setUserPhoto(userPhoto); + saveUser(user); + } + + public void removeUserPhoto(User user) { + user.setUserPhoto(null); + saveUser(user); + } + public String createVerificationToken(User user) { String token = UUID.randomUUID().toString(); Token verificationToken = new Token(AggregateReference.to(user.getUserId()), token); From 9705703a5222e8b0c4cf3c6e3ea27c8d4d0cdb3c Mon Sep 17 00:00:00 2001 From: sk210bs Date: Mon, 13 Jan 2025 15:06:38 +0100 Subject: [PATCH 05/12] added endpoints for loading users profile picture and saving new profile picture --- .../users/controller/UserController.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/server/src/main/java/dev/findfirst/users/controller/UserController.java b/server/src/main/java/dev/findfirst/users/controller/UserController.java index caf2e601..bf677190 100644 --- a/server/src/main/java/dev/findfirst/users/controller/UserController.java +++ b/server/src/main/java/dev/findfirst/users/controller/UserController.java @@ -1,8 +1,14 @@ package dev.findfirst.users.controller; +import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; import java.rmi.UnexpectedException; +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; @@ -29,8 +35,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -40,6 +49,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/user") @@ -60,6 +70,10 @@ public class UserController { @Value("${findfirst.app.domain}") private String domain; + private static final long MAX_FILE_SIZE = 2 * 1024 * 1024; // 2 MB + private static final String[] ALLOWED_TYPES = {"image/jpeg", "image/png"}; + private static final String UPLOAD_DIR = "uploads/profile-pictures/"; + @PostMapping("/signup") public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signUpRequest) { User user; @@ -154,4 +168,78 @@ public ResponseEntity refreshToken( return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(token); }).orElseThrow(() -> new TokenRefreshException(jwt, "Refresh token is not in database!")); } + + @PostMapping("/profile-picture") + public ResponseEntity uploadProfilePicture(@RequestParam("file") MultipartFile file, + @RequestParam("userId") int userId) { + // File size validation + if (file.getSize() > MAX_FILE_SIZE) { + return ResponseEntity.badRequest().body("File size exceeds the maximum limit of 2 MB."); + } + + // File type validation + String contentType = file.getContentType(); + if (Arrays.stream(ALLOWED_TYPES).noneMatch(contentType::equals)) { + return ResponseEntity.badRequest().body("Invalid file type. Only JPG and PNG are allowed."); + } + + try { + // Find user by ID + User user = userService.getUserById(userId).orElseThrow(() -> new NoUserFoundException("User not found")); + + // Create directory for uploads + File uploadDir = new File(UPLOAD_DIR); + if (!uploadDir.exists()) { + uploadDir.mkdirs(); + } + + // Delete old photo (if exists) + String oldPhotoPath = user.getUserPhoto(); + if (oldPhotoPath != null) { + File oldPhoto = new File(oldPhotoPath); + if (oldPhoto.exists()) { + oldPhoto.delete(); + } + } + + // Save new photo + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); + File destinationFile = new File(UPLOAD_DIR + fileName); + file.transferTo(destinationFile); + userService.changeUserPhoto(user, UPLOAD_DIR + fileName); + + return ResponseEntity.ok("File uploaded successfully."); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to upload file."); + } + } + + @GetMapping("/profile-picture") + public ResponseEntity getUserProfilePicture(@RequestParam("userId") int userId) { + try { + // Find user by ID + User user = userService.getUserById(userId) + .orElseThrow(() -> new NoUserFoundException("User not found")); + String userPhotoPath = user.getUserPhoto(); + + if (userPhotoPath == null || userPhotoPath.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + } + + // Load file + File photoFile = new File(userPhotoPath); + if (!photoFile.exists()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + } + + // Create response + Resource fileResource = new FileSystemResource(photoFile); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(Files.probeContentType(photoFile.toPath()))) + .body(fileResource); + + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } } From fe046361850de960e4de7d837f335b29e12e5351 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Mon, 13 Jan 2025 15:17:21 +0100 Subject: [PATCH 06/12] delete picture from database when removing userPhoto + adding log info --- .../findfirst/users/service/UserManagementService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java index 2efa5d6c..e0076fde 100644 --- a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java +++ b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java @@ -1,5 +1,6 @@ package dev.findfirst.users.service; +import java.io.File; import java.nio.charset.StandardCharsets; import java.rmi.UnexpectedException; import java.time.Instant; @@ -82,11 +83,20 @@ public void changeUserPassword(User user, String password) { } public void changeUserPhoto(User user, String userPhoto) { + log.info("Changing profile picture for user ID {}: {}", user.getUserId(), userPhoto); user.setUserPhoto(userPhoto); saveUser(user); } public void removeUserPhoto(User user) { + String userPhoto = user.getUserPhoto(); + if (userPhoto != null) { + File photoFile = new File(userPhoto); + if (photoFile.exists()) { + log.info("Removing profile picture for user ID {}: {}", user.getUserId(), userPhoto); + photoFile.delete(); + } + } user.setUserPhoto(null); saveUser(user); } From 26c762f02044f54f4de4ebb8fd5e6b77f48c6308 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Mon, 13 Jan 2025 15:52:37 +0100 Subject: [PATCH 07/12] test for upload and remove profile picture --- .../users/controller/UserControllerTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java index f3ab7025..0d72f785 100644 --- a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java +++ b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java @@ -3,6 +3,9 @@ import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.Optional; import java.util.Properties; @@ -16,6 +19,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; @@ -28,7 +34,10 @@ import org.springframework.http.HttpStatus; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; @@ -43,11 +52,16 @@ class UserControllerTest { final TestRestTemplate restTemplate; + @Mock + private UserManagementService userManagementService; + @Autowired UserControllerTest(TestRestTemplate tRestTemplate) { this.restTemplate = tRestTemplate; } + private MockMvc mockMvc; + @Container @ServiceConnection static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16.2-alpine3.19"); @@ -181,4 +195,39 @@ void refreshToken() { HttpMethod.POST, new HttpEntity<>(new HttpHeaders()), String.class, refreshTkn); assertEquals(HttpStatus.OK, resp.getStatusCode()); } + + @Test + void testUploadProfilePicture_Success() throws Exception { + mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build(); + + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "dummy content".getBytes()); + int userId = 1; + + when(userManagementService.getUserById(userId)).thenReturn(Optional.of(new User(userId, "test@example.com", "testUser"))); + + mockMvc.perform(multipart("/users/profile-picture") + .file(file) + .param("userId", String.valueOf(userId))) + .andExpect(status().isOk()) // Expected state: 200 OK + .andExpect(content().string("File uploaded successfully.")); + + verify(userManagementService, times(1)).changeUserPhoto(any(User.class), anyString()); + } + + @Test + void testRemoveUserPhoto_Success() { + mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build(); + + User user = new User(1, "test@example.com", "testUser"); + user.setUserPhoto("uploads/profile-pictures/test.jpg"); + + when(userManagementService.getUserById(user.getUserId())).thenReturn(Optional.of(user)); + + userManagementService.removeUserPhoto(user); + + verify(userManagementService, times(1)).saveUser(userCaptor.capture()); + assertNull(userCaptor.getValue().getUserPhoto()); + } + + } From adcf6bfd066ea8b4b88527860d9d08081de40cf5 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Mon, 13 Jan 2025 20:49:35 +0100 Subject: [PATCH 08/12] added tests for UserController --- .../users/controller/UserControllerTest.java | 95 ++++++++++++++++--- 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java index 0d72f785..54656728 100644 --- a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java +++ b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; @@ -15,10 +16,13 @@ import dev.findfirst.security.userauth.models.payload.request.SignupRequest; import dev.findfirst.users.model.MailHogMessage; import dev.findfirst.users.model.user.TokenPassword; +import dev.findfirst.users.model.user.User; +import dev.findfirst.users.service.UserManagementService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,6 +36,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mock.web.MockMultipartFile; @@ -197,37 +202,99 @@ void refreshToken() { } @Test - void testUploadProfilePicture_Success() throws Exception { - mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build(); - + public void testUploadProfilePicture_Success() throws Exception { MockMultipartFile file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "dummy content".getBytes()); int userId = 1; - when(userManagementService.getUserById(userId)).thenReturn(Optional.of(new User(userId, "test@example.com", "testUser"))); + User user = new User(); + user.setUserId(userId); + user.setUsername("testUser"); + when(userManagementService.getUserById(userId)).thenReturn(Optional.of(user)); - mockMvc.perform(multipart("/users/profile-picture") - .file(file) - .param("userId", String.valueOf(userId))) - .andExpect(status().isOk()) // Expected state: 200 OK - .andExpect(content().string("File uploaded successfully.")); + ResponseEntity response = userController.uploadProfilePicture(file, userId); - verify(userManagementService, times(1)).changeUserPhoto(any(User.class), anyString()); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("File uploaded successfully.", response.getBody()); + verify(userManagementService, times(1)).changeUserPhoto(eq(user), anyString()); } @Test - void testRemoveUserPhoto_Success() { - mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build(); - - User user = new User(1, "test@example.com", "testUser"); + public void testRemoveUserPhoto_Success() { + User user = new User(); + user.setUserId(1); + user.setUsername("testUser"); user.setUserPhoto("uploads/profile-pictures/test.jpg"); when(userManagementService.getUserById(user.getUserId())).thenReturn(Optional.of(user)); userManagementService.removeUserPhoto(user); + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); verify(userManagementService, times(1)).saveUser(userCaptor.capture()); assertNull(userCaptor.getValue().getUserPhoto()); } + @Test + public void testUploadProfilePicture_FileSizeExceedsLimit() throws Exception { + byte[] largeContent = new byte[3 * 1024 * 1024]; // 3 MB + MockMultipartFile file = new MockMultipartFile("file", "large.jpg", "image/jpeg", largeContent); + int userId = 1; + User user = new User(); + user.setUserId(userId); + user.setUsername("testUser"); + when(userManagementService.getUserById(userId)).thenReturn(Optional.of(user)); + + ResponseEntity response = userController.uploadProfilePicture(file, userId); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("File size exceeds the maximum limit of 2 MB.", response.getBody()); + } + + @Test + public void testUploadProfilePicture_InvalidFileType() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "dummy content".getBytes()); + int userId = 1; + + User user = new User(); + user.setUserId(userId); + user.setUsername("testUser"); + when(userManagementService.getUserById(userId)).thenReturn(Optional.of(user)); + + ResponseEntity response = userController.uploadProfilePicture(file, userId); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("Invalid file type. Only JPG and PNG are allowed.", response.getBody()); + } + + @Test + public void testGetUserProfilePicture_NotFound() { + int userId = 1; + + User user = new User(); + user.setUserId(userId); + user.setUsername("testUser"); + user.setUserPhoto(null); + when(userManagementService.getUserById(userId)).thenReturn(Optional.of(user)); + + ResponseEntity response = userController.getUserProfilePicture(userId); + + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + public void testGetUserProfilePicture_Success() { + int userId = 1; + + User user = new User(); + user.setUserId(userId); + user.setUsername("testUser"); + user.setUserPhoto("uploads/profile-pictures/test.jpg"); + when(userManagementService.getUserById(userId)).thenReturn(Optional.of(user)); + + ResponseEntity response = userController.getUserProfilePicture(userId); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + } } From ed52c675138de098d6c5eb6299faee685be78b02 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Mon, 13 Jan 2025 21:10:47 +0100 Subject: [PATCH 09/12] small repairs in tests for UserController --- .../users/controller/UserControllerTest.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java index 54656728..964735d6 100644 --- a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java +++ b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java @@ -5,8 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.Optional; import java.util.Properties; @@ -41,8 +39,6 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; @@ -60,13 +56,18 @@ class UserControllerTest { @Mock private UserManagementService userManagementService; + @InjectMocks + private UserController userController; + + public UserControllerTest() { + MockitoAnnotations.openMocks(this); + } + @Autowired UserControllerTest(TestRestTemplate tRestTemplate) { this.restTemplate = tRestTemplate; } - private MockMvc mockMvc; - @Container @ServiceConnection static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16.2-alpine3.19"); @@ -202,7 +203,7 @@ void refreshToken() { } @Test - public void testUploadProfilePicture_Success() throws Exception { + void testUploadProfilePicture_Success() throws Exception { MockMultipartFile file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "dummy content".getBytes()); int userId = 1; @@ -219,7 +220,7 @@ public void testUploadProfilePicture_Success() throws Exception { } @Test - public void testRemoveUserPhoto_Success() { + void testRemoveUserPhoto_Success() { User user = new User(); user.setUserId(1); user.setUsername("testUser"); @@ -235,7 +236,7 @@ public void testRemoveUserPhoto_Success() { } @Test - public void testUploadProfilePicture_FileSizeExceedsLimit() throws Exception { + void testUploadProfilePicture_FileSizeExceedsLimit() throws Exception { byte[] largeContent = new byte[3 * 1024 * 1024]; // 3 MB MockMultipartFile file = new MockMultipartFile("file", "large.jpg", "image/jpeg", largeContent); int userId = 1; @@ -252,7 +253,7 @@ public void testUploadProfilePicture_FileSizeExceedsLimit() throws Exception { } @Test - public void testUploadProfilePicture_InvalidFileType() throws Exception { + void testUploadProfilePicture_InvalidFileType() throws Exception { MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "dummy content".getBytes()); int userId = 1; @@ -268,7 +269,7 @@ public void testUploadProfilePicture_InvalidFileType() throws Exception { } @Test - public void testGetUserProfilePicture_NotFound() { + void testGetUserProfilePicture_NotFound() { int userId = 1; User user = new User(); @@ -283,7 +284,7 @@ public void testGetUserProfilePicture_NotFound() { } @Test - public void testGetUserProfilePicture_Success() { + void testGetUserProfilePicture_Success() { int userId = 1; User user = new User(); From 514aec9ef405bb714e36bb9b54a52e71d37e7761 Mon Sep 17 00:00:00 2001 From: sk210bs Date: Tue, 14 Jan 2025 05:44:57 +0100 Subject: [PATCH 10/12] small bugs repaired --- .../users/controller/UserController.java | 17 ++++++++++++----- .../dev/findfirst/users/model/user/User.java | 2 +- .../users/controller/UserControllerTest.java | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/dev/findfirst/users/controller/UserController.java b/server/src/main/java/dev/findfirst/users/controller/UserController.java index bf677190..24525632 100644 --- a/server/src/main/java/dev/findfirst/users/controller/UserController.java +++ b/server/src/main/java/dev/findfirst/users/controller/UserController.java @@ -184,8 +184,12 @@ public ResponseEntity uploadProfilePicture(@RequestParam("file") MultipartFil } try { - // Find user by ID - User user = userService.getUserById(userId).orElseThrow(() -> new NoUserFoundException("User not found")); + User user; + try { + user = userService.getUserById(userId).orElseThrow(() -> new NoUserFoundException()); + } catch (NoUserFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + } // Create directory for uploads File uploadDir = new File(UPLOAD_DIR); @@ -217,9 +221,12 @@ public ResponseEntity uploadProfilePicture(@RequestParam("file") MultipartFil @GetMapping("/profile-picture") public ResponseEntity getUserProfilePicture(@RequestParam("userId") int userId) { try { - // Find user by ID - User user = userService.getUserById(userId) - .orElseThrow(() -> new NoUserFoundException("User not found")); + User user; + try { + user = userService.getUserById(userId).orElseThrow(() -> new NoUserFoundException()); + } catch (NoUserFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + } String userPhotoPath = user.getUserPhoto(); if (userPhotoPath == null || userPhotoPath.isEmpty()) { diff --git a/server/src/main/java/dev/findfirst/users/model/user/User.java b/server/src/main/java/dev/findfirst/users/model/user/User.java index 0ebeba59..2f2b2f08 100644 --- a/server/src/main/java/dev/findfirst/users/model/user/User.java +++ b/server/src/main/java/dev/findfirst/users/model/user/User.java @@ -43,7 +43,7 @@ public User(SignupRequest signup, String encodedPasswd) { @Column("role_role_id") private AggregateReference role; - @Column(name = "user_photo") + @Column("user_photo") private String userPhoto; } diff --git a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java index 964735d6..ef414fb0 100644 --- a/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java +++ b/server/src/test/java/dev/findfirst/users/controller/UserControllerTest.java @@ -23,7 +23,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; @@ -51,7 +51,7 @@ @TestPropertySource(locations = "classpath:application-test.yml") class UserControllerTest { - final TestRestTemplate restTemplate; + TestRestTemplate restTemplate = new TestRestTemplate(); @Mock private UserManagementService userManagementService; From 672a1ec23f4588e1249240fa920598b6642b6035 Mon Sep 17 00:00:00 2001 From: kinci24 Date: Tue, 14 Jan 2025 11:38:38 +0100 Subject: [PATCH 11/12] Rename V2__add_userPhoto_to_users.sql to V1.0.011_add_userPhoto_to_users.sql --- ...userPhoto_to_users.sql => V1.0.011_add_userPhoto_to_users.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/src/main/resources/db/migration/{V2__add_userPhoto_to_users.sql => V1.0.011_add_userPhoto_to_users.sql} (100%) diff --git a/server/src/main/resources/db/migration/V2__add_userPhoto_to_users.sql b/server/src/main/resources/db/migration/V1.0.011_add_userPhoto_to_users.sql similarity index 100% rename from server/src/main/resources/db/migration/V2__add_userPhoto_to_users.sql rename to server/src/main/resources/db/migration/V1.0.011_add_userPhoto_to_users.sql From 578a8edfdd540219a6c763bfa56426753ffc7e3d Mon Sep 17 00:00:00 2001 From: sk210bs Date: Fri, 17 Jan 2025 04:34:53 +0100 Subject: [PATCH 12/12] requested changes fixed: size check parameter, logic from controller moved to userService, using application properties + docker-compose edit --- docker-compose.yml | 2 + .../users/controller/UserController.java | 53 ++++++------------- .../users/service/UserManagementService.java | 26 +++++++-- .../src/main/resources/application.properties | 8 ++- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b6ce6cea..522295a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,10 @@ services: - SPRING_MAIL_HOST=mail - SCREENSHOT_SERVICE_URL=${SCREENSHOT_SERVICE_URL} - FINDFIRST_SCREENSHOT_LOCATION=/app/screenshots + - FINDFIRST_UPLOAD_LOCATION=/app/profile-pictures/ volumes: - ./data/screenshots:/app/screenshots + - ./data/uploads/profile-pictures:/app/profile-pictures screenshot: image: ghcr.io/r-sandor/findfirst-screenshot:latest ports: diff --git a/server/src/main/java/dev/findfirst/users/controller/UserController.java b/server/src/main/java/dev/findfirst/users/controller/UserController.java index 24525632..c00edf23 100644 --- a/server/src/main/java/dev/findfirst/users/controller/UserController.java +++ b/server/src/main/java/dev/findfirst/users/controller/UserController.java @@ -8,13 +8,13 @@ import java.rmi.UnexpectedException; import java.util.Arrays; import java.util.Optional; -import java.util.UUID; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; import dev.findfirst.security.jwt.exceptions.TokenRefreshException; import dev.findfirst.security.jwt.service.RefreshTokenService; +import dev.findfirst.security.userauth.context.UserContext; import dev.findfirst.security.userauth.models.RefreshToken; import dev.findfirst.security.userauth.models.TokenRefreshResponse; import dev.findfirst.security.userauth.models.payload.request.SignupRequest; @@ -49,6 +49,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.constraints.Size; import org.springframework.web.multipart.MultipartFile; @RestController @@ -58,6 +59,8 @@ public class UserController { private final UserManagementService userService; + private final UserContext uContext; + private final RegistrationService regService; private final ForgotPasswordService pwdService; @@ -70,9 +73,11 @@ public class UserController { @Value("${findfirst.app.domain}") private String domain; - private static final long MAX_FILE_SIZE = 2 * 1024 * 1024; // 2 MB - private static final String[] ALLOWED_TYPES = {"image/jpeg", "image/png"}; - private static final String UPLOAD_DIR = "uploads/profile-pictures/"; + @Value("${findfirst.upload.max-file-size}") + private int maxFileSize; + + @Value("${findfirst.upload.allowed-types}") + private String[] allowedTypes; @PostMapping("/signup") public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signUpRequest) { @@ -170,49 +175,21 @@ public ResponseEntity refreshToken( } @PostMapping("/profile-picture") - public ResponseEntity uploadProfilePicture(@RequestParam("file") MultipartFile file, - @RequestParam("userId") int userId) { - // File size validation - if (file.getSize() > MAX_FILE_SIZE) { - return ResponseEntity.badRequest().body("File size exceeds the maximum limit of 2 MB."); - } + public ResponseEntity uploadProfilePicture(@RequestParam("file") @Size(max = maxFileSize) MultipartFile file) { // File type validation String contentType = file.getContentType(); - if (Arrays.stream(ALLOWED_TYPES).noneMatch(contentType::equals)) { + if (Arrays.stream(allowedTypes).noneMatch(contentType::equals)) { return ResponseEntity.badRequest().body("Invalid file type. Only JPG and PNG are allowed."); } try { - User user; - try { - user = userService.getUserById(userId).orElseThrow(() -> new NoUserFoundException()); - } catch (NoUserFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); - } - - // Create directory for uploads - File uploadDir = new File(UPLOAD_DIR); - if (!uploadDir.exists()) { - uploadDir.mkdirs(); - } - - // Delete old photo (if exists) - String oldPhotoPath = user.getUserPhoto(); - if (oldPhotoPath != null) { - File oldPhoto = new File(oldPhotoPath); - if (oldPhoto.exists()) { - oldPhoto.delete(); - } - } - - // Save new photo - String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); - File destinationFile = new File(UPLOAD_DIR + fileName); - file.transferTo(destinationFile); - userService.changeUserPhoto(user, UPLOAD_DIR + fileName); + User user = userService.getUserById(uContext.getUserId()).orElseThrow(NoUserFoundException::new); + userService.changeUserPhoto(user, file); return ResponseEntity.ok("File uploaded successfully."); + } catch (NoUserFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to upload file."); } diff --git a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java index e0076fde..e9fb7a74 100644 --- a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java +++ b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java @@ -33,6 +33,7 @@ import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor @@ -49,6 +50,9 @@ public class UserManagementService { @Value("${findfirst.app.jwtExpirationMs}") private int jwtExpirationMs; + @Value("${findfirst.upload.location}") + private String uploadLocation; + public User getUserByEmail(String email) throws NoUserFoundException { return userRepo.findByEmail(email).orElseThrow(NoUserFoundException::new); } @@ -82,7 +86,23 @@ public void changeUserPassword(User user, String password) { saveUser(user); } - public void changeUserPhoto(User user, String userPhoto) { + public void changeUserPhoto(User user, MultipartFile file) { + + // Create directory for uploads + File uploadDir = new File(uploadLocation); + if (!uploadDir.exists()) { + uploadDir.mkdirs(); + } + + // Delete old photo (if exists) + removeUserPhoto(user); + + // Save new photo + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); + File destinationFile = new File(uploadLocation + fileName); + file.transferTo(destinationFile); + String userPhoto = uploadLocation + fileName; + log.info("Changing profile picture for user ID {}: {}", user.getUserId(), userPhoto); user.setUserPhoto(userPhoto); saveUser(user); @@ -95,10 +115,10 @@ public void removeUserPhoto(User user) { if (photoFile.exists()) { log.info("Removing profile picture for user ID {}: {}", user.getUserId(), userPhoto); photoFile.delete(); + user.setUserPhoto(null); + saveUser(user); } } - user.setUserPhoto(null); - saveUser(user); } public String createVerificationToken(User user) { diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 167dd636..0b739f8b 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -2,7 +2,7 @@ # Profiles run next. # Set active profiles for the application. -spring.profiles.active=dev +spring.profiles.active=dev # SQL Related properties. spring.sql.init.continue-on-error=false @@ -22,11 +22,17 @@ findfirst.screenshot.location=${FINDFIRST_SCREENSHOT_LOCATION:${findfirst.local. findfirst.app.frontend-url=${FINDFIRST_APP_FRONTEND-URL:http://localhost:3000/} findfirst.app.domain=localhost +findfirst.upload.allowed-types=image/jpeg,image/png +findfirst.local.upload.profile-pictures=../data/uploads/profile-pictures/ +findfirst.upload.location=${FINDFIRST_UPLOAD_LOCATION:${findfirst.local.upload.profile-pictures}} + # ERROR HANDLING # https://docs.spring.io/spring-boot/api/java/org/springframework/boot/autoconfigure/web/ErrorProperties.IncludeStacktrace.html server.error.include-stacktrace=never # Maximum size of a single uploaded file +# 2 MB in bytes +findfirst.upload.max-file-size=2097152 ############################################ # Multipart File Settings