Skip to content

Commit

Permalink
[Feature] Authorization 로직 구현 (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajou4095 authored Jan 25, 2024
1 parent 1f84475 commit c9cd076
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package ac.dnd.bookkeeping.android.data.remote.network.api

import ac.dnd.bookkeeping.android.data.remote.network.di.AuthHttpClient
import ac.dnd.bookkeeping.android.data.remote.network.di.NoAuthHttpClient
import ac.dnd.bookkeeping.android.data.remote.network.environment.BaseUrlProvider
import ac.dnd.bookkeeping.android.data.remote.network.environment.ErrorMessageMapper
import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.GetAccessTokenReq
import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.GetAccessTokenRes
import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.LoginReq
import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.LoginRes
import ac.dnd.bookkeeping.android.data.remote.network.util.convert
import io.ktor.client.HttpClient
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import javax.inject.Inject

class AuthenticationApi @Inject constructor(
@NoAuthHttpClient private val client: HttpClient,
@NoAuthHttpClient private val noAuthClient: HttpClient,
@AuthHttpClient private val client: HttpClient,
private val baseUrlProvider: BaseUrlProvider,
private val errorMessageMapper: ErrorMessageMapper
) {
Expand All @@ -22,12 +26,34 @@ class AuthenticationApi @Inject constructor(
suspend fun getAccessToken(
refreshToken: String
): Result<GetAccessTokenRes> {
return client.post("$baseUrl/member/v1/refresh") {
return noAuthClient.post("$baseUrl/api/v1/token/reissue") {
header("Token-Refresh", refreshToken)
}.convert(errorMessageMapper::map)
}

suspend fun login(
socialId: String,
email: String,
profileImageUrl: String
): Result<LoginRes> {
return noAuthClient.post("$baseUrl/api/v1/auth/login") {
setBody(
GetAccessTokenReq(
refreshToken = refreshToken
LoginReq(
socialId = socialId,
email = email,
profileImageUrl = profileImageUrl
)
)
}.convert(errorMessageMapper::map)
}

suspend fun logout(): Result<Unit> {
return client.post("$baseUrl/api/v1/auth/logout")
.convert(errorMessageMapper::map)
}

suspend fun withdraw(): Result<Unit> {
return client.post("$baseUrl/api/v1/members/me")
.convert(errorMessageMapper::map)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package ac.dnd.bookkeeping.android.data.remote.network.di

import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import android.content.Context
import android.content.pm.ApplicationInfo
import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -63,7 +63,8 @@ internal object KtorModule {
@DebugInterceptor debugInterceptor: Optional<Interceptor>,
authenticationRepository: AuthenticationRepository
): HttpClient {
val isDebug: Boolean = (0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)
val isDebug: Boolean =
(0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)

return HttpClient(OkHttp) {
// default validation to throw exceptions for non-2xx responses
Expand All @@ -89,7 +90,10 @@ internal object KtorModule {
return@loadTokens null
}

BearerTokens(accessToken, refreshToken)
BearerTokens(
accessToken = accessToken,
refreshToken = refreshToken
)
}

refreshTokens {
Expand All @@ -98,12 +102,13 @@ internal object KtorModule {
return@refreshTokens null
}

authenticationRepository.getAccessToken(
authenticationRepository.refreshToken(
refreshToken
).getOrNull()?.let { accessToken ->
authenticationRepository.accessToken = accessToken

BearerTokens(accessToken, refreshToken)
).getOrNull()?.let { token ->
BearerTokens(
accessToken = token.accessToken,
refreshToken = token.refreshToken
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable

@Serializable
data class GetAccessTokenRes(
@SerialName("access_token")
val accessToken: String
@SerialName("accessToken")
val accessToken: String,
@SerialName("refreshToken")
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ac.dnd.bookkeeping.android.data.remote.network.model.authentication

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class LoginReq(
@SerialName("socialId")
val socialId: String,
@SerialName("email")
val email: String,
@SerialName("profileImageUrl")
val profileImageUrl: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GetAccessTokenReq(
@SerialName("refresh_token")
data class LoginRes(
@SerialName("isNew")
val isNew: Boolean,
@SerialName("accessToken")
val accessToken: String,
@SerialName("refreshToken")
val refreshToken: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import kotlinx.serialization.Serializable

@Serializable
data class ErrorRes(
@SerialName("id")
val id: String,
@SerialName("code")
val code: String,
@SerialName("message")
val message: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ suspend inline fun HttpResponse.toThrowable(
}

return@runCatching this.body<ErrorRes?>()?.let { errorRes ->
BadRequestServerException(errorRes.id, errorMessageMapper(errorRes.id))
BadRequestServerException(errorRes.code, errorMessageMapper(errorRes.code))
} ?: InvalidStandardResponseException("Response Empty Body")
}.getOrElse { exception ->
exception
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ac.dnd.bookkeeping.android.data.repository.authentication

import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager
import ac.dnd.bookkeeping.android.domain.model.authentication.JwtToken
import ac.dnd.bookkeeping.android.domain.model.authentication.Login
import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import kotlinx.coroutines.delay
import javax.inject.Inject
Expand All @@ -17,11 +19,37 @@ class MockAuthenticationRepository @Inject constructor(
set(value) = sharedPreferencesManager.setString(ACCESS_TOKEN, value)
get() = sharedPreferencesManager.getString(ACCESS_TOKEN, "")

override suspend fun getAccessToken(
override suspend fun refreshToken(
refreshToken: String
): Result<String> {
): Result<JwtToken> {
randomShortDelay()
return Result.success("refresh_token")
return Result.success(
JwtToken(
accessToken = "mock_access_token",
refreshToken = "mock_refresh_token"
)
)
}

override suspend fun login(
socialId: String,
email: String,
profileImageUrl: String
): Result<Login> {
randomShortDelay()
return Result.success(
Login(isNew = true)
)
}

override suspend fun logout(): Result<Unit> {
randomShortDelay()
return Result.success(Unit)
}

override suspend fun withdraw(): Result<Unit> {
randomLongDelay()
return Result.success(Unit)
}

private suspend fun randomShortDelay() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package ac.dnd.bookkeeping.android.data.repository.authentication

import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager
import ac.dnd.bookkeeping.android.data.remote.network.api.AuthenticationApi
import ac.dnd.bookkeeping.android.domain.model.authentication.JwtToken
import ac.dnd.bookkeeping.android.domain.model.authentication.Login
import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import javax.inject.Inject

Expand All @@ -13,20 +15,60 @@ class RealAuthenticationRepository @Inject constructor(
override var refreshToken: String
set(value) = sharedPreferencesManager.setString(REFRESH_TOKEN, value)
get() = sharedPreferencesManager.getString(REFRESH_TOKEN, "")

override var accessToken: String
set(value) = sharedPreferencesManager.setString(ACCESS_TOKEN, value)
get() = sharedPreferencesManager.getString(ACCESS_TOKEN, "")

override suspend fun getAccessToken(
override suspend fun refreshToken(
refreshToken: String
): Result<String> {
): Result<JwtToken> {
return authenticationApi.getAccessToken(
refreshToken = refreshToken
).map {
it.accessToken
).onSuccess { token ->
this.refreshToken = token.refreshToken
this.accessToken = token.accessToken
}.map { token ->
JwtToken(
accessToken = token.accessToken,
refreshToken = token.refreshToken
)
}
}

override suspend fun login(
socialId: String,
email: String,
profileImageUrl: String
): Result<Login> {
return authenticationApi.login(
socialId = socialId,
email = email,
profileImageUrl = profileImageUrl
).onSuccess { token ->
this.refreshToken = token.refreshToken
this.accessToken = token.accessToken
}.map { login ->
Login(isNew = login.isNew)
}
}

override suspend fun logout(): Result<Unit> {
return authenticationApi.logout()
.onSuccess {
this.refreshToken = ""
this.accessToken = ""
}
}

override suspend fun withdraw(): Result<Unit> {
return authenticationApi.withdraw()
.onSuccess {
this.refreshToken = ""
this.accessToken = ""
}
}

companion object {
private const val REFRESH_TOKEN = "refresh_token"
private const val ACCESS_TOKEN = "access_token"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ac.dnd.bookkeeping.android.domain.model.authentication

data class JwtToken(
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ac.dnd.bookkeeping.android.domain.model.authentication

data class Login(
val isNew: Boolean
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package ac.dnd.bookkeeping.android.domain.repository

import ac.dnd.bookkeeping.android.domain.model.authentication.JwtToken
import ac.dnd.bookkeeping.android.domain.model.authentication.Login

interface AuthenticationRepository {

var refreshToken: String

var accessToken: String

suspend fun getAccessToken(
suspend fun refreshToken(
refreshToken: String
): Result<String>
): Result<JwtToken>

suspend fun login(
socialId: String,
email: String,
profileImageUrl: String
): Result<Login>

suspend fun logout(): Result<Unit>

suspend fun withdraw(): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ac.dnd.bookkeeping.android.domain.usecase.authentication

import ac.dnd.bookkeeping.android.domain.model.authentication.Login
import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import javax.inject.Inject

class LoginUseCase @Inject constructor(
private val authenticationRepository: AuthenticationRepository
) {
suspend operator fun invoke(
socialId: String,
email: String,
profileImageUrl: String
): Result<Login> {
return authenticationRepository.login(
socialId = socialId,
email = email,
profileImageUrl = profileImageUrl
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ac.dnd.bookkeeping.android.domain.usecase.authentication

import ac.dnd.bookkeeping.android.domain.model.authentication.Login
import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import javax.inject.Inject

class LogoutUseCase @Inject constructor(
private val authenticationRepository: AuthenticationRepository
) {
suspend operator fun invoke(
socialId: String,
email: String,
profileImageUrl: String
): Result<Login> {
return authenticationRepository.login(
socialId = socialId,
email = email,
profileImageUrl = profileImageUrl
)
}
}
Loading

0 comments on commit c9cd076

Please sign in to comment.