diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index b16d9db..1b4b8f5 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -27,6 +27,10 @@ services: DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} REDIS_HOST: ${REDIS_HOST} REDIS_PORT: ${REDIS_PORT} + APPLE_PRIVATE_KEY: ${APPLE_PRIVATE_KEY} + APPLE_TEAM_ID: ${APPLE_TEAM_ID} + APPLE_KEY_ID: ${APPLE_KEY_ID} + APPLE_CLIENT_ID: ${APPLE_CLIENT_ID} redis: container_name: moneymong-redis diff --git a/build.gradle b/build.gradle index 0ad9eca..e22ac7e 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.bouncycastle:bcprov-jdk18on:1.75' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.75' + implementation "com.auth0:java-jwt:4.4.0" // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' diff --git a/src/main/java/com/moneymong/domain/agency/api/request/CreateAgencyRequest.java b/src/main/java/com/moneymong/domain/agency/api/request/CreateAgencyRequest.java index 11a9383..15e6f22 100644 --- a/src/main/java/com/moneymong/domain/agency/api/request/CreateAgencyRequest.java +++ b/src/main/java/com/moneymong/domain/agency/api/request/CreateAgencyRequest.java @@ -10,6 +10,5 @@ @AllArgsConstructor public class CreateAgencyRequest { private String name; - private String description; private AgencyType agencyType; } diff --git a/src/main/java/com/moneymong/domain/agency/api/response/AgencyResponse.java b/src/main/java/com/moneymong/domain/agency/api/response/AgencyResponse.java index 1dcc25c..fcd9f36 100644 --- a/src/main/java/com/moneymong/domain/agency/api/response/AgencyResponse.java +++ b/src/main/java/com/moneymong/domain/agency/api/response/AgencyResponse.java @@ -6,6 +6,8 @@ import lombok.Builder; import lombok.Getter; +import java.time.ZonedDateTime; + @Getter @Builder @AllArgsConstructor @@ -14,6 +16,7 @@ public class AgencyResponse { private String name; private int headCount; private AgencyType type; + private ZonedDateTime createdAt; public static AgencyResponse from(Agency agency) { return AgencyResponse.builder() @@ -21,6 +24,7 @@ public static AgencyResponse from(Agency agency) { .name(agency.getAgencyName()) .headCount(agency.getHeadCount()) .type(agency.getAgencyType()) + .createdAt(agency.getCreatedAt()) .build(); } } diff --git a/src/main/java/com/moneymong/domain/agency/entity/Agency.java b/src/main/java/com/moneymong/domain/agency/entity/Agency.java index f6b6a06..7b91201 100644 --- a/src/main/java/com/moneymong/domain/agency/entity/Agency.java +++ b/src/main/java/com/moneymong/domain/agency/entity/Agency.java @@ -52,26 +52,22 @@ public class Agency extends BaseEntity { ) private Integer headCount; - private String description; - @Column(name = "university_name") private String universityName; @Builder - private Agency(Long id, String agencyName, AgencyType agencyType, Integer headCount, String description, String universityName) { + private Agency(Long id, String agencyName, AgencyType agencyType, Integer headCount, String universityName) { this.id = id; this.agencyName = agencyName; this.agencyType = agencyType; this.headCount = headCount; - this.description = description; this.universityName = universityName; } - public static Agency of(String agencyName, AgencyType agencyType, String description, int headCount, String universityName) { + public static Agency of(String agencyName, AgencyType agencyType, int headCount, String universityName) { return Agency.builder() .agencyName(agencyName) .agencyType(agencyType) - .description(description) .headCount(headCount) .universityName(universityName) .build(); diff --git a/src/main/java/com/moneymong/domain/agency/entity/enums/AgencyType.java b/src/main/java/com/moneymong/domain/agency/entity/enums/AgencyType.java index 2264084..f2e05ef 100644 --- a/src/main/java/com/moneymong/domain/agency/entity/enums/AgencyType.java +++ b/src/main/java/com/moneymong/domain/agency/entity/enums/AgencyType.java @@ -2,6 +2,5 @@ public enum AgencyType { STUDENT_COUNCIL, - IN_SCHOOL_CLUB, - OUT_OF_SCHOOL_CLUB + IN_SCHOOL_CLUB } diff --git a/src/main/java/com/moneymong/domain/agency/service/AgencyService.java b/src/main/java/com/moneymong/domain/agency/service/AgencyService.java index e6f34a2..aa476a8 100644 --- a/src/main/java/com/moneymong/domain/agency/service/AgencyService.java +++ b/src/main/java/com/moneymong/domain/agency/service/AgencyService.java @@ -33,6 +33,7 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class AgencyService { private static final int INITIAL_HEAD_COUNT = 1; @@ -65,7 +66,6 @@ public CreateAgencyResponse create(Long userId, CreateAgencyRequest request) { Agency agency = Agency.of( request.getName(), request.getAgencyType(), - request.getDescription(), INITIAL_HEAD_COUNT, universityName ); diff --git a/src/main/java/com/moneymong/domain/invitationcode/api/InviteCodeController.java b/src/main/java/com/moneymong/domain/invitationcode/api/InviteCodeController.java index dcb6372..ec444d9 100644 --- a/src/main/java/com/moneymong/domain/invitationcode/api/InviteCodeController.java +++ b/src/main/java/com/moneymong/domain/invitationcode/api/InviteCodeController.java @@ -6,11 +6,13 @@ import com.moneymong.domain.invitationcode.service.InvitationCodeService; import com.moneymong.global.security.token.dto.jwt.JwtAuthentication; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +@Tag(name = "4. [초대코드]") @RequestMapping("/api/v1/agencies/{agencyId}/invitation-code") @RequiredArgsConstructor @RestController diff --git a/src/main/java/com/moneymong/domain/user/api/request/LoginRequest.java b/src/main/java/com/moneymong/domain/user/api/request/LoginRequest.java index 4730601..6ea1fbd 100644 --- a/src/main/java/com/moneymong/domain/user/api/request/LoginRequest.java +++ b/src/main/java/com/moneymong/domain/user/api/request/LoginRequest.java @@ -8,6 +8,9 @@ public class LoginRequest { @NotBlank private String provider; - @NotBlank private String accessToken; + + private String name; + + private String code; } diff --git a/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java b/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java new file mode 100644 index 0000000..2d71605 --- /dev/null +++ b/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java @@ -0,0 +1,13 @@ +package com.moneymong.domain.user.api.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class UserDeleteRequest { + @NotBlank + private String provider; + + @NotBlank + private String token; +} diff --git a/src/main/java/com/moneymong/domain/user/api/response/UserProfileResponse.java b/src/main/java/com/moneymong/domain/user/api/response/UserProfileResponse.java index bcadf5f..dee85d9 100644 --- a/src/main/java/com/moneymong/domain/user/api/response/UserProfileResponse.java +++ b/src/main/java/com/moneymong/domain/user/api/response/UserProfileResponse.java @@ -11,7 +11,6 @@ @AllArgsConstructor public class UserProfileResponse { private Long id; - private String userToken; private String provider; private String nickname; private String email; @@ -21,7 +20,6 @@ public class UserProfileResponse { public static UserProfileResponse from(User user, UserUniversity userUniversity) { return UserProfileResponse.builder() .id(user.getId()) - .userToken(user.getUserToken()) .provider(user.getProvider()) .nickname(user.getNickname()) .email(user.getEmail()) diff --git a/src/main/java/com/moneymong/domain/user/entity/AppleUser.java b/src/main/java/com/moneymong/domain/user/entity/AppleUser.java new file mode 100644 index 0000000..8e30ad0 --- /dev/null +++ b/src/main/java/com/moneymong/domain/user/entity/AppleUser.java @@ -0,0 +1,37 @@ +package com.moneymong.domain.user.entity; + +import com.moneymong.global.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import static lombok.AccessLevel.PROTECTED; + +@Table(name = "apple_users") +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Where(clause = "deleted = false") +@SQLDelete(sql = "UPDATE users SET deleted = true where id=?") +public class AppleUser extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + + private String appleRefreshToken; + + public static AppleUser of(Long userId, String appleRefreshToken) { + return AppleUser.builder() + .userId(userId) + .appleRefreshToken(appleRefreshToken) + .build(); + } +} diff --git a/src/main/java/com/moneymong/domain/user/entity/User.java b/src/main/java/com/moneymong/domain/user/entity/User.java index 876dd6e..c7b907d 100644 --- a/src/main/java/com/moneymong/domain/user/entity/User.java +++ b/src/main/java/com/moneymong/domain/user/entity/User.java @@ -32,13 +32,6 @@ public class User extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column( - name = "user_token", - unique = true, - nullable = false - ) - private String userToken; - @Column(nullable = false) private String email; @@ -56,9 +49,8 @@ public class User extends BaseEntity { private LocalDate birthDay; - public static User of(String userToken, String email, String nickname, String provider, String oauthId) { + public static User of(String email, String nickname, String provider, String oauthId) { return User.builder() - .userToken(userToken) .email(email) .nickname(nickname) .provider(provider) diff --git a/src/main/java/com/moneymong/domain/user/entity/UserUniversity.java b/src/main/java/com/moneymong/domain/user/entity/UserUniversity.java index cbfcf02..83153ff 100644 --- a/src/main/java/com/moneymong/domain/user/entity/UserUniversity.java +++ b/src/main/java/com/moneymong/domain/user/entity/UserUniversity.java @@ -11,8 +11,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.util.Assert; -import static com.moneymong.utils.TextValidator.checkText; import static lombok.AccessLevel.PROTECTED; @Table(name = "user_universities") @@ -45,14 +45,14 @@ public class UserUniversity extends TimeBaseEntity { private int grade; public void update(String universityName, int grade) { - checkText(universityName, "대학 이름은 필수 입력값입니다."); + Assert.hasText(universityName, "대학 이름은 필수 입력값입니다."); this.universityName = universityName; this.grade = grade; } public static UserUniversity of(Long userId, String universityName, int grade) { - checkText(universityName, "대학 이름은 필수 입력값입니다."); + Assert.hasText(universityName, "대학 이름은 필수 입력값입니다."); return UserUniversity.builder() .userId(userId) diff --git a/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java b/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java new file mode 100644 index 0000000..4de8c0b --- /dev/null +++ b/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java @@ -0,0 +1,7 @@ +package com.moneymong.domain.user.repository; + +import com.moneymong.domain.user.entity.AppleUser; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AppleUserRepository extends JpaRepository { +} diff --git a/src/main/java/com/moneymong/domain/user/repository/UserRepository.java b/src/main/java/com/moneymong/domain/user/repository/UserRepository.java index bf200af..4ce9028 100644 --- a/src/main/java/com/moneymong/domain/user/repository/UserRepository.java +++ b/src/main/java/com/moneymong/domain/user/repository/UserRepository.java @@ -6,5 +6,4 @@ import java.util.Optional; public interface UserRepository extends JpaRepository, UserRepositoryCustom { - Optional findByUserToken(String userToken); } diff --git a/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java b/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java index 0a165b8..c694daa 100644 --- a/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java +++ b/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java @@ -2,6 +2,7 @@ import com.moneymong.domain.agency.service.AgencyUserService; import com.moneymong.domain.user.api.request.LoginRequest; +import com.moneymong.domain.user.api.request.UserDeleteRequest; import com.moneymong.global.security.oauth.dto.AuthUserInfo; import com.moneymong.domain.user.api.response.LoginSuccessResponse; import com.moneymong.global.security.oauth.dto.OAuthUserDataResponse; @@ -43,4 +44,10 @@ public void delete(Long userId) { userUniversityService.delete(userId); agencyUserService.deleteAll(userId); } + + @Transactional + public void revoke(UserDeleteRequest deleteRequest, Long userId) { + oAuthService.revoke(deleteRequest); + delete(userId); + } } diff --git a/src/main/java/com/moneymong/domain/user/service/UserService.java b/src/main/java/com/moneymong/domain/user/service/UserService.java index ec784f3..24baada 100644 --- a/src/main/java/com/moneymong/domain/user/service/UserService.java +++ b/src/main/java/com/moneymong/domain/user/service/UserService.java @@ -1,8 +1,10 @@ package com.moneymong.domain.user.service; import com.moneymong.domain.user.api.response.UserProfileResponse; +import com.moneymong.domain.user.entity.AppleUser; import com.moneymong.domain.user.entity.User; import com.moneymong.domain.user.entity.UserUniversity; +import com.moneymong.domain.user.repository.AppleUserRepository; import com.moneymong.domain.user.repository.UserRepository; import com.moneymong.domain.user.repository.UserUniversityRepository; import com.moneymong.global.exception.custom.NotFoundException; @@ -25,19 +27,13 @@ public class UserService { private final UserRepository userRepository; private final UserUniversityRepository userUniversityRepository; private final RefreshTokenRepository refreshTokenRepository; + private final AppleUserRepository appleUserRepository; @Transactional public AuthUserInfo getOrRegister(OAuthUserInfo oauthUserInfo) { User user = userRepository .findByUserIdByProviderAndOauthId(oauthUserInfo.getProvider(), oauthUserInfo.getOauthId()) - .orElseGet(() -> save( - User.of(UUID.randomUUID().toString(), - oauthUserInfo.getEmail(), - oauthUserInfo.getNickname(), - oauthUserInfo.getProvider(), - oauthUserInfo.getOauthId() - ) - )); + .orElseGet(() -> registerUser(oauthUserInfo)); return AuthUserInfo.from(user.getId(), user.getNickname(), DEFAULT_ROLE); } @@ -47,6 +43,29 @@ public User save(User unsavedUser) { return userRepository.save(unsavedUser); } + @Transactional + public User registerUser(OAuthUserInfo oauthUserInfo) { + User newUser = User.of( + oauthUserInfo.getEmail(), + oauthUserInfo.getNickname(), + oauthUserInfo.getProvider(), + oauthUserInfo.getOauthId() + ); + newUser = save(newUser); + + if (oauthUserInfo.getAppleRefreshToken() != null) { + appleUserRepository.save( + AppleUser.of( + newUser.getId(), + oauthUserInfo.getAppleRefreshToken() + ) + ); + } + + return newUser; + } + + @Transactional(readOnly = true) public UserProfileResponse getUserProfile(Long userId) { User user = userRepository.findById(userId) diff --git a/src/main/java/com/moneymong/global/config/docs/OpenApiConfig.java b/src/main/java/com/moneymong/global/config/docs/OpenApiConfig.java index 8ba453f..4ed207b 100644 --- a/src/main/java/com/moneymong/global/config/docs/OpenApiConfig.java +++ b/src/main/java/com/moneymong/global/config/docs/OpenApiConfig.java @@ -11,7 +11,7 @@ import org.springframework.context.annotation.Profile; @Configuration -@Profile({"local", "dev", "prod"}) +@Profile({"dev", "prod"}) public class OpenApiConfig { @Bean diff --git a/src/main/java/com/moneymong/global/image/api/ImageController.java b/src/main/java/com/moneymong/global/image/api/ImageController.java index f304cbc..c6a1d8b 100644 --- a/src/main/java/com/moneymong/global/image/api/ImageController.java +++ b/src/main/java/com/moneymong/global/image/api/ImageController.java @@ -3,6 +3,8 @@ import com.moneymong.global.image.dto.ImageDeleteRequest; import com.moneymong.global.image.dto.ImageResponse; import com.moneymong.global.image.service.ImageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.ModelAttribute; @@ -16,6 +18,7 @@ import static org.springframework.http.MediaType.*; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; +@Tag(name = "9. [이미지]") @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/images") @@ -23,12 +26,14 @@ public class ImageController { private final ImageService imageService; + @Operation(summary = "이미지 업로드") @PostMapping(consumes = {MULTIPART_FORM_DATA_VALUE, APPLICATION_JSON_VALUE}) public ImageResponse upload(@RequestPart("file") MultipartFile multipartFile) { ImageResponse response = imageService.upload(multipartFile); return response; } + @Operation(summary = "이미지 삭제 API") @DeleteMapping(consumes = APPLICATION_JSON_VALUE) public void remove(@RequestBody ImageDeleteRequest deleteRequest) { imageService.remove(deleteRequest); diff --git a/src/main/java/com/moneymong/global/security/oauth/dto/AppleUserData.java b/src/main/java/com/moneymong/global/security/oauth/dto/AppleUserData.java new file mode 100644 index 0000000..3a066ef --- /dev/null +++ b/src/main/java/com/moneymong/global/security/oauth/dto/AppleUserData.java @@ -0,0 +1,15 @@ +package com.moneymong.global.security.oauth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AppleUserData { + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("id_token") + private String idToken; +} diff --git a/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataRequest.java b/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataRequest.java index d09da41..e9265e6 100644 --- a/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataRequest.java +++ b/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataRequest.java @@ -7,4 +7,6 @@ @AllArgsConstructor public class OAuthUserDataRequest { private String accessToken; + private String code; // Apple Authorization Code + private String name; } diff --git a/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataResponse.java b/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataResponse.java index 3d882fb..1981c7a 100644 --- a/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataResponse.java +++ b/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserDataResponse.java @@ -14,4 +14,5 @@ public class OAuthUserDataResponse { private String oauthId; private String email; private String nickname; + private String appleRefreshToken; } diff --git a/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserInfo.java b/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserInfo.java index 1076f87..7468856 100644 --- a/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserInfo.java +++ b/src/main/java/com/moneymong/global/security/oauth/dto/OAuthUserInfo.java @@ -1,9 +1,6 @@ package com.moneymong.global.security.oauth.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Getter @Builder @@ -14,6 +11,7 @@ public class OAuthUserInfo { private String oauthId; private String nickname; private String email; + private String appleRefreshToken; public static OAuthUserInfo from(OAuthUserDataResponse oAuthUserDataResponse) { return OAuthUserInfo.builder() @@ -21,6 +19,7 @@ public static OAuthUserInfo from(OAuthUserDataResponse oAuthUserDataResponse) { .oauthId(oAuthUserDataResponse.getOauthId()) .nickname(oAuthUserDataResponse.getNickname()) .email(oAuthUserDataResponse.getEmail()) + .appleRefreshToken(oAuthUserDataResponse.getAppleRefreshToken()) .build(); } } diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java b/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java new file mode 100644 index 0000000..ccb3bd2 --- /dev/null +++ b/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java @@ -0,0 +1,160 @@ +package com.moneymong.global.security.oauth.handler; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.moneymong.global.exception.enums.ErrorCode; +import com.moneymong.global.security.oauth.dto.AppleUserData; +import com.moneymong.global.security.oauth.dto.OAuthUserDataRequest; +import com.moneymong.global.security.oauth.dto.OAuthUserDataResponse; +import com.moneymong.global.security.oauth.exception.HttpClientException; +import com.moneymong.global.security.service.OAuthProvider; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.auth0.jwt.interfaces.Claim; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.security.PrivateKey; +import java.security.Security; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.Date; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AppleService implements OAuthAuthenticationHandler { + + private final RestTemplate restTemplate; + + @Value("${spring.security.oauth2.apple.host}") + private String host; + + @Value("${spring.security.oauth2.apple.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.apple.key-id}") + private String keyId; + + @Value("${spring.security.oauth2.apple.team-id}") + private String teamId; + + @Value("${spring.security.oauth2.apple.private-key}") + private String privateKey; + + @Override + public OAuthProvider getAuthProvider() { + return OAuthProvider.APPLE; + } + + @Override + public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { + if (request.getCode() == null) { + return decodePayload(request.getAccessToken(), request.getName()); + } + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity httpRequest = new HttpEntity<>(null, httpHeaders); + + MultiValueMap parameterMap = new LinkedMultiValueMap<>(); + parameterMap.add("client_id", clientId); + parameterMap.add("client_secret", createClientSecret()); + parameterMap.add("grant_type", "authorization_code"); + parameterMap.add("code", request.getCode()); + + URI uri = UriComponentsBuilder + .fromUriString(host + "/auth/oauth2/v2/token") + .queryParams(parameterMap) + .build() + .toUri(); + + try { + AppleUserData userData = restTemplate.postForObject( + uri, + httpRequest, + AppleUserData.class + ); + + assert userData != null; + + String refreshToken = userData.getRefreshToken(); + String idToken = userData.getIdToken(); + + log.info("[AppleService] refreshToken = {}", refreshToken); + return decodePayload(idToken, request.getName()); + } catch (RestClientException e) { + log.info("[AppleService] error message = {}", e.getMessage()); + log.warn("[AppleService] failed to get OAuth User Data = {}", request.getAccessToken()); + throw new HttpClientException(ErrorCode.HTTP_CLIENT_REQUEST_FAILED); + } + } + + @Override + public void unlink(String token) { + + } + + private String createClientSecret() { + ZonedDateTime expiration = ZonedDateTime.now().plusMinutes(5); + + return Jwts.builder() + .setHeaderParam(JwsHeader.KEY_ID, keyId) + .setIssuer(teamId) + .setAudience(host) + .setSubject(clientId) + .setExpiration(Date.from(expiration.withZoneSameInstant(ZoneId.systemDefault()).toInstant())) + .setIssuedAt(new Date()) + .signWith(getPrivateKey(), SignatureAlgorithm.ES256) + .compact(); + } + + private PrivateKey getPrivateKey() { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + + try { + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey); + + PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes); + return converter.getPrivateKey(privateKeyInfo); + } catch (Exception e) { + throw new RuntimeException("Error converting private key from String", e); + } + } + + private OAuthUserDataResponse decodePayload(String idToken, String nickname) { + try { + DecodedJWT decoded = JWT.decode(idToken); + Map claims = decoded.getClaims(); + + String providerUid = decoded.getSubject(); + String email = claims.get("email").asString(); + + return OAuthUserDataResponse.builder() + .provider(getAuthProvider().toString()) + .oauthId(providerUid) + .email(email) + .nickname(nickname) + .build(); + } catch (Exception e) { + throw new RuntimeException("Error decoding payload", e); + } + } +} diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java b/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java index 4f3da5f..ce3d71c 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java @@ -67,4 +67,9 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { throw new HttpClientException(ErrorCode.HTTP_CLIENT_REQUEST_FAILED); } } + + @Override + public void unlink(String token) { + + } } diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java b/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java index 3e00408..7e07524 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java @@ -8,4 +8,6 @@ public interface OAuthAuthenticationHandler { OAuthProvider getAuthProvider(); OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request); + + void unlink(String token); } diff --git a/src/main/java/com/moneymong/global/security/service/OAuthProvider.java b/src/main/java/com/moneymong/global/security/service/OAuthProvider.java index 2a6810d..ab8b9d4 100644 --- a/src/main/java/com/moneymong/global/security/service/OAuthProvider.java +++ b/src/main/java/com/moneymong/global/security/service/OAuthProvider.java @@ -11,7 +11,8 @@ @RequiredArgsConstructor public enum OAuthProvider { - KAKAO("KAKAO"); + KAKAO("KAKAO"), + APPLE("APPLE"); private final String name; diff --git a/src/main/java/com/moneymong/global/security/service/OAuthService.java b/src/main/java/com/moneymong/global/security/service/OAuthService.java index ec3599a..a6161a1 100644 --- a/src/main/java/com/moneymong/global/security/service/OAuthService.java +++ b/src/main/java/com/moneymong/global/security/service/OAuthService.java @@ -1,6 +1,7 @@ package com.moneymong.global.security.service; import com.moneymong.domain.user.api.request.LoginRequest; +import com.moneymong.domain.user.api.request.UserDeleteRequest; import com.moneymong.global.security.oauth.dto.OAuthUserDataRequest; import com.moneymong.global.security.oauth.dto.OAuthUserDataResponse; import com.moneymong.global.security.oauth.handler.OAuthAuthenticationHandler; @@ -27,8 +28,19 @@ public OAuthUserDataResponse login(LoginRequest loginRequest) { OAuthAuthenticationHandler oAuthHandler = this.oAuthAuthenticationHandlers.get(oAuthProvider); - OAuthUserDataRequest request = new OAuthUserDataRequest(loginRequest.getAccessToken()); + OAuthUserDataRequest request = new OAuthUserDataRequest( + loginRequest.getAccessToken(), + loginRequest.getCode(), + loginRequest.getName() + ); return oAuthHandler.getOAuthUserData(request); } + + public void revoke(UserDeleteRequest deleteRequest) { + OAuthProvider oAuthProvider = OAuthProvider.get(deleteRequest.getProvider()); + OAuthAuthenticationHandler oAuthHandler = this.oAuthAuthenticationHandlers.get(oAuthProvider); + + oAuthHandler.unlink(deleteRequest.getToken()); + } } diff --git a/src/main/java/com/moneymong/global/security/token/api/TokenController.java b/src/main/java/com/moneymong/global/security/token/api/TokenController.java index 533bea6..b38bae3 100644 --- a/src/main/java/com/moneymong/global/security/token/api/TokenController.java +++ b/src/main/java/com/moneymong/global/security/token/api/TokenController.java @@ -4,10 +4,12 @@ import com.moneymong.global.security.token.api.request.RefreshAccessTokenRequest; import com.moneymong.global.security.token.api.response.TokenResponse; import com.moneymong.global.security.token.service.TokenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; - +@Tag(name = "0. [로그인]") @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/tokens") @@ -15,12 +17,14 @@ public class TokenController { private final TokenService tokenService; + @Operation(summary = "AccessToken 갱신") @PostMapping public TokenResponse refreshAccessToken(@RequestBody RefreshAccessTokenRequest refreshAccessTokenRequest) { String refreshToken = refreshAccessTokenRequest.getRefreshToken(); return tokenService.getAccessTokensByRefreshToken(refreshToken); } + @Operation(summary = "로그아웃") @DeleteMapping public void deleteRefreshToken(@RequestBody DeleteRefreshTokenRequest deleteRefreshTokenRequest) { String refreshToken = deleteRefreshTokenRequest.getRefreshToken(); diff --git a/src/main/java/com/moneymong/global/security/token/dto/jwt/JwtAuthentication.java b/src/main/java/com/moneymong/global/security/token/dto/jwt/JwtAuthentication.java index b10d591..bd7c850 100644 --- a/src/main/java/com/moneymong/global/security/token/dto/jwt/JwtAuthentication.java +++ b/src/main/java/com/moneymong/global/security/token/dto/jwt/JwtAuthentication.java @@ -15,7 +15,7 @@ public class JwtAuthentication{ public JwtAuthentication(Long id, String accessToken) { this.id = validateId(id); - this.accessToken = validateUserToken(accessToken); + this.accessToken = validateAccessToken(accessToken); } private Long validateId(Long id) { @@ -26,11 +26,11 @@ private Long validateId(Long id) { return id; } - private String validateUserToken(String userToken) { - if (StringUtils.isEmpty(userToken)) { + private String validateAccessToken(String accessToken) { + if (StringUtils.isEmpty(accessToken)) { throw new InvalidTokenException(ErrorCode.INVALID_TOKEN); } - return userToken; + return accessToken; } } diff --git a/src/main/java/com/moneymong/utils/ModificationAmountCalculator.java b/src/main/java/com/moneymong/utils/ModificationAmountCalculator.java deleted file mode 100644 index a74a311..0000000 --- a/src/main/java/com/moneymong/utils/ModificationAmountCalculator.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.moneymong.utils; - -import com.moneymong.domain.ledger.entity.enums.FundType; -import java.math.BigDecimal; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ModificationAmountCalculator { - public static int calculate( - final FundType fundType, - final int amount, - final int newAmount - ) { - BigDecimal undoneAmount = new BigDecimal(-1L * AmountCalculatorByFundType.calculate(fundType, amount)); - BigDecimal appliedAmount = new BigDecimal(AmountCalculatorByFundType.calculate(fundType, newAmount)); - - return undoneAmount.add(appliedAmount).intValue(); - } -} diff --git a/src/main/java/com/moneymong/utils/TextValidator.java b/src/main/java/com/moneymong/utils/TextValidator.java deleted file mode 100644 index 73cac43..0000000 --- a/src/main/java/com/moneymong/utils/TextValidator.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.moneymong.utils; - -import org.springframework.util.Assert; - -public class TextValidator { - public static void checkText(String text, String message) { - Assert.hasText(text, message); - } -} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 08d39d1..0659603 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,13 @@ spring: oauth2: kakao: host: https://kapi.kakao.com + apple: + host: https://appleid.apple.com + grant-type: authorization_code + client-id: ${APPLE_CLIENT_ID} + key-id: ${APPLE_KEY_ID} + team-id: ${APPLE_TEAM_ID} + private-key: ${APPLE_PRIVATE_KEY} data: redis: diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 744976e..eb88ee7 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -11,7 +11,8 @@ spring: oauth2: kakao: host: https://kapi.kakao.com - + apple: + host: https://appleid.apple.com data: redis: host: ${REDIS_HOST} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 08d39d1..0659603 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -11,6 +11,13 @@ spring: oauth2: kakao: host: https://kapi.kakao.com + apple: + host: https://appleid.apple.com + grant-type: authorization_code + client-id: ${APPLE_CLIENT_ID} + key-id: ${APPLE_KEY_ID} + team-id: ${APPLE_TEAM_ID} + private-key: ${APPLE_PRIVATE_KEY} data: redis: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0c05822..3199bac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,7 +32,7 @@ management: # swagger springdoc: - packages-to-scan: com.moneymong.domain + packages-to-scan: com.moneymong default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 swagger-ui: diff --git a/src/test/java/com/moneymong/domain/MoneymongApplicationTests.java b/src/test/java/com/moneymong/domain/MoneymongApplicationTests.java index 86c5a8d..85cc0c9 100644 --- a/src/test/java/com/moneymong/domain/MoneymongApplicationTests.java +++ b/src/test/java/com/moneymong/domain/MoneymongApplicationTests.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest class MoneymongApplicationTests { @Test