From 464dc5d5f23fabdc3dd38f32e73a208682d5de4e Mon Sep 17 00:00:00 2001 From: bbabbi Date: Sun, 2 Jun 2024 21:07:37 +0900 Subject: [PATCH] #13 [ADD] : add files of week 6 seminar --- week6/.idea/.gitignore | 8 ++ week6/build.gradle | 53 +++++++++++ .../java/org/sopt/spring/Application.java | 13 +++ .../sopt/spring/auth/PrincipalHandler.java | 26 ++++++ .../org/sopt/spring/auth/SecurityConfig.java | 48 ++++++++++ .../sopt/spring/auth/UserAuthentication.java | 17 ++++ .../filter/CustomAccessDeniedHandler.java | 21 +++++ .../CustomJwtAuthenticationEntryPoint.java | 36 ++++++++ .../auth/filter/JwtAuthenticationFilter.java | 55 ++++++++++++ .../spring/common/GlobalExceptionHandler.java | 33 +++++++ .../sopt/spring/common/dto/ErrorMessage.java | 17 ++++ .../sopt/spring/common/dto/ErrorResponse.java | 13 +++ .../spring/common/dto/SuccessMessage.java | 16 ++++ .../common/dto/SuccessStatusResponse.java | 12 +++ .../spring/common/jwt/JwtTokenProvider.java | 90 +++++++++++++++++++ .../spring/common/jwt/JwtValidationType.java | 10 +++ .../sopt/spring/config/JpaAuditingConfig.java | 9 ++ .../spring/controller/BlogController.java | 47 ++++++++++ .../spring/controller/MemberController.java | 53 +++++++++++ .../sopt/spring/domain/BaseTimeEntity.java | 21 +++++ .../java/org/sopt/spring/domain/Blog.java | 63 +++++++++++++ .../java/org/sopt/spring/domain/Member.java | 43 +++++++++ .../java/org/sopt/spring/domain/Part.java | 10 +++ .../java/org/sopt/spring/domain/Post.java | 28 ++++++ .../spring/exception/BusinessException.java | 14 +++ .../spring/exception/NotFoundException.java | 9 ++ .../exception/UnauthorizedException.java | 9 ++ .../org/sopt/spring/external/AwsConfig.java | 48 ++++++++++ .../org/sopt/spring/external/S3Service.java | 80 +++++++++++++++++ .../spring/repository/BlogRepository.java | 7 ++ .../spring/repository/MemberRepository.java | 8 ++ .../org/sopt/spring/service/BlogService.java | 49 ++++++++++ .../sopt/spring/service/MemberService.java | 65 ++++++++++++++ .../spring/service/dto/BlogCreateRequest.java | 10 +++ .../service/dto/BlogTitleUpdateRequest.java | 8 ++ .../spring/service/dto/MemberCreateDto.java | 10 +++ .../spring/service/dto/MemberFindDto.java | 14 +++ .../spring/service/dto/UserJoinResponse.java | 17 ++++ .../controller/MemberControllerTest.java | 52 +++++++++++ 39 files changed, 1142 insertions(+) create mode 100644 week6/.idea/.gitignore create mode 100755 week6/build.gradle create mode 100644 week6/src/main/java/org/sopt/spring/Application.java create mode 100644 week6/src/main/java/org/sopt/spring/auth/PrincipalHandler.java create mode 100644 week6/src/main/java/org/sopt/spring/auth/SecurityConfig.java create mode 100644 week6/src/main/java/org/sopt/spring/auth/UserAuthentication.java create mode 100644 week6/src/main/java/org/sopt/spring/auth/filter/CustomAccessDeniedHandler.java create mode 100644 week6/src/main/java/org/sopt/spring/auth/filter/CustomJwtAuthenticationEntryPoint.java create mode 100644 week6/src/main/java/org/sopt/spring/auth/filter/JwtAuthenticationFilter.java create mode 100644 week6/src/main/java/org/sopt/spring/common/GlobalExceptionHandler.java create mode 100644 week6/src/main/java/org/sopt/spring/common/dto/ErrorMessage.java create mode 100644 week6/src/main/java/org/sopt/spring/common/dto/ErrorResponse.java create mode 100644 week6/src/main/java/org/sopt/spring/common/dto/SuccessMessage.java create mode 100644 week6/src/main/java/org/sopt/spring/common/dto/SuccessStatusResponse.java create mode 100644 week6/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java create mode 100644 week6/src/main/java/org/sopt/spring/common/jwt/JwtValidationType.java create mode 100644 week6/src/main/java/org/sopt/spring/config/JpaAuditingConfig.java create mode 100644 week6/src/main/java/org/sopt/spring/controller/BlogController.java create mode 100644 week6/src/main/java/org/sopt/spring/controller/MemberController.java create mode 100644 week6/src/main/java/org/sopt/spring/domain/BaseTimeEntity.java create mode 100644 week6/src/main/java/org/sopt/spring/domain/Blog.java create mode 100644 week6/src/main/java/org/sopt/spring/domain/Member.java create mode 100644 week6/src/main/java/org/sopt/spring/domain/Part.java create mode 100644 week6/src/main/java/org/sopt/spring/domain/Post.java create mode 100644 week6/src/main/java/org/sopt/spring/exception/BusinessException.java create mode 100644 week6/src/main/java/org/sopt/spring/exception/NotFoundException.java create mode 100644 week6/src/main/java/org/sopt/spring/exception/UnauthorizedException.java create mode 100644 week6/src/main/java/org/sopt/spring/external/AwsConfig.java create mode 100644 week6/src/main/java/org/sopt/spring/external/S3Service.java create mode 100644 week6/src/main/java/org/sopt/spring/repository/BlogRepository.java create mode 100644 week6/src/main/java/org/sopt/spring/repository/MemberRepository.java create mode 100644 week6/src/main/java/org/sopt/spring/service/BlogService.java create mode 100644 week6/src/main/java/org/sopt/spring/service/MemberService.java create mode 100644 week6/src/main/java/org/sopt/spring/service/dto/BlogCreateRequest.java create mode 100644 week6/src/main/java/org/sopt/spring/service/dto/BlogTitleUpdateRequest.java create mode 100644 week6/src/main/java/org/sopt/spring/service/dto/MemberCreateDto.java create mode 100644 week6/src/main/java/org/sopt/spring/service/dto/MemberFindDto.java create mode 100644 week6/src/main/java/org/sopt/spring/service/dto/UserJoinResponse.java create mode 100755 week6/src/test/java/org/sopt/spring/controller/MemberControllerTest.java diff --git a/week6/.idea/.gitignore b/week6/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/week6/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/week6/build.gradle b/week6/build.gradle new file mode 100755 index 0000000..098b93b --- /dev/null +++ b/week6/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.4' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'org.sopt' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3' + testImplementation 'io.rest-assured:rest-assured' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + //JWT + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + + //Multipart file + implementation("software.amazon.awssdk:bom:2.21.0") + implementation("software.amazon.awssdk:s3:2.21.0") + + //Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/week6/src/main/java/org/sopt/spring/Application.java b/week6/src/main/java/org/sopt/spring/Application.java new file mode 100644 index 0000000..9331693 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/Application.java @@ -0,0 +1,13 @@ +package org.sopt.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/week6/src/main/java/org/sopt/spring/auth/PrincipalHandler.java b/week6/src/main/java/org/sopt/spring/auth/PrincipalHandler.java new file mode 100644 index 0000000..4521f5c --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/auth/PrincipalHandler.java @@ -0,0 +1,26 @@ +package org.sopt.spring.auth; + +import org.sopt.spring.common.dto.ErrorMessage; +import org.sopt.spring.exception.UnauthorizedException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class PrincipalHandler { + + private static final String ANONYMOUS_USER = "anonymousUser"; + + public Long getUserIdFromPrincipal() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + isPrincipalNull(principal); + return Long.valueOf(principal.toString()); + } + + public void isPrincipalNull( + final Object principal + ) { + if (principal.toString().equals(ANONYMOUS_USER)) { + throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION); + } + } +} diff --git a/week6/src/main/java/org/sopt/spring/auth/SecurityConfig.java b/week6/src/main/java/org/sopt/spring/auth/SecurityConfig.java new file mode 100644 index 0000000..557e864 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/auth/SecurityConfig.java @@ -0,0 +1,48 @@ +package org.sopt.spring.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.spring.auth.filter.CustomAccessDeniedHandler; +import org.sopt.spring.auth.filter.CustomJwtAuthenticationEntryPoint; +import org.sopt.spring.auth.filter.JwtAuthenticationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity //web Security를 사용할 수 있게 +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + + private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"}; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .requestCache(RequestCacheConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> + { + exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); + exception.accessDeniedHandler(customAccessDeniedHandler); + }); + + + http.authorizeHttpRequests(auth -> { + auth.requestMatchers(AUTH_WHITE_LIST).permitAll(); + auth.anyRequest().authenticated(); + }) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/week6/src/main/java/org/sopt/spring/auth/UserAuthentication.java b/week6/src/main/java/org/sopt/spring/auth/UserAuthentication.java new file mode 100644 index 0000000..a5ae4e5 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/auth/UserAuthentication.java @@ -0,0 +1,17 @@ +package org.sopt.spring.auth; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } + + public static UserAuthentication createUserAuthentication(Long userId) { + return new UserAuthentication(userId, null, null); + } +} diff --git a/week6/src/main/java/org/sopt/spring/auth/filter/CustomAccessDeniedHandler.java b/week6/src/main/java/org/sopt/spring/auth/filter/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..edd84ad --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/auth/filter/CustomAccessDeniedHandler.java @@ -0,0 +1,21 @@ +package org.sopt.spring.auth.filter; + +import io.jsonwebtoken.io.IOException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} diff --git a/week6/src/main/java/org/sopt/spring/auth/filter/CustomJwtAuthenticationEntryPoint.java b/week6/src/main/java/org/sopt/spring/auth/filter/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..59c30c0 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/auth/filter/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,36 @@ +package org.sopt.spring.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.sopt.spring.common.dto.ErrorMessage; +import org.sopt.spring.common.dto.ErrorResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.getWriter() + .write(objectMapper.writeValueAsString( + ErrorResponse.of(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION.getStatus(), + ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION.getMessage()))); + } +} \ No newline at end of file diff --git a/week6/src/main/java/org/sopt/spring/auth/filter/JwtAuthenticationFilter.java b/week6/src/main/java/org/sopt/spring/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..3c4fa3b --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,55 @@ +package org.sopt.spring.auth.filter; + +import io.micrometer.common.lang.NonNull; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sopt.spring.auth.UserAuthentication; +import org.sopt.spring.common.dto.ErrorMessage; +import org.sopt.spring.common.jwt.JwtTokenProvider; +import org.sopt.spring.exception.UnauthorizedException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static org.sopt.spring.common.jwt.JwtValidationType.VALID_JWT; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + final String token = getJwtFromRequest(request); + if (jwtTokenProvider.validateToken(token) == VALID_JWT) { + Long memberId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception exception) { + throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION); + } + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring("Bearer ".length()); + } + return null; + } +} \ No newline at end of file diff --git a/week6/src/main/java/org/sopt/spring/common/GlobalExceptionHandler.java b/week6/src/main/java/org/sopt/spring/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..9482657 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package org.sopt.spring.common; + + +import jakarta.persistence.EntityNotFoundException; +import org.sopt.spring.common.dto.ErrorMessage; +import org.sopt.spring.exception.NotFoundException; +import org.sopt.spring.exception.UnauthorizedException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.sopt.spring.common.dto.ErrorResponse; + +import java.util.Objects; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e){ + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage())); + } + @ExceptionHandler(NotFoundException.class) + protected ResponseEntity handleNotFoundException(NotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(e.getErrorMessage())); + } + @ExceptionHandler(UnauthorizedException.class) + protected ResponseEntity handlerUnauthorizedException(UnauthorizedException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ErrorResponse.of(e.getErrorMessage().getStatus(), e.getErrorMessage().getMessage())); + } +} diff --git a/week6/src/main/java/org/sopt/spring/common/dto/ErrorMessage.java b/week6/src/main/java/org/sopt/spring/common/dto/ErrorMessage.java new file mode 100644 index 0000000..971cdc6 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/dto/ErrorMessage.java @@ -0,0 +1,17 @@ +package org.sopt.spring.common.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorMessage { + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 사용자가 존재하지 않습니다."), + BLOG_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 블로그가 없습니다."), + JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 유저의 리프레시 토큰이 존재하지 않습니다."), + ; + private final int status; + private final String message; +} diff --git a/week6/src/main/java/org/sopt/spring/common/dto/ErrorResponse.java b/week6/src/main/java/org/sopt/spring/common/dto/ErrorResponse.java new file mode 100644 index 0000000..d61f5e4 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/dto/ErrorResponse.java @@ -0,0 +1,13 @@ +package org.sopt.spring.common.dto; + +public record ErrorResponse( + int status, + String message +) { + public static ErrorResponse of(int status, String message) { + return new ErrorResponse(status, message); + } + public static ErrorResponse of(ErrorMessage errorMessage) { + return new ErrorResponse(errorMessage.getStatus(), errorMessage.getMessage()); + } +} diff --git a/week6/src/main/java/org/sopt/spring/common/dto/SuccessMessage.java b/week6/src/main/java/org/sopt/spring/common/dto/SuccessMessage.java new file mode 100644 index 0000000..760864e --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/dto/SuccessMessage.java @@ -0,0 +1,16 @@ +package org.sopt.spring.common.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum SuccessMessage { + + BLOG_CREATE_SUCCESS(HttpStatus.CREATED.value(),"블로그 생성이 완료되었습니다."), + ; + private final int status; + private final String message; +} diff --git a/week6/src/main/java/org/sopt/spring/common/dto/SuccessStatusResponse.java b/week6/src/main/java/org/sopt/spring/common/dto/SuccessStatusResponse.java new file mode 100644 index 0000000..c9b4072 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/dto/SuccessStatusResponse.java @@ -0,0 +1,12 @@ +package org.sopt.spring.common.dto; + +public record SuccessStatusResponse( + int status, + String message +) { + + public static SuccessStatusResponse of(SuccessMessage successMessage) { + return new SuccessStatusResponse(successMessage.getStatus(), successMessage.getMessage()); + } + +} diff --git a/week6/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java b/week6/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..da10eca --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java @@ -0,0 +1,90 @@ +package org.sopt.spring.common.jwt; + + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import org.springframework.security.core.Authentication; + +import javax.crypto.SecretKey; +import java.util.Base64; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private static final String USER_ID = "userId"; + + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 60 * 60 * 1000L; // 60분으로 변경 + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; + + @Value("${jwt.secret}") + private String JWT_SECRET; + + + public String issueAccessToken(final Authentication authentication) { + return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); + } + + public String issueRefreshToken(final Authentication authentication) { + return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + } + + + public String generateToken(Authentication authentication, Long tokenExpirationTime) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 + + claims.put(USER_ID, authentication.getPrincipal()); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header + .setClaims(claims) // Claim + .signWith(getSigningKey()) // Signature + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성 + return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 + } + + public JwtValidationType validateToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); + } +} \ No newline at end of file diff --git a/week6/src/main/java/org/sopt/spring/common/jwt/JwtValidationType.java b/week6/src/main/java/org/sopt/spring/common/jwt/JwtValidationType.java new file mode 100644 index 0000000..04cf542 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/common/jwt/JwtValidationType.java @@ -0,0 +1,10 @@ +package org.sopt.spring.common.jwt; + +public enum JwtValidationType { + VALID_JWT, // 유효한 JWT + INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 + INVALID_JWT_TOKEN, // 유효하지 않은 토큰 + EXPIRED_JWT_TOKEN, // 만료된 토큰 + UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 + EMPTY_JWT // 빈 JWT +} diff --git a/week6/src/main/java/org/sopt/spring/config/JpaAuditingConfig.java b/week6/src/main/java/org/sopt/spring/config/JpaAuditingConfig.java new file mode 100644 index 0000000..1d247eb --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package org.sopt.spring.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaAuditingConfig { +} diff --git a/week6/src/main/java/org/sopt/spring/controller/BlogController.java b/week6/src/main/java/org/sopt/spring/controller/BlogController.java new file mode 100644 index 0000000..a10e5bc --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/controller/BlogController.java @@ -0,0 +1,47 @@ +package org.sopt.spring.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.sopt.spring.auth.PrincipalHandler; +import org.sopt.spring.common.dto.SuccessMessage; +import org.sopt.spring.common.dto.SuccessStatusResponse; +import org.sopt.spring.service.BlogService; +import org.sopt.spring.service.dto.BlogCreateRequest; +import org.sopt.spring.service.dto.BlogTitleUpdateRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class BlogController { + + private final BlogService blogService; + private final PrincipalHandler principalHandler; + + @PostMapping("blog") + public ResponseEntity createBlog( + @ModelAttribute BlogCreateRequest blogCreateRequest + ) { + return ResponseEntity.created(URI.create(blogService.create( + principalHandler.getUserIdFromPrincipal(), blogCreateRequest))).build(); + } + @PatchMapping("/blog/{blogId}/title") + public ResponseEntity updateBlogTitle( + @PathVariable Long blogId, + @Valid @RequestBody BlogTitleUpdateRequest blogTitleUpdateRequest + ) { + blogService.updateTitle(blogId, blogTitleUpdateRequest); + return ResponseEntity.noContent().build(); + } +} diff --git a/week6/src/main/java/org/sopt/spring/controller/MemberController.java b/week6/src/main/java/org/sopt/spring/controller/MemberController.java new file mode 100644 index 0000000..3a8cd0c --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/controller/MemberController.java @@ -0,0 +1,53 @@ +package org.sopt.spring.controller; + + +import lombok.RequiredArgsConstructor; +import org.sopt.spring.service.MemberService; +import org.sopt.spring.service.dto.MemberCreateDto; +import org.sopt.spring.service.dto.MemberFindDto; +import org.sopt.spring.service.dto.UserJoinResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/member") +public class MemberController { + + private final MemberService memberService; + + @PostMapping + public ResponseEntity postMember( + @RequestBody MemberCreateDto memberCreate + ) { + UserJoinResponse userJoinResponse = memberService.createMember(memberCreate); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", userJoinResponse.userId()) + .body( + userJoinResponse + ); + } + + @GetMapping("/{memberId}") + public ResponseEntity findMemberById(@PathVariable Long memberId) { + return ResponseEntity.ok(memberService.findMemberById(memberId)); + } + + @DeleteMapping("/{memberId}") + public ResponseEntity deleteMemberById(@PathVariable Long memberId){ + memberService.deleteMemberById(memberId); + return ResponseEntity.noContent().build(); + } + +} diff --git a/week6/src/main/java/org/sopt/spring/domain/BaseTimeEntity.java b/week6/src/main/java/org/sopt/spring/domain/BaseTimeEntity.java new file mode 100644 index 0000000..0ea75c9 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/domain/BaseTimeEntity.java @@ -0,0 +1,21 @@ +package org.sopt.spring.domain; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/week6/src/main/java/org/sopt/spring/domain/Blog.java b/week6/src/main/java/org/sopt/spring/domain/Blog.java new file mode 100644 index 0000000..80106da --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/domain/Blog.java @@ -0,0 +1,63 @@ +package org.sopt.spring.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.sopt.spring.service.dto.BlogCreateRequest; +import org.sopt.spring.service.dto.BlogTitleUpdateRequest; + +@Entity +@Getter +@NoArgsConstructor +public class Blog extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + private Member member; + + @Column(length = 200) + private String title; + + private String description; + + private String imageUrl; + + private Blog(Member member, String title, String imageUrl, String description) { + this.member = member; + this.title = title; + this.imageUrl = imageUrl; + this.description = description; + } + + public static Blog create( + Member member, + String title, + String description, + String imageUrl + ) { + return new Blog(member, title, imageUrl, description); + } + + private Blog(Member member, String title, String description) { + this.member = member; + this.title = title; + this.description = description; + } + + public static Blog create(Member member, BlogCreateRequest blogCreateRequest) { + return new Blog(member, blogCreateRequest.title(), blogCreateRequest.description()); + } + + public void updateTitle(BlogTitleUpdateRequest blogTitleUpdateRequest) { + this.title = blogTitleUpdateRequest.title(); + } +} diff --git a/week6/src/main/java/org/sopt/spring/domain/Member.java b/week6/src/main/java/org/sopt/spring/domain/Member.java new file mode 100644 index 0000000..e743ed7 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/domain/Member.java @@ -0,0 +1,43 @@ +package org.sopt.spring.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name; + + @Enumerated(EnumType.STRING) + private Part part; + + private int age; + + @Builder + private Member(String name, Part part, int age) { + this.name = name; + this.part = part; + this.age = age; + } + + public static Member create(String name, Part part, int age) { + return Member.builder() + .name(name) + .age(age) + .part(part) + .build(); + } +} diff --git a/week6/src/main/java/org/sopt/spring/domain/Part.java b/week6/src/main/java/org/sopt/spring/domain/Part.java new file mode 100644 index 0000000..eefaf1e --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/domain/Part.java @@ -0,0 +1,10 @@ +package org.sopt.spring.domain; + +public enum Part { + IOS, + SERVER, + ANDROID, + WEB, + PLAN, + DESIGN +} diff --git a/week6/src/main/java/org/sopt/spring/domain/Post.java b/week6/src/main/java/org/sopt/spring/domain/Post.java new file mode 100644 index 0000000..eac1e55 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/domain/Post.java @@ -0,0 +1,28 @@ +package org.sopt.spring.domain; + + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Post extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + private Blog blog; +} diff --git a/week6/src/main/java/org/sopt/spring/exception/BusinessException.java b/week6/src/main/java/org/sopt/spring/exception/BusinessException.java new file mode 100644 index 0000000..83c344e --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/exception/BusinessException.java @@ -0,0 +1,14 @@ +package org.sopt.spring.exception; + +import lombok.Getter; +import org.sopt.spring.common.dto.ErrorMessage; + +@Getter +public class BusinessException extends RuntimeException{ + private ErrorMessage errorMessage; + + public BusinessException(ErrorMessage errorMessage) { + super(errorMessage.getMessage()); + this.errorMessage = errorMessage; + } +} diff --git a/week6/src/main/java/org/sopt/spring/exception/NotFoundException.java b/week6/src/main/java/org/sopt/spring/exception/NotFoundException.java new file mode 100644 index 0000000..41ddc67 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/exception/NotFoundException.java @@ -0,0 +1,9 @@ +package org.sopt.spring.exception; + +import org.sopt.spring.common.dto.ErrorMessage; + +public class NotFoundException extends BusinessException { + public NotFoundException(ErrorMessage errorMessage) { + super(errorMessage); + } +} diff --git a/week6/src/main/java/org/sopt/spring/exception/UnauthorizedException.java b/week6/src/main/java/org/sopt/spring/exception/UnauthorizedException.java new file mode 100644 index 0000000..6ad2ddd --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/exception/UnauthorizedException.java @@ -0,0 +1,9 @@ +package org.sopt.spring.exception; + +import org.sopt.spring.common.dto.ErrorMessage; + +public class UnauthorizedException extends BusinessException { + public UnauthorizedException(ErrorMessage errorMessage) { + super(errorMessage); + } +} diff --git a/week6/src/main/java/org/sopt/spring/external/AwsConfig.java b/week6/src/main/java/org/sopt/spring/external/AwsConfig.java new file mode 100644 index 0000000..8d060d9 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/external/AwsConfig.java @@ -0,0 +1,48 @@ +package org.sopt.spring.external; + +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.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class AwsConfig { + + private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId"; + private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + + private final String accessKey; + private final String secretKey; + private final String regionString; + + public AwsConfig(@Value("${aws-property.access-key}") final String accessKey, + @Value("${aws-property.secret-key}") final String secretKey, + @Value("${aws-property.aws-region}") final String regionString) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.regionString = regionString; + } + + + @Bean + public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() { + System.setProperty(AWS_ACCESS_KEY_ID, accessKey); + System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey); + return SystemPropertyCredentialsProvider.create(); + } + + @Bean + public Region getRegion() { + return Region.of(regionString); + } + + @Bean + public S3Client getS3Client() { + return S3Client.builder() + .region(getRegion()) + .credentialsProvider(systemPropertyCredentialsProvider()) + .build(); + } +} \ No newline at end of file diff --git a/week6/src/main/java/org/sopt/spring/external/S3Service.java b/week6/src/main/java/org/sopt/spring/external/S3Service.java new file mode 100644 index 0000000..31903fc --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/external/S3Service.java @@ -0,0 +1,80 @@ +package org.sopt.spring.external; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Component +public class S3Service { + + private final String bucketName; + private final AwsConfig awsConfig; + private static final List IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp"); + + + public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AwsConfig awsConfig) { + this.bucketName = bucketName; + this.awsConfig = awsConfig; + } + + + public String uploadImage(String directoryPath, MultipartFile image) throws IOException { + final String key = directoryPath + generateImageFileName(); + final S3Client s3Client = awsConfig.getS3Client(); + + validateExtension(image); + validateFileSize(image); + + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(image.getContentType()) + .contentDisposition("inline") + .build(); + + RequestBody requestBody = RequestBody.fromBytes(image.getBytes()); + s3Client.putObject(request, requestBody); + return key; + } + + public void deleteImage(String key) throws IOException { + final S3Client s3Client = awsConfig.getS3Client(); + + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucketName) + .key(key) + .build() + ); + } + + + private String generateImageFileName() { + return UUID.randomUUID() + ".jpg"; + } + + + private void validateExtension(MultipartFile image) { + String contentType = image.getContentType(); + if (!IMAGE_EXTENSIONS.contains(contentType)) { + throw new RuntimeException("이미지 확장자는 jpg, png, webp만 가능합니다."); + } + } + + private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L; + + private void validateFileSize(MultipartFile image) { + if (image.getSize() > MAX_FILE_SIZE) { + throw new RuntimeException("이미지 사이즈는 5MB를 넘을 수 없습니다."); + } + } + +} \ No newline at end of file diff --git a/week6/src/main/java/org/sopt/spring/repository/BlogRepository.java b/week6/src/main/java/org/sopt/spring/repository/BlogRepository.java new file mode 100644 index 0000000..c039b2d --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/repository/BlogRepository.java @@ -0,0 +1,7 @@ +package org.sopt.spring.repository; + +import org.sopt.spring.domain.Blog; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BlogRepository extends JpaRepository { +} \ No newline at end of file diff --git a/week6/src/main/java/org/sopt/spring/repository/MemberRepository.java b/week6/src/main/java/org/sopt/spring/repository/MemberRepository.java new file mode 100644 index 0000000..89331e0 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/repository/MemberRepository.java @@ -0,0 +1,8 @@ +package org.sopt.spring.repository; + +import org.sopt.spring.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + +} diff --git a/week6/src/main/java/org/sopt/spring/service/BlogService.java b/week6/src/main/java/org/sopt/spring/service/BlogService.java new file mode 100644 index 0000000..a0628b4 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/BlogService.java @@ -0,0 +1,49 @@ +package org.sopt.spring.service; + +import java.io.IOException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.sopt.spring.common.dto.ErrorMessage; +import org.sopt.spring.domain.Blog; +import org.sopt.spring.domain.Member; +import org.sopt.spring.exception.NotFoundException; +import org.sopt.spring.external.S3Service; +import org.sopt.spring.repository.BlogRepository; +import org.sopt.spring.service.dto.BlogCreateRequest; +import org.sopt.spring.service.dto.BlogTitleUpdateRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BlogService { + private final BlogRepository blogRepository; + private final MemberService memberService; + private final S3Service s3Service; + private static final String BLOG_S3_UPLOAD_FOLER = "blog/"; + + + @Transactional + public String create(Long memberId, BlogCreateRequest createRequest) { + //member찾기 + Member member = memberService.findById(memberId); + try { + Blog blog = blogRepository.save(Blog.create(member, createRequest.title(), createRequest.description(), + s3Service.uploadImage(BLOG_S3_UPLOAD_FOLER, createRequest.image()))); + return blog.getId().toString(); + } catch (RuntimeException | IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + private Blog findById(Long blogId) { + return blogRepository.findById(blogId).orElseThrow( + () -> new NotFoundException(ErrorMessage.BLOG_NOT_FOUND) + ); + } + + @Transactional + public void updateTitle(Long blogId, BlogTitleUpdateRequest blogTitleUpdateRequest) { + Blog blog = findById(blogId); + blog.updateTitle(blogTitleUpdateRequest); + } +} diff --git a/week6/src/main/java/org/sopt/spring/service/MemberService.java b/week6/src/main/java/org/sopt/spring/service/MemberService.java new file mode 100644 index 0000000..c57bbb3 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/MemberService.java @@ -0,0 +1,65 @@ +package org.sopt.spring.service; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.sopt.spring.auth.redis.Service.TokenService; +import org.sopt.spring.auth.redis.repository.TokenRepository; +import org.sopt.spring.common.dto.ErrorMessage; +import org.sopt.spring.common.jwt.JwtTokenProvider; +import org.sopt.spring.auth.UserAuthentication; +import org.sopt.spring.domain.Member; +import org.sopt.spring.exception.NotFoundException; +import org.sopt.spring.repository.MemberRepository; +import org.sopt.spring.service.dto.MemberCreateDto; +import org.sopt.spring.service.dto.MemberFindDto; +import org.sopt.spring.service.dto.UserJoinResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + private final TokenService tokenService; + + @Transactional + public UserJoinResponse createMember( + MemberCreateDto memberCreate + ) { + Member member = memberRepository.save( + Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age()) + ); + Long memberId = member.getId(); + // issue a token + String accessToken = jwtTokenProvider.issueAccessToken( + UserAuthentication.createUserAuthentication(memberId) + ); + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + // save RefreshToken in redis + tokenService.saveRefreshToken(memberId, refreshToken); + return UserJoinResponse.of(accessToken, refreshToken,memberId.toString()); + } + + + public Member findById(Long memberId) { + return memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND) + ); + } + public MemberFindDto findMemberById(Long memberId) { + return MemberFindDto.of(memberRepository.findById(memberId).orElseThrow( + () -> new EntityNotFoundException("ID에 해당하는 사용자가 존재하지 않습니다.") + )); + } + + @Transactional + public void deleteMemberById(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new EntityNotFoundException("ID에 해당하는 사용자가 존재하지 않습니다.")); + memberRepository.delete(member); + } +} diff --git a/week6/src/main/java/org/sopt/spring/service/dto/BlogCreateRequest.java b/week6/src/main/java/org/sopt/spring/service/dto/BlogCreateRequest.java new file mode 100644 index 0000000..0d9ce37 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/dto/BlogCreateRequest.java @@ -0,0 +1,10 @@ +package org.sopt.spring.service.dto; + +import org.springframework.web.multipart.MultipartFile; + +public record BlogCreateRequest( + String title, + String description, + MultipartFile image +) { +} diff --git a/week6/src/main/java/org/sopt/spring/service/dto/BlogTitleUpdateRequest.java b/week6/src/main/java/org/sopt/spring/service/dto/BlogTitleUpdateRequest.java new file mode 100644 index 0000000..6fa58ef --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/dto/BlogTitleUpdateRequest.java @@ -0,0 +1,8 @@ +package org.sopt.spring.service.dto; + +import jakarta.validation.constraints.Size; + +public record BlogTitleUpdateRequest( + @Size(max = 10, message = "블로그 제목이 최대 글자 수(10자)를 초과했습니다.") String title +) { +} diff --git a/week6/src/main/java/org/sopt/spring/service/dto/MemberCreateDto.java b/week6/src/main/java/org/sopt/spring/service/dto/MemberCreateDto.java new file mode 100644 index 0000000..f4e84e9 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/dto/MemberCreateDto.java @@ -0,0 +1,10 @@ +package org.sopt.spring.service.dto; + +import org.sopt.spring.domain.Part; + +public record MemberCreateDto( + String name, + Part part, + int age +) { +} diff --git a/week6/src/main/java/org/sopt/spring/service/dto/MemberFindDto.java b/week6/src/main/java/org/sopt/spring/service/dto/MemberFindDto.java new file mode 100644 index 0000000..c50426a --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/dto/MemberFindDto.java @@ -0,0 +1,14 @@ +package org.sopt.spring.service.dto; + +import org.sopt.spring.domain.Member; +import org.sopt.spring.domain.Part; + +public record MemberFindDto( + String name, + Part part, + int age +) { + public static MemberFindDto of(Member member) { + return new MemberFindDto(member.getName(), member.getPart(), member.getAge()); + } +} diff --git a/week6/src/main/java/org/sopt/spring/service/dto/UserJoinResponse.java b/week6/src/main/java/org/sopt/spring/service/dto/UserJoinResponse.java new file mode 100644 index 0000000..bb9db70 --- /dev/null +++ b/week6/src/main/java/org/sopt/spring/service/dto/UserJoinResponse.java @@ -0,0 +1,17 @@ +package org.sopt.spring.service.dto; + +public record UserJoinResponse( + String accessToken, + String refreshToken, + String userId +) { + + public static UserJoinResponse of( + String accessToken, + String refreshToken, + String userId + ) { + return new UserJoinResponse(accessToken, refreshToken, userId); + } +} + diff --git a/week6/src/test/java/org/sopt/spring/controller/MemberControllerTest.java b/week6/src/test/java/org/sopt/spring/controller/MemberControllerTest.java new file mode 100755 index 0000000..38142ec --- /dev/null +++ b/week6/src/test/java/org/sopt/spring/controller/MemberControllerTest.java @@ -0,0 +1,52 @@ +package org.sopt.spring.controller; + +import io.restassured.RestAssured; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.sopt.spring.domain.Part; +import org.sopt.spring.repository.MemberRepository; +import org.sopt.spring.service.MemberService; +import org.sopt.spring.service.dto.MemberCreateDto; +import org.sopt.spring.settings.ApiTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class MemberControllerTest extends ApiTest { + + @Autowired + private MemberService memberService; + + @Autowired + private MemberRepository memberRepository; + + @Nested + @DisplayName("멤버 생성 테스트") + public class CreateMember { + + @Test + @DisplayName("요청 성공 테스트") + public void createMemberSuccess() throws Exception { + //given + final var request = new MemberCreateDto( + "도소현", + Part.SERVER, + 24 + ); + + //when + final var response = RestAssured + .given() + .log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when() + .post("/api/v1/member") + .then().log().all().extract(); + //then + Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + } +}