Skip to content

Commit

Permalink
feat: User 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
son-daehyeon committed Sep 28, 2024
1 parent a8e4af0 commit f380677
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 3 deletions.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
developmentOnly 'org.springframework.boot:spring-boot-devtools'

implementation("com.auth0:java-jwt:4.4.0")
implementation 'com.auth0:java-jwt:4.4.0'
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
}
3 changes: 2 additions & 1 deletion src/main/java/com/github/cokothon/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

import com.github.cokothon.common.property.AwsProperty;
import com.github.cokothon.common.property.JwtProperty;
import com.github.cokothon.common.property.RedisProperty;

@SpringBootApplication
@EnableAsync
@EnableMongoAuditing
@EnableConfigurationProperties({JwtProperty.class, RedisProperty.class})
@EnableConfigurationProperties({AwsProperty.class, JwtProperty.class, RedisProperty.class})
public class Application {

public static void main(String[] args) {
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/github/cokothon/common/property/AwsProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.github.cokothon.common.property;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.context.annotation.Configuration;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
@Configuration
@ConfigurationProperties(prefix = "cloud.aws")
public class AwsProperty {

@NotBlank
private String region;

@NestedConfigurationProperty
private S3 s3;

@NestedConfigurationProperty
private Credential credential;

@Data
public static class S3 {

@NotBlank
private String bucket;
}

@Data
public static class Credential {

@NotBlank
private String accessKey;

@NotBlank
private String secretKey;
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/github/cokothon/common/s3/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.github.cokothon.common.s3;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.github.cokothon.common.property.AwsProperty;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class S3Config {

private final AwsProperty awsProperty;

@Bean
public AWSCredentialsProvider awsCredentials() {
AWSCredentials awsCredentials = new BasicAWSCredentials(awsProperty.getCredential().getAccessKey(), awsProperty.getCredential().getSecretKey());
return new AWSStaticCredentialsProvider(awsCredentials);
}

@Bean
public AmazonS3Client amazonS3Client() {
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(awsProperty.getRegion())
.withCredentials(awsCredentials())
.build();
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/github/cokothon/common/s3/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.github.cokothon.common.s3;

import java.io.IOException;
import java.io.InputStream;

import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.github.cokothon.common.property.AwsProperty;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;

@Component
@RequiredArgsConstructor
public class S3Service {

private final AwsProperty awsProperty;
private final AmazonS3Client amazonS3Client;

@SneakyThrows(IOException.class)
public String uploadFile(String key, MultipartFile file) {

ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(file.getContentType());
objectMetadata.setContentLength(file.getSize());

return uploadFile(key, file.getInputStream(), objectMetadata);
}

public String uploadFile(String key, InputStream inputStream, ObjectMetadata objectMetadata) {

PutObjectRequest putObjectRequest = new PutObjectRequest(awsProperty.getS3().getBucket(), key, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead);

amazonS3Client.putObject(putObjectRequest);

return "https://" + awsProperty.getS3().getBucket() + ".s3." + awsProperty.getRegion() + ".amazonaws.com/" + key;
}

public void deleteFile(String key) {

amazonS3Client.deleteObject(awsProperty.getS3().getBucket(), key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public String generateToken(String userId) {

return JWT.create()
.withIssuedAt(Instant.now())
.withExpiresAt(Instant.now().plus(jwtProperty.getExpirationHours(), ChronoUnit.SECONDS))
.withExpiresAt(Instant.now().plus(jwtProperty.getExpirationHours(), ChronoUnit.HOURS))
.withClaim("id", userId)
.sign(algorithm);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.github.cokothon.domain.user.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.github.cokothon.common.api.dto.response.ApiResponse;
import com.github.cokothon.common.security.util.UserContext;
import com.github.cokothon.domain.user.dto.request.ChangeInfoRequest;
import com.github.cokothon.domain.user.dto.request.ChangePasswordRequest;
import com.github.cokothon.domain.user.schema.User;
import com.github.cokothon.domain.user.service.UserService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

private final UserService userService;

@PutMapping("/my/info")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Void> changeInfo(@RequestBody @Valid ChangeInfoRequest request) {

User user = UserContext.getUser();

userService.changeInfo(user, request);

return ApiResponse.ok();
}

@PutMapping("/my/password")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Void> changePassword(@RequestBody @Valid ChangePasswordRequest request) {

User user = UserContext.getUser();

userService.changePassword(user, request);

return ApiResponse.ok();
}

@PutMapping("/my/avatar")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Void> changeAvatar(@RequestPart("avatar") MultipartFile avatar) {

User user = UserContext.getUser();

userService.changeAvatar(user, avatar);

return ApiResponse.ok();
}

@DeleteMapping("/my/avatar")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Void> deleteAvatar() {

User user = UserContext.getUser();

userService.deleteAvatar(user);

return ApiResponse.ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.cokothon.domain.user.dto.request;

import jakarta.validation.constraints.NotBlank;

public record ChangeInfoRequest(

@NotBlank
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.cokothon.domain.user.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public record ChangePasswordRequest(

@NotBlank
String password,

@NotBlank
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)\\S{8,}$")
String newPassword
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.cokothon.domain.user.exception;

import org.springframework.http.HttpStatus;

import com.github.cokothon.common.api.exception.ApiException;

public class AvatarNotImageException extends ApiException {

public AvatarNotImageException() {

super(HttpStatus.BAD_REQUEST, "프로필 사진이 이미지 파일이 아닙니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class User extends BaseSchema {

private String nickname;

private String avatar;

private int level;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.github.cokothon.domain.user.service;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.github.cokothon.common.s3.S3Service;
import com.github.cokothon.common.security.authentication.UserAuthentication;
import com.github.cokothon.domain.user.dto.request.ChangeInfoRequest;
import com.github.cokothon.domain.user.dto.request.ChangePasswordRequest;
import com.github.cokothon.domain.user.exception.AvatarNotImageException;
import com.github.cokothon.domain.user.repository.UserRepository;
import com.github.cokothon.domain.user.schema.User;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;
private final S3Service s3Service;

private final AuthenticationManager authenticationManager;
private final PasswordEncoder passwordEncoder;

public void changeInfo(User user, ChangeInfoRequest dto) {

String nickname = dto.nickname();

user.setNickname(nickname);

userRepository.save(user);
}

public void changePassword(User user, ChangePasswordRequest dto) {

String password = dto.password();
String newPassword = dto.newPassword();

UserAuthentication authentication = new UserAuthentication(user, password);
Authentication authenticate = authenticationManager.authenticate(authentication);

assert authenticate.isAuthenticated();

user.setPassword(passwordEncoder.encode(newPassword));

userRepository.save(user);
}

public void changeAvatar(User user, MultipartFile avatar) {

if (avatar.getOriginalFilename() == null || avatar.getContentType() != null && !avatar.getContentType().startsWith("image")) {

throw new AvatarNotImageException();
}

String extension = avatar.getOriginalFilename().substring(avatar.getOriginalFilename().lastIndexOf(".") + 1);
String avatarUrl = s3Service.uploadFile("avatar/%s.%s".formatted(user.getId(), extension), avatar);

user.setAvatar(avatarUrl);

userRepository.save(user);
}

public void deleteAvatar(User user) {

s3Service.deleteFile(user.getAvatar().split("amazonaws.com/")[1]);

user.setAvatar(null);

userRepository.save(user);
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ spring:
log-resolved-exception: false
config:
import:
- classpath:config/aws.yaml
- classpath:config/email.yaml
- classpath:config/jwt.yaml
- classpath:config/mongo.yaml
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/config/aws.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cloud:
aws:
region: ap-northeast-2
s3:
bucket: cokothon
credential:
accessKey: accessKey
secretKey: secretKey

0 comments on commit f380677

Please sign in to comment.