diff --git a/.github/workflows/gradle_task.yml b/.github/workflows/gradle_task.yml new file mode 100644 index 0000000..e6e93a4 --- /dev/null +++ b/.github/workflows/gradle_task.yml @@ -0,0 +1,41 @@ +on: + workflow_call: + inputs: + tasks: + description: gradle execute option + type: string + +jobs: + gradle-task: + strategy: + matrix: + gradle: ${{ fromJSON(inputs.tasks) }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: true + + - name: Gradle cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Gradle Task + run: ./gradlew ${{matrix.gradle}} diff --git a/.github/workflows/pr_ci_checker.yml b/.github/workflows/pr_ci_checker.yml index 615ef31..9359d46 100644 --- a/.github/workflows/pr_ci_checker.yml +++ b/.github/workflows/pr_ci_checker.yml @@ -11,95 +11,57 @@ permissions: pull-requests: write jobs: - ktlint: + ci: + uses: ./.github/workflows/gradle_task.yml + with: + tasks: '["ktlintCheck", "build -x test -x ktlintCheck", "test"]' + secrets: inherit + + publish-test-result: + needs: + - ci runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 with: - token: ${{ secrets.CI_TOKEN }} - submodules: true + files: | + module-domain/build/test-results/**/*.xml + module-infrastructure/build/test-results/**/*.xml + module-usecase/build/test-results/**/*.xml - - name: Gradle cache - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: ktlintCheck - run: ./gradlew ktlintCheck - build: + result-success: + needs: + - ci + if: success() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.CI_TOKEN }} - submodules: true - - - name: Gradle cache - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: build - run: ./gradlew build -x test -x ktlintCheck - - test: + - name: On Success!! Congratulations + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: '#53A551' + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_TITLE: 'Uni/PR Check S.U.C.C.E.S.S ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰' + SLACK_ICON: ${{ github.event.org.avatar_url }} + MSG_MINIMAL: event,actions url, commit + SLACK_USERNAME: Uni-server + SLACK_MESSAGE: '${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.number}}' + + result-failure: + needs: + - ci + if: failure() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.CI_TOKEN }} - submodules: true - - - name: Gradle cache - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: test - run: ./gradlew test + - name: On Failed, Notify in Slack + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: '#ff0000' + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_TITLE: 'Uni/Server Debug build FailโŒ ์—๋Ÿฌ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”' + SLACK_ICON: ${{ github.event.org.avatar_url }} + MSG_MINIMAL: event,actions url, commit + SLACK_USERNAME: Uni-server + SLACK_MESSAGE: '${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.number}}' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f21800a..dc625d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ springdoc = "2.1.0" jjwt = "0.11.5" kotlin = "1.8.22" ktlint = "11.0.0" +mockk = "1.13.7" service = "0.0.1-SNAPSHOT" @@ -47,6 +48,7 @@ test-spring-boot-starter = { module = "org.springframework.boot:spring-boot-star test-spring-security = { module = "org.springframework.security:spring-security-test" } test-junit5-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] # common @@ -62,3 +64,8 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } # kotlin [bundles] +jjwt = [ + "jjwt-api", + "jjwt-impl", + "jjwt-jackson" +] diff --git a/module-api/src/main/kotlin/universe/sparkle/moduleapi/ModuleApiApplication.kt b/module-api/src/main/kotlin/universe/sparkle/moduleapi/ModuleApiApplication.kt deleted file mode 100644 index e0d88c9..0000000 --- a/module-api/src/main/kotlin/universe/sparkle/moduleapi/ModuleApiApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package universe.sparkle.moduleapi - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class ModuleApiApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/module-api/src/test/kotlin/universe/sparkle/moduleapi/ModuleApiApplicationTests.kt b/module-api/src/test/kotlin/universe/sparkle/moduleapi/ModuleApiApplicationTests.kt deleted file mode 100644 index bf8cfc8..0000000 --- a/module-api/src/test/kotlin/universe/sparkle/moduleapi/ModuleApiApplicationTests.kt +++ /dev/null @@ -1,12 +0,0 @@ -package universe.sparkle.moduleapi - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ModuleApiApplicationTests { - - @Test - fun contextLoads() { - } -} diff --git a/module-core-domain/src/main/kotlin/universe/sparkle/modulecoredomain/ModuleCoreDomainApplication.kt b/module-core-domain/src/main/kotlin/universe/sparkle/modulecoredomain/ModuleCoreDomainApplication.kt deleted file mode 100644 index ed5e64c..0000000 --- a/module-core-domain/src/main/kotlin/universe/sparkle/modulecoredomain/ModuleCoreDomainApplication.kt +++ /dev/null @@ -1,4 +0,0 @@ -package universe.sparkle.modulecoredomain - -fun main(args: Array) { -} diff --git a/module-data/build.gradle b/module-data/build.gradle deleted file mode 100644 index 0bf97e4..0000000 --- a/module-data/build.gradle +++ /dev/null @@ -1,55 +0,0 @@ -plugins { - alias libs.plugins.spring.boot - alias libs.plugins.spring.dependency.manage - alias libs.plugins.kotlin.jvm - alias libs.plugins.kotlin.plugin.spring - alias libs.plugins.kotlin.plugin.jpa -} - -version = libs.versions.service.get() - -java { - sourceCompatibility = '17' -} - -kotlin { - jvmToolchain(17) -} - -repositories { - mavenCentral() -} - -allOpen { - annotation("jakarta.persistence.Entity") - annotation("jakarta.persistence.MappedSuperclass") - annotation("jakarta.persistence.Embeddable") -} - -noArg { - annotation("jakarta.persistence.Entity") -} - -dependencies { - implementation project(':module-service') - implementation project(':module-core-domain') - implementation libs.spring.boot.starter.jpa - implementation libs.kotlin.reflect - - runtimeOnly libs.db.mysql - - testImplementation libs.test.spring.boot.starter -} - -tasks.named('test') { - useJUnitPlatform() -} - -copy { - from('../uni-kotlin-config') - include('src/**/*.yml') - into('.') - eachFile { file -> - println "> copy file: ${file.sourcePath}" - } -} diff --git a/module-data/settings.gradle b/module-data/settings.gradle deleted file mode 100644 index fb5d311..0000000 --- a/module-data/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -rootProject.name = 'module-data' - -dependencyResolutionManagement { - versionCatalogs { - libs { - from(files("../gradle/libs.versions.toml")) - } - } -} diff --git a/module-data/src/main/kotlin/universe/sparkle/moduledata/ModuleDataApplication.kt b/module-data/src/main/kotlin/universe/sparkle/moduledata/ModuleDataApplication.kt deleted file mode 100644 index d109d52..0000000 --- a/module-data/src/main/kotlin/universe/sparkle/moduledata/ModuleDataApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package universe.sparkle.moduledata - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class ModuleDataApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/module-data/src/main/resources/application.properties b/module-data/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/module-data/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/module-data/src/test/kotlin/universe/sparkle/moduledata/ModuleDataApplicationTests.kt b/module-data/src/test/kotlin/universe/sparkle/moduledata/ModuleDataApplicationTests.kt deleted file mode 100644 index b6df43e..0000000 --- a/module-data/src/test/kotlin/universe/sparkle/moduledata/ModuleDataApplicationTests.kt +++ /dev/null @@ -1,12 +0,0 @@ -package universe.sparkle.moduledata - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ModuleDataApplicationTests { - - @Test - fun contextLoads() { - } -} diff --git a/module-api/.gitignore b/module-domain/.gitignore similarity index 100% rename from module-api/.gitignore rename to module-domain/.gitignore diff --git a/module-core-domain/build.gradle b/module-domain/build.gradle similarity index 100% rename from module-core-domain/build.gradle rename to module-domain/build.gradle diff --git a/module-api/gradle/wrapper/gradle-wrapper.jar b/module-domain/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from module-api/gradle/wrapper/gradle-wrapper.jar rename to module-domain/gradle/wrapper/gradle-wrapper.jar diff --git a/module-api/gradle/wrapper/gradle-wrapper.properties b/module-domain/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from module-api/gradle/wrapper/gradle-wrapper.properties rename to module-domain/gradle/wrapper/gradle-wrapper.properties diff --git a/module-api/gradlew b/module-domain/gradlew similarity index 100% rename from module-api/gradlew rename to module-domain/gradlew diff --git a/module-api/gradlew.bat b/module-domain/gradlew.bat similarity index 100% rename from module-api/gradlew.bat rename to module-domain/gradlew.bat diff --git a/module-core-domain/settings.gradle b/module-domain/settings.gradle similarity index 100% rename from module-core-domain/settings.gradle rename to module-domain/settings.gradle diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/JwtConfigContract.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/JwtConfigContract.kt new file mode 100644 index 0000000..8d18291 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/JwtConfigContract.kt @@ -0,0 +1,13 @@ +package universe.sparkle.domain + +import java.time.ZoneId + +interface JwtConfigContract { + fun getSecret(): String + fun getAccessExpiryPeriodDay(): Long + fun getRefreshExpiryPeriodDay(): Long + + companion object { + val zoneIdKST = ZoneId.of("Asia/Seoul") + } +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/SnsType.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/SnsType.kt new file mode 100644 index 0000000..fd99eca --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/SnsType.kt @@ -0,0 +1,12 @@ +package universe.sparkle.domain + +enum class SnsType { + KAKAO, GOOGLE, APPLE; + + companion object { + fun findSnsTypeBy(snsTypeName: String): SnsType { + return values().find { it.name == snsTypeName } + ?: throw IllegalArgumentException("Unsupported social login type: $snsTypeName") + } + } +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/BadRequest.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/BadRequest.kt new file mode 100644 index 0000000..9097e83 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/BadRequest.kt @@ -0,0 +1,34 @@ +package universe.sparkle.domain.exception + +sealed class BadRequest( + code: Int, + message: String, +) : BusinessException(code, message) { + data class InvalidRequestMethod( + override val message: String = "์ž˜๋ชป๋œ ๋ฐฉ์‹์˜ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", + ) : BadRequest(1001, message) + + data class AlreadyGameCreated( + override val message: String = "์ด๋ฏธ ์Šน๋ถ€๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + ) : BadRequest(1002, message) + + data class UserNotExistent( + override val message: String = "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €์˜ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", + ) : BadRequest(1003, message) + + data class AlreadyGameDone( + override val message: String = "์ด๋ฏธ ๊ฒŒ์ž„์ด ์ข…๋ฃŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + ) : BadRequest(1004, message) + + data class CoupleNotExistent( + override val message: String = "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปคํ”Œ์˜ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", + ) : BadRequest(1005, message) + + data class InvalidInviteCode( + override val message: String = "์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์ดˆ๋Œ€ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.", + ) : BadRequest(1006, message) + + data class PartnerResultNotEntered( + override val message: String = "์ƒ๋Œ€๋ฐฉ์ด ์•„์ง ๊ฒฐ๊ณผ๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", + ) : BadRequest(1007, message) +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/BusinessException.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/BusinessException.kt new file mode 100644 index 0000000..a0a5a9a --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/BusinessException.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class BusinessException( + open val code: Int, + override val message: String, +) : RuntimeException(message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/Conflict.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/Conflict.kt new file mode 100644 index 0000000..4e8df49 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/Conflict.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class Conflict( + override val code: Int, + override val message: String, +) : BusinessException(code, message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/InternalServerError.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/InternalServerError.kt new file mode 100644 index 0000000..e99eace --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/InternalServerError.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class InternalServerError( + override val code: Int, + override val message: String, +) : BusinessException(code, message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/NotAcceptable.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/NotAcceptable.kt new file mode 100644 index 0000000..7d7b318 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/NotAcceptable.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class NotAcceptable( + override val code: Int, + override val message: String, +) : BusinessException(code, message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/NotFound.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/NotFound.kt new file mode 100644 index 0000000..26bd26f --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/NotFound.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class NotFound( + override val code: Int, + override val message: String, +) : BusinessException(code, message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/ServiceUnavailable.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/ServiceUnavailable.kt new file mode 100644 index 0000000..e6eb83d --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/ServiceUnavailable.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class ServiceUnavailable( + override val code: Int, + override val message: String, +) : BusinessException(code, message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/Unauthorized.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/Unauthorized.kt new file mode 100644 index 0000000..f063fda --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/Unauthorized.kt @@ -0,0 +1,18 @@ +package universe.sparkle.domain.exception + +sealed class Unauthorized( + code: Int, + message: String, +) : BusinessException(code, message) { + data class TokenNotExistent( + override val message: String = "ํ† ํฐ ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", + ) : Unauthorized(2001, message) + + data class ExpiredToken( + override val message: String = "ํ† ํฐ์ด ๋งŒ๋ฃŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + ) : Unauthorized(2002, message) + + data class UnsupportedToken( + override val message: String = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์˜ ํ† ํฐ ํ˜น์€ ๋ณ€์กฐ๋œ ํ† ํฐ์„ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.", + ) : Unauthorized(2003, message) +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/exception/UnsupportedMediaType.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/UnsupportedMediaType.kt new file mode 100644 index 0000000..024640e --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/exception/UnsupportedMediaType.kt @@ -0,0 +1,6 @@ +package universe.sparkle.domain.exception + +sealed class UnsupportedMediaType( + override val code: Int, + override val message: String, +) : BusinessException(code, message) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/gateway/UserDetailGateway.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/gateway/UserDetailGateway.kt new file mode 100644 index 0000000..2a3147f --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/gateway/UserDetailGateway.kt @@ -0,0 +1,7 @@ +package universe.sparkle.domain.gateway + +import universe.sparkle.domain.model.AuthenticationToken + +interface UserDetailGateway { + fun loadUserById(userId: String?): AuthenticationToken +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/gateway/UserGateway.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/gateway/UserGateway.kt new file mode 100644 index 0000000..3b306bf --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/gateway/UserGateway.kt @@ -0,0 +1,8 @@ +package universe.sparkle.domain.gateway + +import universe.sparkle.domain.model.User + +interface UserGateway { + fun findUserById(userId: Long): User + fun saveUser(user: User) +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/model/AuthToken.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/model/AuthToken.kt new file mode 100644 index 0000000..d0928e0 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/model/AuthToken.kt @@ -0,0 +1,7 @@ +package universe.sparkle.domain.model + +data class AuthToken( + val accessToken: String, + val refreshToken: String? = null, + val coupleId: Long? = null, +) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/model/AuthenticationToken.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/model/AuthenticationToken.kt new file mode 100644 index 0000000..3cbf430 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/model/AuthenticationToken.kt @@ -0,0 +1,7 @@ +package universe.sparkle.domain.model + +data class AuthenticationToken( + val id: Long, + val nickname: String?, + val image: String?, +) diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/model/User.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/model/User.kt new file mode 100644 index 0000000..2c2998d --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/model/User.kt @@ -0,0 +1,41 @@ +package universe.sparkle.domain.model + +import universe.sparkle.domain.SnsType + +data class User( + val id: Long? = null, + val snsType: SnsType, + val snsAuthCode: String, + val nickname: String? = null, + val image: String? = null, + val fcmToken: String? = null, +) { + + init { + validNickname(nickname) + } + + fun updateProfile(updateNickname: String?, updateImage: String?): User { + validNickname(updateNickname) + return copy( + id = this.id, + snsType = this.snsType, + snsAuthCode = this.snsAuthCode, + nickname = updateNickname, + image = updateImage, + fcmToken = this.fcmToken, + ) + } + + private fun validNickname(value: String?) { + require(value?.let { it.length > 10 } ?: true) + } +} + +fun User.toAuthenticationToken() = this.id?.let { userId -> + AuthenticationToken( + id = userId, + nickname = this.nickname, + image = this.image, + ) +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/BeIssuedAuthTokenInputBoundary.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/BeIssuedAuthTokenInputBoundary.kt new file mode 100644 index 0000000..037b0fc --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/BeIssuedAuthTokenInputBoundary.kt @@ -0,0 +1,14 @@ +package universe.sparkle.domain.usecase + +import universe.sparkle.domain.model.AuthToken +import universe.sparkle.domain.model.User + +interface BeIssuedAuthTokenInputBoundary { + operator fun invoke(user: User): AuthToken + operator fun invoke(user: User, accessExpiryPeriodDay: Long, refreshExpiryPeriodDay: Long): AuthToken + + companion object { + const val ACCESS_TOKEN_SUBJECT = "AccessToken" + const val CLAIMS_USER_ID = "userId" + } +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/ExtractTokenInputBoundary.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/ExtractTokenInputBoundary.kt new file mode 100644 index 0000000..f90d848 --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/ExtractTokenInputBoundary.kt @@ -0,0 +1,8 @@ +package universe.sparkle.domain.usecase + +import universe.sparkle.domain.model.AuthenticationToken + +interface ExtractTokenInputBoundary { + operator fun invoke(token: String): AuthenticationToken + fun getExpiredUserAuthenticationToken(token: String): AuthenticationToken +} diff --git a/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/LoadUserInputBoundary.kt b/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/LoadUserInputBoundary.kt new file mode 100644 index 0000000..12d724e --- /dev/null +++ b/module-domain/src/main/kotlin/universe/sparkle/domain/usecase/LoadUserInputBoundary.kt @@ -0,0 +1,3 @@ +package universe.sparkle.domain.usecase + +interface LoadUserInputBoundary diff --git a/module-core-domain/src/test/kotlin/universe/sparkle/modulecoredomain/ModuleCoreDomainApplicationTests.kt b/module-domain/src/test/kotlin/universe/sparkle/domain/ModuleCoreDomainApplicationTests.kt similarity index 74% rename from module-core-domain/src/test/kotlin/universe/sparkle/modulecoredomain/ModuleCoreDomainApplicationTests.kt rename to module-domain/src/test/kotlin/universe/sparkle/domain/ModuleCoreDomainApplicationTests.kt index 210e8f2..1932999 100644 --- a/module-core-domain/src/test/kotlin/universe/sparkle/modulecoredomain/ModuleCoreDomainApplicationTests.kt +++ b/module-domain/src/test/kotlin/universe/sparkle/domain/ModuleCoreDomainApplicationTests.kt @@ -1,4 +1,4 @@ -package universe.sparkle.modulecoredomain +package universe.sparkle.domain import org.junit.jupiter.api.Test diff --git a/module-core-domain/.gitignore b/module-infrastructure/.gitignore similarity index 100% rename from module-core-domain/.gitignore rename to module-infrastructure/.gitignore diff --git a/module-api/build.gradle b/module-infrastructure/build.gradle similarity index 71% rename from module-api/build.gradle rename to module-infrastructure/build.gradle index 03552aa..2f8e8b8 100644 --- a/module-api/build.gradle +++ b/module-infrastructure/build.gradle @@ -3,6 +3,7 @@ plugins { alias libs.plugins.spring.dependency.manage alias libs.plugins.kotlin.jvm alias libs.plugins.kotlin.plugin.spring + alias libs.plugins.kotlin.plugin.jpa } group = 'universe.spakle' @@ -31,22 +32,37 @@ repositories { } dependencies { - implementation project(':module-service') - implementation project(':module-core-domain') + implementation project(':module-usecase') + implementation project(':module-domain') implementation libs.spring.boot.starter.web implementation libs.spring.boot.starter.security implementation libs.spring.boot.starter.validation implementation libs.spring.boot.starter.actuator + implementation libs.spring.boot.starter.jpa + implementation libs.jackson.kotlin implementation libs.kotlin.reflect + implementation libs.db.mysql + + implementation libs.springdoc developmentOnly libs.spring.boot.devtools testImplementation libs.test.spring.boot.starter testImplementation libs.test.spring.security } +allOpen { + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") +} + +noArg { + annotation("jakarta.persistence.Entity") +} + tasks.named('test') { useJUnitPlatform() } diff --git a/module-core-domain/gradle/wrapper/gradle-wrapper.jar b/module-infrastructure/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from module-core-domain/gradle/wrapper/gradle-wrapper.jar rename to module-infrastructure/gradle/wrapper/gradle-wrapper.jar diff --git a/module-core-domain/gradle/wrapper/gradle-wrapper.properties b/module-infrastructure/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from module-core-domain/gradle/wrapper/gradle-wrapper.properties rename to module-infrastructure/gradle/wrapper/gradle-wrapper.properties diff --git a/module-core-domain/gradlew b/module-infrastructure/gradlew similarity index 100% rename from module-core-domain/gradlew rename to module-infrastructure/gradlew diff --git a/module-core-domain/gradlew.bat b/module-infrastructure/gradlew.bat similarity index 100% rename from module-core-domain/gradlew.bat rename to module-infrastructure/gradlew.bat diff --git a/module-api/settings.gradle b/module-infrastructure/settings.gradle similarity index 100% rename from module-api/settings.gradle rename to module-infrastructure/settings.gradle diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/SparkleApplication.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/SparkleApplication.kt new file mode 100644 index 0000000..51e38b3 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/SparkleApplication.kt @@ -0,0 +1,13 @@ +package universe.sparkle.infrastructure + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = ["universe.sparkle.infrastructure", "universe.sparkle.usecase"]) +class SparkleApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/EnvironmentManager.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/EnvironmentManager.kt new file mode 100644 index 0000000..370f1af --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/EnvironmentManager.kt @@ -0,0 +1,13 @@ +package universe.sparkle.infrastructure.config + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.env.Environment +import org.springframework.stereotype.Component + +@Component +class EnvironmentManager @Autowired constructor( + private val environment: Environment, +) { + + val isDevelopment = environment.activeProfiles.contains("dev") +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/JwtConfig.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/JwtConfig.kt new file mode 100644 index 0000000..57e6cea --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/JwtConfig.kt @@ -0,0 +1,19 @@ +package universe.sparkle.infrastructure.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import universe.sparkle.domain.JwtConfigContract + +@Configuration +data class JwtConfig( + @Value("\${jwt.secret}") private val _secret: String, + @Value("\${jwt.accessExpiryPeriodDay}") private val _accessExpiryPeriodDay: Long, + @Value("\${jwt.refreshExpiryPeriodDay}") private val _refreshExpiryPeriodDay: Long, +) : JwtConfigContract { + + override fun getSecret() = _secret + + override fun getAccessExpiryPeriodDay() = _accessExpiryPeriodDay + + override fun getRefreshExpiryPeriodDay() = _refreshExpiryPeriodDay +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/SecurityConfig.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/SecurityConfig.kt new file mode 100644 index 0000000..8581efd --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/config/SecurityConfig.kt @@ -0,0 +1,51 @@ +package universe.sparkle.infrastructure.config + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import universe.sparkle.infrastructure.filter.JwtAuthenticationFilter +import universe.sparkle.infrastructure.filter.JwtExceptionFilter + +@Configuration +@EnableWebSecurity +class SecurityConfig @Autowired constructor( + private val jwtAuthenticationFilter: JwtAuthenticationFilter, + private val jwtExceptionFilter: JwtExceptionFilter, +) { + + @Bean + @Throws(Exception::class) + fun filterChain(http: HttpSecurity): SecurityFilterChain { + return http.csrf { it.disable() } + .formLogin { it.disable() } + .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } + .cors { it.configurationSource(corsConfigurationSource()) } + .authorizeHttpRequests { + it.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/auth/*", "/status/uni/*").permitAll() + it.requestMatchers("/api").authenticated() + }.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) + .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter::class.java) + .build() + } + + @Bean + fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { + val configuration = CorsConfiguration().apply { + addAllowedOrigin("http://uni-sparkle.kro.kr") + addAllowedOrigin("http://localhost:8080") + addAllowedHeader("*") + addAllowedMethod("*") + allowCredentials = true + } + return UrlBasedCorsConfigurationSource().also { + it.registerCorsConfiguration("/**", configuration) + } + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/controller/response/ErrorResponseDto.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/controller/response/ErrorResponseDto.kt new file mode 100644 index 0000000..5b69aba --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/controller/response/ErrorResponseDto.kt @@ -0,0 +1,29 @@ +package universe.sparkle.infrastructure.controller.response + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.http.HttpStatus +import universe.sparkle.domain.exception.BusinessException + +@Schema(description = "Error Response DTO") +data class ErrorResponseDto( + @JsonProperty("code") val errorCode: String, + @JsonProperty("message") val message: String, +) + +fun BusinessException.toErrorResponseDto(isDevelopment: Boolean = false) = ErrorResponseDto( + errorCode = "UE${this.code}", + message = if (isDevelopment) this.message else this.javaClass.name, +) + +fun BusinessException.toHttpStatus() = when (this.code) { + 500 -> HttpStatus.INTERNAL_SERVER_ERROR + 503 -> HttpStatus.SERVICE_UNAVAILABLE + in 1000 until 2000 -> HttpStatus.BAD_REQUEST + in 2000 until 3000 -> HttpStatus.UNAUTHORIZED + in 5000 until 6000 -> HttpStatus.NOT_FOUND + in 7000 until 8000 -> HttpStatus.NOT_ACCEPTABLE + in 10000 until 11000 -> HttpStatus.CONFLICT + in 16000 until 17000 -> HttpStatus.UNSUPPORTED_MEDIA_TYPE + else -> throw IllegalArgumentException("unsupported error code") +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/CachedInputStream.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/CachedInputStream.kt new file mode 100644 index 0000000..8b2ffe3 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/CachedInputStream.kt @@ -0,0 +1,34 @@ +package universe.sparkle.infrastructure.filter + +import jakarta.servlet.ReadListener +import jakarta.servlet.ServletInputStream +import java.io.ByteArrayInputStream +import java.io.IOException + +class CachedInputStream( + cachedInputStream: ByteArray, +) : ServletInputStream() { + + private val cachedBodyInputStream: ByteArrayInputStream + + init { + this.cachedBodyInputStream = ByteArrayInputStream(cachedInputStream) + } + + override fun read(): Int = cachedBodyInputStream.read() + + override fun isFinished(): Boolean { + try { + return cachedBodyInputStream.available() == 0 + } catch (exception: IOException) { + exception.printStackTrace() + } + return false + } + + override fun isReady(): Boolean = true + + override fun setReadListener(listener: ReadListener?) { + throw UnsupportedOperationException("Unsupported Operation setReadListener in CachedInputStream Object") + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/DuplicateAccessibleRequest.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/DuplicateAccessibleRequest.kt new file mode 100644 index 0000000..930b9b5 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/DuplicateAccessibleRequest.kt @@ -0,0 +1,21 @@ +package universe.sparkle.infrastructure.filter + +import jakarta.servlet.ServletInputStream +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletRequestWrapper +import org.springframework.util.StreamUtils + +class DuplicateAccessibleRequest( + request: HttpServletRequest, +) : HttpServletRequestWrapper(request) { + + private val cachedInputStream: ByteArray + + init { + this.cachedInputStream = StreamUtils.copyToByteArray(request.inputStream) + } + + override fun getInputStream(): ServletInputStream { + return CachedInputStream(cachedInputStream) + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/JwtAuthenticationFilter.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..34e77e6 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,55 @@ +package universe.sparkle.infrastructure.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import universe.sparkle.domain.exception.BadRequest +import universe.sparkle.domain.exception.Unauthorized +import universe.sparkle.domain.usecase.ExtractTokenInputBoundary +import universe.sparkle.infrastructure.security.UserDetail + +@Component +class JwtAuthenticationFilter @Autowired constructor( + private val extractTokenUseCase: ExtractTokenInputBoundary, +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + val uri = request.requestURI + if (isStartApiPath(uri)) { + val token = getJwtFromRequest(request) + setSecurityContextHolder(token) + } + filterChain.doFilter(request, response) + } + + private fun isStartApiPath(uri: String) = uri.startsWith("/api") + + private fun getJwtFromRequest(request: HttpServletRequest): String { + val tokenType = "Bearer " + return runCatching { + request.getHeader("Authentication").substring(tokenType.length) + }.getOrNull() ?: throw Unauthorized.TokenNotExistent() + } + + private fun setSecurityContextHolder(token: String) { + val userDetail: UserDetails = UserDetail.of(extractTokenUseCase(token)) + ?: throw BadRequest.UserNotExistent() + UsernamePasswordAuthenticationToken( + userDetail, + userDetail.password, + userDetail.authorities, + ).also { usernamePasswordAuthenticationToken -> + SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken + } + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/JwtExceptionFilter.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/JwtExceptionFilter.kt new file mode 100644 index 0000000..7a52733 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/JwtExceptionFilter.kt @@ -0,0 +1,51 @@ +package universe.sparkle.infrastructure.filter + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import universe.sparkle.domain.exception.BadRequest +import universe.sparkle.domain.exception.BusinessException +import universe.sparkle.domain.exception.Unauthorized +import universe.sparkle.infrastructure.config.EnvironmentManager +import universe.sparkle.infrastructure.controller.response.toErrorResponseDto +import universe.sparkle.infrastructure.controller.response.toHttpStatus + +@Component +class JwtExceptionFilter @Autowired constructor( + private val environmentManager: EnvironmentManager, +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + runCatching { + filterChain.doFilter(request, response) + }.getOrElse { + when (it) { + is BadRequest -> setErrorResponse(response, it) + is Unauthorized -> setErrorResponse(response, it) + else -> throw it + } + } + } + + private fun setErrorResponse( + response: HttpServletResponse, + exception: BusinessException, + ) { + val errorResponse = exception.toErrorResponseDto(environmentManager.isDevelopment) + with(response) { + characterEncoding = "UTF-8" + contentType = MediaType.APPLICATION_JSON_VALUE + status = exception.toHttpStatus().value() + writer.write(ObjectMapper().writeValueAsString(errorResponse)) + } + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/Logging.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/Logging.kt new file mode 100644 index 0000000..bc54771 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/Logging.kt @@ -0,0 +1,13 @@ +package universe.sparkle.infrastructure.filter + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +class Logging : ReadOnlyProperty { + + override fun getValue(thisRef: Any, property: KProperty<*>): Logger { + return LoggerFactory.getLogger(thisRef.javaClass) + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/RequestAndResponseLogFilter.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/RequestAndResponseLogFilter.kt new file mode 100644 index 0000000..bd09d25 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/filter/RequestAndResponseLogFilter.kt @@ -0,0 +1,80 @@ +package universe.sparkle.infrastructure.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.util.StreamUtils +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingResponseWrapper +import java.io.IOException +import java.io.InputStream + +@Component +class RequestAndResponseLogFilter : OncePerRequestFilter() { + val requestLogger by Logging() + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + if (isAsyncDispatch(request)) { + filterChain.doFilter(request, response) + } else { + val duplicateAccessibleRequest = DuplicateAccessibleRequest(request) + val cachingResponseWrapper = ContentCachingResponseWrapper(response) + loggingDoFilter(duplicateAccessibleRequest, cachingResponseWrapper, filterChain) + cachingResponseWrapper.copyBodyToResponse() + } + } + + private fun loggingDoFilter( + request: DuplicateAccessibleRequest, + response: ContentCachingResponseWrapper, + filterChain: FilterChain, + ) = try { + logRequest(request) + logPayload(request.contentType ?: "application/json", request.inputStream) + filterChain.doFilter(request, response) + } finally { + logResponse(response) + logPayload(response.contentType ?: "application/json", response.contentInputStream) + } + + @Throws(IOException::class) + private fun logRequest(request: DuplicateAccessibleRequest) = requestLogger.run { + info("<< Request : [${request.method}] ${request.requestURI}${request.queryString ?: ""}") + info("content-Type : ${request.contentType ?: "Unknown"}") + info("IP : ${request.remoteAddr}") + } + + @Throws(IOException::class) + private fun logResponse(response: ContentCachingResponseWrapper) = requestLogger.run { + info(">> Response : ${HttpStatus.valueOf(response.status)}") + info("Content-Type : ${response.contentType ?: "Unknown"}") + } + + private fun logPayload(contentType: String, inputStream: InputStream) = requestLogger.run { + if (isVisible(MediaType.valueOf(contentType))) { + val content = StreamUtils.copyToByteArray(inputStream) + info("Body : ${if (content.isNotEmpty()) String(content) else ""}") + } else { + info("Body : Binary Content") + } + } + + private fun isVisible(mediaType: MediaType): Boolean { + return listOf( + MediaType.valueOf("text/*"), + MediaType.APPLICATION_FORM_URLENCODED, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.valueOf("application/*+json"), + MediaType.valueOf("application/*+xml"), + MediaType.MULTIPART_FORM_DATA, + ).stream().anyMatch { it.includes(mediaType) } + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/entity/UserEntity.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/entity/UserEntity.kt new file mode 100644 index 0000000..8b48e6a --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/entity/UserEntity.kt @@ -0,0 +1,49 @@ +package universe.sparkle.infrastructure.persistence.entity + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import universe.sparkle.domain.SnsType +import universe.sparkle.infrastructure.persistence.entity.converter.SnsTypeAttributeConverter + +@Entity +@Table(name = "user") +class UserEntity( + id: Long? = null, + snsType: SnsType, + snsAuthCode: String, + nickname: String? = null, + image: String? = null, + fcmToken: String? = null, +) { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "user_id") + var id: Long? = id + protected set + + @Convert(converter = SnsTypeAttributeConverter::class) + @Column(name = "sns_type", nullable = false) + var snsType: SnsType = snsType + protected set + + @Column(name = "sns_auth", nullable = false) + var snsAuthCode: String = snsAuthCode + protected set + + @Column(name = "nickname", length = 10) + var nickname: String? = nickname + protected set + + @Column(name = "image") + var image: String? = image + protected set + + @Column(name = "fcm_token") + var fcmToken: String? = fcmToken + protected set +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/entity/converter/SnsTypeAttributeConverter.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/entity/converter/SnsTypeAttributeConverter.kt new file mode 100644 index 0000000..480859f --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/entity/converter/SnsTypeAttributeConverter.kt @@ -0,0 +1,18 @@ +package universe.sparkle.infrastructure.persistence.entity.converter + +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import universe.sparkle.domain.SnsType + +@Converter +class SnsTypeAttributeConverter : AttributeConverter { + + override fun convertToDatabaseColumn(attribute: SnsType?): String { + return attribute?.name ?: throw IllegalArgumentException("can not convert SnsType to database column") + } + + override fun convertToEntityAttribute(dbData: String?): SnsType { + return dbData?.let { SnsType.findSnsTypeBy(it) } + ?: throw IllegalArgumentException("DB data is not null") + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/gateway/UserAdapter.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/gateway/UserAdapter.kt new file mode 100644 index 0000000..016ed30 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/gateway/UserAdapter.kt @@ -0,0 +1,29 @@ +package universe.sparkle.infrastructure.persistence.gateway + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Repository +import universe.sparkle.domain.exception.BadRequest +import universe.sparkle.domain.gateway.UserGateway +import universe.sparkle.domain.model.User +import universe.sparkle.infrastructure.persistence.entity.UserEntity +import universe.sparkle.infrastructure.persistence.mapper.toDomain +import universe.sparkle.infrastructure.persistence.mapper.toEntity +import universe.sparkle.infrastructure.persistence.repository.UserJpaRepository + +@Repository +internal class UserAdapter @Autowired constructor( + private val userJpaRepository: UserJpaRepository, +) : UserGateway { + + override fun findUserById(userId: Long): User { + val userEntity: UserEntity = userJpaRepository.findByIdOrNull(userId) + ?: throw BadRequest.UserNotExistent() + return userEntity.toDomain() + } + + override fun saveUser(user: User) { + user.toEntity() + .also { userEntity -> userJpaRepository.save(userEntity) } + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/mapper/UserMapper.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/mapper/UserMapper.kt new file mode 100644 index 0000000..b61ceec --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/mapper/UserMapper.kt @@ -0,0 +1,23 @@ +package universe.sparkle.infrastructure.persistence.mapper + +import universe.sparkle.domain.model.User +import universe.sparkle.infrastructure.persistence.entity.UserEntity +import java.lang.IllegalArgumentException + +fun UserEntity.toDomain() = User( + id = this.id ?: throw IllegalArgumentException("ID has not been initialized yet"), + snsType = this.snsType, + snsAuthCode = this.snsAuthCode, + nickname = this.nickname, + image = this.image, + fcmToken = this.fcmToken, +) + +fun User.toEntity() = UserEntity( + id = this.id, + snsType = this.snsType, + snsAuthCode = this.snsAuthCode, + nickname = this.nickname, + image = this.image, + fcmToken = this.fcmToken, +) diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/repository/UserJpaRepository.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/repository/UserJpaRepository.kt new file mode 100644 index 0000000..766267a --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/persistence/repository/UserJpaRepository.kt @@ -0,0 +1,6 @@ +package universe.sparkle.infrastructure.persistence.repository + +import org.springframework.data.jpa.repository.JpaRepository +import universe.sparkle.infrastructure.persistence.entity.UserEntity + +interface UserJpaRepository : JpaRepository diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetail.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetail.kt new file mode 100644 index 0000000..831fb32 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetail.kt @@ -0,0 +1,45 @@ +package universe.sparkle.infrastructure.security + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import universe.sparkle.domain.model.AuthenticationToken +import java.util.stream.Collectors + +class UserDetail constructor( + val id: Long, + private val nickname: String?, + val image: String?, + private val authorities: List = listOf(SimpleGrantedAuthority("User")), +) : UserDetails { + + override fun getAuthorities(): MutableCollection { + return authorities.stream().collect( + Collectors.toSet(), + ) + } + + override fun getPassword(): String? = null + + override fun getUsername(): String? = nickname + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = true + + companion object { + fun of(user: AuthenticationToken): UserDetail? { + return user.id?.let { userId -> + UserDetail( + id = userId, + nickname = user.nickname, + image = user.image, + ) + } + } + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetailAdapter.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetailAdapter.kt new file mode 100644 index 0000000..e7abfba --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetailAdapter.kt @@ -0,0 +1,34 @@ +package universe.sparkle.infrastructure.security + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Repository +import universe.sparkle.domain.exception.BadRequest +import universe.sparkle.domain.exception.Unauthorized +import universe.sparkle.domain.gateway.UserDetailGateway +import universe.sparkle.domain.model.AuthenticationToken +import universe.sparkle.domain.model.toAuthenticationToken +import universe.sparkle.infrastructure.persistence.mapper.toDomain +import universe.sparkle.infrastructure.persistence.repository.UserJpaRepository + +@Repository +class UserDetailAdapter @Autowired constructor( + private val userJpaRepository: UserJpaRepository, +) : UserDetailGateway, UserDetailsService { + + override fun loadUserByUsername(username: String?): UserDetails { + val userId = username?.toLong() ?: throw Unauthorized.UnsupportedToken() + val user = userJpaRepository.findByIdOrNull(userId)?.toDomain() + ?: throw BadRequest.UserNotExistent() + val userAuthenticationToken = user.toAuthenticationToken() ?: throw BadRequest.UserNotExistent() + return UserDetail.of(userAuthenticationToken) ?: throw BadRequest.UserNotExistent() + } + + override fun loadUserById(userId: String?): AuthenticationToken { + return (this.loadUserByUsername(userId) as? UserDetail) + ?.toAuthenticationToken() + ?: throw IllegalArgumentException("can not change userDetail interface to UserDetailModel") + } +} diff --git a/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetailMapper.kt b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetailMapper.kt new file mode 100644 index 0000000..18893b5 --- /dev/null +++ b/module-infrastructure/src/main/kotlin/universe/sparkle/infrastructure/security/UserDetailMapper.kt @@ -0,0 +1,9 @@ +package universe.sparkle.infrastructure.security + +import universe.sparkle.domain.model.AuthenticationToken + +fun UserDetail.toAuthenticationToken() = AuthenticationToken( + id = this.id, + nickname = this.username, + image = this.image, +) diff --git a/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/gateway/UserAdapterTest.kt b/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/gateway/UserAdapterTest.kt new file mode 100644 index 0000000..62ec978 --- /dev/null +++ b/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/gateway/UserAdapterTest.kt @@ -0,0 +1,45 @@ +package universe.sparkle.infrastructure.persistence.gateway + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import universe.sparkle.domain.SnsType +import universe.sparkle.domain.exception.BadRequest +import universe.sparkle.domain.gateway.UserGateway +import universe.sparkle.domain.model.User + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class UserAdapterTest @Autowired constructor( + private val userAdapter: UserGateway, +) { + @Test + fun ์ง€์ •๋œ_์•„์ด๋””๋กœ_์œ ์ €๋ฅผ_์ฐพ์„_์ˆ˜_์žˆ๋‹ค() { + // given + ์œ ์ €_์•„์ด๋””_1์ธ_์œ ์ €๋ฅผ_์ €์žฅํ•œ๋‹ค() + // when + val actualUser = userAdapter.findUserById(1L) + // then + assertThat(actualUser.id).isEqualTo(1L) + } + + private fun ์œ ์ €_์•„์ด๋””_1์ธ_์œ ์ €๋ฅผ_์ €์žฅํ•œ๋‹ค() { + User( + id = 1L, + snsType = SnsType.KAKAO, + snsAuthCode = "Test", + ).also { user -> + userAdapter.saveUser(user) + } + } + + @Test + fun ์œ ์ €๊ฐ€_์กด์žฌํ•˜์ง€_์•Š๋Š”๋‹ค๋ฉด_์œ ์ €๊ฐ€_์—†๋‹ค๋Š”_์—๋Ÿฌ๋ฅผ_๋„์›Œ์•ผํ•œ๋‹ค() { + assertThatThrownBy { userAdapter.findUserById(1L) } + .isInstanceOf(BadRequest.UserNotExistent::class.java) + .hasMessage("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €์˜ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.") + } +} diff --git a/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/mapper/UserMapperTest.kt b/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/mapper/UserMapperTest.kt new file mode 100644 index 0000000..67bbab1 --- /dev/null +++ b/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/mapper/UserMapperTest.kt @@ -0,0 +1,51 @@ +package universe.sparkle.infrastructure.persistence.mapper + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import universe.sparkle.domain.SnsType +import universe.sparkle.domain.model.User +import universe.sparkle.infrastructure.persistence.entity.UserEntity + +class UserMapperTest { + @Test + fun ์œ ์ €_์—”ํ‹ฐํ‹ฐ๋ฅผ_๋„๋ฉ”์ธ_๋ชจ๋ธ๋กœ_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { + // given + val expectedUserEntity = UserEntity( + id = 1L, + snsType = SnsType.APPLE, + snsAuthCode = "testAuthCode", + ) + // when + val user = expectedUserEntity.toDomain() + // then + assertThat(user).isInstanceOf(User::class.java) + } + + @Test + fun ์œ ์ €_๋„๋ฉ”์ธ_๋ชจ๋ธ์„_์—”ํ‹ฐํ‹ฐ๋กœ_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { + // given + val expectedUser = User( + snsType = SnsType.GOOGLE, + snsAuthCode = "test", + ) + // when + val userEntity = expectedUser.toEntity() + // then + assertThat(userEntity).isInstanceOf(UserEntity::class.java) + } + + @Test + fun ์œ ์ €_์—”ํ‹ฐํ‹ฐ์—์„œ_์•„์ด๋””๊ฐ€_์ง€์ •๋˜์ง€_์•Š์œผ๋ฉด_๋„๋ฉ”์ธ_๋ชจ๋ธ๋กœ_๋ณ€๊ฒฝ_ํ• _์ˆ˜_์—†๋‹ค() { + // given + val expectedUserEntity = UserEntity( + snsType = SnsType.APPLE, + snsAuthCode = "testAuthCode", + ) + // when + // then + assertThatThrownBy { expectedUserEntity.toDomain() } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("ID has not been initialized yet") + } +} diff --git a/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/repository/UserJpaRepositoryTest.kt b/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/repository/UserJpaRepositoryTest.kt new file mode 100644 index 0000000..e74d040 --- /dev/null +++ b/module-infrastructure/src/test/kotlin/universe/sparkle/infrastructure/persistence/repository/UserJpaRepositoryTest.kt @@ -0,0 +1,39 @@ +package universe.sparkle.infrastructure.persistence.repository + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import universe.sparkle.domain.SnsType +import universe.sparkle.infrastructure.persistence.entity.UserEntity + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class UserJpaRepositoryTest @Autowired constructor( + private val userJpaRepository: UserJpaRepository, +) { + + @ParameterizedTest + @CsvSource("KAKAO,kakaoTestAuthCode", "GOOGLE, googleTestAuthCode", "APPLE, appleTestAuthCode") + fun ์œ ์ €์˜_์†Œ์„ค_๋กœ๊ทธ์ธ_์ •๋ณด๊ฐ€_์ฃผ์–ด์ง€๋ฉด_์œ ์ €๋ฅผ_์ƒ์„ฑํ•˜์—ฌ_์œ ์ €๋ฅผ_์ €์žฅํ• _์ˆ˜_์žˆ๋‹ค( + expectedSnsType: SnsType, + expectedAuthCode: String, + ) { + // given + val expectedUser = UserEntity( + snsType = expectedSnsType, + snsAuthCode = expectedAuthCode, + ) + // when + val actualUser = userJpaRepository.save(expectedUser) + // then + assertAll( + { assertThat(actualUser.id).isNotNull() }, + { assertThat(actualUser.snsType).isEqualTo(expectedSnsType) }, + { assertThat(actualUser.snsAuthCode).isEqualTo(expectedAuthCode) }, + ) + } +} diff --git a/module-service/.gitignore b/module-service/.gitignore deleted file mode 100644 index c2065bc..0000000 --- a/module-service/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -HELP.md -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ diff --git a/module-service/gradle/wrapper/gradle-wrapper.jar b/module-service/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 033e24c..0000000 Binary files a/module-service/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/module-service/gradle/wrapper/gradle-wrapper.properties b/module-service/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9f4197d..0000000 --- a/module-service/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/module-service/gradlew b/module-service/gradlew deleted file mode 100644 index fcb6fca..0000000 --- a/module-service/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/bin/sh - -# -# Copyright ยฉ 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป, -# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป; -# * compound commands having a testable exit status, especially ยซcaseยป; -# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป. -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/module-service/gradlew.bat b/module-service/gradlew.bat deleted file mode 100644 index 93e3f59..0000000 --- a/module-service/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/module-service/src/main/kotlin/universe/sparkle/moduleservice/ModuleServiceApplication.kt b/module-service/src/main/kotlin/universe/sparkle/moduleservice/ModuleServiceApplication.kt deleted file mode 100644 index ac77772..0000000 --- a/module-service/src/main/kotlin/universe/sparkle/moduleservice/ModuleServiceApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package universe.sparkle.moduleservice - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class ModuleServiceApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/module-service/src/test/kotlin/universe/sparkle/moduleservice/ModuleServiceApplicationTests.kt b/module-service/src/test/kotlin/universe/sparkle/moduleservice/ModuleServiceApplicationTests.kt deleted file mode 100644 index 7160987..0000000 --- a/module-service/src/test/kotlin/universe/sparkle/moduleservice/ModuleServiceApplicationTests.kt +++ /dev/null @@ -1,12 +0,0 @@ -package universe.sparkle.moduleservice - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ModuleServiceApplicationTests { - - @Test - fun contextLoads() { - } -} diff --git a/module-data/.gitignore b/module-usecase/.gitignore similarity index 100% rename from module-data/.gitignore rename to module-usecase/.gitignore diff --git a/module-service/build.gradle b/module-usecase/build.gradle similarity index 69% rename from module-service/build.gradle rename to module-usecase/build.gradle index 2e04855..092ff32 100644 --- a/module-service/build.gradle +++ b/module-usecase/build.gradle @@ -15,14 +15,30 @@ kotlin { jvmToolchain(17) } +bootJar { + enabled = false +} + +jar { + enabled = true +} + repositories { mavenCentral() } dependencies { + implementation project(':module-domain') implementation libs.test.spring.boot.starter implementation libs.kotlin.reflect - testImplementation libs.test.spring.boot.starter + + implementation libs.bundles.jjwt + + testImplementation(libs.test.spring.boot.starter) { + exclude module: 'mockito-core' + } + testImplementation libs.test.mockk + } tasks.named('test') { diff --git a/module-data/gradle/wrapper/gradle-wrapper.jar b/module-usecase/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from module-data/gradle/wrapper/gradle-wrapper.jar rename to module-usecase/gradle/wrapper/gradle-wrapper.jar diff --git a/module-data/gradle/wrapper/gradle-wrapper.properties b/module-usecase/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from module-data/gradle/wrapper/gradle-wrapper.properties rename to module-usecase/gradle/wrapper/gradle-wrapper.properties diff --git a/module-data/gradlew b/module-usecase/gradlew similarity index 100% rename from module-data/gradlew rename to module-usecase/gradlew diff --git a/module-data/gradlew.bat b/module-usecase/gradlew.bat similarity index 100% rename from module-data/gradlew.bat rename to module-usecase/gradlew.bat diff --git a/module-service/settings.gradle b/module-usecase/settings.gradle similarity index 100% rename from module-service/settings.gradle rename to module-usecase/settings.gradle diff --git a/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/BeIssuedAuthTokenUseCase.kt b/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/BeIssuedAuthTokenUseCase.kt new file mode 100644 index 0000000..7898047 --- /dev/null +++ b/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/BeIssuedAuthTokenUseCase.kt @@ -0,0 +1,68 @@ +package universe.sparkle.usecase.user + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import jakarta.xml.bind.DatatypeConverter +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import universe.sparkle.domain.usecase.BeIssuedAuthTokenInputBoundary +import universe.sparkle.domain.JwtConfigContract +import universe.sparkle.domain.model.AuthToken +import universe.sparkle.domain.model.User +import java.time.Instant +import java.time.LocalDateTime +import java.util.Date +import javax.crypto.spec.SecretKeySpec + +@Service +internal class BeIssuedAuthTokenUseCase @Autowired constructor( + private val jwtConfig: JwtConfigContract, +) : BeIssuedAuthTokenInputBoundary { + + private val signatureAlgorithm = SignatureAlgorithm.HS256 + + override fun invoke( + user: User, + accessExpiryPeriodDay: Long, + refreshExpiryPeriodDay: Long, + ): AuthToken { + val secretKeyBytes = DatatypeConverter.parseBase64Binary(jwtConfig.getSecret()) + val signingKey = SecretKeySpec(secretKeyBytes, signatureAlgorithm.jcaName) + return AuthToken( + accessToken = beIssuedAccessToken(user, accessExpiryPeriodDay, signingKey), + ) + } + + override fun invoke(user: User): AuthToken { + return this.invoke( + user = user, + accessExpiryPeriodDay = jwtConfig.getAccessExpiryPeriodDay(), + refreshExpiryPeriodDay = jwtConfig.getRefreshExpiryPeriodDay(), + ) + } + + private fun beIssuedAccessToken( + user: User, + accessExpiryPeriodDay: Long, + signingKey: SecretKeySpec, + ): String = createExpiration(accessExpiryPeriodDay).let { expirationDay -> + Jwts.builder() + .setClaims(createAccessTokenClaims(user)) + .setExpiration(Date.from(expirationDay)) + .signWith(signingKey, signatureAlgorithm) + .compact() + } + + private fun createAccessTokenClaims(user: User) = Jwts.claims() + .setSubject(BeIssuedAuthTokenInputBoundary.ACCESS_TOKEN_SUBJECT) + .also { + it[BeIssuedAuthTokenInputBoundary.CLAIMS_USER_ID] = user.id + } + + private fun createExpiration(expiryPeriod: Long): Instant = LocalDateTime.now() + .atZone(JwtConfigContract.zoneIdKST) + .toLocalDateTime() + .plusDays(expiryPeriod) + .atZone(JwtConfigContract.zoneIdKST) + .toInstant() +} diff --git a/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/ExtractTokenUseCase.kt b/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/ExtractTokenUseCase.kt new file mode 100644 index 0000000..c7901a0 --- /dev/null +++ b/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/ExtractTokenUseCase.kt @@ -0,0 +1,66 @@ +package universe.sparkle.usecase.user + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.ExpiredJwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.security.SignatureException +import jakarta.xml.bind.DatatypeConverter +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import universe.sparkle.domain.JwtConfigContract +import universe.sparkle.domain.exception.Unauthorized +import universe.sparkle.domain.gateway.UserDetailGateway +import universe.sparkle.domain.model.AuthenticationToken +import universe.sparkle.domain.usecase.BeIssuedAuthTokenInputBoundary +import universe.sparkle.domain.usecase.ExtractTokenInputBoundary +import java.time.LocalDateTime + +@Service +internal class ExtractTokenUseCase @Autowired constructor( + private val jwtConfig: JwtConfigContract, + private val userDetailGateway: UserDetailGateway, +) : ExtractTokenInputBoundary { + + override fun invoke(token: String): AuthenticationToken { + val claims = getClaimsFromToken(token).also { validateExpired(it) } + return getUserIdAt(claims) + } + + override fun getExpiredUserAuthenticationToken(token: String): AuthenticationToken { + val claims = getClaimsFromToken(token) + return getUserIdAt(claims) + } + + private fun getUserIdAt(claims: Claims): AuthenticationToken { + val userId = claims[BeIssuedAuthTokenInputBoundary.CLAIMS_USER_ID]?.toString() + return userDetailGateway.loadUserById(userId) + } + + private fun getClaimsFromToken(token: String): Claims { + return runCatching { + Jwts.parserBuilder() + .setSigningKey(DatatypeConverter.parseBase64Binary(jwtConfig.getSecret())) + .build() + .parseClaimsJws(token) + .body + }.getOrElse { + when (it) { + is MalformedJwtException, + is UnsupportedJwtException, + is SignatureException, + -> throw Unauthorized.UnsupportedToken() + + is ExpiredJwtException -> it.claims + else -> throw IllegalStateException("Problem occurred during JWT processing") + } + } + } + + private fun validateExpired(claims: Claims) { + val nowDateTime = LocalDateTime.now(JwtConfigContract.zoneIdKST) + val tokenExpiredDate = claims.expiration.toInstant().atZone(JwtConfigContract.zoneIdKST).toLocalDateTime() + if (tokenExpiredDate.isBefore(nowDateTime)) throw Unauthorized.ExpiredToken() + } +} diff --git a/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/LoadUserUseCase.kt b/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/LoadUserUseCase.kt new file mode 100644 index 0000000..9fd2ebd --- /dev/null +++ b/module-usecase/src/main/kotlin/universe/sparkle/usecase/user/LoadUserUseCase.kt @@ -0,0 +1,6 @@ +package universe.sparkle.usecase.user + +class LoadUserUseCase { + operator fun invoke(userId: String) { + } +} diff --git a/module-usecase/src/test/kotlin/universe/sparkle/usecase/AuthTokenUseCaseTest.kt b/module-usecase/src/test/kotlin/universe/sparkle/usecase/AuthTokenUseCaseTest.kt new file mode 100644 index 0000000..2b77af0 --- /dev/null +++ b/module-usecase/src/test/kotlin/universe/sparkle/usecase/AuthTokenUseCaseTest.kt @@ -0,0 +1,160 @@ +package universe.sparkle.usecase + +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.extension.ExtendWith +import universe.sparkle.domain.JwtConfigContract +import universe.sparkle.domain.SnsType +import universe.sparkle.domain.exception.Unauthorized +import universe.sparkle.domain.gateway.UserDetailGateway +import universe.sparkle.domain.model.AuthenticationToken +import universe.sparkle.domain.model.User +import universe.sparkle.usecase.user.BeIssuedAuthTokenUseCase +import universe.sparkle.usecase.user.ExtractTokenUseCase + +@ExtendWith(MockKExtension::class) +class AuthTokenUseCaseTest { + + @MockK + private lateinit var jwtConfig: JwtConfigContract + + @MockK + private lateinit var userDetailGateway: UserDetailGateway + + @InjectMockKs + private lateinit var beIssuedAuthTokenUseCase: BeIssuedAuthTokenUseCase + + @InjectMockKs + private lateinit var extractTokenUseCase: ExtractTokenUseCase + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this.beIssuedAuthTokenUseCase, relaxed = true, relaxUnitFun = true) + MockKAnnotations.init(this.extractTokenUseCase, relaxed = true, relaxUnitFun = true) + } + + @Test + fun ์œ ์ €_์ •๋ณด๊ฐ€_์ฃผ์–ด์ง€๋ฉด_์œ ์ €_์ •๋ณด๋ฅผ_ํ†ตํ•ด_์ธ์ฆ_ํ† ํฐ์„_์ƒ์„ฑํ•œ๋‹ค() { + // given + val user = User( + id = 1, + snsType = SnsType.APPLE, + snsAuthCode = "test", + ) + every { jwtConfig.getSecret() } answers { "secretSecretSecretSecretSecretSecretSecretSecretSecret" } + every { jwtConfig.getAccessExpiryPeriodDay() } answers { 1L } + every { jwtConfig.getRefreshExpiryPeriodDay() } answers { 2L } + // when + val authToken = beIssuedAuthTokenUseCase(user) + // then + assertAll( + { assertThat(authToken.accessToken).isNotNull() }, + { assertThat(authToken.refreshToken).isNull() }, + { assertThat(authToken.coupleId).isNull() }, + ) + } + + @Test + fun ๋งŒ๋ฃŒ๋˜์ง€_์•Š์€_์œ ์ €์˜_ํ† ํฐ์ด_์ฃผ์–ด์ง„_๊ฒฝ์šฐ_AuthenticationToken์„_ํ†ตํ•ด_userId๋ฅผ_์–ป์„_์ˆ˜_์žˆ๋‹ค() { + // given + val user = User( + id = 1L, + snsType = SnsType.APPLE, + snsAuthCode = "test", + ) + every { jwtConfig.getSecret() } answers { "secretSecretSecretSecretSecretSecretSecretSecretSecret" } + every { jwtConfig.getAccessExpiryPeriodDay() } answers { 1L } + every { jwtConfig.getRefreshExpiryPeriodDay() } answers { 2L } + every { userDetailGateway.loadUserById("1") } answers { + AuthenticationToken(id = 1L, nickname = null, image = null) + } + // when + val authToken = beIssuedAuthTokenUseCase(user) + // then + assertAll( + { + assertThat(extractTokenUseCase(authToken.accessToken)) + .isInstanceOf(AuthenticationToken::class.java) + }, + { + assertThat(extractTokenUseCase(authToken.accessToken).id) + .isEqualTo(1L) + }, + ) + } + + @Test + fun ๋งŒ๋ฃŒ๋œ_์œ ์ €์˜_ํ† ํฐ์ด_์ฃผ์–ด์ง„_๊ฒฝ์šฐ_ํ† ํฐ_๋งŒ๋ฃŒ_์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + val user = User( + id = 1, + snsType = SnsType.APPLE, + snsAuthCode = "test", + ) + every { jwtConfig.getSecret() } answers { "secretSecretSecretSecretSecretSecretSecretSecretSecret" } + every { jwtConfig.getAccessExpiryPeriodDay() } answers { -1L } + every { jwtConfig.getRefreshExpiryPeriodDay() } answers { 2L } + every { userDetailGateway.loadUserById("1") } answers { + AuthenticationToken(id = 1L, nickname = null, image = null) + } + // when + val authToken = beIssuedAuthTokenUseCase(user) + // then + assertThatThrownBy { extractTokenUseCase(authToken.accessToken) } + .isInstanceOf(Unauthorized.ExpiredToken::class.java) + .hasMessage("ํ† ํฐ์ด ๋งŒ๋ฃŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + } + + @Test + fun ์œ ์ €์˜_ํ† ํฐ์ด_๋ณ€์กฐ๋œ_๊ฒฝ์šฐ_ํ† ํฐ_๋ณ€์กฐ_์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + val user = User( + id = 1, + snsType = SnsType.APPLE, + snsAuthCode = "test", + ) + every { jwtConfig.getSecret() } answers { + "secretSecretSecretSecretSecretSecretSecretSecretSecret" + } andThenAnswer { "wiredSecretSecretSecretSecretSecretSecretSecretSecret" } + every { jwtConfig.getAccessExpiryPeriodDay() } answers { 1L } + every { jwtConfig.getRefreshExpiryPeriodDay() } answers { 2L } + every { userDetailGateway.loadUserById("1") } answers { + AuthenticationToken(id = 1L, nickname = null, image = null) + } + // when + val authToken = beIssuedAuthTokenUseCase(user) + // then + assertThatThrownBy { extractTokenUseCase(authToken.accessToken) } + .isInstanceOf(Unauthorized.UnsupportedToken::class.java) + .hasMessage("์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์˜ ํ† ํฐ ํ˜น์€ ๋ณ€์กฐ๋œ ํ† ํฐ์„ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.") + } + + @Test + fun ํ† ํฐ์—_์œ ์ €_์•„์ด๋””๊ฐ€_์—†๋‹ค๋ฉด_์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + val user = User( + snsType = SnsType.GOOGLE, + snsAuthCode = "test", + ) + every { jwtConfig.getSecret() } answers { "secretSecretSecretSecretSecretSecretSecretSecretSecret" } + every { jwtConfig.getAccessExpiryPeriodDay() } answers { 1L } + every { jwtConfig.getRefreshExpiryPeriodDay() } answers { 2L } + every { userDetailGateway.loadUserById(null) } answers { + throw Unauthorized.UnsupportedToken() + } + // when + val authToken = beIssuedAuthTokenUseCase(user) + // then + assertThatThrownBy { extractTokenUseCase(authToken.accessToken) } + .isInstanceOf(Unauthorized.UnsupportedToken::class.java) + .hasMessage("์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์˜ ํ† ํฐ ํ˜น์€ ๋ณ€์กฐ๋œ ํ† ํฐ์„ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.") + } +} diff --git a/settings.gradle b/settings.gradle index ec87152..52e5644 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,8 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' +} rootProject.name = 'sparkle' -include("module-api") -include("module-core-domain") -include("module-service") -include("module-data") +include("module-infrastructure") +include("module-domain") +include("module-usecase") diff --git a/uni-kotlin-config b/uni-kotlin-config index 1046438..44e4f11 160000 --- a/uni-kotlin-config +++ b/uni-kotlin-config @@ -1 +1 @@ -Subproject commit 1046438b497e516028feaee672cf2b4af15a113f +Subproject commit 44e4f113d46cc38cbf88c4e09618847c803be547