From 2043ed62a5b0cb892300d74529d60dc2390d8e86 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:40:22 +0900 Subject: [PATCH 01/24] =?UTF-8?q?[chore]=20#7=20spring=20security=20&=20jw?= =?UTF-8?q?t=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/build.gradle b/api/build.gradle index 3bd0525..4a7a1d3 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -3,8 +3,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' // spring data jpa implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // spring security + implementation 'org.springframework.boot:spring-boot-starter-security' // h2 runtimeOnly 'com.h2database:h2' + // 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' // domain dependency implementation project(path: ':domain') // common dependency From dc0746cc3b76c331fb167b107dc03d3ef1914a24 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:40:39 +0900 Subject: [PATCH 02/24] =?UTF-8?q?[feat]=20#7=20token=20dto=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/api/auth/jwt/Token.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/jwt/Token.java diff --git a/api/src/main/java/org/sopt/api/auth/jwt/Token.java b/api/src/main/java/org/sopt/api/auth/jwt/Token.java new file mode 100644 index 0000000..7b995bd --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/jwt/Token.java @@ -0,0 +1,17 @@ +package org.sopt.api.auth.jwt; + +import lombok.AccessLevel; +import lombok.Builder; + +@Builder(access = AccessLevel.PRIVATE) +public record Token( + String accessToken, + String refreshToken +) { + public static Token of(String accessToken, String refreshToken) { + return Token.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} From 32a17fcd0017a69dd8041801a7ea951f50b68a2a Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:41:02 +0900 Subject: [PATCH 03/24] =?UTF-8?q?[feat]=20#7=20unauthorized=20exception=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/common/error/UnauthorizedException.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/src/main/java/org/sopt/common/error/UnauthorizedException.java diff --git a/common/src/main/java/org/sopt/common/error/UnauthorizedException.java b/common/src/main/java/org/sopt/common/error/UnauthorizedException.java new file mode 100644 index 0000000..0d3f9d7 --- /dev/null +++ b/common/src/main/java/org/sopt/common/error/UnauthorizedException.java @@ -0,0 +1,11 @@ +package org.sopt.common.error; + +public class UnauthorizedException extends BusinessException { + public UnauthorizedException() { + super(ErrorStatus.CONFLICT); + } + + public UnauthorizedException(ErrorStatus errorStatus) { + super(errorStatus); + } +} From 643b180cbcf5b1ce03e07e8e57149bafc1ba6051 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:41:38 +0900 Subject: [PATCH 04/24] =?UTF-8?q?[feat]=20#7=20unauthorized=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/common/error/ErrorStatus.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/common/src/main/java/org/sopt/common/error/ErrorStatus.java b/common/src/main/java/org/sopt/common/error/ErrorStatus.java index b99f12f..be875e5 100644 --- a/common/src/main/java/org/sopt/common/error/ErrorStatus.java +++ b/common/src/main/java/org/sopt/common/error/ErrorStatus.java @@ -13,6 +13,19 @@ public enum ErrorStatus { */ BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + /** + * 401 Unauthorized + */ + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "리소스 접근 권한이 없습니다."), + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요."), + INVALID_ACCESS_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "액세스 토큰의 값이 올바르지 않습니다."), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰이 만료되었습니다. 재발급 받아주세요."), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 형식이 올바르지 않습니다."), + INVALID_REFRESH_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 값이 올바르지 않습니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다. 다시 로그인해 주세요."), + NOT_MATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "일치하지 않는 리프레시 토큰입니다."), + MISMATCH_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), + /** * 404 Not Found */ From 10f8ae6fc2b8f7967ec097823021e53c33d6cbde Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:42:06 +0900 Subject: [PATCH 05/24] =?UTF-8?q?[feat]=20#7=20jwt=20generator=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/jwt/JwtGenerator.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java diff --git a/api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java b/api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java new file mode 100644 index 0000000..129f7fa --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java @@ -0,0 +1,46 @@ +package org.sopt.api.auth.jwt; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtGenerator { + @Value("${jwt.secret}") + private String secretKey; + @Value("${jwt.access-token-expire-time}") + private long ACCESS_TOKEN_EXPIRE_TIME; + @Value("${jwt.refresh-token-expire-time}") + private long REFRESH_TOKEN_EXPIRE_TIME; + + public String generateToken(Long memberId, boolean isAccessToken) { + final Date now = new Date(); + final Date expiration = new Date(now.getTime() + (isAccessToken ? ACCESS_TOKEN_EXPIRE_TIME : REFRESH_TOKEN_EXPIRE_TIME)); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setSubject(String.valueOf(memberId)) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public JwtParser getJwtParser() { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build(); + } + + private Key getSigningKey() { + String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); + return Keys.hmacShaKeyFor(encoded.getBytes()); + } +} From ec087558d28ac2271b29371f6fa91b07511d4cb9 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:42:22 +0900 Subject: [PATCH 06/24] =?UTF-8?q?[feat]=20#7=20jwt=20provider=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/jwt/JwtProvider.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java diff --git a/api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java b/api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java new file mode 100644 index 0000000..7e48772 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java @@ -0,0 +1,23 @@ +package org.sopt.api.auth.jwt; + +import io.jsonwebtoken.JwtParser; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class JwtProvider { + private final JwtGenerator jwtGenerator; + + public Token issueToken(Long memberId) { + return Token.of(jwtGenerator.generateToken(memberId, true), + jwtGenerator.generateToken(memberId, false)); + } + + public Long getSubject(String token) { + JwtParser jwtParser = jwtGenerator.getJwtParser(); + return Long.valueOf(jwtParser.parseClaimsJws(token) + .getBody() + .getSubject()); + } +} From 0ed5b2eab0896d1ccfe7514a38a405bf8ac65c61 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:43:06 +0900 Subject: [PATCH 07/24] =?UTF-8?q?[feat]=20#7=20jwt=20validator=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/jwt/JwtValidator.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java diff --git a/api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java b/api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java new file mode 100644 index 0000000..8ab0ef4 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java @@ -0,0 +1,42 @@ +package org.sopt.api.auth.jwt; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtParser; +import lombok.RequiredArgsConstructor; +import org.sopt.common.error.ErrorStatus; +import org.sopt.common.error.UnauthorizedException; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class JwtValidator { + private final JwtGenerator jwtGenerator; + + public void validateAccessToken(String accessToken) { + try { + JwtParser jwtParser = jwtGenerator.getJwtParser(); + jwtParser.parseClaimsJws(accessToken); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(ErrorStatus.EXPIRED_ACCESS_TOKEN); + } catch (Exception e) { + throw new UnauthorizedException(ErrorStatus.INVALID_ACCESS_TOKEN_VALUE); + } + } + + public void validateRefreshToken(String refreshToken) { + try { + JwtParser jwtParser = jwtGenerator.getJwtParser(); + jwtParser.parseClaimsJws(refreshToken); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(ErrorStatus.EXPIRED_REFRESH_TOKEN); + } catch (Exception e) { + throw new UnauthorizedException(ErrorStatus.INVALID_REFRESH_TOKEN_VALUE); + } + } + + public void equalsRefreshToken(String providedRefreshToken, String storedRefreshToken) { + if (!providedRefreshToken.equals(storedRefreshToken)) { + throw new UnauthorizedException(ErrorStatus.NOT_MATCH_REFRESH_TOKEN); + } + } +} From 2113ad1f6921a8593eacabd3d993b96aaafd0a6c Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:43:30 +0900 Subject: [PATCH 08/24] =?UTF-8?q?[feat]=20#7=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/auth/JwtAuthenticationFilter.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java diff --git a/api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java b/api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..15767e1 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package org.sopt.api.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.sopt.api.auth.jwt.JwtProvider; +import org.sopt.api.auth.jwt.JwtValidator; +import org.sopt.common.error.ErrorStatus; +import org.sopt.common.error.UnauthorizedException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static org.sopt.api.auth.UserAuthentication.createDefaultUserAuthentication; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + private final JwtValidator jwtValidator; + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + final String accessToken = getAccessToken(request); + jwtValidator.validateAccessToken(accessToken); + setAuthentication(request, jwtProvider.getSubject(accessToken)); + filterChain.doFilter(request, response); + } + + private String getAccessToken(HttpServletRequest request) { + String accessToken = request.getHeader(AUTHORIZATION); + if (StringUtils.hasText(accessToken) && accessToken.startsWith(BEARER)) { + return accessToken.substring(BEARER.length()); + } + throw new UnauthorizedException(ErrorStatus.INVALID_ACCESS_TOKEN); + } + + private void setAuthentication(HttpServletRequest request, Long memberId) { + UserAuthentication authentication = createDefaultUserAuthentication(memberId); + createWebAuthenticationDetailsAndSet(request, authentication); + SecurityContext securityContext = SecurityContextHolder.getContext(); + securityContext.setAuthentication(authentication); + } + + private void createWebAuthenticationDetailsAndSet(HttpServletRequest request, UserAuthentication authentication) { + WebAuthenticationDetailsSource webAuthenticationDetailsSource = new WebAuthenticationDetailsSource(); + WebAuthenticationDetails webAuthenticationDetails = webAuthenticationDetailsSource.buildDetails(request); + authentication.setDetails(webAuthenticationDetails); + } +} From 7197bdcda26712878faed99807ec504bfc572017 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:43:48 +0900 Subject: [PATCH 09/24] =?UTF-8?q?[feat]=20#7=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20entry=20point=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/auth/JwtAuthenticationEntryPoint.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java diff --git a/api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java b/api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..a582388 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java @@ -0,0 +1,38 @@ +package org.sopt.api.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.sopt.api.common.ApiResponse; +import org.sopt.common.error.ErrorStatus; +import org.springframework.http.HttpStatus; +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; +import java.io.PrintWriter; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private static final String CHARACTER_TYPE = "utf-8"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + handleException(response); + } + + private void handleException(HttpServletResponse response) throws IOException { + setResponse(response, HttpStatus.UNAUTHORIZED, ErrorStatus.UNAUTHORIZED); + } + + private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorStatus errorStatus) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(CHARACTER_TYPE); + response.setStatus(httpStatus.value()); + PrintWriter writer = response.getWriter(); + writer.write(objectMapper.writeValueAsString(ApiResponse.of(errorStatus))); + } +} From 1a496334e38003f4bce663d13c381a9dcb7b5acf Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:44:10 +0900 Subject: [PATCH 10/24] =?UTF-8?q?[feat]=20#7=20=EC=8B=9C=ED=81=90=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=ED=95=84=ED=84=B0=20=EC=98=88=EC=99=B8=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=A7=81=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/api/auth/ExceptionHandlerFilter.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java diff --git a/api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java b/api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java new file mode 100644 index 0000000..614328c --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java @@ -0,0 +1,50 @@ +package org.sopt.api.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.sopt.api.common.ApiResponse; +import org.sopt.common.error.ErrorStatus; +import org.sopt.common.error.UnauthorizedException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.io.PrintWriter; + +public class ExceptionHandlerFilter extends OncePerRequestFilter { + private static final String CHARACTER_TYPE = "utf-8"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException { + try { + filterChain.doFilter(request, response); + } catch (UnauthorizedException e) { + handleUnauthorizedException(response, e); + } catch (Exception ee) { + handleException(response); + } + } + + private void handleUnauthorizedException(HttpServletResponse response, Exception e) throws IOException { + UnauthorizedException ue = (UnauthorizedException) e; + ErrorStatus errorStatus = ue.getErrorStatus(); + HttpStatus httpStatus = errorStatus.getHttpStatus(); + setResponse(response, httpStatus, errorStatus); + } + + private void handleException(HttpServletResponse response) throws IOException { + setResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, ErrorStatus.INTERNAL_SERVER_ERROR); + } + + private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorStatus errorStatus) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(CHARACTER_TYPE); + response.setStatus(httpStatus.value()); + PrintWriter writer = response.getWriter(); + writer.write(objectMapper.writeValueAsString(ApiResponse.of(errorStatus))); + } +} From cbf17d25026c19fc94cacd9daca0bd029985c3c6 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:44:38 +0900 Subject: [PATCH 11/24] =?UTF-8?q?[feat]=20#7=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20authentication=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/UserAuthentication.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/UserAuthentication.java diff --git a/api/src/main/java/org/sopt/api/auth/UserAuthentication.java b/api/src/main/java/org/sopt/api/auth/UserAuthentication.java new file mode 100644 index 0000000..dc59559 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/UserAuthentication.java @@ -0,0 +1,16 @@ +package org.sopt.api.auth; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + private UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } + + public static UserAuthentication createDefaultUserAuthentication(Long memberId) { + return new UserAuthentication(memberId, null, null); + } +} From b421a2bf11912de0047b13b733365366f28b181e Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:45:07 +0900 Subject: [PATCH 12/24] =?UTF-8?q?[chore]=20#7=20=EC=8B=9C=ED=81=90?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/SecurityConfig.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/SecurityConfig.java diff --git a/api/src/main/java/org/sopt/api/auth/SecurityConfig.java b/api/src/main/java/org/sopt/api/auth/SecurityConfig.java new file mode 100644 index 0000000..ff59db1 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/SecurityConfig.java @@ -0,0 +1,46 @@ +package org.sopt.api.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.api.auth.jwt.JwtProvider; +import org.sopt.api.auth.jwt.JwtValidator; +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.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@RequiredArgsConstructor +@EnableWebSecurity +@Configuration +public class SecurityConfig { + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtProvider jwtProvider; + private final JwtValidator jwtValidator; + private static final String[] whiteList = {"/api/member/signin", "/api/member/signup", "/api/member/reissue", "/"}; + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring().requestMatchers(whiteList); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagementConfigurer -> + sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exceptionHandlingConfigurer -> + exceptionHandlingConfigurer.authenticationEntryPoint(jwtAuthenticationEntryPoint)) + .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> + authorizationManagerRequestMatcherRegistry.anyRequest().authenticated()) + .addFilterBefore(new JwtAuthenticationFilter(jwtValidator, jwtProvider), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new ExceptionHandlerFilter(), JwtAuthenticationFilter.class) + .build(); + } +} From 7ad5c68a99986ea8d1f518198309d5bb3c03fb8d Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:45:28 +0900 Subject: [PATCH 13/24] =?UTF-8?q?[feat]=20#7=20member=20id=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/org/sopt/api/auth/MemberId.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/MemberId.java diff --git a/api/src/main/java/org/sopt/api/auth/MemberId.java b/api/src/main/java/org/sopt/api/auth/MemberId.java new file mode 100644 index 0000000..58a40cc --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/MemberId.java @@ -0,0 +1,11 @@ +package org.sopt.api.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MemberId { +} From be60f2a51c424b700892dfe107d7d23a97e8f0b4 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:46:07 +0900 Subject: [PATCH 14/24] =?UTF-8?q?[feat]=20#7=20@MemberId=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=A7=81=20argument=20resolver=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/auth/MemberIdArgumentResolver.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java diff --git a/api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java b/api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java new file mode 100644 index 0000000..33218eb --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java @@ -0,0 +1,26 @@ +package org.sopt.api.auth; + +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class MemberIdArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasAnnotation = parameter.hasParameterAnnotation(MemberId.class); + boolean isValidType = Long.class.isAssignableFrom(parameter.getParameterType()); + return hasAnnotation && isValidType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + } +} From ead224c945ba031dfbaf96cfe08a48792e129895 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:46:24 +0900 Subject: [PATCH 15/24] =?UTF-8?q?[chore]=20#7=20password=20encoder=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/BCryptPasswordConfig.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java diff --git a/api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java b/api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java new file mode 100644 index 0000000..2dbcc33 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java @@ -0,0 +1,16 @@ +package org.sopt.api.auth; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class BCryptPasswordConfig { + private static final int STRENGTH = 10; + + @Bean + public PasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(STRENGTH); + } +} From 9af85c5be0f4dc2ecc43f2d71587d55199c8727a Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:46:31 +0900 Subject: [PATCH 16/24] =?UTF-8?q?[chore]=20#7=20argument=20resolver=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/api/auth/WebConfig.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/WebConfig.java diff --git a/api/src/main/java/org/sopt/api/auth/WebConfig.java b/api/src/main/java/org/sopt/api/auth/WebConfig.java new file mode 100644 index 0000000..e467f99 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/WebConfig.java @@ -0,0 +1,19 @@ +package org.sopt.api.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final MemberIdArgumentResolver memberIdArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(memberIdArgumentResolver); + } +} From 1d09f00b591cf2ac933f726bc5a1395ca5e92d0f Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:46:52 +0900 Subject: [PATCH 17/24] =?UTF-8?q?[feat]=20#7=20password=20encoder=20handle?= =?UTF-8?q?r=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/api/auth/PasswordHandler.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 api/src/main/java/org/sopt/api/auth/PasswordHandler.java diff --git a/api/src/main/java/org/sopt/api/auth/PasswordHandler.java b/api/src/main/java/org/sopt/api/auth/PasswordHandler.java new file mode 100644 index 0000000..dfa2427 --- /dev/null +++ b/api/src/main/java/org/sopt/api/auth/PasswordHandler.java @@ -0,0 +1,23 @@ +package org.sopt.api.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.common.error.ErrorStatus; +import org.sopt.common.error.UnauthorizedException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class PasswordHandler { + private final PasswordEncoder passwordEncoder; + + public String encode(String password) { + return passwordEncoder.encode(password); + } + + public void validatePassword(String password, String encodedPassword) { + if (!passwordEncoder.matches(password, encodedPassword)) { + throw new UnauthorizedException(ErrorStatus.MISMATCH_PASSWORD); + } + } +} From 7b9bd97c57d76f1409946a9874ff0e9978984c7e Mon Sep 17 00:00:00 2001 From: sunwoong Date: Thu, 30 Nov 2023 23:47:57 +0900 Subject: [PATCH 18/24] =?UTF-8?q?[feat]=20#7=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20&=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20&=20jwt=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89=20API=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/api/common/ApiResponse.java | 2 +- .../GlobalExceptionHandler.java | 3 +- .../api/member/api/MemberApiController.java | 49 +++++++------ .../dto/request/MemberSignInRequest.java | 7 ++ ...eRequest.java => MemberSignUpRequest.java} | 3 +- .../dto/response/MemberSaveResponse.java | 11 --- .../api/member/service/MemberService.java | 68 ++++++++++++------- .../api/member/service/MemberValidator.java | 14 ++++ .../org/sopt/domain/member/domain/Member.java | 9 ++- .../member/repository/MemberRepository.java | 9 +++ 10 files changed, 111 insertions(+), 64 deletions(-) rename api/src/main/java/org/sopt/api/{error => common}/GlobalExceptionHandler.java (95%) create mode 100644 api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java rename api/src/main/java/org/sopt/api/member/dto/request/{MemberSaveRequest.java => MemberSignUpRequest.java} (74%) delete mode 100644 api/src/main/java/org/sopt/api/member/dto/response/MemberSaveResponse.java create mode 100644 api/src/main/java/org/sopt/api/member/service/MemberValidator.java diff --git a/api/src/main/java/org/sopt/api/common/ApiResponse.java b/api/src/main/java/org/sopt/api/common/ApiResponse.java index c785d3c..7036768 100644 --- a/api/src/main/java/org/sopt/api/common/ApiResponse.java +++ b/api/src/main/java/org/sopt/api/common/ApiResponse.java @@ -32,7 +32,7 @@ private static ApiResponse of(SuccessStatus successStatus, T data) { .build(); } - private static ApiResponse of(ErrorStatus errorStatus) { + public static ApiResponse of(ErrorStatus errorStatus) { return builder() .status(errorStatus.getHttpStatus().value()) .message(errorStatus.getMessage()) diff --git a/api/src/main/java/org/sopt/api/error/GlobalExceptionHandler.java b/api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java similarity index 95% rename from api/src/main/java/org/sopt/api/error/GlobalExceptionHandler.java rename to api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java index 931b85c..c3a2350 100644 --- a/api/src/main/java/org/sopt/api/error/GlobalExceptionHandler.java +++ b/api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java @@ -1,7 +1,6 @@ -package org.sopt.api.error; +package org.sopt.api.common; import lombok.extern.slf4j.Slf4j; -import org.sopt.api.common.ApiResponse; import org.sopt.common.error.BusinessException; import org.sopt.common.error.ErrorStatus; import org.springframework.http.ResponseEntity; diff --git a/api/src/main/java/org/sopt/api/member/api/MemberApiController.java b/api/src/main/java/org/sopt/api/member/api/MemberApiController.java index cf6ec07..cbce751 100644 --- a/api/src/main/java/org/sopt/api/member/api/MemberApiController.java +++ b/api/src/main/java/org/sopt/api/member/api/MemberApiController.java @@ -1,53 +1,58 @@ package org.sopt.api.member.api; import lombok.RequiredArgsConstructor; +import org.sopt.api.auth.MemberId; +import org.sopt.api.auth.jwt.Token; import org.sopt.api.common.ApiResponse; import org.sopt.api.common.SuccessStatus; -import org.sopt.api.member.dto.request.MemberSaveRequest; +import org.sopt.api.member.dto.request.MemberSignInRequest; +import org.sopt.api.member.dto.request.MemberSignUpRequest; import org.sopt.api.member.dto.request.MemberUpdateRequest; import org.sopt.api.member.dto.response.MemberGetResponse; -import org.sopt.api.member.dto.response.MemberSaveResponse; import org.sopt.api.member.service.MemberService; -import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RequiredArgsConstructor @RequestMapping("/api/member") @Controller public class MemberApiController { private final MemberService memberService; - @PostMapping - public ResponseEntity> saveMember(@RequestBody final MemberSaveRequest memberSaveRequest) { - final MemberSaveResponse savedMember = memberService.saveMember(memberSaveRequest); - return ApiResponse.success(SuccessStatus.CREATED, savedMember); + @PostMapping("/signup") + public ResponseEntity> signUp(@RequestBody final MemberSignUpRequest request) { + final Token response = memberService.signUp(request); + return ApiResponse.success(SuccessStatus.CREATED, response); + } + + @PostMapping("/signin") + public ResponseEntity> signIn(@RequestBody final MemberSignInRequest request) { + final Token response = memberService.signIn(request); + return ApiResponse.success(SuccessStatus.OK, response); } - @GetMapping("/{memberId}") - public ResponseEntity> getMember(@PathVariable final Long memberId) { - final MemberGetResponse findMember = memberService.getMember(memberId); - return ApiResponse.success(SuccessStatus.OK, findMember); + @PostMapping("/reissue") + public ResponseEntity> reissue(@RequestHeader("Authorization") final String refreshToken) { + final Token response = memberService.reissue(refreshToken); + return ApiResponse.success(SuccessStatus.OK, response); } @GetMapping - public ResponseEntity> getMembers(final Pageable pageable) { - final List findMembers = memberService.getMembers(pageable); - return ApiResponse.success(SuccessStatus.OK, findMembers); + public ResponseEntity> getMember(@MemberId final Long memberId) { + final MemberGetResponse response = memberService.getMember(memberId); + return ApiResponse.success(SuccessStatus.OK, response); } - @PatchMapping("/{memberId}") - public ResponseEntity> updateMember(@PathVariable final Long memberId, - @RequestBody final MemberUpdateRequest memberUpdateRequest) { - memberService.updateMember(memberId, memberUpdateRequest); + @PatchMapping + public ResponseEntity> updateMember(@MemberId final Long memberId, + @RequestBody final MemberUpdateRequest request) { + memberService.updateMember(memberId, request); return ApiResponse.success(SuccessStatus.OK); } - @DeleteMapping("/{memberId}") - public ResponseEntity> deleteMember(@PathVariable final Long memberId) { + @DeleteMapping + public ResponseEntity> deleteMember(@MemberId final Long memberId) { memberService.deleteMember(memberId); return ApiResponse.success(SuccessStatus.OK); } diff --git a/api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java b/api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java new file mode 100644 index 0000000..845ada8 --- /dev/null +++ b/api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java @@ -0,0 +1,7 @@ +package org.sopt.api.member.dto.request; + +public record MemberSignInRequest( + String nickname, + String password +) { +} diff --git a/api/src/main/java/org/sopt/api/member/dto/request/MemberSaveRequest.java b/api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java similarity index 74% rename from api/src/main/java/org/sopt/api/member/dto/request/MemberSaveRequest.java rename to api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java index b891b19..668ca01 100644 --- a/api/src/main/java/org/sopt/api/member/dto/request/MemberSaveRequest.java +++ b/api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java @@ -2,9 +2,10 @@ import org.sopt.domain.member.domain.Sopt; -public record MemberSaveRequest( +public record MemberSignUpRequest( String name, String nickname, + String password, int age, Sopt sopt) { } diff --git a/api/src/main/java/org/sopt/api/member/dto/response/MemberSaveResponse.java b/api/src/main/java/org/sopt/api/member/dto/response/MemberSaveResponse.java deleted file mode 100644 index 7137d00..0000000 --- a/api/src/main/java/org/sopt/api/member/dto/response/MemberSaveResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.sopt.api.member.dto.response; - -import org.sopt.domain.member.domain.Member; - -public record MemberSaveResponse( - Long memberId -) { - public static MemberSaveResponse of(Member member) { - return new MemberSaveResponse(member.getId()); - } -} diff --git a/api/src/main/java/org/sopt/api/member/service/MemberService.java b/api/src/main/java/org/sopt/api/member/service/MemberService.java index e8531b3..dd51469 100644 --- a/api/src/main/java/org/sopt/api/member/service/MemberService.java +++ b/api/src/main/java/org/sopt/api/member/service/MemberService.java @@ -1,21 +1,20 @@ package org.sopt.api.member.service; import lombok.RequiredArgsConstructor; -import org.sopt.common.error.ConflictException; -import org.sopt.common.error.ErrorStatus; -import org.sopt.domain.member.domain.Member; -import org.sopt.domain.member.domain.Sopt; -import org.sopt.api.member.dto.request.MemberSaveRequest; +import org.sopt.api.auth.PasswordHandler; +import org.sopt.api.auth.jwt.JwtProvider; +import org.sopt.api.auth.jwt.JwtValidator; +import org.sopt.api.auth.jwt.Token; +import org.sopt.api.member.dto.request.MemberSignInRequest; +import org.sopt.api.member.dto.request.MemberSignUpRequest; import org.sopt.api.member.dto.request.MemberUpdateRequest; import org.sopt.api.member.dto.response.MemberGetResponse; -import org.sopt.api.member.dto.response.MemberSaveResponse; +import org.sopt.domain.member.domain.Member; +import org.sopt.domain.member.domain.Sopt; import org.sopt.domain.member.repository.MemberRepository; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - import static org.sopt.domain.member.domain.Member.createMember; import static org.sopt.domain.member.domain.Sopt.createSopt; @@ -23,33 +22,52 @@ @Transactional(readOnly = true) @Service public class MemberService { + private final PasswordHandler passwordHandler; + private final JwtProvider jwtProvider; + private final JwtValidator jwtValidator; + private final MemberValidator memberValidator; private final MemberRepository memberRepository; @Transactional - public MemberSaveResponse saveMember(MemberSaveRequest memberSaveRequest) { - validateDuplicateMember(memberSaveRequest.nickname()); - Member member = createMember(memberSaveRequest.name(), memberSaveRequest.nickname(), - memberSaveRequest.age(), memberSaveRequest.sopt()); + public Token signUp(MemberSignUpRequest request) { + validateDuplicateMember(request.nickname()); + Member member = createMember(request.name(), request.nickname(), + passwordHandler.encode(request.password()), request.age(), request.sopt()); Member savedMember = memberRepository.save(member); - return MemberSaveResponse.of(savedMember); + Token token = jwtProvider.issueToken(savedMember.getId()); + savedMember.updateRefreshToken(token.refreshToken()); + return token; } - public MemberGetResponse getMember(Long memberId) { + @Transactional + public Token signIn(MemberSignInRequest request) { + Member findMember = memberRepository.findByNicknameOrThrow(request.nickname()); + passwordHandler.validatePassword(request.password(), findMember.getPassword()); + Token token = jwtProvider.issueToken(findMember.getId()); + findMember.updateRefreshToken(token.refreshToken()); + return token; + } + + @Transactional + public Token reissue(String refreshToken) { + jwtValidator.validateRefreshToken(refreshToken); + Long memberId = jwtProvider.getSubject(refreshToken); Member findMember = memberRepository.findByIdOrThrow(memberId); - return MemberGetResponse.of(findMember); + jwtValidator.equalsRefreshToken(refreshToken, findMember.getRefreshToken()); + Token token = jwtProvider.issueToken(memberId); + findMember.updateRefreshToken(token.refreshToken()); + return token; } - public List getMembers(Pageable pageable) { - return memberRepository.findAll(pageable) - .stream() - .map(MemberGetResponse::of) - .toList(); + public MemberGetResponse getMember(Long memberId) { + Member findMember = memberRepository.findByIdOrThrow(memberId); + return MemberGetResponse.of(findMember); } @Transactional - public void updateMember(Long memberId, MemberUpdateRequest memberUpdateRequest) { + public void updateMember(Long memberId, MemberUpdateRequest request) { Member findMember = memberRepository.findByIdOrThrow(memberId); - Sopt sopt = createSopt(memberUpdateRequest.generation(), memberUpdateRequest.part()); + Sopt sopt = createSopt(request.generation(), request.part()); findMember.updateSopt(sopt); } @@ -59,8 +77,6 @@ public void deleteMember(Long memberId) { } private void validateDuplicateMember(String nickname) { - if (memberRepository.existsByNickname(nickname)) { - throw new ConflictException(ErrorStatus.DUPLICATE_MEMBER); - } + memberValidator.validateDuplicateMember(memberRepository.existsByNickname(nickname)); } } diff --git a/api/src/main/java/org/sopt/api/member/service/MemberValidator.java b/api/src/main/java/org/sopt/api/member/service/MemberValidator.java new file mode 100644 index 0000000..239e309 --- /dev/null +++ b/api/src/main/java/org/sopt/api/member/service/MemberValidator.java @@ -0,0 +1,14 @@ +package org.sopt.api.member.service; + +import org.sopt.common.error.ConflictException; +import org.sopt.common.error.ErrorStatus; +import org.springframework.stereotype.Component; + +@Component +public class MemberValidator { + public void validateDuplicateMember(boolean isExist) { + if (isExist) { + throw new ConflictException(ErrorStatus.DUPLICATE_MEMBER); + } + } +} diff --git a/domain/src/main/java/org/sopt/domain/member/domain/Member.java b/domain/src/main/java/org/sopt/domain/member/domain/Member.java index e2d767c..d5ad7f1 100644 --- a/domain/src/main/java/org/sopt/domain/member/domain/Member.java +++ b/domain/src/main/java/org/sopt/domain/member/domain/Member.java @@ -22,17 +22,20 @@ public class Member extends BaseTimeEntity { private Long id; private String name; private String nickname; + private String password; private Integer age; @Embedded private Sopt sopt; + private String refreshToken; @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE) @Builder.Default private List posts = new ArrayList<>(); - public static Member createMember(final String name, final String nickname, final Integer age, final Sopt sopt) { + public static Member createMember(final String name, final String nickname, final String password, final Integer age, final Sopt sopt) { return Member.builder() .name(name) .nickname(nickname) + .password(password) .age(age) .sopt(sopt) .build(); @@ -49,4 +52,8 @@ public void removePost(Post post) { public void updateSopt(Sopt sopt) { this.sopt = sopt; } + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } } diff --git a/domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java b/domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java index 551173c..7b08a3e 100644 --- a/domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java +++ b/domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java @@ -5,11 +5,20 @@ import org.sopt.domain.member.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { boolean existsByNickname(String nickname); + Optional findByNickname(String nickname); + default Member findByIdOrThrow(Long memberId) { return findById(memberId) .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.MEMBER_NOT_FOUND)); } + + default Member findByNicknameOrThrow(String nickname) { + return findByNickname(nickname) + .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.MEMBER_NOT_FOUND)); + } } \ No newline at end of file From 11981d194fc63ca759944692e4c3fb5ac5f07036 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Fri, 1 Dec 2023 01:39:13 +0900 Subject: [PATCH 19/24] =?UTF-8?q?[docs]=20#7=20.gitignore=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0debf03..22ab161 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,4 @@ out/ .DS_Store ### Yml ### -api/src/main/resources/application.yml \ No newline at end of file +seminar-api/src/main/resources/application.yml \ No newline at end of file From fe042820424658f2a5347c578bcafe01406ad153 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Fri, 1 Dec 2023 01:40:14 +0900 Subject: [PATCH 20/24] =?UTF-8?q?[feat]=20#7=20infra=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20&=20s3=20handler=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar-infra/build.gradle | 17 ++++++ .../main/java/org/sopt/infra/s3/S3Config.java | 30 ++++++++++ .../java/org/sopt/infra/s3/S3Handler.java | 60 +++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 seminar-infra/build.gradle create mode 100644 seminar-infra/src/main/java/org/sopt/infra/s3/S3Config.java create mode 100644 seminar-infra/src/main/java/org/sopt/infra/s3/S3Handler.java diff --git a/seminar-infra/build.gradle b/seminar-infra/build.gradle new file mode 100644 index 0000000..3982684 --- /dev/null +++ b/seminar-infra/build.gradle @@ -0,0 +1,17 @@ +dependencies { + // spring boot web + implementation 'org.springframework.boot:spring-boot-starter-web' + // aws + implementation 'software.amazon.awssdk:bom:2.21.0' + implementation 'software.amazon.awssdk:s3:2.21.0' + // common dependency + implementation project(path: ':seminar-common') +} + +bootJar { + enabled = false +} + +jar { + enabled = true +} \ No newline at end of file diff --git a/seminar-infra/src/main/java/org/sopt/infra/s3/S3Config.java b/seminar-infra/src/main/java/org/sopt/infra/s3/S3Config.java new file mode 100644 index 0000000..f8c8d78 --- /dev/null +++ b/seminar-infra/src/main/java/org/sopt/infra/s3/S3Config.java @@ -0,0 +1,30 @@ +package org.sopt.infra.s3; + +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 S3Config { + private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId"; + private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + @Value("${aws-property.access-key}") + private String accessKey; + @Value("${aws-property.secret-key}") + private String secretKey; + @Value("${aws-property.aws-region}") + private String region; + + @Bean + public S3Client s3Client() { + System.setProperty(AWS_ACCESS_KEY_ID, accessKey); + System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey); + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(SystemPropertyCredentialsProvider.create()) + .build(); + } +} diff --git a/seminar-infra/src/main/java/org/sopt/infra/s3/S3Handler.java b/seminar-infra/src/main/java/org/sopt/infra/s3/S3Handler.java new file mode 100644 index 0000000..ef7bb53 --- /dev/null +++ b/seminar-infra/src/main/java/org/sopt/infra/s3/S3Handler.java @@ -0,0 +1,60 @@ +package org.sopt.infra.s3; + +import lombok.RequiredArgsConstructor; +import org.sopt.common.error.ErrorStatus; +import org.sopt.common.error.InvalidValueException; +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.UUID; + +@RequiredArgsConstructor +@Component +public class S3Handler { + @Value("${aws-property.s3-bucket-name}") + private String bucketName; + private final S3Client s3Client; + + public String uploadImage(MultipartFile image) { + String key = generateImageFileName(); + PutObjectRequest request = generateRequest(image, key); + RequestBody requestBody = generateRequestBody(image); + s3Client.putObject(request, requestBody); + return key; + } + + public void deleteImage(String key) { + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucketName) + .key(key) + .build() + ); + } + + private PutObjectRequest generateRequest(MultipartFile image, String key) { + return PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(image.getContentType()) + .contentDisposition("inline") + .build(); + } + + private RequestBody generateRequestBody(MultipartFile image) { + try { + return RequestBody.fromBytes(image.getBytes()); + } catch (IOException e) { + throw new InvalidValueException(ErrorStatus.BAD_IMAGE_FILE); + } + } + + private String generateImageFileName() { + return "images/" + UUID.randomUUID() + ".jpg"; + } +} \ No newline at end of file From 0ecb50d13d7a56952c631affd289a662ecddd508 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Fri, 1 Dec 2023 01:40:51 +0900 Subject: [PATCH 21/24] =?UTF-8?q?[chore]=20#7=20=EB=A9=80=ED=8B=B0=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {api => seminar-api}/build.gradle | 5 +-- .../java/org/sopt/SeminarApplication.java | 0 .../sopt/api/auth/BCryptPasswordConfig.java | 0 .../sopt/api/auth/ExceptionHandlerFilter.java | 4 +-- .../api/auth/JwtAuthenticationEntryPoint.java | 4 +-- .../api/auth/JwtAuthenticationFilter.java | 9 +++--- .../main/java/org/sopt/api/auth/MemberId.java | 0 .../api/auth/MemberIdArgumentResolver.java | 2 +- .../org/sopt/api/auth/PasswordHandler.java | 0 .../org/sopt/api/auth/SecurityConfig.java | 0 .../org/sopt/api/auth/UserAuthentication.java | 0 .../java/org/sopt/api/auth/WebConfig.java | 0 .../org/sopt/api/auth/jwt/JwtGenerator.java | 19 +++++++++-- .../org/sopt/api/auth/jwt/JwtProvider.java | 0 .../org/sopt/api/auth/jwt/JwtValidator.java | 4 +-- .../java/org/sopt/api/auth/jwt/Token.java | 0 .../java/org/sopt/api/common/ApiResponse.java | 0 .../api/common/GlobalExceptionHandler.java | 0 .../org/sopt/api/common/SuccessStatus.java | 0 .../api/member/api/MemberApiController.java | 3 +- .../dto/request/MemberSignInRequest.java | 0 .../dto/request/MemberSignUpRequest.java | 0 .../dto/request/MemberUpdateRequest.java | 0 .../dto/response/MemberGetResponse.java | 0 .../api/member/service/MemberService.java | 0 .../api/member/service/MemberValidator.java | 0 .../sopt/api/post/api/PostApiController.java | 26 ++++++++------- .../dto/request/PostSaveOrUpdateRequest.java | 0 .../post/dto/response/PostGetResponse.java | 2 ++ .../post/dto/response/PostSaveResponse.java | 0 .../sopt/api/post/service/PostService.java | 32 +++++++++++-------- {common => seminar-common}/build.gradle | 0 .../sopt/common/error/BusinessException.java | 0 .../sopt/common/error/ConflictException.java | 0 .../common/error/EntityNotFoundException.java | 0 .../org/sopt/common/error/ErrorStatus.java | 1 + .../common/error/InvalidValueException.java | 0 .../common/error/UnauthorizedException.java | 0 {domain => seminar-domain}/build.gradle | 2 +- .../sopt/domain/common/BaseTimeEntity.java | 2 +- .../org/sopt/domain/config/JpaConfig.java | 0 .../org/sopt/domain/member/domain/Member.java | 0 .../org/sopt/domain/member/domain/Part.java | 0 .../org/sopt/domain/member/domain/Sopt.java | 0 .../member/repository/MemberRepository.java | 0 .../org/sopt/domain/post/domain/Category.java | 0 .../org/sopt/domain/post/domain/Post.java | 4 ++- .../post/repository/CategoryRepository.java | 0 .../post/repository/PostRepository.java | 0 settings.gradle | 7 ++-- 50 files changed, 78 insertions(+), 48 deletions(-) rename {api => seminar-api}/build.gradle (81%) rename {api => seminar-api}/src/main/java/org/sopt/SeminarApplication.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java (94%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java (92%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java (90%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/MemberId.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java (95%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/PasswordHandler.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/SecurityConfig.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/UserAuthentication.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/WebConfig.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java (72%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java (89%) rename {api => seminar-api}/src/main/java/org/sopt/api/auth/jwt/Token.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/common/ApiResponse.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/common/SuccessStatus.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/api/MemberApiController.java (96%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/dto/request/MemberUpdateRequest.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/dto/response/MemberGetResponse.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/service/MemberService.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/member/service/MemberValidator.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/post/api/PostApiController.java (59%) rename {api => seminar-api}/src/main/java/org/sopt/api/post/dto/request/PostSaveOrUpdateRequest.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java (89%) rename {api => seminar-api}/src/main/java/org/sopt/api/post/dto/response/PostSaveResponse.java (100%) rename {api => seminar-api}/src/main/java/org/sopt/api/post/service/PostService.java (68%) rename {common => seminar-common}/build.gradle (100%) rename {common => seminar-common}/src/main/java/org/sopt/common/error/BusinessException.java (100%) rename {common => seminar-common}/src/main/java/org/sopt/common/error/ConflictException.java (100%) rename {common => seminar-common}/src/main/java/org/sopt/common/error/EntityNotFoundException.java (100%) rename {common => seminar-common}/src/main/java/org/sopt/common/error/ErrorStatus.java (96%) rename {common => seminar-common}/src/main/java/org/sopt/common/error/InvalidValueException.java (100%) rename {common => seminar-common}/src/main/java/org/sopt/common/error/UnauthorizedException.java (100%) rename {domain => seminar-domain}/build.gradle (82%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/common/BaseTimeEntity.java (94%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/config/JpaConfig.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/member/domain/Member.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/member/domain/Part.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/member/domain/Sopt.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/member/repository/MemberRepository.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/post/domain/Category.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/post/domain/Post.java (92%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/post/repository/CategoryRepository.java (100%) rename {domain => seminar-domain}/src/main/java/org/sopt/domain/post/repository/PostRepository.java (100%) diff --git a/api/build.gradle b/seminar-api/build.gradle similarity index 81% rename from api/build.gradle rename to seminar-api/build.gradle index 4a7a1d3..342bb6f 100644 --- a/api/build.gradle +++ b/seminar-api/build.gradle @@ -5,6 +5,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // spring security implementation 'org.springframework.boot:spring-boot-starter-security' + implementation project(path: ':seminar-infra') // h2 runtimeOnly 'com.h2database:h2' // jwt @@ -12,9 +13,9 @@ dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' // domain dependency - implementation project(path: ':domain') + implementation project(path: ':seminar-domain') // common dependency - implementation project(path: ':common') + implementation project(path: ':seminar-common') } jar { diff --git a/api/src/main/java/org/sopt/SeminarApplication.java b/seminar-api/src/main/java/org/sopt/SeminarApplication.java similarity index 100% rename from api/src/main/java/org/sopt/SeminarApplication.java rename to seminar-api/src/main/java/org/sopt/SeminarApplication.java diff --git a/api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java b/seminar-api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java rename to seminar-api/src/main/java/org/sopt/api/auth/BCryptPasswordConfig.java diff --git a/api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java b/seminar-api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java similarity index 94% rename from api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java rename to seminar-api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java index 614328c..6738891 100644 --- a/api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java +++ b/seminar-api/src/main/java/org/sopt/api/auth/ExceptionHandlerFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.sopt.api.common.ApiResponse; +import org.sopt.api.common.Constants; import org.sopt.common.error.ErrorStatus; import org.sopt.common.error.UnauthorizedException; import org.springframework.http.HttpStatus; @@ -15,7 +16,6 @@ import java.io.PrintWriter; public class ExceptionHandlerFilter extends OncePerRequestFilter { - private static final String CHARACTER_TYPE = "utf-8"; private final ObjectMapper objectMapper = new ObjectMapper(); @Override @@ -42,7 +42,7 @@ private void handleException(HttpServletResponse response) throws IOException { private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorStatus errorStatus) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding(CHARACTER_TYPE); + response.setCharacterEncoding(Constants.CHARACTER_TYPE); response.setStatus(httpStatus.value()); PrintWriter writer = response.getWriter(); writer.write(objectMapper.writeValueAsString(ApiResponse.of(errorStatus))); diff --git a/api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java b/seminar-api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java similarity index 92% rename from api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java rename to seminar-api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java index a582388..679dd63 100644 --- a/api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java +++ b/seminar-api/src/main/java/org/sopt/api/auth/JwtAuthenticationEntryPoint.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.sopt.api.common.ApiResponse; +import org.sopt.api.common.Constants; import org.sopt.common.error.ErrorStatus; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -16,7 +17,6 @@ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { - private static final String CHARACTER_TYPE = "utf-8"; private final ObjectMapper objectMapper = new ObjectMapper(); @Override @@ -30,7 +30,7 @@ private void handleException(HttpServletResponse response) throws IOException { private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorStatus errorStatus) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding(CHARACTER_TYPE); + response.setCharacterEncoding(Constants.CHARACTER_TYPE); response.setStatus(httpStatus.value()); PrintWriter writer = response.getWriter(); writer.write(objectMapper.writeValueAsString(ApiResponse.of(errorStatus))); diff --git a/api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java b/seminar-api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java similarity index 90% rename from api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java rename to seminar-api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java index 15767e1..657718b 100644 --- a/api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java +++ b/seminar-api/src/main/java/org/sopt/api/auth/JwtAuthenticationFilter.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import org.sopt.api.auth.jwt.JwtProvider; import org.sopt.api.auth.jwt.JwtValidator; +import org.sopt.api.common.Constants; import org.sopt.common.error.ErrorStatus; import org.sopt.common.error.UnauthorizedException; import org.springframework.security.core.context.SecurityContext; @@ -22,8 +23,6 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - private static final String AUTHORIZATION = "Authorization"; - private static final String BEARER = "Bearer "; private final JwtValidator jwtValidator; private final JwtProvider jwtProvider; @@ -36,9 +35,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private String getAccessToken(HttpServletRequest request) { - String accessToken = request.getHeader(AUTHORIZATION); - if (StringUtils.hasText(accessToken) && accessToken.startsWith(BEARER)) { - return accessToken.substring(BEARER.length()); + String accessToken = request.getHeader(Constants.AUTHORIZATION); + if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) { + return accessToken.substring(Constants.BEARER.length()); } throw new UnauthorizedException(ErrorStatus.INVALID_ACCESS_TOKEN); } diff --git a/api/src/main/java/org/sopt/api/auth/MemberId.java b/seminar-api/src/main/java/org/sopt/api/auth/MemberId.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/MemberId.java rename to seminar-api/src/main/java/org/sopt/api/auth/MemberId.java diff --git a/api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java b/seminar-api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java similarity index 95% rename from api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java rename to seminar-api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java index 33218eb..40ce73f 100644 --- a/api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java +++ b/seminar-api/src/main/java/org/sopt/api/auth/MemberIdArgumentResolver.java @@ -18,7 +18,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { return SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); diff --git a/api/src/main/java/org/sopt/api/auth/PasswordHandler.java b/seminar-api/src/main/java/org/sopt/api/auth/PasswordHandler.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/PasswordHandler.java rename to seminar-api/src/main/java/org/sopt/api/auth/PasswordHandler.java diff --git a/api/src/main/java/org/sopt/api/auth/SecurityConfig.java b/seminar-api/src/main/java/org/sopt/api/auth/SecurityConfig.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/SecurityConfig.java rename to seminar-api/src/main/java/org/sopt/api/auth/SecurityConfig.java diff --git a/api/src/main/java/org/sopt/api/auth/UserAuthentication.java b/seminar-api/src/main/java/org/sopt/api/auth/UserAuthentication.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/UserAuthentication.java rename to seminar-api/src/main/java/org/sopt/api/auth/UserAuthentication.java diff --git a/api/src/main/java/org/sopt/api/auth/WebConfig.java b/seminar-api/src/main/java/org/sopt/api/auth/WebConfig.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/WebConfig.java rename to seminar-api/src/main/java/org/sopt/api/auth/WebConfig.java diff --git a/api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java b/seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java similarity index 72% rename from api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java rename to seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java index 129f7fa..92fc8e9 100644 --- a/api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java +++ b/seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtGenerator.java @@ -22,8 +22,8 @@ public class JwtGenerator { private long REFRESH_TOKEN_EXPIRE_TIME; public String generateToken(Long memberId, boolean isAccessToken) { - final Date now = new Date(); - final Date expiration = new Date(now.getTime() + (isAccessToken ? ACCESS_TOKEN_EXPIRE_TIME : REFRESH_TOKEN_EXPIRE_TIME)); + final Date now = generateNowDate(); + final Date expiration = generateExpirationDate(isAccessToken, now); return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) .setSubject(String.valueOf(memberId)) @@ -39,6 +39,21 @@ public JwtParser getJwtParser() { .build(); } + private Date generateNowDate() { + return new Date(); + } + + private Date generateExpirationDate(boolean isAccessToken, Date now) { + return new Date(now.getTime() + calculateExpireTime(isAccessToken)); + } + + private long calculateExpireTime(boolean isAccessToken) { + if (isAccessToken) { + return ACCESS_TOKEN_EXPIRE_TIME; + } + return REFRESH_TOKEN_EXPIRE_TIME; + } + private Key getSigningKey() { String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); return Keys.hmacShaKeyFor(encoded.getBytes()); diff --git a/api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java b/seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java rename to seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtProvider.java diff --git a/api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java b/seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java similarity index 89% rename from api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java rename to seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java index 8ab0ef4..d6e60b4 100644 --- a/api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java +++ b/seminar-api/src/main/java/org/sopt/api/auth/jwt/JwtValidator.java @@ -34,8 +34,8 @@ public void validateRefreshToken(String refreshToken) { } } - public void equalsRefreshToken(String providedRefreshToken, String storedRefreshToken) { - if (!providedRefreshToken.equals(storedRefreshToken)) { + public void equalsRefreshToken(String refreshToken, String storedRefreshToken) { + if (!refreshToken.equals(storedRefreshToken)) { throw new UnauthorizedException(ErrorStatus.NOT_MATCH_REFRESH_TOKEN); } } diff --git a/api/src/main/java/org/sopt/api/auth/jwt/Token.java b/seminar-api/src/main/java/org/sopt/api/auth/jwt/Token.java similarity index 100% rename from api/src/main/java/org/sopt/api/auth/jwt/Token.java rename to seminar-api/src/main/java/org/sopt/api/auth/jwt/Token.java diff --git a/api/src/main/java/org/sopt/api/common/ApiResponse.java b/seminar-api/src/main/java/org/sopt/api/common/ApiResponse.java similarity index 100% rename from api/src/main/java/org/sopt/api/common/ApiResponse.java rename to seminar-api/src/main/java/org/sopt/api/common/ApiResponse.java diff --git a/api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java b/seminar-api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java similarity index 100% rename from api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java rename to seminar-api/src/main/java/org/sopt/api/common/GlobalExceptionHandler.java diff --git a/api/src/main/java/org/sopt/api/common/SuccessStatus.java b/seminar-api/src/main/java/org/sopt/api/common/SuccessStatus.java similarity index 100% rename from api/src/main/java/org/sopt/api/common/SuccessStatus.java rename to seminar-api/src/main/java/org/sopt/api/common/SuccessStatus.java diff --git a/api/src/main/java/org/sopt/api/member/api/MemberApiController.java b/seminar-api/src/main/java/org/sopt/api/member/api/MemberApiController.java similarity index 96% rename from api/src/main/java/org/sopt/api/member/api/MemberApiController.java rename to seminar-api/src/main/java/org/sopt/api/member/api/MemberApiController.java index cbce751..5caeee2 100644 --- a/api/src/main/java/org/sopt/api/member/api/MemberApiController.java +++ b/seminar-api/src/main/java/org/sopt/api/member/api/MemberApiController.java @@ -4,6 +4,7 @@ import org.sopt.api.auth.MemberId; import org.sopt.api.auth.jwt.Token; import org.sopt.api.common.ApiResponse; +import org.sopt.api.common.Constants; import org.sopt.api.common.SuccessStatus; import org.sopt.api.member.dto.request.MemberSignInRequest; import org.sopt.api.member.dto.request.MemberSignUpRequest; @@ -33,7 +34,7 @@ public ResponseEntity> signIn(@RequestBody final MemberSignInRequ } @PostMapping("/reissue") - public ResponseEntity> reissue(@RequestHeader("Authorization") final String refreshToken) { + public ResponseEntity> reissue(@RequestHeader(Constants.AUTHORIZATION) final String refreshToken) { final Token response = memberService.reissue(refreshToken); return ApiResponse.success(SuccessStatus.OK, response); } diff --git a/api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java b/seminar-api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java similarity index 100% rename from api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java rename to seminar-api/src/main/java/org/sopt/api/member/dto/request/MemberSignInRequest.java diff --git a/api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java b/seminar-api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java similarity index 100% rename from api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java rename to seminar-api/src/main/java/org/sopt/api/member/dto/request/MemberSignUpRequest.java diff --git a/api/src/main/java/org/sopt/api/member/dto/request/MemberUpdateRequest.java b/seminar-api/src/main/java/org/sopt/api/member/dto/request/MemberUpdateRequest.java similarity index 100% rename from api/src/main/java/org/sopt/api/member/dto/request/MemberUpdateRequest.java rename to seminar-api/src/main/java/org/sopt/api/member/dto/request/MemberUpdateRequest.java diff --git a/api/src/main/java/org/sopt/api/member/dto/response/MemberGetResponse.java b/seminar-api/src/main/java/org/sopt/api/member/dto/response/MemberGetResponse.java similarity index 100% rename from api/src/main/java/org/sopt/api/member/dto/response/MemberGetResponse.java rename to seminar-api/src/main/java/org/sopt/api/member/dto/response/MemberGetResponse.java diff --git a/api/src/main/java/org/sopt/api/member/service/MemberService.java b/seminar-api/src/main/java/org/sopt/api/member/service/MemberService.java similarity index 100% rename from api/src/main/java/org/sopt/api/member/service/MemberService.java rename to seminar-api/src/main/java/org/sopt/api/member/service/MemberService.java diff --git a/api/src/main/java/org/sopt/api/member/service/MemberValidator.java b/seminar-api/src/main/java/org/sopt/api/member/service/MemberValidator.java similarity index 100% rename from api/src/main/java/org/sopt/api/member/service/MemberValidator.java rename to seminar-api/src/main/java/org/sopt/api/member/service/MemberValidator.java diff --git a/api/src/main/java/org/sopt/api/post/api/PostApiController.java b/seminar-api/src/main/java/org/sopt/api/post/api/PostApiController.java similarity index 59% rename from api/src/main/java/org/sopt/api/post/api/PostApiController.java rename to seminar-api/src/main/java/org/sopt/api/post/api/PostApiController.java index c822e41..0ac2450 100644 --- a/api/src/main/java/org/sopt/api/post/api/PostApiController.java +++ b/seminar-api/src/main/java/org/sopt/api/post/api/PostApiController.java @@ -1,6 +1,7 @@ package org.sopt.api.post.api; import lombok.RequiredArgsConstructor; +import org.sopt.api.auth.MemberId; import org.sopt.api.common.ApiResponse; import org.sopt.api.common.SuccessStatus; import org.sopt.api.post.dto.request.PostSaveOrUpdateRequest; @@ -11,6 +12,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -18,33 +20,33 @@ @RequestMapping("/api/post") @Controller public class PostApiController { - private static final String CUSTOM_AUTHENTICATION = "X-Auth-Id"; private final PostService postService; @PostMapping - public ResponseEntity> savePost(@RequestHeader(CUSTOM_AUTHENTICATION) final Long memberId, - @RequestBody final PostSaveOrUpdateRequest postSaveOrUpdateRequest) { - final PostSaveResponse postSaveResponse = postService.savePost(memberId, postSaveOrUpdateRequest); - return ApiResponse.success(SuccessStatus.CREATED, postSaveResponse); + public ResponseEntity> savePost(@MemberId final Long memberId, + @RequestPart final MultipartFile image, + @RequestPart final PostSaveOrUpdateRequest request) { + final PostSaveResponse response = postService.savePost(memberId, image, request); + return ApiResponse.success(SuccessStatus.CREATED, response); } @GetMapping("/{postId}") public ResponseEntity> getPost(@PathVariable final Long postId) { - final PostGetResponse post = postService.getPost(postId); - return ApiResponse.success(SuccessStatus.OK, post); + final PostGetResponse response = postService.getPost(postId); + return ApiResponse.success(SuccessStatus.OK, response); } @GetMapping - public ResponseEntity> getPosts(@RequestHeader(CUSTOM_AUTHENTICATION) final Long memberId, + public ResponseEntity> getPosts(@MemberId final Long memberId, final Pageable pageable) { - final List posts = postService.getPosts(memberId, pageable); - return ApiResponse.success(SuccessStatus.OK, posts); + final List response = postService.getPosts(memberId, pageable); + return ApiResponse.success(SuccessStatus.OK, response); } @PatchMapping("/{postId}") public ResponseEntity> updatePost(@PathVariable final Long postId, - @RequestBody final PostSaveOrUpdateRequest postSaveOrUpdateRequest) { - postService.updatePost(postId, postSaveOrUpdateRequest); + @RequestBody final PostSaveOrUpdateRequest request) { + postService.updatePost(postId, request); return ApiResponse.success(SuccessStatus.OK); } diff --git a/api/src/main/java/org/sopt/api/post/dto/request/PostSaveOrUpdateRequest.java b/seminar-api/src/main/java/org/sopt/api/post/dto/request/PostSaveOrUpdateRequest.java similarity index 100% rename from api/src/main/java/org/sopt/api/post/dto/request/PostSaveOrUpdateRequest.java rename to seminar-api/src/main/java/org/sopt/api/post/dto/request/PostSaveOrUpdateRequest.java diff --git a/api/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java b/seminar-api/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java similarity index 89% rename from api/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java rename to seminar-api/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java index 4263163..e72ceaf 100644 --- a/api/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java +++ b/seminar-api/src/main/java/org/sopt/api/post/dto/response/PostGetResponse.java @@ -9,6 +9,7 @@ public record PostGetResponse( String title, String postContent, + String imageUrl, String categoryContent ) { public static PostGetResponse of(Post post) { @@ -16,6 +17,7 @@ public static PostGetResponse of(Post post) { return builder() .title(post.getTitle()) .postContent(post.getContent()) + .imageUrl(post.getImageUrl()) .categoryContent(category.getContent()) .build(); } diff --git a/api/src/main/java/org/sopt/api/post/dto/response/PostSaveResponse.java b/seminar-api/src/main/java/org/sopt/api/post/dto/response/PostSaveResponse.java similarity index 100% rename from api/src/main/java/org/sopt/api/post/dto/response/PostSaveResponse.java rename to seminar-api/src/main/java/org/sopt/api/post/dto/response/PostSaveResponse.java diff --git a/api/src/main/java/org/sopt/api/post/service/PostService.java b/seminar-api/src/main/java/org/sopt/api/post/service/PostService.java similarity index 68% rename from api/src/main/java/org/sopt/api/post/service/PostService.java rename to seminar-api/src/main/java/org/sopt/api/post/service/PostService.java index 0b4ba2d..98a00f7 100644 --- a/api/src/main/java/org/sopt/api/post/service/PostService.java +++ b/seminar-api/src/main/java/org/sopt/api/post/service/PostService.java @@ -11,9 +11,11 @@ import org.sopt.domain.post.domain.Post; import org.sopt.domain.post.repository.CategoryRepository; import org.sopt.domain.post.repository.PostRepository; +import org.sopt.infra.s3.S3Handler; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -24,15 +26,17 @@ @Transactional(readOnly = true) @Service public class PostService { + private final S3Handler s3Handler; private final MemberRepository memberRepository; private final PostRepository postRepository; private final CategoryRepository categoryRepository; @Transactional - public PostSaveResponse savePost(Long memberId, PostSaveOrUpdateRequest postSaveOrUpdateRequest) { + public PostSaveResponse savePost(Long memberId, MultipartFile image, PostSaveOrUpdateRequest request) { Member findMember = findMember(memberId); - Category savedCategory = createCategoryAndGetSavedCategoryOrGetCategory(postSaveOrUpdateRequest); - Post savedPost = createPostAndGetSavedPost(postSaveOrUpdateRequest, findMember, savedCategory); + Category savedCategory = createCategoryAndGetSavedCategoryOrGetCategory(request); + String imageUrl = s3Handler.uploadImage(image); + Post savedPost = createPostAndGetSavedPost(imageUrl, request, findMember, savedCategory); return PostSaveResponse.of(savedPost); } @@ -49,14 +53,16 @@ public List getPosts(Long memberId, Pageable pageable) { } @Transactional - public void updatePost(Long postId, PostSaveOrUpdateRequest postSaveOrUpdateRequest) { + public void updatePost(Long postId, PostSaveOrUpdateRequest request) { Post findPost = findPost(postId); Category category = findPost.getCategory(); - updatePostAndCategory(postSaveOrUpdateRequest, findPost, category); + updatePostAndCategory(request, findPost, category); } @Transactional public void deletePost(Long postId) { + Post findPost = findPost(postId); + s3Handler.deleteImage(findPost.getImageUrl()); postRepository.deleteById(postId); } @@ -64,17 +70,17 @@ private Member findMember(Long memberId) { return memberRepository.findByIdOrThrow(memberId); } - private Category createCategoryAndGetSavedCategoryOrGetCategory(PostSaveOrUpdateRequest postSaveOrUpdateRequest) { + private Category createCategoryAndGetSavedCategoryOrGetCategory(PostSaveOrUpdateRequest request) { try { - return findCategory(postSaveOrUpdateRequest.categoryContent()); + return findCategory(request.categoryContent()); } catch (EntityNotFoundException e) { - Category category = createCategory(postSaveOrUpdateRequest.categoryContent()); + Category category = createCategory(request.categoryContent()); return categoryRepository.save(category); } } - private Post createPostAndGetSavedPost(PostSaveOrUpdateRequest postSaveOrUpdateRequest, Member member, Category category) { - Post post = createPost(postSaveOrUpdateRequest.title(), postSaveOrUpdateRequest.postContent(), category, member); + private Post createPostAndGetSavedPost(String imageUrl, PostSaveOrUpdateRequest request, Member member, Category category) { + Post post = createPost(request.title(), request.postContent(), imageUrl, category, member); return postRepository.save(post); } @@ -82,9 +88,9 @@ private Post findPost(Long postId) { return postRepository.findByIdOrThrow(postId); } - private void updatePostAndCategory(PostSaveOrUpdateRequest postSaveOrUpdateRequest, Post post, Category category) { - post.updateTitleAndContent(postSaveOrUpdateRequest.title(), postSaveOrUpdateRequest.postContent()); - category.updateContent(postSaveOrUpdateRequest.categoryContent()); + private void updatePostAndCategory(PostSaveOrUpdateRequest request, Post post, Category category) { + post.updateTitleAndContent(request.title(), request.postContent()); + category.updateContent(request.categoryContent()); } private Category findCategory(String content) { diff --git a/common/build.gradle b/seminar-common/build.gradle similarity index 100% rename from common/build.gradle rename to seminar-common/build.gradle diff --git a/common/src/main/java/org/sopt/common/error/BusinessException.java b/seminar-common/src/main/java/org/sopt/common/error/BusinessException.java similarity index 100% rename from common/src/main/java/org/sopt/common/error/BusinessException.java rename to seminar-common/src/main/java/org/sopt/common/error/BusinessException.java diff --git a/common/src/main/java/org/sopt/common/error/ConflictException.java b/seminar-common/src/main/java/org/sopt/common/error/ConflictException.java similarity index 100% rename from common/src/main/java/org/sopt/common/error/ConflictException.java rename to seminar-common/src/main/java/org/sopt/common/error/ConflictException.java diff --git a/common/src/main/java/org/sopt/common/error/EntityNotFoundException.java b/seminar-common/src/main/java/org/sopt/common/error/EntityNotFoundException.java similarity index 100% rename from common/src/main/java/org/sopt/common/error/EntityNotFoundException.java rename to seminar-common/src/main/java/org/sopt/common/error/EntityNotFoundException.java diff --git a/common/src/main/java/org/sopt/common/error/ErrorStatus.java b/seminar-common/src/main/java/org/sopt/common/error/ErrorStatus.java similarity index 96% rename from common/src/main/java/org/sopt/common/error/ErrorStatus.java rename to seminar-common/src/main/java/org/sopt/common/error/ErrorStatus.java index be875e5..1734b1f 100644 --- a/common/src/main/java/org/sopt/common/error/ErrorStatus.java +++ b/seminar-common/src/main/java/org/sopt/common/error/ErrorStatus.java @@ -12,6 +12,7 @@ public enum ErrorStatus { * 400 Bad Request */ BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + BAD_IMAGE_FILE(HttpStatus.BAD_REQUEST, "이미지 변환에 실패했습니다."), /** * 401 Unauthorized diff --git a/common/src/main/java/org/sopt/common/error/InvalidValueException.java b/seminar-common/src/main/java/org/sopt/common/error/InvalidValueException.java similarity index 100% rename from common/src/main/java/org/sopt/common/error/InvalidValueException.java rename to seminar-common/src/main/java/org/sopt/common/error/InvalidValueException.java diff --git a/common/src/main/java/org/sopt/common/error/UnauthorizedException.java b/seminar-common/src/main/java/org/sopt/common/error/UnauthorizedException.java similarity index 100% rename from common/src/main/java/org/sopt/common/error/UnauthorizedException.java rename to seminar-common/src/main/java/org/sopt/common/error/UnauthorizedException.java diff --git a/domain/build.gradle b/seminar-domain/build.gradle similarity index 82% rename from domain/build.gradle rename to seminar-domain/build.gradle index 4b3bd02..f4923c7 100644 --- a/domain/build.gradle +++ b/seminar-domain/build.gradle @@ -4,7 +4,7 @@ dependencies { // h2 runtimeOnly 'com.h2database:h2' // common dependency - implementation project(path: ':common') + implementation project(path: ':seminar-common') } bootJar { diff --git a/domain/src/main/java/org/sopt/domain/common/BaseTimeEntity.java b/seminar-domain/src/main/java/org/sopt/domain/common/BaseTimeEntity.java similarity index 94% rename from domain/src/main/java/org/sopt/domain/common/BaseTimeEntity.java rename to seminar-domain/src/main/java/org/sopt/domain/common/BaseTimeEntity.java index 1a8475f..74dda5e 100644 --- a/domain/src/main/java/org/sopt/domain/common/BaseTimeEntity.java +++ b/seminar-domain/src/main/java/org/sopt/domain/common/BaseTimeEntity.java @@ -13,7 +13,7 @@ @Getter @EntityListeners(AuditingEntityListener.class) @MappedSuperclass -public class BaseTimeEntity { +public abstract class BaseTimeEntity { @Column(updatable = false) @CreatedDate private LocalDateTime createdDate; diff --git a/domain/src/main/java/org/sopt/domain/config/JpaConfig.java b/seminar-domain/src/main/java/org/sopt/domain/config/JpaConfig.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/config/JpaConfig.java rename to seminar-domain/src/main/java/org/sopt/domain/config/JpaConfig.java diff --git a/domain/src/main/java/org/sopt/domain/member/domain/Member.java b/seminar-domain/src/main/java/org/sopt/domain/member/domain/Member.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/member/domain/Member.java rename to seminar-domain/src/main/java/org/sopt/domain/member/domain/Member.java diff --git a/domain/src/main/java/org/sopt/domain/member/domain/Part.java b/seminar-domain/src/main/java/org/sopt/domain/member/domain/Part.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/member/domain/Part.java rename to seminar-domain/src/main/java/org/sopt/domain/member/domain/Part.java diff --git a/domain/src/main/java/org/sopt/domain/member/domain/Sopt.java b/seminar-domain/src/main/java/org/sopt/domain/member/domain/Sopt.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/member/domain/Sopt.java rename to seminar-domain/src/main/java/org/sopt/domain/member/domain/Sopt.java diff --git a/domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java b/seminar-domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java rename to seminar-domain/src/main/java/org/sopt/domain/member/repository/MemberRepository.java diff --git a/domain/src/main/java/org/sopt/domain/post/domain/Category.java b/seminar-domain/src/main/java/org/sopt/domain/post/domain/Category.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/post/domain/Category.java rename to seminar-domain/src/main/java/org/sopt/domain/post/domain/Category.java diff --git a/domain/src/main/java/org/sopt/domain/post/domain/Post.java b/seminar-domain/src/main/java/org/sopt/domain/post/domain/Post.java similarity index 92% rename from domain/src/main/java/org/sopt/domain/post/domain/Post.java rename to seminar-domain/src/main/java/org/sopt/domain/post/domain/Post.java index 169c184..c320b7c 100644 --- a/domain/src/main/java/org/sopt/domain/post/domain/Post.java +++ b/seminar-domain/src/main/java/org/sopt/domain/post/domain/Post.java @@ -17,6 +17,7 @@ public class Post extends BaseTimeEntity { private Long id; private String title; private String content; + private String imageUrl; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; @@ -24,10 +25,11 @@ public class Post extends BaseTimeEntity { @JoinColumn(name = "category_id") private Category category; - public static Post createPost(final String title, final String content, final Category category, final Member member) { + public static Post createPost(final String title, final String content, final String imageUrl, final Category category, final Member member) { Post post = Post.builder() .title(title) .content(content) + .imageUrl(imageUrl) .build(); post.changeMember(member); post.changeCategory(category); diff --git a/domain/src/main/java/org/sopt/domain/post/repository/CategoryRepository.java b/seminar-domain/src/main/java/org/sopt/domain/post/repository/CategoryRepository.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/post/repository/CategoryRepository.java rename to seminar-domain/src/main/java/org/sopt/domain/post/repository/CategoryRepository.java diff --git a/domain/src/main/java/org/sopt/domain/post/repository/PostRepository.java b/seminar-domain/src/main/java/org/sopt/domain/post/repository/PostRepository.java similarity index 100% rename from domain/src/main/java/org/sopt/domain/post/repository/PostRepository.java rename to seminar-domain/src/main/java/org/sopt/domain/post/repository/PostRepository.java diff --git a/settings.gradle b/settings.gradle index 39131b8..b2f6892 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'seminar' -include 'api' -include 'domain' -include 'common' +include 'seminar-api' +include 'seminar-domain' +include 'seminar-common' +include 'seminar-infra' From 00b560b5242efbb9dd456f3bde47d2b20a9d71f5 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Fri, 1 Dec 2023 01:41:14 +0900 Subject: [PATCH 22/24] =?UTF-8?q?[feat]=20#7=20=EB=AC=B8=EC=9E=90=EC=97=B4?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/org/sopt/api/common/Constants.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 seminar-api/src/main/java/org/sopt/api/common/Constants.java diff --git a/seminar-api/src/main/java/org/sopt/api/common/Constants.java b/seminar-api/src/main/java/org/sopt/api/common/Constants.java new file mode 100644 index 0000000..d1a7a0a --- /dev/null +++ b/seminar-api/src/main/java/org/sopt/api/common/Constants.java @@ -0,0 +1,7 @@ +package org.sopt.api.common; + +public abstract class Constants { + public static final String AUTHORIZATION = "Authorization"; + public static final String BEARER = "Bearer "; + public static final String CHARACTER_TYPE = "utf-8"; +} From 2b14d3b4c87b023481efac6605518b471fab0082 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Fri, 1 Dec 2023 01:41:38 +0900 Subject: [PATCH 23/24] =?UTF-8?q?[chore]=20#7=20ci=20workflow=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb29113..2eadbf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,16 +10,16 @@ jobs: runs-on: ubuntu-latest steps: - - name: ✔️ 체크아웃 합니다. + - name: ✔️ checkout uses: actions/checkout@v3 - - name: ✔️ JDK 17로 설정합니다. + - name: ✔️ set JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - - name: ✔️ application.yml 파일을 생성합니다. + - name: ✔️ create application.yml run: | cd ./api/src/main mkdir resources @@ -28,8 +28,8 @@ jobs: env: APPLICATION: ${{ secrets.APPLICATION }} - - name: ✔️ gradle build 를 위한 권한을 부여합니다. + - name: ✔️ set permission run: chmod +x gradlew - - name: ✔️ gradle build 합니다. - run: ./gradlew api:build \ No newline at end of file + - name: ✔️ build gradle + run: ./gradlew seminar-api:build \ No newline at end of file From c53e90e4f50acb45d7e0266600766c6ff9ba79cd Mon Sep 17 00:00:00 2001 From: sunwoong Date: Fri, 1 Dec 2023 02:25:00 +0900 Subject: [PATCH 24/24] =?UTF-8?q?[chore]=20#7=20ci=20workflow=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2eadbf8..693a5f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: ✔️ create application.yml run: | - cd ./api/src/main + cd ./seminar-api/src/main mkdir resources cd ./resources echo "$APPLICATION" > ./application.yml