Skip to content

Commit

Permalink
[YS-65] feat: 테스트용 토큰 강제 발급 API & AccessToken 갱신 API 구현 (#16)
Browse files Browse the repository at this point in the history
* style: add line for EOF

* chore: update springdoc version

* feat: add generateTestTokenForTestMember method for test

* feat: add UseCase interface

* feat: add GenerateTestToken api for test member

* test: change memberId type from String to Long in test code

* chore: add mock dependency for test

* test: add GenerateTestToken test code

* test: refactor JwtTokenProviderTest to use Kotest style

* style: rename dto name

* feat: add @componentscan to include UseCase beans in application context

* refact: refactor TokenProvider code

* feat: add dto for SignIn logic

* feat: add GenerateTokenWithRefreshToken

* test: add GenerateTokenWithRefreshToken test code

* refact: rename entity

* refact: refactor member domain

* test: fix test due to changed domain

* feat: add MemberGateway

* feat: add GetMemberById usecase

* refact: update MemberRefreshToken response

* style: add line for eof

* style: update role example

* [YS-31] feat: 구글 OAuth 로그인 구현 (#13)

* feat: define domain models and enums for the project

- 프로젝트의 핵심 데이터를 표현하는 엔티티 클래스 정의
- 표준화를 위한 enum 클래스 구현
- 프로젝트 확장성과 명확성을 위한 기본 도메인 설계 완료

* refact: update domain structures for refactoring

- `AuditingEntityListener` 추가
- 기존: 리스트 순차 탐색 ➡️  Map의 key값으로 탐색 개선
- 패키지명 `enums` ➡️  `enum` 으로 변경

* fix: fixing some errors(Member, AuditingEntity)

- `AuditingEntity` 변경 내용 추가
- `Member` 의존성 알맞게 주입

* feat: 구글 OAuth 로그인 구현

- Google OAuth 로그인 구현
- 추후 회원가입 로직의 OAuth 정보 받아오는 과정 고려하여, 도메인 클래스
  일부 수정
- `.env` 파일 로드를 위한 의존성 파일 추가

* fix: add dotenv libraries

- 에러 수정을 위해 빌드 파일에 `dotenv` 라이브러리 추가

* fix: delete import which occurs dependency error

* fix: resolve context loading issue with active test profile

- 테스트 중 적절한 빈 구성 보장을 위해 `@ActiveProfiles("test")` 추가
- `SecurityFilterChain` 관련 Application Context 로딩 오류 해결

* feat: implement google oauth login

- 구글 oauth 로그인 구현
- 구글 oauth token, email, name 정보 가져오도록 설정

* refact: seperate responsibilities according to Clean Architecture
principal

- Google API 호출 로직을 `FetchGoogleUserInfoUseCase`로 이동하여 책임 분리
  개선
- `OauthService` 를 수정하여 사용자 정보 조회를 새로운 UseCase로 위임

* test: test code for Google OAuth login logic, AuditingEntity

- 구글 OAuth 로직에 대한 test code 작성: `OauthServiceTest`
  `FetchGoogleUserInfoTest`
- `AuditingEntityTest` 에 대한 test code 작성

- JaCoco 커버리지 테스트에서 AuditingEntityTest 커버리지 0.5로 목표치
  미달 → 추후 테스트 커버리지 개선 필요

* test: add OauthMapperTest and MemberServiceTest for refact test coverage

- `OauthMapperTest` 테스트 코드 추가
- `MemberServiceTest` 테스트 코드 추가

- `AuditingEntityTest` → 테스트 커버리지 50%로 목표치 70% 달성 실패,
  추후 개선 필요

* feat : add Google OAuth integration

- Google OAuth 연동을 위한 `GoogleAuthFeignClient` 추가
- 사용자 정보를 가져오는 `GoogleUserInfoFeignClient` 구현
- GoogleTokenRequest 및 GoogleTokenResponse DTO 정의
- OauthLoginResponse에 사용자 정보 및 accessToken, refreshToken 포함
- 기존 WebClientConfig 삭제 → FeignClient로 대체

* fix: add dependency code and custom exception code

- Google OAuth 로그인 시도 시 발생할 수 있는 예외처리
- Application에 FeignClient 설정

* fix: adjust things to build safely

* refact: update test codes to satisfy the newly requirement

* refact: reflect pr reviews and update changes

- 중복된 선언: `@Repository` 어노테이션 제거
- SwaggerConfig 가독성 개선

* refact: reflect code from reviews

- 빌드 파일에 중복된 의존성 제거
- `OAuthController` 제거 → `AuthController` 로 통일
- 파일 끝에 EOF 추가

* feat: add GenerateTestToken api for test member

* test: refactor JwtTokenProviderTest to use Kotest style

* feat: add @componentscan to include UseCase beans in application context

* feat: add GenerateTokenWithRefreshToken

* refact: rename entity

* test: fix test due to changed domain

* refact: update MemberRefreshToken response

* fix: fix conflicts for merge

* refact: move usecase file to application

* refact: refactor OAuthLoginResponse response structure

* fix: fix conflicts for merge

---------

Co-authored-by: Sujung Shin <[email protected]>
  • Loading branch information
Ji-soo708 and chock-cho authored Jan 3, 2025
1 parent efc8f9d commit 88e6ff5
Show file tree
Hide file tree
Showing 36 changed files with 469 additions and 117 deletions.
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("com.github.f4b6a3:ulid-creator:5.2.3")
implementation("org.mariadb.jdbc:mariadb-java-client:2.7.3")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation ("io.awspring.cloud:spring-cloud-starter-aws:2.4.4")
compileOnly("org.projectlombok:lombok")
Expand All @@ -65,6 +64,8 @@ dependencies {
testImplementation("io.kotest:kotest-assertions-core:$koTestVersion")
testImplementation("io.kotest:kotest-property:$koTestVersion")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2")

testImplementation("io.mockk:mockk:1.13.10")
}

dependencyManagement {
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/com/dobby/backend/DobbyBackendApplication.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.dobby.backend

import com.dobby.backend.application.usecase.UseCase
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.FilterType

@ComponentScan(
includeFilters = [ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = [UseCase::class]
)]
)
@SpringBootApplication
@ConfigurationPropertiesScan
@EnableFeignClients
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.dobby.backend.application.mapper

import com.dobby.backend.infrastructure.database.entity.Member
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
import com.dobby.backend.presentation.api.dto.request.OauthUserDto
import com.dobby.backend.presentation.api.dto.response.MemberResponse
import com.dobby.backend.presentation.api.dto.response.OauthLoginResponse

object OauthUserMapper {
Expand All @@ -22,7 +20,7 @@ object OauthUserMapper {
isRegistered = isRegistered,
accessToken = accessToken,
refreshToken = refreshToken,
memberInfo = OauthLoginResponse.MemberInfo(
memberInfo = MemberResponse(
memberId = memberId,
oauthEmail = oauthEmail,
name = oauthName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ class FetchGoogleUserInfoUseCase(
private val jwtTokenProvider: JwtTokenProvider,
private val googleAuthProperties: GoogleAuthProperties,
private val memberRepository: MemberRepository
){
fun execute(oauthLoginRequest: OauthLoginRequest) : OauthLoginResponse{
) : UseCase<OauthLoginRequest, OauthLoginResponse> {

override fun execute(input: OauthLoginRequest): OauthLoginResponse {
try {
val googleTokenRequest = GoogleTokenRequest(
code = oauthLoginRequest.authorizationCode,
code = input.authorizationCode,
clientId = googleAuthProperties.clientId,
clientSecret = googleAuthProperties.clientSecret,
redirectUri = googleAuthProperties.redirectUri
Expand All @@ -40,7 +41,7 @@ class FetchGoogleUserInfoUseCase(
val oauthToken = oauthRes.accessToken

val userInfo = googleUserInfoFeginClient.getUserInfo("Bearer $oauthToken")
val email = userInfo.email as? String?: throw OAuth2EmailNotFoundException()
val email = userInfo.email ?: throw OAuth2EmailNotFoundException()
val regMember = memberRepository.findByOauthEmailAndStatus(email, MemberStatus.ACTIVE)
?: throw SignInMemberException()

Expand All @@ -53,11 +54,11 @@ class FetchGoogleUserInfoUseCase(
accessToken = jwtAccessToken,
refreshToken = jwtRefreshToken,
oauthEmail = regMember.oauthEmail,
oauthName = regMember.name?: throw SignInMemberException(),
role = regMember.role?: throw SignInMemberException(),
oauthName = regMember.name ?: throw SignInMemberException(),
role = regMember.role ?: throw SignInMemberException(),
provider = ProviderType.GOOGLE
)
} catch (e : FeignException) {
} catch (e: FeignException) {
throw OAuth2ProviderMissingException()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dobby.backend.application.usecase

import com.dobby.backend.domain.gateway.TokenGateway

class GenerateTestToken(
private val tokenGateway: TokenGateway
) : UseCase<GenerateTestToken.Input, GenerateTestToken.Output> {
data class Input(
val memberId: Long
)

data class Output(
val accessToken: String,
val refreshToken: String,
)

override fun execute(input: Input): Output {
val memberId = input.memberId
return Output(
accessToken = tokenGateway.generateAccessToken(memberId),
refreshToken = tokenGateway.generateRefreshToken(memberId)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dobby.backend.application.usecase

import com.dobby.backend.domain.gateway.TokenGateway

class GenerateTokenWithRefreshToken(
private val tokenGateway: TokenGateway
) : UseCase<GenerateTokenWithRefreshToken.Input, GenerateTokenWithRefreshToken.Output> {
data class Input(
val refreshToken: String,
)

data class Output(
val accessToken: String,
val refreshToken: String,
val memberId: Long
)

override fun execute(input: Input): Output {
val memberId = tokenGateway.extractMemberIdFromRefreshToken(input.refreshToken).toLong()
return Output(
accessToken = tokenGateway.generateAccessToken(memberId),
refreshToken = tokenGateway.generateRefreshToken(memberId),
memberId = memberId
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dobby.backend.application.usecase

import com.dobby.backend.domain.gateway.MemberGateway
import com.dobby.backend.domain.model.Member

class GetMemberById(
private val memberGateway: MemberGateway,
) : UseCase<GetMemberById.Input, Member> {
data class Input(
val memberId: Long,
)

override fun execute(input: Input): Member {
return memberGateway.getById(input.memberId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.dobby.backend.application.usecase

fun interface UseCase<I, O> {
fun execute(input: I): O
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dobby.backend.domain.gateway

import com.dobby.backend.domain.model.Member

interface MemberGateway {
fun getById(memberId: Long): Member
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.dobby.backend.domain.gateway

import com.dobby.backend.domain.model.Member

interface TokenGateway {
fun generateAccessToken(member: Member): String
fun generateRefreshToken(member: Member): String
fun generateAccessToken(memberId: Long): String
fun generateRefreshToken(memberId: Long): String
fun extractMemberIdFromRefreshToken(token: String): String
}
33 changes: 26 additions & 7 deletions src/main/kotlin/com/dobby/backend/domain/model/Member.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
package com.dobby.backend.domain.model

import com.dobby.backend.util.generateULID
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
import java.time.LocalDate

data class Member(
val memberId: String,
val name: String,
val email: String,
val memberId: Long,
val name: String?,
val oauthEmail: String,
val contactEmail: String?,
val provider: ProviderType,
val status: MemberStatus,
val role: RoleType?,
val birthDate: LocalDate?
) {

companion object {
fun newMember(
memberId: Long,
name: String,
email: String,
oauthEmail: String,
contactEmail: String,
provider: ProviderType,
status: MemberStatus,
role: RoleType,
birthDate: LocalDate,
) = Member(
memberId = generateULID(),
memberId = memberId,
name = name,
email = email,
oauthEmail = oauthEmail,
contactEmail = contactEmail,
provider = provider,
status = status,
role = role,
birthDate = birthDate,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dobby.backend.infrastructure.database.entity

import AuditingEntity
import com.dobby.backend.domain.model.Member
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
Expand All @@ -10,7 +11,7 @@ import java.time.LocalDate
@Entity(name = "member")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "role_type")
open class Member (
class MemberEntity (
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Expand Down Expand Up @@ -39,4 +40,31 @@ open class Member (

@Column(name = "birth_date", nullable = true)
val birthDate : LocalDate?,
) : AuditingEntity()
) : AuditingEntity() {

fun toDomain() = Member(
memberId = id,
name = name,
oauthEmail = oauthEmail,
contactEmail = contactEmail,
provider = provider,
status = status,
role = role,
birthDate = birthDate
)

companion object {
fun fromDomain(member: Member) = with(member) {
MemberEntity(
id = memberId,
name = name,
oauthEmail = oauthEmail,
contactEmail = contactEmail,
provider = provider,
status = status,
role = role,
birthDate = birthDate
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import java.time.LocalDate

@Entity(name = "participant")
@DiscriminatorValue("PARTICIPANT")
class Participant (
class ParticipantEntity (
@OneToOne
@JoinColumn(name = "member_id", nullable = false)
val member: Member,
val member: MemberEntity,

@Column(name = "gender", nullable = false)
@Enumerated(EnumType.STRING)
Expand Down Expand Up @@ -46,7 +46,7 @@ class Participant (
contactEmail: String,
name: String,
birthDate: LocalDate
) : Member(
) : MemberEntity(
id= id,
oauthEmail = oauthEmail,
provider = provider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import java.time.LocalDate

@Entity(name = "researcher")
@DiscriminatorValue("RESEARCHER")
class Researcher (
class ResearcherEntity (
@OneToOne
@JoinColumn(name = "member_id", nullable = false)
val member: Member,
val member: MemberEntity,

@Column(name = "univ_email", length = 100, nullable = false)
val univEmail : String,
Expand All @@ -33,7 +33,7 @@ class Researcher (
contactEmail: String,
name: String,
birthDate: LocalDate
) : Member(
) : MemberEntity(
id= id,
oauthEmail = oauthEmail,
provider = provider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ abstract class AuditingEntity {
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
lateinit var updatedAt: LocalDateTime
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.dobby.backend.infrastructure.database.entity.enum

enum class ProviderType {
NAVER, GOOGLE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.dobby.backend.infrastructure.database.entity.enum

enum class RoleType {
RESEARCHER, PARTICIPANT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dobby.backend.infrastructure.database.repository

import com.dobby.backend.infrastructure.database.entity.MemberEntity
import org.springframework.data.jpa.repository.JpaRepository

interface MemberJpaRepository : JpaRepository<MemberEntity, Long> {
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.dobby.backend.infrastructure.database.repository

import com.dobby.backend.infrastructure.database.entity.Member
import com.dobby.backend.infrastructure.database.entity.MemberEntity
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

interface MemberRepository : JpaRepository<Member, Long> {
fun findByOauthEmailAndStatus(oauthEmail: String, status: MemberStatus): Member?
interface MemberRepository : JpaRepository<MemberEntity, Long> {
fun findByOauthEmailAndStatus(oauthEmail: String, status: MemberStatus): MemberEntity?
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.dobby.backend.infrastructure.database.repository

import com.dobby.backend.infrastructure.database.entity.Participant
import com.dobby.backend.infrastructure.database.entity.ParticipantEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

interface ParticipantRepository: JpaRepository<Participant, Long> {
}
interface ParticipantRepository: JpaRepository<ParticipantEntity, Long> {
}
Loading

0 comments on commit 88e6ff5

Please sign in to comment.