Skip to content
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

개선된 로그인 반영 (Oauth) #396

Merged
merged 12 commits into from
Feb 13, 2024
Merged
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.ohdodok.catchytape.core.data.api

import com.ohdodok.catchytape.core.data.model.AuthTokenResponse
import com.ohdodok.catchytape.core.data.model.LoginRequest
import com.ohdodok.catchytape.core.data.model.LoginResponse
import com.ohdodok.catchytape.core.data.model.MusicIdRequest
import com.ohdodok.catchytape.core.data.model.MusicResponse
import com.ohdodok.catchytape.core.data.model.NicknameResponse
import com.ohdodok.catchytape.core.data.model.RefreshRequest
import com.ohdodok.catchytape.core.data.model.SignUpRequest
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
Expand All @@ -19,12 +19,17 @@ interface UserApi {
@POST("users/login")
suspend fun login(
@Body loginRequest: LoginRequest
): LoginResponse
): AuthTokenResponse

@POST("users/signup")
suspend fun signUp(
@Body signUpRequest: SignUpRequest
): LoginResponse
): AuthTokenResponse

@POST("users/refresh")
suspend fun refresh(
@Body refreshRequest: RefreshRequest
): AuthTokenResponse

@GET("users/duplicate/{nickname}")
suspend fun verifyDuplicatedNickname(
Expand All @@ -33,7 +38,6 @@ interface UserApi {

@GET("users/verify")
suspend fun verify(
@Header("Authorization") accessToken: String,
): Response<Unit>

@GET("users/recent-played")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ import javax.inject.Inject
class TokenLocalDataSource @Inject constructor(
private val dataStore: DataStore<Preferences>
) {

private val accessTokenKey = stringPreferencesKey("accessToken")
private val refreshTokenKey = stringPreferencesKey("refreshToken")

suspend fun getAccessToken(): String =
dataStore.data.map { preferences -> preferences[accessTokenKey] ?: "" }.first()

suspend fun saveAccessToken(token: String) {
dataStore.edit { preferences -> preferences[accessTokenKey] = token }
}

suspend fun getRefreshToken(): String =
dataStore.data.map { preferences -> preferences[refreshTokenKey] ?: "" }.first()

suspend fun saveRefreshToken(token: String) {
dataStore.edit { preferences -> preferences[refreshTokenKey] = token }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ import com.ohdodok.catchytape.core.data.model.ErrorResponse
import com.ohdodok.catchytape.core.domain.model.CtErrorType
import com.ohdodok.catchytape.core.domain.model.CtErrorType.Companion.ctErrorEnums
import com.ohdodok.catchytape.core.domain.model.CtException
import com.ohdodok.catchytape.core.domain.repository.AuthRepository
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import timber.log.Timber
Expand All @@ -33,10 +37,10 @@ object NetworkModule {
@AuthInterceptor
@Singleton
@Provides
fun provideAuthInterceptor(tokenDataSource: TokenLocalDataSource): Interceptor {
fun provideAuthInterceptor(tokenLocalDataSource: TokenLocalDataSource): Interceptor {

return Interceptor { chain ->
val accessToken = runBlocking { tokenDataSource.getAccessToken() }
val accessToken = runBlocking { tokenLocalDataSource.getAccessToken() }
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $accessToken")
.build()
Expand All @@ -63,7 +67,7 @@ object NetworkModule {
fun provideOkHttpClient(
loggingInterceptor: HttpLoggingInterceptor,
@AuthInterceptor authInterceptor: Interceptor,
@ErrorInterceptor errorInterceptor : Interceptor
@ErrorInterceptor errorInterceptor: Interceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
Expand All @@ -87,16 +91,24 @@ object NetworkModule {
@ErrorInterceptor
@Singleton
@Provides
fun provideErrorInterceptor(): Interceptor {
fun provideErrorInterceptor(
authRepository: Lazy<AuthRepository>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 혹시 Lazy인 이유가 있을까?
  2. repository는 데이터 모듈을 추상화하는 역할이라, 데이터 모듈 내부적으로는 repository 안 거치고 바로 data에 접근하는 것도 가능해
  3. 데이터 모듈은 interceptor ← Api(or DataStore) ← repository 이런 구조인데 interceptor가 다시 repository를 참조하면 순환 참조가 일어나는데 주의가 필요할 것 같아

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy 인 이유는 말씀하신, 3번 처럼 cycle이 일어나는 것 때문에 사용하였습니다.
더 개선할 수 있을거 같은데, 이슈로 따로 빼서 해보겠습니다~�

<참고>

): Interceptor {
return Interceptor { chain ->
try {
val response = chain.proceed(chain.request())
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
if (response.isSuccessful) return@Interceptor response
val errorString = response.body?.string()
val errorResponse = Json.decodeFromString<ErrorResponse>(errorString ?: "")

if (errorResponse.statusCode == 401) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드를 자세히 보진 않았는데, 원래 로그인 시도할 때 LoginViewModel에서 401을 받으면 회원가입(닉네임) 화면으로 이동했던 걸로 기억하는데, 이렇게 변경했을 때도 로그인했을 때 미가입 회원일 경우 회원가입 화면으로 잘 이동하는지 확인해 주세용
이 코드가 이상하다기보다는, 기존의 회원가입 여부 처리 방식의 문제라고 생각하긴 해

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.notion.so/2024-01-29-refresh-9dbaae6a05f3448ca0294ecbafeba23c 참고하여 개발하였습니다.

참고하여 테스트 해보겠습니다~

throw CtException(errorResponse.message, CtErrorType.UN_AUTHORIZED)
val newAccessToken = runBlocking {
val authTokenResponse = authRepository.get().refreshToken()
authTokenResponse.first().accessToken
}
val newRequest = originalRequest.putTokenHeader(newAccessToken)
chain.proceed(newRequest)
}
Comment on lines 105 to 112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 로직은 retrofit의 authenticator를 이용해서 구현할 수도 있어.


val ctError = ctErrorEnums.find { it.errorCode == errorResponse.errorCode }
Expand All @@ -108,11 +120,21 @@ object NetworkModule {
} catch (e: Exception) {
when (e) {
is ConnectException -> throw CtException(e.message, CtErrorType.CONNECTION)
is SSLHandshakeException -> throw CtException(e.message, CtErrorType.SSL_HAND_SHAKE)
is SSLHandshakeException -> throw CtException(
e.message,
CtErrorType.SSL_HAND_SHAKE
)

is CtException -> throw e
else -> throw CtException(e.message, CtErrorType.IO)
}
}
}
}
}

private fun Request.putTokenHeader(accessToken: String): Request {
return this.newBuilder()
.addHeader("Authorization", accessToken)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import com.ohdodok.catchytape.core.data.repository.AuthRepositoryImpl
import com.ohdodok.catchytape.core.data.repository.MusicRepositoryImpl
import com.ohdodok.catchytape.core.data.repository.PlaylistRepositoryImpl
import com.ohdodok.catchytape.core.data.repository.UploadRepositoryImpl
import com.ohdodok.catchytape.core.data.repository.UserTokenRepositoryImpl
import com.ohdodok.catchytape.core.data.repository.UuidRepositoryImpl
import com.ohdodok.catchytape.core.domain.repository.AuthRepository
import com.ohdodok.catchytape.core.domain.repository.MusicRepository
import com.ohdodok.catchytape.core.domain.repository.PlaylistRepository
import com.ohdodok.catchytape.core.domain.repository.UploadRepository
import com.ohdodok.catchytape.core.domain.repository.UserTokenRepository
import com.ohdodok.catchytape.core.domain.repository.UuidRepository
import dagger.Binds
import dagger.Module
Expand All @@ -26,10 +24,6 @@ interface RepositoryModule {
@Singleton
fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository

@Binds
@Singleton
fun bindUserTokenRepository(userTokenRepositoryImpl: UserTokenRepositoryImpl): UserTokenRepository

@Binds
@Singleton
fun bindMusicRepository(musicRepositoryImpl: MusicRepositoryImpl): MusicRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ohdodok.catchytape.core.data.model

import com.ohdodok.catchytape.core.domain.model.AuthToken
import kotlinx.serialization.Serializable

@Serializable
data class AuthTokenResponse(
val accessToken: String,
val refreshToken: String,
) {
internal fun toDomain(): AuthToken {
return AuthToken(
accessToken = accessToken,
refreshToken = refreshToken
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package com.ohdodok.catchytape.core.data.model
import kotlinx.serialization.Serializable

@Serializable
data class LoginResponse(
val accessToken: String
data class RefreshRequest(
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.ohdodok.catchytape.core.data.repository

import com.ohdodok.catchytape.core.data.api.UserApi
import com.ohdodok.catchytape.core.data.datasource.TokenLocalDataSource
import com.ohdodok.catchytape.core.data.model.LoginRequest
import com.ohdodok.catchytape.core.data.model.RefreshRequest
import com.ohdodok.catchytape.core.data.model.SignUpRequest
import com.ohdodok.catchytape.core.domain.model.AuthToken
import com.ohdodok.catchytape.core.domain.model.CtErrorType
import com.ohdodok.catchytape.core.domain.model.CtException
import com.ohdodok.catchytape.core.domain.repository.AuthRepository
Expand All @@ -12,16 +15,23 @@ import javax.inject.Inject

class AuthRepositoryImpl @Inject constructor(
private val userApi: UserApi,
private val tokenLocalDataSource: TokenLocalDataSource
) : AuthRepository {

override fun loginWithGoogle(googleToken: String): Flow<String> = flow {
val loginResponse = userApi.login(LoginRequest(idToken = googleToken))
emit(loginResponse.accessToken)
override fun loginWithGoogle(googleToken: String): Flow<AuthToken> = flow {
val authTokenResponse = userApi.login(LoginRequest(idToken = googleToken))
val authToken = authTokenResponse.toDomain()
saveAuthToken(authToken)
emit(authToken)
}

override fun signUpWithGoogle(googleToken: String, nickname: String): Flow<String> = flow {
val loginResponse = userApi.signUp(SignUpRequest(idToken = googleToken, nickname = nickname))
emit(loginResponse.accessToken)
override fun signUpWithGoogle(googleToken: String, nickname: String): Flow<AuthToken> = flow {
val authTokenResponse = userApi.signUp(
SignUpRequest(idToken = googleToken, nickname = nickname)
)
val authToken = authTokenResponse.toDomain()
saveAuthToken(authToken)
emit(authToken)
}

override fun isDuplicatedNickname(nickname: String): Flow<Boolean> = flow {
Expand All @@ -34,8 +44,21 @@ class AuthRepositoryImpl @Inject constructor(
}
}

override suspend fun verifyToken(token: String): Boolean {
return userApi.verify("Bearer ${token}").isSuccessful
override suspend fun verifyAccessToken(): Boolean {
val accessToken = tokenLocalDataSource.getAccessToken()
if (accessToken.isBlank()) return false
return userApi.verify().isSuccessful
}

override fun refreshToken(): Flow<AuthToken> = flow {
val refreshToken = tokenLocalDataSource.getRefreshToken()
val authTokenResponse = userApi.refresh(RefreshRequest(refreshToken))

saveAuthToken(authTokenResponse.toDomain())
}

override suspend fun saveAuthToken(authToken: AuthToken) {
tokenLocalDataSource.saveAccessToken(authToken.accessToken)
tokenLocalDataSource.saveRefreshToken(authToken.refreshToken)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ohdodok.catchytape.core.domain.model

data class AuthToken(
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum class CtErrorType(val errorCode: Int) {
BAD_IMAGE(4013),
WRONG_TOKEN(4100),
EXPIRED_TOKEN(4101),
NOT_EXIST_REFRESH_TOKEN(4102),
SERVER(5000),
SERVICE(5001),
ENCODING_FAILURE(5002),
Expand All @@ -28,6 +29,4 @@ enum class CtErrorType(val errorCode: Int) {
companion object {
val ctErrorEnums = CtErrorType.values().toList()
}
}


}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.ohdodok.catchytape.core.domain.repository

import com.ohdodok.catchytape.core.domain.model.AuthToken
import kotlinx.coroutines.flow.Flow

interface AuthRepository {

fun loginWithGoogle(googleToken: String): Flow<String>
fun loginWithGoogle(googleToken: String): Flow<AuthToken>

fun signUpWithGoogle(googleToken: String, nickname: String): Flow<String>
fun signUpWithGoogle(googleToken: String, nickname: String): Flow<AuthToken>

fun isDuplicatedNickname(nickname: String): Flow<Boolean>

suspend fun verifyToken(token: String): Boolean
suspend fun verifyAccessToken(): Boolean

fun refreshToken(): Flow<AuthToken>

suspend fun saveAuthToken(authToken: AuthToken)
}

This file was deleted.

This file was deleted.

Loading
Loading