-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#70 kakao login server #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.chukchukhaksa.mobile.common.kmp | ||
|
|
||
| import io.ktor.client.engine.HttpClientEngineFactory | ||
| import io.ktor.client.engine.okhttp.OkHttp | ||
|
|
||
| actual val httpClientEngineFactory: HttpClientEngineFactory<*> = OkHttp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.chukchukhaksa.mobile.common.kmp | ||
|
|
||
| import java.security.MessageDigest | ||
|
|
||
| actual fun sha256Hex(input: String): String { | ||
| val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray()) | ||
| return bytes.joinToString("") { "%02x".format(it) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.chukchukhaksa.mobile.common.kmp | ||
|
|
||
| import io.ktor.client.engine.HttpClientEngineFactory | ||
|
|
||
| expect val httpClientEngineFactory: HttpClientEngineFactory<*> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package com.chukchukhaksa.mobile.common.kmp | ||
|
|
||
| expect fun sha256Hex(input: String): String |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.chukchukhaksa.mobile.data.auth.datasource | ||
|
|
||
| import com.chukchukhaksa.mobile.domain.auth.model.SignInResult | ||
|
|
||
| interface RemoteAuthDataSource { | ||
| suspend fun signIn(idToken: String, nonce: String): SignInResult | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.chukchukhaksa.mobile.data.auth.di | ||
|
|
||
| import com.chukchukhaksa.mobile.data.auth.datasource.RemoteAuthDataSource | ||
| import com.chukchukhaksa.mobile.data.auth.repository.AuthRepositoryImpl | ||
| import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository | ||
| import com.chukchukhaksa.mobile.remote.auth.RemoteAuthDataSourceImpl | ||
| import org.koin.dsl.module | ||
|
|
||
| val authRepositoryModule = module { | ||
| single<RemoteAuthDataSource> { RemoteAuthDataSourceImpl(get()) } | ||
| single<AuthRepository> { AuthRepositoryImpl(get()) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.chukchukhaksa.mobile.data.auth.repository | ||
|
|
||
| import com.chukchukhaksa.mobile.data.auth.datasource.RemoteAuthDataSource | ||
| import com.chukchukhaksa.mobile.domain.auth.model.SignInResult | ||
| import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository | ||
|
|
||
| class AuthRepositoryImpl( | ||
| private val remoteAuthDataSource: RemoteAuthDataSource, | ||
| ) : AuthRepository { | ||
|
|
||
| override suspend fun signIn(idToken: String, nonce: String): SignInResult { | ||
| return remoteAuthDataSource.signIn(idToken = idToken, nonce = nonce) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.chukchukhaksa.mobile.domain.auth.model | ||
|
|
||
| data class SignInResult( | ||
| val accessToken: String, | ||
| val refreshToken: String, | ||
| val isPortalLinked: Boolean, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.chukchukhaksa.mobile.domain.auth.repository | ||
|
|
||
| import com.chukchukhaksa.mobile.domain.auth.model.SignInResult | ||
|
|
||
| interface AuthRepository { | ||
| suspend fun signIn(idToken: String, nonce: String): SignInResult | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,20 @@ | ||
| package com.chukchukhaksa.mobile.domain.auth.usecase | ||
|
|
||
| import com.chukchukhaksa.mobile.common.kmp.KakaoSignInClient | ||
| import com.chukchukhaksa.mobile.common.kmp.KakaoSignInResult | ||
| import com.chukchukhaksa.mobile.domain.auth.model.SignInResult | ||
| import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository | ||
| import com.chukchukhaksa.mobile.domain.common.runCatchingIgnoreCancelled | ||
|
|
||
| class KakaoLoginUseCase( | ||
| private val kakaoSignInClient: KakaoSignInClient, | ||
| private val authRepository: AuthRepository, | ||
| ) { | ||
| suspend operator fun invoke(context: Any? = null): Result<KakaoSignInResult> = | ||
| suspend operator fun invoke(context: Any? = null): Result<SignInResult> = | ||
| runCatchingIgnoreCancelled { | ||
| kakaoSignInClient.signIn(context) | ||
| val kakaoResult = kakaoSignInClient.signIn(context) | ||
| authRepository.signIn( | ||
| idToken = kakaoResult.idToken, | ||
| nonce = kakaoResult.nonce, | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.chukchukhaksa.mobile.remote.auth | ||
|
|
||
| import com.chukchukhaksa.mobile.data.auth.datasource.RemoteAuthDataSource | ||
| import com.chukchukhaksa.mobile.domain.auth.model.SignInResult | ||
| import com.chukchukhaksa.mobile.remote.auth.model.SignInRequest | ||
| import com.chukchukhaksa.mobile.remote.auth.model.SignInResponse | ||
| import com.chukchukhaksa.mobile.remote.common.ApiResponse | ||
| import com.chukchukhaksa.mobile.remote.common.getDataOrThrow | ||
| import io.ktor.client.HttpClient | ||
| import io.ktor.client.call.body | ||
| import io.ktor.client.request.post | ||
| import io.ktor.client.request.setBody | ||
|
|
||
| class RemoteAuthDataSourceImpl( | ||
| private val httpClient: HttpClient, | ||
| ) : RemoteAuthDataSource { | ||
|
|
||
| override suspend fun signIn(idToken: String, nonce: String): SignInResult { | ||
| val response = httpClient.post("users/signin") { | ||
| setBody(SignInRequest(idToken = idToken, nonce = nonce)) | ||
| }.body<ApiResponse<SignInResponse>>() | ||
|
|
||
| return response.getDataOrThrow().toSignInResult() | ||
| } | ||
| } | ||
|
|
||
| private fun SignInResponse.toSignInResult() = SignInResult( | ||
| accessToken = accessToken, | ||
| refreshToken = refreshToken, | ||
| isPortalLinked = isPortalLinked, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.chukchukhaksa.mobile.remote.auth.model | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class SignInRequest( | ||
| @SerialName("id_token") val idToken: String, | ||
| val nonce: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.chukchukhaksa.mobile.remote.auth.model | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class SignInResponse( | ||
| val accessToken: String, | ||
| val refreshToken: String, | ||
| val isPortalLinked: Boolean, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.chukchukhaksa.mobile.remote.common | ||
|
|
||
| class ApiException( | ||
| val code: String, | ||
| override val message: String, | ||
| ) : RuntimeException(message) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.chukchukhaksa.mobile.remote.common | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class ApiResponse<T>( | ||
| val success: Boolean, | ||
| val data: T? = null, | ||
| val error: ApiError? = null, | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class ApiError( | ||
| val code: String, | ||
| val message: String, | ||
| ) | ||
|
|
||
| fun <T> ApiResponse<T>.getDataOrThrow(): T { | ||
| return data ?: throw ApiException( | ||
| code = error?.code.orEmpty(), | ||
| message = error?.message ?: "์ ์ ์๋ ์๋ฌ๊ฐ ๋ฐ์ํ์ด์.", | ||
| ) | ||
| } | ||
|
lluke0 marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package com.chukchukhaksa.mobile.remote.di | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import com.chukchukhaksa.mobile.common.kmp.httpClientEngineFactory | ||||||||||||||||||||||||||||||||||||||||||||||
| import com.chukchukhaksa.mobile.common.kmp.isDebug | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.github.aakira.napier.Napier | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.HttpClient | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.HttpTimeout | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.contentnegotiation.ContentNegotiation | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.defaultRequest | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.logging.LogLevel | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.logging.Logger | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.logging.Logging | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.plugins.observer.ResponseObserver | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.statement.bodyAsText | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.ContentType | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.HttpHeaders | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.contentType | ||||||||||||||||||||||||||||||||||||||||||||||
| import io.ktor.serialization.kotlinx.json.json | ||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.Json | ||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.JsonElement | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.koin.dsl.module | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private val prettyJson = Json { prettyPrint = true } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private fun String.prettyPrintJson(): String = try { | ||||||||||||||||||||||||||||||||||||||||||||||
| val element = prettyJson.decodeFromString<JsonElement>(this) | ||||||||||||||||||||||||||||||||||||||||||||||
| prettyJson.encodeToString(JsonElement.serializer(), element) | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (_: Exception) { | ||||||||||||||||||||||||||||||||||||||||||||||
| this | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| val httpClientModule = module { | ||||||||||||||||||||||||||||||||||||||||||||||
| single { | ||||||||||||||||||||||||||||||||||||||||||||||
| HttpClient(httpClientEngineFactory) { | ||||||||||||||||||||||||||||||||||||||||||||||
| install(HttpTimeout) { | ||||||||||||||||||||||||||||||||||||||||||||||
| requestTimeoutMillis = 15_000 | ||||||||||||||||||||||||||||||||||||||||||||||
| connectTimeoutMillis = 10_000 | ||||||||||||||||||||||||||||||||||||||||||||||
| socketTimeoutMillis = 15_000 | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| install(ContentNegotiation) { | ||||||||||||||||||||||||||||||||||||||||||||||
| json( | ||||||||||||||||||||||||||||||||||||||||||||||
| Json { | ||||||||||||||||||||||||||||||||||||||||||||||
| ignoreUnknownKeys = true | ||||||||||||||||||||||||||||||||||||||||||||||
| isLenient = true | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| install(Logging) { | ||||||||||||||||||||||||||||||||||||||||||||||
| logger = object : Logger { | ||||||||||||||||||||||||||||||||||||||||||||||
| override fun log(message: String) { | ||||||||||||||||||||||||||||||||||||||||||||||
| Napier.d(message, tag = "HttpClient") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| level = if (isDebug) LogLevel.HEADERS else LogLevel.NONE | ||||||||||||||||||||||||||||||||||||||||||||||
| sanitizeHeader { header -> | ||||||||||||||||||||||||||||||||||||||||||||||
| header.equals(HttpHeaders.Authorization, ignoreCase = true) || | ||||||||||||||||||||||||||||||||||||||||||||||
| header.equals(HttpHeaders.Cookie, ignoreCase = true) || | ||||||||||||||||||||||||||||||||||||||||||||||
| header.equals(HttpHeaders.SetCookie, ignoreCase = true) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (isDebug) { | ||||||||||||||||||||||||||||||||||||||||||||||
| install(ResponseObserver) { | ||||||||||||||||||||||||||||||||||||||||||||||
| onResponse { response -> | ||||||||||||||||||||||||||||||||||||||||||||||
| val contentType = response.headers[HttpHeaders.ContentType] | ||||||||||||||||||||||||||||||||||||||||||||||
| if (contentType?.contains("application/json", ignoreCase = true) != true) return@onResponse | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| val body = response.bodyAsText() | ||||||||||||||||||||||||||||||||||||||||||||||
| if (body.isNotBlank()) { | ||||||||||||||||||||||||||||||||||||||||||||||
| val preview = body.take(4096) | ||||||||||||||||||||||||||||||||||||||||||||||
| Napier.d( | ||||||||||||||||||||||||||||||||||||||||||||||
| "RESPONSE BODY:\n${preview.prettyPrintJson()}", | ||||||||||||||||||||||||||||||||||||||||||||||
| tag = "HttpClient", | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+73
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐งน Nitpick | ๐ต Trivial ์๋ต ๋ณธ๋ฌธ ์๋ฆผ ์ฌ๋ถ๋ฅผ ๋ก๊ทธ ๋ฉ์์ง์ ํ์ํ๋ฉด ์ข๊ฒ ์ต๋๋ค. ํ์ฌ ๋ก๊ทธ ๋ฉ์์ง๊ฐ "RESPONSE BODY"๋ก ๊ณ ์ ๋์ด ์์ด 4096์ ์ด๊ณผ ์ ์๋ ธ๋์ง ์๊ธฐ ์ด๋ ต์ต๋๋ค. ๋๋ฒ๊น ์ ํผ๋์ ์ค์ด๊ธฐ ์ํด ์๋ฆผ ์ฌ๋ถ๋ฅผ ํ์ํ๋ ๊ฒ์ ๊ณ ๋ คํด ์ฃผ์ธ์. โจ ์ ์ ์์ ์ val body = response.bodyAsText()
if (body.isNotBlank()) {
+ val isTruncated = body.length > 4096
val preview = body.take(4096)
+ val label = if (isTruncated) "RESPONSE BODY (truncated ${body.length} -> 4096)" else "RESPONSE BODY"
Napier.d(
- "RESPONSE BODY:\n${preview.prettyPrintJson()}",
+ "$label:\n${preview.prettyPrintJson()}",
tag = "HttpClient",
)
}๐ Committable suggestion
Suggested change
๐ค Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| defaultRequest { | ||||||||||||||||||||||||||||||||||||||||||||||
| url("https://api.cchaksa.com/api/") | ||||||||||||||||||||||||||||||||||||||||||||||
| contentType(ContentType.Application.Json) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.