Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
456 changes: 234 additions & 222 deletions pom.xml

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions src/main/java/hng_java_boilerplate/config/AwsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package hng_java_boilerplate.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AwsConfig {

@Value("${aws.s3.access-key}")
private String accessKey;

@Value("${aws.s3.secret-key}")
private String secretKey;

@Value("${aws.s3.region}")
private String region;

@Bean
public AmazonS3 amazonS3() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ public SecurityFilterChain httpSecurity(HttpSecurity httpSecurity) throws Except
"/api/v1/categories",
"/api/v1/payment/plans",
"/api/v1/payment/webhook",
"/api/v1/notification-settings"
"/api/v1/notification-settings",
"/api/v1/profile/upload-image"
).permitAll()
.requestMatchers(

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Optional;

@RestController
@RequestMapping("/api/v1/profile")
@Tag(name = "User Profile Management", description = "APIs for managing user profiles")
@CrossOrigin("*")
@Slf4j
public class ProfileController {

private final ProfileService profileService;
Expand Down Expand Up @@ -54,4 +59,10 @@ public ResponseEntity<DeactivateUserResponse> deactivateUser(@RequestBody @Valid
public ResponseEntity<ProfileResponse> getUserProfile(@PathVariable String userId) {
return ResponseEntity.ok(profileService.getUserProfile(userId));
}

@PostMapping("/upload-image")
public ResponseEntity<?> updateProfilePicture(@RequestParam("image") MultipartFile file) throws IOException {
log.info("New Profile Image received");
return profileService.uploadProfileImage(file);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package hng_java_boilerplate.profile.dto.response;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor

public class ProfilePictureResponse {
private boolean success;
private String message;
private String imageUrl;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
import hng_java_boilerplate.profile.dto.request.UpdateUserProfileDto;
import hng_java_boilerplate.profile.dto.response.DeactivateUserResponse;
import hng_java_boilerplate.profile.dto.response.ProfileResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Optional;

public interface ProfileService {
public DeactivateUserResponse deactivateUser(DeactivateUserRequest request);
Optional<?> updateUserProfile(String userId, UpdateUserProfileDto updateUserProfileDto);
ProfileResponse getUserProfile(String userId);
ResponseEntity<?> uploadProfileImage(MultipartFile file) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
package hng_java_boilerplate.profile.serviceImpl;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import hng_java_boilerplate.exception.BadRequestException;
import hng_java_boilerplate.exception.NotFoundException;
import hng_java_boilerplate.profile.dto.request.DeactivateUserRequest;
import hng_java_boilerplate.profile.dto.request.UpdateUserProfileDto;
import hng_java_boilerplate.profile.dto.response.DeactivateUserResponse;
import hng_java_boilerplate.profile.dto.response.ProfileDto;
import hng_java_boilerplate.profile.dto.response.ProfileResponse;
import hng_java_boilerplate.profile.dto.response.ProfileUpdateResponseDto;
import hng_java_boilerplate.profile.dto.response.*;
import hng_java_boilerplate.profile.entity.Profile;
import hng_java_boilerplate.profile.repository.ProfileRepository;
import hng_java_boilerplate.profile.service.ProfileService;
import hng_java_boilerplate.user.entity.User;
import hng_java_boilerplate.user.repository.UserRepository;
import hng_java_boilerplate.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.UUID;

@Service
@RequiredArgsConstructor
Expand All @@ -27,6 +38,24 @@ public class ProfileServiceImpl implements ProfileService {
private final UserRepository userRepository;
private final ProfileRepository profileRepository;

private final AmazonS3 amazonS3;

@Value("${aws.s3.bucket-name}")
private String bucketName;

public void uploadFileToS3(MultipartFile file, String keyName) {
try (InputStream inputStream = file.getInputStream()) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());

amazonS3.putObject(new PutObjectRequest(bucketName, keyName, inputStream, metadata));
} catch (IOException e) {
throw new RuntimeException("Error uploading file to S3", e);
}
}


@Override
public DeactivateUserResponse deactivateUser(DeactivateUserRequest request) {
User authUser = userService.getLoggedInUser();
Expand All @@ -37,7 +66,8 @@ public DeactivateUserResponse deactivateUser(DeactivateUserRequest request) {
throw new BadRequestException("User has been deactivated");
}

if (!confirmation.equals("true")) throw new BadRequestException("Confirmation needs to be true for deactivation");
if (!confirmation.equals("true"))
throw new BadRequestException("Confirmation needs to be true for deactivation");

authUser.setIsDeactivated(true);
userRepository.save(authUser);
Expand All @@ -50,30 +80,30 @@ public DeactivateUserResponse deactivateUser(DeactivateUserRequest request) {
@Override
public Optional<?> updateUserProfile(String id, UpdateUserProfileDto updateUserProfileDto) {

Optional<User> user = userRepository.findById(id);
if (user.isPresent()) {
Profile profile = user.get().getProfile();

profile.setFirstName(updateUserProfileDto.getFirstName());
profile.setLastName(updateUserProfileDto.getLastName());
profile.setJobTitle(updateUserProfileDto.getJobTitle());
profile.setPronouns(updateUserProfileDto.getPronouns());
profile.setJobTitle(updateUserProfileDto.getJobTitle());
profile.setDepartment(updateUserProfileDto.getDepartment());
profile.setSocial(updateUserProfileDto.getSocial());
profile.setBio(updateUserProfileDto.getBio());
profile.setPhone(updateUserProfileDto.getPhoneNumber());
profile.setAvatarUrl(updateUserProfileDto.getAvatarUrl());

profile = profileRepository.save(profile);
return Optional.of(ProfileUpdateResponseDto.builder()
.statusCode(HttpStatus.OK.value())
.message("Profile updated successfully")
.data(profile)
.build()
);
}
throw new NotFoundException("User not found");
Optional<User> user = userRepository.findById(id);
if (user.isPresent()) {
Profile profile = user.get().getProfile();

profile.setFirstName(updateUserProfileDto.getFirstName());
profile.setLastName(updateUserProfileDto.getLastName());
profile.setJobTitle(updateUserProfileDto.getJobTitle());
profile.setPronouns(updateUserProfileDto.getPronouns());
profile.setJobTitle(updateUserProfileDto.getJobTitle());
profile.setDepartment(updateUserProfileDto.getDepartment());
profile.setSocial(updateUserProfileDto.getSocial());
profile.setBio(updateUserProfileDto.getBio());
profile.setPhone(updateUserProfileDto.getPhoneNumber());
profile.setAvatarUrl(updateUserProfileDto.getAvatarUrl());

profile = profileRepository.save(profile);
return Optional.of(ProfileUpdateResponseDto.builder()
.statusCode(HttpStatus.OK.value())
.message("Profile updated successfully")
.data(profile)
.build()
);
}
throw new NotFoundException("User not found");
}

@Override
Expand All @@ -97,4 +127,28 @@ public ProfileResponse getUserProfile(String userId) {

return new ProfileResponse(200, "user profile", profileDto);
}
}

@Override
public ResponseEntity<ProfilePictureResponse> uploadProfileImage(MultipartFile file) {
if (file.isEmpty() || !isValidImage(file)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ProfilePictureResponse(false, "Invalid file type or missing image. Only JPG or JPEG formats are allowed.", null));
}
try {
String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
uploadFileToS3(file, filename);

String fileUrl = amazonS3.getUrl(bucketName, filename).toString();
return ResponseEntity.ok(new ProfilePictureResponse(true, "Profile image uploaded successfully", fileUrl));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ProfilePictureResponse(false, "An error occurred while uploading your profile image. Please try again later.", null));
}
}


private boolean isValidImage(MultipartFile file) {
String contentType = file.getContentType();
return contentType != null && (contentType.equals("image/jpeg") || contentType.equals("image/jpg"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.web.bind.annotation.*;

@RestController
@CrossOrigin("*")
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Tag(name="Authentication")
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/application-example.properties
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,11 @@ flutterwave.secret.key=
stripe.api.key=
client.url=
stripe.secret.key=

# AWS S3 Configuration
aws.s3.bucket-name=
aws.s3.region=
aws.s3.access-key=
aws.s3.secret-key=


Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.multipart.MultipartFile;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -62,4 +65,6 @@ void shouldDeactivateUser() throws Exception {
.andExpect(jsonPath("$.status_code").value(200))
.andExpect(jsonPath("$.message").value(response.message()));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.multipart.MultipartFile;

import java.util.Optional;

Expand Down Expand Up @@ -136,4 +137,6 @@ void shouldGetUserProfile() {

verify(userRepository).findById(anyString());
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import hng_java_boilerplate.exception.NotFoundException;
import hng_java_boilerplate.profile.dto.request.UpdateUserProfileDto;
import hng_java_boilerplate.profile.dto.response.ProfilePictureResponse;
import hng_java_boilerplate.profile.dto.response.ProfileUpdateResponseDto;
import hng_java_boilerplate.profile.entity.Profile;
import hng_java_boilerplate.profile.repository.ProfileRepository;
Expand All @@ -15,6 +16,8 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;

import java.util.Optional;

Expand Down Expand Up @@ -80,4 +83,33 @@ public void test_that_updateUserProfile_returns_error_with_status_400_when_user_
.hasMessage("User not found");
}

@Test
public void test_uploadProfileImage_returns_successful_response() {
MockMultipartFile file = new MockMultipartFile(
"file", "test.jpg", "image/jpeg", "dummy image content".getBytes()
);

ResponseEntity<ProfilePictureResponse> response = underTest.uploadProfileImage(file);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().isSuccess()).isTrue();
assertThat(response.getBody().getMessage()).isEqualTo("Profile image uploaded successfully");
assertThat(response.getBody().getImageUrl()).isNotBlank();
}

@Test
public void test_uploadProfileImage_returns_error_for_invalid_file() {
MockMultipartFile file = new MockMultipartFile(
"file", "test.txt", "text/plain", "invalid file content".getBytes()
);

ResponseEntity<ProfilePictureResponse> response = underTest.uploadProfileImage(file);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().isSuccess()).isFalse();
assertThat(response.getBody().getMessage()).isEqualTo("Invalid file type or missing image. Only JPG or JPEG formats are allowed.");
}

}
Loading