diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 855c0c54..00000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -max_line_length = 120 -tab_width = 4 -trim_trailing_whitespace = true - -[*.bat] -end_of_line = crlf - -[*.java] -indent_style = tab -ij_java_blank_lines_after_class_header = 1 diff --git a/build.gradle b/build.gradle index 1fc75afe..0bb89395 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,9 @@ dependencies { // H2 implementation 'com.h2database:h2' + + // Configuration Binding + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } tasks.named('test') { diff --git a/config/naver-checkstyle-rules.xml b/config/naver-checkstyle-rules.xml deleted file mode 100644 index dafbb4d1..00000000 --- a/config/naver-checkstyle-rules.xml +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/naver-intellij-formatter-custom.xml b/config/naver-intellij-formatter-custom.xml index 26f28954..dc4c4a2e 100644 --- a/config/naver-intellij-formatter-custom.xml +++ b/config/naver-intellij-formatter-custom.xml @@ -1,74 +1,75 @@ - - - \ No newline at end of file diff --git a/src/main/java/com/moabam/MoabamServerApplication.java b/src/main/java/com/moabam/MoabamServerApplication.java index 5390acc3..e2dbce1d 100644 --- a/src/main/java/com/moabam/MoabamServerApplication.java +++ b/src/main/java/com/moabam/MoabamServerApplication.java @@ -2,12 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +@ConfigurationPropertiesScan @SpringBootApplication public class MoabamServerApplication { public static void main(String[] args) { SpringApplication.run(MoabamServerApplication.class, args); } - } diff --git a/src/main/java/com/moabam/api/application/auth/AuthenticationService.java b/src/main/java/com/moabam/api/application/auth/AuthenticationService.java new file mode 100644 index 00000000..3688b540 --- /dev/null +++ b/src/main/java/com/moabam/api/application/auth/AuthenticationService.java @@ -0,0 +1,24 @@ +package com.moabam.api.application.auth; + +import org.springframework.stereotype.Service; + +import com.moabam.api.dto.auth.AuthorizationCodeIssue; +import com.moabam.global.config.OAuthConfig; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + + private final OAuthConfig oAuthConfig; + + public String getAuthorizaionCodeUri() { + return AuthorizationCodeIssue.builder() + .clientId(oAuthConfig.client().clientId()) + .redirectUri(oAuthConfig.provider().redirectUri()) + .scope(oAuthConfig.client().scope()) + .build() + .generateQueryParamsWith(oAuthConfig.provider().authorizationUri()); + } +} diff --git a/src/main/java/com/moabam/api/domain/member/Member.java b/src/main/java/com/moabam/api/domain/member/Member.java new file mode 100644 index 00000000..58440858 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/member/Member.java @@ -0,0 +1,90 @@ +package com.moabam.api.domain.member; + +import static java.util.Objects.*; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.ColumnDefault; + +import com.moabam.global.common.entity.BaseTimeEntity; +import com.moabam.global.common.util.BaseImageUrl; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "members") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + + @Column(name = "social_id", nullable = false, unique = true) + private String socialId; + + @Column(name = "nickname", nullable = false, unique = true) + private String nickname; + + @Column(name = "intro") + private String intro; + + @Column(name = "profile_image", nullable = false) + private String profileImage; + + @Column(name = "total_certify_count", nullable = false) + @ColumnDefault("0") + private long totalCertifyCount; + + @Column(name = "report_count", nullable = false) + @ColumnDefault("0") + private int reportCount; + + @Column(name = "current_night_count", nullable = false) + @ColumnDefault("0") + private int currentNightCount; + + @Column(name = "current_morning_count", nullable = false) + @ColumnDefault("0") + private int currentMorningCount; + + @Column(name = "morning_bug", nullable = false) + @ColumnDefault("0") + private int morningBug; + + @Column(name = "night_bug", nullable = false) + @ColumnDefault("0") + private int nightBug; + + @Column(name = "golden_bug", nullable = false) + @ColumnDefault("0") + private int goldenBug; + + @Enumerated(EnumType.STRING) + @Column(name = "role", nullable = false) + private Role role; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder + private Member(String socialId, String nickname, String profileImage) { + this.socialId = requireNonNull(socialId); + this.nickname = requireNonNull(nickname); + this.profileImage = requireNonNullElse(profileImage, BaseImageUrl.PROFILE_URL.getUrl()); + this.role = Role.USER; + } +} diff --git a/src/main/java/com/moabam/api/domain/member/Role.java b/src/main/java/com/moabam/api/domain/member/Role.java new file mode 100644 index 00000000..b168314b --- /dev/null +++ b/src/main/java/com/moabam/api/domain/member/Role.java @@ -0,0 +1,8 @@ +package com.moabam.api.domain.member; + +public enum Role { + + USER, + @SuppressWarnings("checkstyle:JavadocVariable") BLACK, + ADMIN +} diff --git a/src/main/java/com/moabam/api/dto/auth/AuthorizationCodeIssue.java b/src/main/java/com/moabam/api/dto/auth/AuthorizationCodeIssue.java new file mode 100644 index 00000000..80a7ec23 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/auth/AuthorizationCodeIssue.java @@ -0,0 +1,45 @@ +package com.moabam.api.dto.auth; + +import static com.moabam.global.common.util.OAuthParameterNames.*; + +import java.util.List; + +import org.springframework.web.util.UriComponentsBuilder; + +import com.moabam.global.common.util.GlobalConstant; + +import lombok.Builder; + +public record AuthorizationCodeIssue( + String clientId, + String redirectUri, + String responseType, + List scope, + String state +) { + + @Builder + public AuthorizationCodeIssue(String clientId, String redirectUri, String responseType, List scope, + String state) { + this.clientId = clientId; + this.redirectUri = redirectUri; + this.responseType = responseType; + this.scope = scope; + this.state = state; + } + + public String generateQueryParamsWith(String baseUrl) { + UriComponentsBuilder authorizationCodeUri = UriComponentsBuilder + .fromPath(baseUrl) + .queryParam(RESPONSE_TYPE, CODE) + .queryParam(CLIENT_ID, clientId) + .queryParam(REDIRECT_URI, redirectUri); + + if (!scope.isEmpty()) { + String scopes = String.join(GlobalConstant.COMMA, scope); + authorizationCodeUri.queryParam(SCOPE, scopes); + } + + return authorizationCodeUri.toUriString(); + } +} diff --git a/src/main/java/com/moabam/api/presentation/member/MemberController.java b/src/main/java/com/moabam/api/presentation/member/MemberController.java new file mode 100644 index 00000000..703b415a --- /dev/null +++ b/src/main/java/com/moabam/api/presentation/member/MemberController.java @@ -0,0 +1,29 @@ +package com.moabam.api.presentation.member; + +import java.io.IOException; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.moabam.api.application.auth.AuthenticationService; +import com.moabam.global.common.util.GlobalConstant; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/members") +@RequiredArgsConstructor +public class MemberController { + + private final AuthenticationService authenticationService; + + @GetMapping + public void socialLogin(HttpServletResponse httpServletResponse) throws IOException { + String authorizationCodeUri = authenticationService.getAuthorizaionCodeUri(); + httpServletResponse.setContentType(MediaType.APPLICATION_FORM_URLENCODED + GlobalConstant.CHARSET_UTF_8); + httpServletResponse.sendRedirect(authorizationCodeUri); + } +} diff --git a/src/main/java/com/moabam/global/common/util/BaseImageUrl.java b/src/main/java/com/moabam/global/common/util/BaseImageUrl.java new file mode 100644 index 00000000..8a302328 --- /dev/null +++ b/src/main/java/com/moabam/global/common/util/BaseImageUrl.java @@ -0,0 +1,15 @@ +package com.moabam.global.common.util; + +import lombok.Getter; + +@Getter +public enum BaseImageUrl { + + PROFILE_URL("/profile/baseUrl"); + + private String url; + + BaseImageUrl(String url) { + this.url = url; + } +} diff --git a/src/main/java/com/moabam/global/common/util/GlobalConstant.java b/src/main/java/com/moabam/global/common/util/GlobalConstant.java new file mode 100644 index 00000000..5b87cd58 --- /dev/null +++ b/src/main/java/com/moabam/global/common/util/GlobalConstant.java @@ -0,0 +1,11 @@ +package com.moabam.global.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GlobalConstant { + + public static final String COMMA = ","; + public static final String CHARSET_UTF_8 = ";charset=UTF-8"; +} diff --git a/src/main/java/com/moabam/global/common/util/OAuthParameterNames.java b/src/main/java/com/moabam/global/common/util/OAuthParameterNames.java new file mode 100644 index 00000000..efc65e59 --- /dev/null +++ b/src/main/java/com/moabam/global/common/util/OAuthParameterNames.java @@ -0,0 +1,14 @@ +package com.moabam.global.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class OAuthParameterNames { + + public static final String RESPONSE_TYPE = "response_type"; + public static final String CODE = "code"; + public static final String CLIENT_ID = "client_id"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String SCOPE = "scope"; +} diff --git a/src/main/java/com/moabam/global/config/OAuthConfig.java b/src/main/java/com/moabam/global/config/OAuthConfig.java new file mode 100644 index 00000000..cff7f45b --- /dev/null +++ b/src/main/java/com/moabam/global/config/OAuthConfig.java @@ -0,0 +1,28 @@ +package com.moabam.global.config; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth2") +public record OAuthConfig( + Provider provider, + Client client +) { + + public record Client( + String provider, + String clientId, + String authorizationGrantType, + List scope + ) { + + } + + public record Provider( + String authorizationUri, + String redirectUri + ) { + + } +} diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index 662874f6..0ad98085 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -7,7 +7,8 @@ @RequiredArgsConstructor public enum ErrorMessage { - INVALID_REQUEST_FIELD("올바른 요청 정보가 아닙니다."); + INVALID_REQUEST_FIELD("올바른 요청 정보가 아닙니다."), + LOGIN_FAILED("로그인에 실패했습니다."); private final String message; } diff --git a/src/main/resources/config b/src/main/resources/config index 8bc59e64..600a2467 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 8bc59e6455ce1220e00acf676849951cbd935373 +Subproject commit 600a2467b779690dd9c24f172425832b0d5eb87b diff --git a/src/test/java/com/moabam/api/application/auth/AuthenticationServiceTest.java b/src/test/java/com/moabam/api/application/auth/AuthenticationServiceTest.java new file mode 100644 index 00000000..6f29348e --- /dev/null +++ b/src/test/java/com/moabam/api/application/auth/AuthenticationServiceTest.java @@ -0,0 +1,45 @@ +package com.moabam.api.application.auth; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import com.moabam.global.config.OAuthConfig; + +@ExtendWith(MockitoExtension.class) +class AuthenticationServiceTest { + + @InjectMocks + AuthenticationService authenticationService; + + OAuthConfig oauthConfig; + + @BeforeEach + public void initParams() { + oauthConfig = new OAuthConfig( + new OAuthConfig.Provider("https://test.com/test/test", "http://test:8080/test"), + new OAuthConfig.Client("provider", "testtestetsttest", "authorization_code", + List.of("profile_nickname", "profile_image")) + ); + ReflectionTestUtils.setField(authenticationService, "oAuthConfig", oauthConfig); + } + + @DisplayName("authentication code Url 생성 성공") + @Test + void authenticationUrl() { + // Given + When + String authorizaionCodeUri = authenticationService.getAuthorizaionCodeUri(); + + // Then + assertThat(authorizaionCodeUri).contains(oauthConfig.client().clientId(), + String.join(",", oauthConfig.client().scope()), oauthConfig.provider().redirectUri()); + } +} diff --git a/src/test/java/com/moabam/api/domain/member/MemberTest.java b/src/test/java/com/moabam/api/domain/member/MemberTest.java new file mode 100644 index 00000000..caa56681 --- /dev/null +++ b/src/test/java/com/moabam/api/domain/member/MemberTest.java @@ -0,0 +1,63 @@ +package com.moabam.api.domain.member; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.moabam.global.common.util.BaseImageUrl; + +class MemberTest { + + String socialId = "1"; + String nickname = "밥세공기"; + String profileImage = "kakao/profile/url"; + + @DisplayName("회원 생성 성공") + @Test + void create_member_success() { + // When + Then + assertThatNoException().isThrownBy(() -> Member.builder() + .socialId(socialId) + .nickname(nickname) + .profileImage(profileImage) + .build()); + } + + @DisplayName("프로필 이미지 없이 회원 생성 성공") + @Test + void create_member_noImage_success() { + // When + Then + assertThatNoException().isThrownBy(() -> { + Member member = Member.builder() + .socialId(socialId) + .nickname(nickname) + .profileImage(null) + .build(); + + assertAll( + () -> assertThat(member.getProfileImage()).isEqualTo(BaseImageUrl.PROFILE_URL.getUrl()), + () -> assertThat(member.getRole()).isEqualTo(Role.USER) + ); + }); + } + + @DisplayName("소셜ID에 따른 회원 생성 실패") + @Test + void creat_member_failBy_socialId() { + // When + Then + assertThatThrownBy(Member.builder() + .nickname(nickname)::build) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("닉네임에 따른 회원 생성 실패") + @Test + void create_member_failBy_nickname() { + // When + Then + assertThatThrownBy(Member.builder() + .socialId(socialId)::build) + .isInstanceOf(NullPointerException.class); + } +} diff --git a/src/test/java/com/moabam/api/presentation/member/MemberControllerTest.java b/src/test/java/com/moabam/api/presentation/member/MemberControllerTest.java new file mode 100644 index 00000000..bf09a503 --- /dev/null +++ b/src/test/java/com/moabam/api/presentation/member/MemberControllerTest.java @@ -0,0 +1,67 @@ +package com.moabam.api.presentation.member; + +import static com.moabam.global.common.util.OAuthParameterNames.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.util.UriComponentsBuilder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.application.auth.AuthenticationService; +import com.moabam.global.common.util.GlobalConstant; +import com.moabam.global.config.OAuthConfig; + +@SpringBootTest( + properties = { + "oauth2.provider.authorization-uri=https://kauth.kakao.com/oauth/authorize", + "oauth2.provider.redirect-uri=http://localhost:8080/members/login/kakao/oauth", + "oauth2.client.client-id=testtesttesttesttesttesttesttesttest", + "oauth2.client.authorization-grant-type=authorization_code", + "oauth2.client.scope=profile_nickname,profile_image" + } +) +@AutoConfigureMockMvc +class MemberControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + AuthenticationService authenticationService; + + @Autowired + OAuthConfig oAuthConfig; + + @DisplayName("인가 코드 받기 위한 로그인 페이지 요청") + @Test + void authorization_code_request_success() throws Exception { + // given + String uri = UriComponentsBuilder + .fromPath(oAuthConfig.provider().authorizationUri()) + .queryParam(RESPONSE_TYPE, "code") + .queryParam(CLIENT_ID, oAuthConfig.client().clientId()) + .queryParam(REDIRECT_URI, oAuthConfig.provider().redirectUri()) + .queryParam(SCOPE, String.join(",", oAuthConfig.client().scope())) + .toUriString(); + + // when + ResultActions result = mockMvc.perform(get("/members")); + + // then + result.andExpect(status().is3xxRedirection()) + .andExpect(header().string("Content-type", + MediaType.APPLICATION_FORM_URLENCODED_VALUE + GlobalConstant.CHARSET_UTF_8)) + .andExpect(redirectedUrl(uri)); + } +}