diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..87f9960 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,75 @@ +name: Java CI with Gradle + +# 동작 조건 설정 : main 브랜치에 push 혹은 pull request가 발생할 경우 동작한다. +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + # Spring Boot 애플리케이션을 빌드하여 도커허브에 푸시하는 과정 + build-docker-image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # 1. Java 17 세팅 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + ## create application-database.yaml + - name: create application.properties file + run: | + mkdir ./src/main/resources + touch ./src/main/resources/application.yml + echo "${{ secrets.APPLICATION_YML }}" >> src/main/resources/application.yml + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash + + # 2. Spring Boot 애플리케이션 빌드 + - name: Build with Gradle + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: clean bootJar + + # 3. Docker 이미지 빌드 + - name: docker image build + run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/finfellow . + + # 4. DockerHub 로그인 + - name: docker login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + # 5. Docker Hub 이미지 푸시 + - name: docker Hub push + run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/finfellow + + run-docker-image-on-ec2: + needs: build-docker-image + runs-on: self-hosted + + steps: + # 1. 최신 이미지를 풀받습니다 + - name: docker pull + run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/finfellow + + # 2. 기존의 컨테이너를 중지시킵니다 + - name: docker stop container + run: sudo docker stop $(sudo docker ps -q) 2>/dev/null || true + + # 3. 최신 이미지를 컨테이너화하여 실행시킵니다 + - name: docker run new container + run: sudo docker run --name finfellow --rm -d -p 80:8080 ${{ secrets.DOCKERHUB_USERNAME }}/finfellow + + # 4. 미사용 이미지를 정리합니다 + - name: delete old docker image + run: sudo docker system prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore index ca257ab..75ddae2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ out/ ### VS Code ### .vscode/ /src/main/resources/ +application.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 45d07ca..ca14d4d 100644 --- a/build.gradle +++ b/build.gradle @@ -22,15 +22,28 @@ repositories { } dependencies { +// implementation 'net.bytebuddy:byte-buddy:1.11.22' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + + //Swagger, RestDocs + implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.1.0' + testImplementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-api', version: '2.1.0' + + //jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' } diff --git a/src/main/java/com/finfellows/domain/auth/application/CustomTokenProviderService.java b/src/main/java/com/finfellows/domain/auth/application/CustomTokenProviderService.java new file mode 100644 index 0000000..e624052 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/application/CustomTokenProviderService.java @@ -0,0 +1,150 @@ +package com.finfellows.domain.auth.application; + +import com.finfellows.domain.auth.dto.TokenMapping; +import com.finfellows.global.config.security.OAuth2Config; +import com.finfellows.global.config.security.token.UserPrincipal; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; + +@Slf4j +@Service +public class CustomTokenProviderService { + + @Autowired + private OAuth2Config oAuth2Config; + + @Autowired + private final CustomUserDetailsService customUserDetailsService; + + public CustomTokenProviderService(CustomUserDetailsService customUserDetailsService) { + this.customUserDetailsService = customUserDetailsService; + } + + + public TokenMapping refreshToken(Authentication authentication, String refreshToken) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + Date now = new Date(); + + Date accessTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getAccessTokenExpirationMsec()); + + String secretKey = oAuth2Config.getAuth().getTokenSecret(); + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + Key key = Keys.hmacShaKeyFor(keyBytes); + + String accessToken = Jwts.builder() + .setSubject(Long.toString(userPrincipal.getId())) + .setIssuedAt(new Date()) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + + return TokenMapping.builder() + .email(userPrincipal.getEmail()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + public Long getExpiration(String token) { + // accessToken 남은 유효시간 + Date expiration = Jwts.parserBuilder().setSigningKey(oAuth2Config.getAuth().getTokenSecret()).build().parseClaimsJws(token).getBody().getExpiration(); + // 현재 시간 + Long now = new Date().getTime(); + //시간 계산 + return (expiration.getTime() - now); + } + + + //토큰 만들기 + public TokenMapping createToken(Authentication authentication) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + + + Date now = new Date(); + + Date accessTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getAccessTokenExpirationMsec()); + Date refreshTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getRefreshTokenExpirationMsec()); + + String secretKey = oAuth2Config.getAuth().getTokenSecret(); + + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + Key key = Keys.hmacShaKeyFor(keyBytes); + + String accessToken = Jwts.builder() + .setSubject(Long.toString(userPrincipal.getId())) + .setIssuedAt(new Date()) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(refreshTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + + + return TokenMapping.builder() + .email(userPrincipal.getEmail()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + } + + + public boolean validateToken(String token) { + try { + //log.info("bearerToken = {} \n oAuth2Config.getAuth()={}", token, oAuth2Config.getAuth().getTokenSecret()); + Jwts.parserBuilder().setSigningKey(oAuth2Config.getAuth().getTokenSecret()).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException ex) { + log.error("잘못된 JWT 서명입니다."); + } catch (MalformedJwtException ex) { + log.error("잘못된 JWT 서명입니다."); + } catch (ExpiredJwtException ex) { + log.error("만료된 JWT 토큰입니다."); + } catch (UnsupportedJwtException ex) { + log.error("지원되지 않는 JWT 토큰입니다."); + } catch (IllegalArgumentException ex) { + log.error("JWT 토큰이 잘못되었습니다."); + } + return false; + } + + + + public UsernamePasswordAuthenticationToken getAuthenticationById(String token) { + Long userId = getUserIdFromToken(token); + UserDetails userDetails = customUserDetailsService.loadUserById(userId); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + return authentication; + } + + public UsernamePasswordAuthenticationToken getAuthenticationByEmail(String email) { + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + return authentication; + } + + public Long getUserIdFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(oAuth2Config.getAuth().getTokenSecret()) + .build() + .parseClaimsJws(token) + .getBody(); + + return Long.parseLong(claims.getSubject()); + } + + +} diff --git a/src/main/java/com/finfellows/domain/auth/application/CustomUserDetailsService.java b/src/main/java/com/finfellows/domain/auth/application/CustomUserDetailsService.java new file mode 100644 index 0000000..b50f3de --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/application/CustomUserDetailsService.java @@ -0,0 +1,49 @@ +package com.finfellows.domain.auth.application; + + +import com.finfellows.domain.user.domain.User; +import com.finfellows.domain.user.domain.repository.UserRepository; +import com.finfellows.global.config.security.token.UserPrincipal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +@Slf4j +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Optional user = userRepository.findByEmail(email); + if (user.isPresent()) { + return UserPrincipal.createUser(user.get()); + } + + throw new UsernameNotFoundException("유효하지 않는 유저입니다."); + } + + public UserDetails loadUserById(Long id) { + log.debug("Attempting to load user by ID: {}", id); + return userRepository.findById(id) + .map(UserPrincipal::createUser) + .orElseThrow(() -> { + log.error("User with ID: {} could not be found.", id); + return new UsernameNotFoundException("유효하지 않는 유저입니다."); + }); + } + + + + + + +} diff --git a/src/main/java/com/finfellows/domain/auth/application/KakaoService.java b/src/main/java/com/finfellows/domain/auth/application/KakaoService.java new file mode 100644 index 0000000..dc64a43 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/application/KakaoService.java @@ -0,0 +1,334 @@ +package com.finfellows.domain.auth.application; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.finfellows.domain.auth.domain.Token; +import com.finfellows.domain.auth.domain.repository.TokenRepository; +import com.finfellows.domain.auth.dto.*; +import com.finfellows.domain.user.domain.Role; +import com.finfellows.domain.user.domain.User; +import com.finfellows.domain.user.domain.repository.UserRepository; +import com.finfellows.global.DefaultAssert; +import com.finfellows.global.config.security.OAuth2Config; +import com.finfellows.global.config.security.token.UserPrincipal; +import com.finfellows.global.error.DefaultAuthenticationException; +import com.finfellows.global.payload.ErrorCode; +import com.finfellows.global.payload.Message; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class KakaoService { + + @Autowired + private OAuth2Config oAuth2Config; + + private final AuthenticationManager authenticationManager; + + private final RestTemplate rt; + private final ObjectMapper objectMapper; + private final HttpServletResponse response; + + private final UserRepository userRepository; + private final TokenRepository tokenRepository; + private final CustomTokenProviderService customTokenProviderService; + + + @Value("${spring.security.oauth2.client.provider.kakao.authorization-uri}") + private String KAKAO_SNS_URL; + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String KAKAO_SNS_CLIENT_ID; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String redirect_Uri; + + @Transactional + public String getKakaoAccessToken(String code) { + + String access_Token = ""; + String refresh_Token = ""; + + // Post 요청 라이브러리 + RestTemplate rt = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + // http 바디 오브젝트 생성 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", KAKAO_SNS_CLIENT_ID); + params.add("redirect_uri", redirect_Uri); + params.add("code", code); + + // httpHeader와 httpBody를 하나의 오브젝트에 담기 + HttpEntity> kakaoTokenRequest = + new HttpEntity<>(params, headers); + + // 실제 요청 Http post 방식 그리고 response 변수에 응답 받는다 + ResponseEntity response = rt.exchange( + "https://kauth.kakao.com/oauth/token", + HttpMethod.POST, + kakaoTokenRequest, + String.class + ); + + ObjectMapper objectMapper = new ObjectMapper(); + + OAuthToken oauthToken = null; + + try { + oauthToken = objectMapper.readValue(response.getBody(), OAuthToken.class); + } catch (JsonMappingException e) { + e.printStackTrace(); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + + access_Token = oauthToken.getAccess_token(); + + return access_Token; + + } + + @Transactional + public void accessRequest() throws IOException { + + Map params = new HashMap<>(); +// params.put("scope", "email"); + params.put("response_type", "code"); + params.put("client_id", KAKAO_SNS_CLIENT_ID); + params.put("redirect_uri", redirect_Uri); + + + //parameter를 형식에 맞춰 구성해주는 함수 + String parameterString = params.entrySet().stream() + .map(x -> x.getKey() + "=" + x.getValue()) + .collect(Collectors.joining("&")); + String redirectURL = KAKAO_SNS_URL + "?" + parameterString; + log.info("redirectURL = ", redirectURL); + + response.sendRedirect(redirectURL); + } + + @Transactional + public KakaoProfile getKakaoProfile(String accessToken) { + HttpHeaders headers2 = new HttpHeaders(); + headers2.add("Authorization", "Bearer " + accessToken); + headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity> kakaoProfileRequest = + new HttpEntity<>(headers2); + + ResponseEntity response2 = rt.exchange( + "https://kapi.kakao.com/v2/user/me", + HttpMethod.POST, + kakaoProfileRequest, + String.class + ); + + + KakaoProfile kakaoProfile = null; + + try { + kakaoProfile = objectMapper.readValue(response2.getBody(), KakaoProfile.class); + } catch (Exception e) { + e.printStackTrace(); + } + + + return kakaoProfile; + } + + + @Transactional + public AuthRes kakaoLogin(KakaoProfile kakaoProfile) { + + // 이미 DB에 회원 정보가 저장되어 있으면 로그인 시키고, 없다면 DB에 등록 후 로그인. + + Optional byEmail = userRepository.findByEmail(kakaoProfile.getKakaoAccount().getEmail()); + if (!byEmail.isPresent()) { + User user = User.builder() + .providerId(kakaoProfile.getId()) + .email(kakaoProfile.getKakaoAccount().getEmail()) + .name(kakaoProfile.getKakaoAccount().getProfile().getNickname()) + .role(Role.USER) + .build(); + + User saveUser = userRepository.save(user); + + + } + + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + kakaoProfile.getKakaoAccount().getEmail(), + kakaoProfile.getId() //providerId랑 같다. + ) + ); + + + + TokenMapping tokenMapping = customTokenProviderService.createToken(authentication); + + + Token token = Token.builder() + .refreshToken(tokenMapping.getRefreshToken()) + .email(tokenMapping.getEmail()) + .build(); + + tokenRepository.save(token); + + Token savedToken = tokenRepository.save(token); + + + return AuthRes.builder() + .accessToken(tokenMapping.getAccessToken()) + .refreshToken(token.getRefreshToken()) + .build(); + } + + @Transactional + public Message signOut(final RefreshTokenReq tokenRefreshRequest) { + Token token = tokenRepository.findByRefreshToken(tokenRefreshRequest.getRefreshToken()) + .orElseThrow(() -> new DefaultAuthenticationException(ErrorCode.INVALID_AUTHENTICATION)); + tokenRepository.delete(token); + + return Message.builder() + .message("로그아웃 하였습니다.") + .build(); + } + + + @Transactional + public Message deleteAccount(UserPrincipal userPrincipal) { + Optional user = userRepository.findById(userPrincipal.getId()); + DefaultAssert.isTrue(user.isPresent(), "유저가 올바르지 않습니다."); + + Optional token = tokenRepository.findByEmail(userPrincipal.getEmail()); + DefaultAssert.isTrue(token.isPresent(), "토큰이 유효하지 않습니다."); + + userRepository.delete(user.get()); + tokenRepository.delete(token.get()); + + + return Message.builder() + .message("회원 탈퇴 하였습니다.") + .build(); + } + + @Transactional + public AuthRes adminSignIn(KakaoProfile kakaoProfile) { + Optional byEmail = userRepository.findByEmail(kakaoProfile.getKakaoAccount().getEmail()); + if (!byEmail.isPresent()) { + User user = User.builder() + .providerId(kakaoProfile.getId()) + .email(kakaoProfile.getKakaoAccount().getEmail()) + .name(kakaoProfile.getKakaoAccount().getProfile().getNickname()) + .role(Role.ADMIN) + .build(); + + User saveUser = userRepository.save(user); + + + } + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + kakaoProfile.getKakaoAccount().getEmail(), + kakaoProfile.getId() //providerId랑 같다. + ) + ); + + + + TokenMapping tokenMapping = customTokenProviderService.createToken(authentication); + + + Token token = Token.builder() + .refreshToken(tokenMapping.getRefreshToken()) + .email(tokenMapping.getEmail()) + .build(); + + tokenRepository.save(token); + + Token savedToken = tokenRepository.save(token); + + + return AuthRes.builder() + .accessToken(tokenMapping.getAccessToken()) + .refreshToken(token.getRefreshToken()) + .build(); + + } + + public ResponseEntity refresh(RefreshTokenReq refreshTokenReq) { + //1차 검증 + boolean checkValid = valid(refreshTokenReq.getRefreshToken()); + DefaultAssert.isAuthentication(checkValid); + + Optional token = tokenRepository.findByRefreshToken(refreshTokenReq.getRefreshToken()); + Authentication authentication = customTokenProviderService.getAuthenticationByEmail(token.get().getEmail()); + + //4. refresh token 정보 값을 업데이트 한다. + //시간 유효성 확인 + TokenMapping tokenMapping; + + Long expirationTime = customTokenProviderService.getExpiration(refreshTokenReq.getRefreshToken()); + if(expirationTime > 0){ + tokenMapping = customTokenProviderService.refreshToken(authentication, token.get().getRefreshToken()); + }else{ + tokenMapping = customTokenProviderService.createToken(authentication); + } + + Token updateToken = token.get().updateRefreshToken(tokenMapping.getRefreshToken()); + tokenRepository.save(updateToken); + + AuthRes authResponse = AuthRes.builder().accessToken(tokenMapping.getAccessToken()).refreshToken(updateToken.getRefreshToken()).build(); + + return ResponseEntity.ok(authResponse); + } + + private boolean valid(String refreshToken) { + + // 1. 토큰 형식 물리적 검증 + boolean validateCheck = customTokenProviderService.validateToken(refreshToken); + DefaultAssert.isTrue(validateCheck, "Token 검증에 실패하였습니다."); + + // 2. refresh token 값을 불러온다. + Optional token = tokenRepository.findByRefreshToken(refreshToken); + DefaultAssert.isTrue(token.isPresent(), "탈퇴 처리된 회원입니다."); + + // 3. email 값을 통해 인증값을 불러온다. + Authentication authentication = customTokenProviderService.getAuthenticationByEmail(token.get().getEmail()); + DefaultAssert.isTrue(token.get().getEmail().equals(authentication.getName()), "사용자 인증에 실패했습니다."); + + return true; + } +} diff --git a/src/main/java/com/finfellows/domain/auth/domain/Token.java b/src/main/java/com/finfellows/domain/auth/domain/Token.java new file mode 100644 index 0000000..43ff0f7 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/domain/Token.java @@ -0,0 +1,38 @@ +package com.finfellows.domain.auth.domain; + +import com.finfellows.domain.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "token") +@NoArgsConstructor // 생성자 직접 생성 대신 써줬는데 문제가 된다면 삭제하고 token 생성자 직접 만들어 수정해야할듯 +@Entity +public class Token extends BaseEntity { + + @Id // 이 컬럼이 email로 저장안되고 providerId로 저장되고 있음. 11.25 + @Column(name = "email", nullable = false) + private String email; + + @Column(name = "refresh_token", nullable = false) + private String refreshToken; + + + public Token updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + + @Builder + public Token(String email, String refreshToken) { + this.email = email; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/com/finfellows/domain/auth/domain/repository/TokenRepository.java b/src/main/java/com/finfellows/domain/auth/domain/repository/TokenRepository.java new file mode 100644 index 0000000..a551f4b --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/domain/repository/TokenRepository.java @@ -0,0 +1,14 @@ +package com.finfellows.domain.auth.domain.repository; + +import com.finfellows.domain.auth.domain.Token; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TokenRepository extends JpaRepository { + Optional findByEmail(String email); + + Optional findByRefreshToken(String refreshToken); +} diff --git a/src/main/java/com/finfellows/domain/auth/dto/AuthRes.java b/src/main/java/com/finfellows/domain/auth/dto/AuthRes.java new file mode 100644 index 0000000..30e1df7 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/dto/AuthRes.java @@ -0,0 +1,25 @@ +package com.finfellows.domain.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +public class AuthRes { + + @Schema(type = "string", example = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTI3OTgxOTh9.6CoxHB_siOuz6PxsxHYQCgUT1_QbdyKTUwStQDutEd1-cIIARbQ0cyrnAmpIgi3IBoLRaqK7N1vXO42nYy4g5g", description = "access token 을 출력합니다.") + private String accessToken; + + @Schema( type = "string", example = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTI3OTgxOTh9.asdf8as4df865as4dfasdf65_asdfweioufsdoiuf_432jdsaFEWFSDV_sadf" , description="refresh token 을 출력합니다.") + private String refreshToken; + + @Schema( type = "string", example ="Bearer", description="권한(Authorization) 값 해더의 명칭을 지정합니다.") + private String tokenType = "Bearer"; + + + @Builder + public AuthRes(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/com/finfellows/domain/auth/dto/KakaoProfile.java b/src/main/java/com/finfellows/domain/auth/dto/KakaoProfile.java new file mode 100644 index 0000000..cfa0101 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/dto/KakaoProfile.java @@ -0,0 +1,39 @@ +package com.finfellows.domain.auth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class KakaoProfile { + + private long id; + + @JsonProperty("connected_at") + private String connectedAt; + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + + + @Data + public class KakaoAccount { + @JsonProperty("has_email") + private boolean hasEmail; + @JsonProperty("email_needs_agreement") + private boolean emailNeedsAgreement; + @JsonProperty("is_email_valid") + private boolean isEmailValid; + @JsonProperty("is_email_verified") + private boolean isEmailVerified; + private String email; + private Profile profile; + + @Data + public static class Profile { + private String nickname; + } + } + + +} diff --git a/src/main/java/com/finfellows/domain/auth/dto/OAuthToken.java b/src/main/java/com/finfellows/domain/auth/dto/OAuthToken.java new file mode 100644 index 0000000..435f16b --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/dto/OAuthToken.java @@ -0,0 +1,15 @@ +package com.finfellows.domain.auth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class OAuthToken { + + private String access_token; + private String token_type; + private String refresh_token; + private int expires_in; + private int refresh_token_expires_in; + private String scope; +} diff --git a/src/main/java/com/finfellows/domain/auth/dto/RefreshTokenReq.java b/src/main/java/com/finfellows/domain/auth/dto/RefreshTokenReq.java new file mode 100644 index 0000000..a5cb825 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/dto/RefreshTokenReq.java @@ -0,0 +1,13 @@ +package com.finfellows.domain.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class RefreshTokenReq { + + @Schema(type = "string", example = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTI3OTgxOTh9.6CoxHB_siOuz6PxsxHYQCgUT1_QbdyKTUwStQDutEd1-cIIARbQ0cyrnAmpIgi3IBoLRaqK7N1vXO42nYy4g5g") + @NotBlank + private String refreshToken; +} diff --git a/src/main/java/com/finfellows/domain/auth/dto/TokenMapping.java b/src/main/java/com/finfellows/domain/auth/dto/TokenMapping.java new file mode 100644 index 0000000..ae4c5a9 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/dto/TokenMapping.java @@ -0,0 +1,22 @@ +package com.finfellows.domain.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor // Jackson 역직렬화를 위한 기본 생성자 +public class TokenMapping { + + private String email; + private String accessToken; + private String refreshToken; + + @Builder + public TokenMapping(String email, String accessToken, String refreshToken) { + this.email = email; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/com/finfellows/domain/auth/presentation/AuthController.java b/src/main/java/com/finfellows/domain/auth/presentation/AuthController.java new file mode 100644 index 0000000..c518d84 --- /dev/null +++ b/src/main/java/com/finfellows/domain/auth/presentation/AuthController.java @@ -0,0 +1,119 @@ +package com.finfellows.domain.auth.presentation; + +import com.finfellows.domain.auth.application.KakaoService; +import com.finfellows.domain.auth.dto.AuthRes; +import com.finfellows.domain.auth.dto.KakaoProfile; +import com.finfellows.domain.auth.dto.RefreshTokenReq; +import com.finfellows.domain.auth.dto.TokenMapping; +import com.finfellows.global.config.security.token.CurrentUser; +import com.finfellows.global.config.security.token.UserPrincipal; +import com.finfellows.global.payload.ErrorResponse; +import com.finfellows.global.payload.Message; +import com.finfellows.global.payload.ResponseCustom; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; + +@Tag(name = "Authorization", description = "Authorization API") +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final KakaoService kakaoService; + + @Operation(summary = "카카오 code 발급", description = "카카오 API 서버에 접근 권한을 인가하는 code를 발급받습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "code 발급 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class))}), + @ApiResponse(responseCode = "400", description = "code 발급 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}), + }) + @GetMapping(value = "/login") + public void socialLoginRedirect() throws IOException { + kakaoService.accessRequest(); + } + + @Operation(summary = "카카오 로그인", description = "카카오 로그인을 수행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class))}), + @ApiResponse(responseCode = "400", description = "로그인 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}), + }) + @GetMapping(value = "/kakao/sign-in") + public ResponseCustom kakaoCallback( + @Parameter(description = "code를 입력해주세요.", required = true) @RequestParam("code") String code + ) { + String accessToken = kakaoService.getKakaoAccessToken(code); + KakaoProfile kakaoProfile = kakaoService.getKakaoProfile(accessToken); + + + return ResponseCustom.OK(kakaoService.kakaoLogin(kakaoProfile)); + + } + + + @Operation(summary = "관리자 로그인", description = "관리자 권한으로 로그인을 수행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class))}), + @ApiResponse(responseCode = "400", description = "로그인 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}), + }) + @GetMapping(value = "/admin/sign-in") + public ResponseCustom adminSignIn( + @Parameter(description = "code를 입력해주세요.", required = true) @RequestParam("code") String code + ) { + String accessToken = kakaoService.getKakaoAccessToken(code); + KakaoProfile kakaoProfile = kakaoService.getKakaoProfile(accessToken); + + return ResponseCustom.OK(kakaoService.adminSignIn(kakaoProfile)); + } + + + @Operation(summary = "유저 로그아웃", description = "유저 로그아웃을 수행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))}), + @ApiResponse(responseCode = "400", description = "로그아웃 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}), + }) + @PostMapping(value = "sign-out") + public ResponseCustom signOut( + @Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal, + @Parameter(description = "Schemas의 RefreshTokenRequest를 참고해주세요.") @Valid @RequestBody RefreshTokenReq tokenRefreshRequest + ) { + return ResponseCustom.OK(kakaoService.signOut(tokenRefreshRequest)); + } + + + @Operation(summary = "회원 탈퇴", description = "현재 접속된 유저 계정을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원 탈퇴 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class) ) } ), + @ApiResponse(responseCode = "400", description = "회원 탈퇴 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), + }) + @DeleteMapping + public ResponseCustom deleteAccount( + @Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal + ){ + return ResponseCustom.OK(kakaoService.deleteAccount(userPrincipal)); + } + + @Operation(summary = "토큰 갱신", description = "토큰을 갱신을 수행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 갱신 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class) ) } ), + @ApiResponse(responseCode = "400", description = "토큰 갱신 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), + }) + public ResponseEntity refresh( + @Parameter(description = "Schemas의 RefreshTokenReq를 참고해주세요.", required = true) @Valid @RequestBody RefreshTokenReq refreshTokenReq + ) { + return kakaoService.refresh(refreshTokenReq); + } + + +} diff --git a/src/main/java/com/finfellows/domain/bookmark/domain/Bookmark.java b/src/main/java/com/finfellows/domain/bookmark/domain/Bookmark.java new file mode 100644 index 0000000..70daaa8 --- /dev/null +++ b/src/main/java/com/finfellows/domain/bookmark/domain/Bookmark.java @@ -0,0 +1,47 @@ +package com.finfellows.domain.bookmark.domain; + +import com.finfellows.domain.common.BaseEntity; +import com.finfellows.domain.educontent.domain.EduContent; +import com.finfellows.domain.policyinfo.domain.PolicyInfo; +import com.finfellows.domain.product.domain.FinancialProduct; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +@Entity +@Table(name = "Bookmark") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Where(clause = "status = 'ACTIVE'") +public class Bookmark extends BaseEntity{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + + // 금융 뭐하지 id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "financial_product_id") + private FinancialProduct financialProduct; + + // 금융 배우자 id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "edu_content_id") + private EduContent eduContent; + + // 금융 고마워 id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "policy_info_id") + private PolicyInfo policyInfo; + + @Builder + public Bookmark(FinancialProduct financialProduct, EduContent eduContent, PolicyInfo policyInfo) { + this.financialProduct = financialProduct; + this.eduContent = eduContent; + this.policyInfo = policyInfo; + } +} diff --git a/src/main/java/com/finfellows/domain/educontent/domain/EduContent.java b/src/main/java/com/finfellows/domain/educontent/domain/EduContent.java new file mode 100644 index 0000000..38e3b6d --- /dev/null +++ b/src/main/java/com/finfellows/domain/educontent/domain/EduContent.java @@ -0,0 +1,39 @@ +package com.finfellows.domain.educontent.domain; + +import com.finfellows.domain.common.BaseEntity; +import com.finfellows.domain.post.domain.Post; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +@Entity +@Table(name = "EduContent") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Where(clause = "status = 'ACTIVE'") +public class EduContent extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="post_id") + private Post post_id; + + @Column(name="sequence", nullable = false) + private Long sequence; + + @Column(name="content") + private String content; + + @Builder + public EduContent(Post post_id, Long sequence, String content){ + this.post_id=post_id; + this.sequence=sequence; + this.content=content; + } +} diff --git a/src/main/java/com/finfellows/domain/educontent/domain/repository/EduContentRepository.java b/src/main/java/com/finfellows/domain/educontent/domain/repository/EduContentRepository.java new file mode 100644 index 0000000..cc97b6a --- /dev/null +++ b/src/main/java/com/finfellows/domain/educontent/domain/repository/EduContentRepository.java @@ -0,0 +1,8 @@ +package com.finfellows.domain.educontent.domain.repository; + +import com.finfellows.domain.educontent.domain.EduContent; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface EduContentRepository extends JpaRepository { +} diff --git a/src/main/java/com/finfellows/domain/policyinfo/domain/PolicyInfo.java b/src/main/java/com/finfellows/domain/policyinfo/domain/PolicyInfo.java new file mode 100644 index 0000000..12e87ac --- /dev/null +++ b/src/main/java/com/finfellows/domain/policyinfo/domain/PolicyInfo.java @@ -0,0 +1,30 @@ +package com.finfellows.domain.policyinfo.domain; + +import com.finfellows.domain.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +@Entity +@Table(name = "PolicyInfo") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Where(clause = "status = 'ACTIVE'") +public class PolicyInfo extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + + @Column(name="url", nullable = false, unique = true) + private String url; + + @Builder + public PolicyInfo(String url){ + this.url=url; + } +} diff --git a/src/main/java/com/finfellows/domain/policyinfo/domain/repository/PolicyInfoRepository.java b/src/main/java/com/finfellows/domain/policyinfo/domain/repository/PolicyInfoRepository.java new file mode 100644 index 0000000..e65703b --- /dev/null +++ b/src/main/java/com/finfellows/domain/policyinfo/domain/repository/PolicyInfoRepository.java @@ -0,0 +1,7 @@ +package com.finfellows.domain.policyinfo.domain.repository; + +import com.finfellows.domain.policyinfo.domain.PolicyInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PolicyInfoRepository extends JpaRepository { +} diff --git a/src/main/java/com/finfellows/domain/post/domain/Content.java b/src/main/java/com/finfellows/domain/post/domain/Content.java new file mode 100644 index 0000000..71c171f --- /dev/null +++ b/src/main/java/com/finfellows/domain/post/domain/Content.java @@ -0,0 +1,37 @@ +package com.finfellows.domain.post.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +@Entity +@Table(name="Content") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Where(clause = "status = 'ACTIVE'") +public class Content { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="post_id") + private Post post_id; + + @Column(name="sequence", nullable = false) + private Long sequence; + + @Column(name="content") + private String content; + + @Builder + public Content(Post post_id, Long sequence, String content){ + this.post_id=post_id; + this.sequence=sequence; + this.content=content; + } +} diff --git a/src/main/java/com/finfellows/domain/post/domain/Post.java b/src/main/java/com/finfellows/domain/post/domain/Post.java new file mode 100644 index 0000000..6b9e988 --- /dev/null +++ b/src/main/java/com/finfellows/domain/post/domain/Post.java @@ -0,0 +1,34 @@ +package com.finfellows.domain.post.domain; + +import com.finfellows.domain.common.BaseEntity; +import com.finfellows.domain.user.domain.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +@Entity +@Table(name="Post") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Where(clause = "status = 'ACTIVE'") +public class Post extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="id",updatable = false) + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="writer_id") + private User writer; + + @Builder + public Post(User writer){ + this.writer=writer; + } + +} diff --git a/src/main/java/com/finfellows/domain/post/domain/repository/ContentRepository.java b/src/main/java/com/finfellows/domain/post/domain/repository/ContentRepository.java new file mode 100644 index 0000000..eafd9f6 --- /dev/null +++ b/src/main/java/com/finfellows/domain/post/domain/repository/ContentRepository.java @@ -0,0 +1,7 @@ +package com.finfellows.domain.post.domain.repository; + +import com.finfellows.domain.post.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ContentRepository extends JpaRepository { +} diff --git a/src/main/java/com/finfellows/domain/post/domain/repository/PostRepository.java b/src/main/java/com/finfellows/domain/post/domain/repository/PostRepository.java new file mode 100644 index 0000000..29d4a10 --- /dev/null +++ b/src/main/java/com/finfellows/domain/post/domain/repository/PostRepository.java @@ -0,0 +1,8 @@ +package com.finfellows.domain.post.domain.repository; + +import com.finfellows.domain.post.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + +} diff --git a/src/main/java/com/finfellows/domain/product/domain/FinancialProduct.java b/src/main/java/com/finfellows/domain/product/domain/FinancialProduct.java index 1a93d1f..9ca1727 100644 --- a/src/main/java/com/finfellows/domain/product/domain/FinancialProduct.java +++ b/src/main/java/com/finfellows/domain/product/domain/FinancialProduct.java @@ -1,5 +1,6 @@ package com.finfellows.domain.product.domain; +import com.finfellows.domain.common.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -15,7 +16,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Where(clause = "status = 'ACTIVE'") -public class FinancialProduct { +public class FinancialProduct extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/finfellows/domain/user/application/UserService.java b/src/main/java/com/finfellows/domain/user/application/UserService.java new file mode 100644 index 0000000..2d6074d --- /dev/null +++ b/src/main/java/com/finfellows/domain/user/application/UserService.java @@ -0,0 +1,22 @@ +package com.finfellows.domain.user.application; + +import com.finfellows.domain.auth.domain.Token; +import com.finfellows.domain.auth.domain.repository.TokenRepository; +import com.finfellows.domain.common.Status; +import com.finfellows.domain.user.domain.User; +import com.finfellows.domain.user.domain.repository.UserRepository; +import com.finfellows.global.error.DefaultException; +import com.finfellows.global.payload.ApiResponse; +import com.finfellows.global.payload.ErrorCode; +import com.finfellows.global.payload.Message; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class UserService { + + +} diff --git a/src/main/java/com/finfellows/domain/user/domain/Role.java b/src/main/java/com/finfellows/domain/user/domain/Role.java new file mode 100644 index 0000000..e00502b --- /dev/null +++ b/src/main/java/com/finfellows/domain/user/domain/Role.java @@ -0,0 +1,10 @@ +package com.finfellows.domain.user.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Role { + ADMIN, USER; +} diff --git a/src/main/java/com/finfellows/domain/user/domain/User.java b/src/main/java/com/finfellows/domain/user/domain/User.java index 3b20576..5cfe219 100644 --- a/src/main/java/com/finfellows/domain/user/domain/User.java +++ b/src/main/java/com/finfellows/domain/user/domain/User.java @@ -9,7 +9,6 @@ @Table(name = "User") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -@Where(clause = "status = 'ACTIVE'") public class User extends BaseEntity { @Id @@ -23,14 +22,20 @@ public class User extends BaseEntity { @Column(name = "name") private String name; - @Column(name = "provider_id", nullable = false, unique = true, updatable = false) - private String providerId; + @Column(name = "provider_id", unique = true, updatable = false) + private Long providerId; + + @Enumerated(EnumType.STRING) + @Column(name = "role", nullable = false) + private Role role; + + @Builder - public User(String email, String name, String providerId) { + public User(String email, String name, Long providerId, Role role) { this.email = email; this.name = name; this.providerId = providerId; + this.role = role; } - } diff --git a/src/main/java/com/finfellows/domain/user/domain/repository/UserRepository.java b/src/main/java/com/finfellows/domain/user/domain/repository/UserRepository.java index 0b73800..e77dbf4 100644 --- a/src/main/java/com/finfellows/domain/user/domain/repository/UserRepository.java +++ b/src/main/java/com/finfellows/domain/user/domain/repository/UserRepository.java @@ -2,6 +2,14 @@ import com.finfellows.domain.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + Boolean existsByEmail(String email); + + Optional findByProviderId(Long providerId); } diff --git a/src/main/java/com/finfellows/domain/user/presentation/UserController.java b/src/main/java/com/finfellows/domain/user/presentation/UserController.java new file mode 100644 index 0000000..c9e2ffc --- /dev/null +++ b/src/main/java/com/finfellows/domain/user/presentation/UserController.java @@ -0,0 +1,26 @@ +package com.finfellows.domain.user.presentation; + +import com.finfellows.domain.user.application.UserService; +import com.finfellows.domain.user.domain.User; +import com.finfellows.global.payload.Message; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/users") +public class UserController { + + +} diff --git a/src/main/java/com/finfellows/global/DefaultAssert.java b/src/main/java/com/finfellows/global/DefaultAssert.java new file mode 100644 index 0000000..e7fa2d8 --- /dev/null +++ b/src/main/java/com/finfellows/global/DefaultAssert.java @@ -0,0 +1,32 @@ +package com.finfellows.global; + +import com.finfellows.domain.user.domain.User; +import com.finfellows.global.error.DefaultAuthenticationException; +import com.finfellows.global.error.DefaultException; +import com.finfellows.global.payload.ErrorCode; +import org.springframework.util.Assert; + +import java.util.Optional; + +public class DefaultAssert extends Assert { + + public static void isTrue(boolean value) { + if (!value) { + throw new DefaultException(ErrorCode.INVALID_CHECK); + } + } + + + public static void isAuthentication(String message) { + throw new DefaultAuthenticationException(message); + } + + public static void isAuthentication(boolean value) { + if (!value) { + throw new DefaultAuthenticationException(ErrorCode.INVALID_AUTHENTICATION); + } + } + + public static void isOptionalPresent(Optional user) { + } +} diff --git a/src/main/java/com/finfellows/global/config/RestTemplateConfig.java b/src/main/java/com/finfellows/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..ba98ce9 --- /dev/null +++ b/src/main/java/com/finfellows/global/config/RestTemplateConfig.java @@ -0,0 +1,23 @@ +package com.finfellows.global.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.Charset; + +@Configuration +public class RestTemplateConfig { + //HTTP get,post 요청을 날릴때 일정한 형식에 맞춰주는 template + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder + .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())) + .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8"))) + .build(); + } +} diff --git a/src/main/java/com/finfellows/global/config/security/CustomAuthenticationProvider.java b/src/main/java/com/finfellows/global/config/security/CustomAuthenticationProvider.java new file mode 100644 index 0000000..2a026a3 --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/CustomAuthenticationProvider.java @@ -0,0 +1,36 @@ +package com.finfellows.global.config.security; + +import com.finfellows.domain.user.domain.repository.UserRepository; +import com.finfellows.global.config.security.token.UserPrincipal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class CustomAuthenticationProvider implements AuthenticationProvider { + + @Autowired + private UserRepository userRepository; + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String providerId = authentication.getCredentials().toString(); + Long providerIdToInteger = Long.parseLong(providerId); + + UserDetails user = userRepository.findByProviderId(providerIdToInteger) + .map(UserPrincipal::createUser) + .orElseThrow(() -> new UsernameNotFoundException("Provider ID not found: " + providerId)); + + return new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/finfellows/global/config/security/OAuth2Config.java b/src/main/java/com/finfellows/global/config/security/OAuth2Config.java new file mode 100644 index 0000000..b0e59ee --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/OAuth2Config.java @@ -0,0 +1,22 @@ +package com.finfellows.global.config.security; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app") +public class OAuth2Config { + private final Auth auth = new Auth(); + + @Data + public static class Auth { + private String tokenSecret; + private long accessTokenExpirationMsec; + private long refreshTokenExpirationMsec; + } + + public Auth getAuth() { + return auth; + } +} diff --git a/src/main/java/com/finfellows/global/config/security/SecurityConfig.java b/src/main/java/com/finfellows/global/config/security/SecurityConfig.java new file mode 100644 index 0000000..b12190e --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/SecurityConfig.java @@ -0,0 +1,71 @@ +package com.finfellows.global.config.security; + +import com.finfellows.global.config.security.token.CustomAuthenticationEntryPoint; +import com.finfellows.global.config.security.token.CustomOncePerRequestFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +import static org.springframework.security.config.Customizer.*; + + + +@RequiredArgsConstructor +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Autowired + private CustomAuthenticationProvider customAuthenticationProvider; + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(customAuthenticationProvider); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public CustomOncePerRequestFilter customOncePerRequestFilter() { + return new CustomOncePerRequestFilter(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(withDefaults()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> exception.authenticationEntryPoint(new CustomAuthenticationEntryPoint())) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/", "/error", "/favicon.ico", "/**/*.png", "/**/*.gif", "/**/*.svg", "/**/*.jpg", "/**/*.html", "/**/*.css", "/**/*.js") + .permitAll() + .requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**") + .permitAll() + .requestMatchers("/login/**", "/auth/**", "/oauth2/**") + .permitAll() + .requestMatchers("/blog/**") + .permitAll() + .anyRequest() + .authenticated()); + + http.addFilterBefore(customOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/finfellows/global/config/security/token/CurrentUser.java b/src/main/java/com/finfellows/global/config/security/token/CurrentUser.java new file mode 100644 index 0000000..e33bb7f --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/token/CurrentUser.java @@ -0,0 +1,12 @@ +package com.finfellows.global.config.security.token; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@AuthenticationPrincipal +public @interface CurrentUser { +} diff --git a/src/main/java/com/finfellows/global/config/security/token/CustomAuthenticationEntryPoint.java b/src/main/java/com/finfellows/global/config/security/token/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..c06e0b7 --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/token/CustomAuthenticationEntryPoint.java @@ -0,0 +1,17 @@ +package com.finfellows.global.config.security.token; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getLocalizedMessage()); + } +} diff --git a/src/main/java/com/finfellows/global/config/security/token/CustomOncePerRequestFilter.java b/src/main/java/com/finfellows/global/config/security/token/CustomOncePerRequestFilter.java new file mode 100644 index 0000000..b009a89 --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/token/CustomOncePerRequestFilter.java @@ -0,0 +1,45 @@ +package com.finfellows.global.config.security.token; + +import com.finfellows.domain.auth.application.CustomTokenProviderService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +public class CustomOncePerRequestFilter extends OncePerRequestFilter { + + @Autowired + private CustomTokenProviderService customTokenProviderService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String jwt = getJwtFromRequest(request); + + if (StringUtils.hasText(jwt) && customTokenProviderService.validateToken(jwt)) { + UsernamePasswordAuthenticationToken authentication = customTokenProviderService.getAuthenticationById(jwt); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + log.info("bearerToken = {}", bearerToken.substring(7, bearerToken.length())); + return bearerToken.substring(7, bearerToken.length()); + } + return null; + } +} diff --git a/src/main/java/com/finfellows/global/config/security/token/UserPrincipal.java b/src/main/java/com/finfellows/global/config/security/token/UserPrincipal.java new file mode 100644 index 0000000..7c132a1 --- /dev/null +++ b/src/main/java/com/finfellows/global/config/security/token/UserPrincipal.java @@ -0,0 +1,87 @@ +package com.finfellows.global.config.security.token; + +import com.finfellows.domain.user.domain.User; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Map; + +@Getter +public class UserPrincipal implements OAuth2User, UserDetails { + + private final User user; + + private final Long id; + private final String email; + private final String name; + private Map attributes; + + public UserPrincipal(User user, Long id, String email, String name) { + this.user = user; + this.id = id; + this.email = email; + this.name = name; + } + + public static UserPrincipal createUser(User user) { + return new UserPrincipal( + user, + user.getId(), + user.getEmail(), + user.getName() + ); + } + + @Override + public A getAttribute(String name) { + return OAuth2User.super.getAttribute(name); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getName() { + return name; + } +} diff --git a/src/main/java/com/finfellows/global/error/ApiControllerAdvice.java b/src/main/java/com/finfellows/global/error/ApiControllerAdvice.java new file mode 100644 index 0000000..34fb68a --- /dev/null +++ b/src/main/java/com/finfellows/global/error/ApiControllerAdvice.java @@ -0,0 +1,143 @@ +package com.finfellows.global.error; + +import com.finfellows.global.payload.ApiResponse; +import com.finfellows.global.payload.ErrorCode; +import com.finfellows.global.payload.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice(annotations = RestController.class) +public class ApiControllerAdvice { + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ResponseEntity handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException e) { + + final ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.METHOD_NOT_ALLOWED.value()) + .code(e.getMessage()) + .clazz(e.getMethod()) + .message(e.getMessage()) + .build(); + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.METHOD_NOT_ALLOWED); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + + ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.METHOD_NOT_ALLOWED.value()) + .code(e.getMessage()) + .clazz(e.getBindingResult().getObjectName()) + .message(e.toString()) + .fieldErrors(e.getFieldErrors()) + .build(); + + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.OK); + } + + @ExceptionHandler(InvalidParameterException.class) + public ResponseEntity handleInvalidParameterException(InvalidParameterException e) { + ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.METHOD_NOT_ALLOWED.value()) + .code(e.getMessage()) + .clazz(e.getErrors().getObjectName()) + .message(e.toString()) + .fieldErrors(e.getFieldErrors()) + .build(); + + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.OK); + } + + @ExceptionHandler(DefaultException.class) + protected ResponseEntity handleDefaultException(DefaultException e) { + + ErrorCode errorCode = e.getErrorCode(); + + ErrorResponse response = ErrorResponse + .builder() + .status(errorCode.getStatus()) + .code(errorCode.getCode()) + .message(e.toString()) + .build(); + + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.resolve(errorCode.getStatus())); + } + + @ExceptionHandler(RuntimeException.class) + protected ResponseEntity handleDefaultException(RuntimeException e) { + + ErrorCode errorCode = ErrorCode.INVALID_AUTHENTICATION; + + ErrorResponse response = ErrorResponse + .builder() + .status(errorCode.getStatus()) + .code(errorCode.getCode()) + .message(e.toString()) + .build(); + + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.resolve(errorCode.getStatus())); + } + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(Exception e) { + + ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .message(e.toString()) + .build(); + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(AuthenticationException.class) + protected ResponseEntity handleAuthenticationException(AuthenticationException e) { + + ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value()) + .message(e.getMessage()) + .build(); + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(DefaultAuthenticationException.class) + protected ResponseEntity handleCustomAuthenticationException(DefaultAuthenticationException e) { + ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value()) + .message(e.getMessage()) + .build(); + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + + @ExceptionHandler(DefaultNullPointerException.class) + protected ResponseEntity handleNullPointerException(DefaultNullPointerException e) { + ErrorResponse response = ErrorResponse + .builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .message(e.getMessage()) + .build(); + + ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); + return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/com/finfellows/global/error/DefaultAuthenticationException.java b/src/main/java/com/finfellows/global/error/DefaultAuthenticationException.java new file mode 100644 index 0000000..ac6567a --- /dev/null +++ b/src/main/java/com/finfellows/global/error/DefaultAuthenticationException.java @@ -0,0 +1,27 @@ +package com.finfellows.global.error; + +import com.finfellows.global.payload.ErrorCode; +import lombok.Getter; +import org.springframework.security.core.AuthenticationException; + + +@Getter +public class DefaultAuthenticationException extends AuthenticationException{ + + private ErrorCode errorCode; + + public DefaultAuthenticationException(String msg, Throwable t) { + super(msg, t); + this.errorCode = ErrorCode.INVALID_REPRESENTATION; + } + + public DefaultAuthenticationException(String msg) { + super(msg); + } + + public DefaultAuthenticationException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + +} diff --git a/src/main/java/com/finfellows/global/error/DefaultException.java b/src/main/java/com/finfellows/global/error/DefaultException.java new file mode 100644 index 0000000..cb9c849 --- /dev/null +++ b/src/main/java/com/finfellows/global/error/DefaultException.java @@ -0,0 +1,22 @@ +package com.finfellows.global.error; + + +import com.finfellows.global.payload.ErrorCode; +import lombok.Getter; + +@Getter +public class DefaultException extends RuntimeException{ + + private ErrorCode errorCode; + + public DefaultException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public DefaultException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + +} diff --git a/src/main/java/com/finfellows/global/error/DefaultNullPointerException.java b/src/main/java/com/finfellows/global/error/DefaultNullPointerException.java new file mode 100644 index 0000000..556bb63 --- /dev/null +++ b/src/main/java/com/finfellows/global/error/DefaultNullPointerException.java @@ -0,0 +1,15 @@ +package com.finfellows.global.error; + +import com.finfellows.global.payload.ErrorCode; +import lombok.Getter; + +@Getter +public class DefaultNullPointerException extends NullPointerException{ + + private ErrorCode errorCode; + + public DefaultNullPointerException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/finfellows/global/error/InvalidParameterException.java b/src/main/java/com/finfellows/global/error/InvalidParameterException.java new file mode 100644 index 0000000..480bbcf --- /dev/null +++ b/src/main/java/com/finfellows/global/error/InvalidParameterException.java @@ -0,0 +1,24 @@ +package com.finfellows.global.error; + +import com.finfellows.global.payload.ErrorCode; +import lombok.Getter; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; + +import java.util.List; + +@Getter +public class InvalidParameterException extends DefaultException{ + + private final Errors errors; + + public InvalidParameterException(Errors errors) { + super(ErrorCode.INVALID_PARAMETER); + this.errors = errors; + } + + public List getFieldErrors() { + return errors.getFieldErrors(); + } + +} diff --git a/src/main/java/com/finfellows/global/payload/ApiResponse.java b/src/main/java/com/finfellows/global/payload/ApiResponse.java new file mode 100644 index 0000000..861b90d --- /dev/null +++ b/src/main/java/com/finfellows/global/payload/ApiResponse.java @@ -0,0 +1,24 @@ +package com.finfellows.global.payload; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +@ToString +@Data +public class ApiResponse { + + @Schema( type = "boolean", example = "true", description="올바르게 로직을 처리했으면 True, 아니면 False를 반환합니다.") + private boolean check; + + @Schema( type = "object", example = "information", description="restful의 정보를 감싸 표현합니다. object형식으로 표현합니다.") + private Object information; + + @Builder + public ApiResponse(boolean check, Object information) { + this.check = check; + this.information = information; + } + +} diff --git a/src/main/java/com/finfellows/global/payload/ErrorCode.java b/src/main/java/com/finfellows/global/payload/ErrorCode.java new file mode 100644 index 0000000..837333e --- /dev/null +++ b/src/main/java/com/finfellows/global/payload/ErrorCode.java @@ -0,0 +1,25 @@ +package com.finfellows.global.payload; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + + INVALID_PARAMETER(400, null, "잘못된 요청 데이터 입니다."), + INVALID_REPRESENTATION(400, null, "잘못된 표현 입니다."), + INVALID_FILE_PATH(400, null, "잘못된 파일 경로 입니다."), + INVALID_OPTIONAL_ISPRESENT(400, null, "해당 값이 존재하지 않습니다."), + INVALID_CHECK(400, null, "해당 값이 유효하지 않습니다."), + INVALID_AUTHENTICATION(400, null, "잘못된 인증입니다."); + + private final String code; + private final String message; + private final int status; + + ErrorCode(final int status, final String code, final String message) { + this.status = status; + this.message = message; + this.code = code; + } + +} diff --git a/src/main/java/com/finfellows/global/payload/ErrorResponse.java b/src/main/java/com/finfellows/global/payload/ErrorResponse.java new file mode 100644 index 0000000..4965ebb --- /dev/null +++ b/src/main/java/com/finfellows/global/payload/ErrorResponse.java @@ -0,0 +1,79 @@ +package com.finfellows.global.payload; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import org.springframework.validation.FieldError; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +public class ErrorResponse { + + private LocalDateTime timestamp = LocalDateTime.now(); + + private String message; + + private String code; + + @JsonProperty("class") + private String clazz; + + private int status; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonProperty("errors") + private List customFieldErrors = new ArrayList<>(); + + public ErrorResponse() {} + + @Builder + public ErrorResponse(String code, int status, String message, String clazz, List fieldErrors){ + this.code = code; + this.status = status; + this.message = message; + this.clazz = clazz; + //setFieldErrors(fieldErrors); + } + + public void setFieldErrors(List fieldErrors) { + if(fieldErrors != null){ + fieldErrors.forEach(error -> { + customFieldErrors.add(new CustomFieldError( + error.getField(), + error.getRejectedValue(), + error.getDefaultMessage() + )); + }); + } + } + + public static class CustomFieldError { + + private String field; + private Object value; + private String reason; + + public CustomFieldError(String field, Object value, String reason) { + this.field = field; + this.value = value; + this.reason = reason; + } + + public String getField() { + return field; + } + + public Object getValue() { + return value; + } + + public String getReason() { + return reason; + } + } + +} diff --git a/src/main/java/com/finfellows/global/payload/Message.java b/src/main/java/com/finfellows/global/payload/Message.java new file mode 100644 index 0000000..9d56bb9 --- /dev/null +++ b/src/main/java/com/finfellows/global/payload/Message.java @@ -0,0 +1,22 @@ +package com.finfellows.global.payload; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +@ToString +@Data +public class Message { + + @Schema( type = "string", example = "메시지 문구를 출력합니다.", description="메시지 입니다.") + private String message; + + public Message(){}; + + @Builder + public Message(String message) { + this.message = message; + } + +} diff --git a/src/main/java/com/finfellows/global/payload/ResponseCustom.java b/src/main/java/com/finfellows/global/payload/ResponseCustom.java new file mode 100644 index 0000000..d28e51e --- /dev/null +++ b/src/main/java/com/finfellows/global/payload/ResponseCustom.java @@ -0,0 +1,143 @@ +package com.finfellows.global.payload; + + +import lombok.*; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; + +import java.time.LocalDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +public class ResponseCustom{ + + private T data; + private LocalDateTime transaction_time; + private HttpStatus status; + private String description; + private int statusCode; + + @Builder + public ResponseCustom(T data, LocalDateTime transaction_time, HttpStatus status, String description, int statusCode) { + this.data = data; + this.transaction_time = transaction_time; + this.status = status; + this.description = description; + this.statusCode = statusCode; + } + + // OK + public static ResponseCustom CREATED(@Nullable T data) { + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.CREATED) + .statusCode(HttpStatus.OK.value()) + .data(data) + .build(); + } + + public static ResponseCustom OK(@Nullable T data) { + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.OK) + .statusCode(HttpStatus.OK.value()) + .data(data) + .build(); + } + + public static ResponseCustom OK() { + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.OK) + .statusCode(HttpStatus.OK.value()) + .build(); + } + + public static ResponseCustom BAD_REQUEST(@Nullable String description){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.BAD_REQUEST) + .statusCode(HttpStatus.BAD_REQUEST.value()) + .description(description) + .build(); + } + + public static ResponseCustom BAD_REQUEST(@Nullable T data){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.BAD_REQUEST) + .statusCode(HttpStatus.BAD_REQUEST.value()) + .data(data) + .build(); + } + + public static ResponseCustom NOT_FOUND(@Nullable T data){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.NOT_FOUND) + .statusCode(HttpStatus.NOT_FOUND.value()) + .data(data) + .build(); + } + + public static ResponseCustom NOT_FOUND(@Nullable String description){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.NOT_FOUND) + .statusCode(HttpStatus.NOT_FOUND.value()) + .description(description) + .build(); + } + + public static ResponseCustom FORBIDDEN(){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.FORBIDDEN) + .statusCode(HttpStatus.FORBIDDEN.value()) + .build(); + } + + public static ResponseCustom FORBIDDEN(String description){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.FORBIDDEN) + .statusCode(HttpStatus.FORBIDDEN.value()) + .description(description) + .build(); + } + + public static ResponseCustom UNAUTHORIZED(){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.UNAUTHORIZED) + .statusCode(HttpStatus.UNAUTHORIZED.value()) + .build(); + } + + public static ResponseCustom INTERNAL_SERVER_ERROR(){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .build(); + } + + public static ResponseCustom INTERNAL_SERVER_ERROR(String description){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .description(description) + .build(); + } + + public static ResponseCustom JWT_EXPIRED(){ + return (ResponseCustom) ResponseCustom.builder() + .transaction_time(LocalDateTime.now()) + .description("JWT_EXPIRED") + .statusCode(441) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -