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

[Feature] 구글 자동 로그인을 구현 한다. #129

Merged
merged 26 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b846483
feat: Access Token이 저장되어있다면 자동로그인한다.
no1msh Nov 4, 2024
fdfb666
feat: 토큰의 유효성을 판단하는 API 연결
no1msh Nov 5, 2024
0d42800
feat: 토큰의 유효성을 판단하는 객체 구현
no1msh Nov 5, 2024
9b59d5b
feat: 토큰의 유효성을 판단하여 startDestination을 정하는 함수 구현
no1msh Nov 5, 2024
002eab0
feat: 가입시 액세스 토큰을 새로운 Role(USER)로 갱신
no1msh Nov 5, 2024
54f8dbf
refactor: 중복되는 로직 함수화
no1msh Nov 6, 2024
e7a62e4
Merge branch 'refs/heads/develop' into Feature/#126-auto-login
no1msh Nov 6, 2024
ee8ea8d
Merge branch 'refs/heads/develop' into Feature/#126-auto-login
no1msh Nov 11, 2024
936ad9c
feat: 401일 때 로그인 화면으로 이동
no1msh Nov 11, 2024
d5b71f6
feat: 불필요한 토큰 저장 제거
no1msh Nov 11, 2024
74a2842
refactor: Authenticator 이름 변경
no1msh Nov 11, 2024
5924e94
chore: 토큰 ROLE 주석 명시
no1msh Nov 11, 2024
45b300f
refactor: StartDestination과 Urls를 병렬로 가져옴
no1msh Nov 11, 2024
5ed501f
refactor: 랭킹 4위 이하 텍스트 색상 변경, 텍스트 스타일 변경
librarywon Nov 11, 2024
851b1ba
refactor: 세팅 information 이미지 교체
librarywon Nov 11, 2024
217bf2d
refactor: RecordBody 상단값 조절
librarywon Nov 11, 2024
ac0edfe
refactor: 열람실 이용 연장, 종료 버튼 높이 조정
librarywon Nov 11, 2024
ac02f16
refactor: 세팅 화면 bottom padding 조절
librarywon Nov 11, 2024
97e1617
refactor: 랭킹 기존 월 이동 이미지 삭제
librarywon Nov 11, 2024
baf79f7
refactor: 랭킹 기존 월 이동 새로운 이미지 추가
librarywon Nov 11, 2024
03256aa
refactor: StudyDurationCard 상단 패딩값 변경
librarywon Nov 11, 2024
d8356d7
refactor: SettingUserProfile 텍스트 컬러 변경
librarywon Nov 11, 2024
7d91c21
refactor: 세팅 버튼 높이 조절
librarywon Nov 11, 2024
ba09535
refactor: 랭킹 상단 패딩 변경
librarywon Nov 11, 2024
a21107a
refactor: 시스템 바텀네비게이션바 색상 변경
librarywon Nov 11, 2024
d55bcd4
feat: 바텀 네비게이션 이미지 아이콘 변경
librarywon Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.benenfeldt.remote.api

import com.benenfeldt.remote.dto.BaseResponse
import com.benenfeldt.remote.dto.TokenInformationResponse
import retrofit2.http.GET

interface TokenService {
@GET("/api/v1/token")
suspend fun getTokenInformation(): Result<BaseResponse<TokenInformationResponse>>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.benenfeldt.remote.di

import com.benenfeldt.remote.api.StudyService
import com.benenfeldt.remote.api.TokenService
import com.benenfeldt.remote.api.UserPublicService
import com.benenfeldt.remote.api.UserService
import com.benenfeldt.remote.api.WeeklyService
Expand Down Expand Up @@ -56,4 +57,12 @@ object ApiModule {
): WeeklyService {
return retrofit.create(WeeklyService::class.java)
}

@Singleton
@Provides
fun provideTokenService(
@NeedAuthRetrofit retrofit: Retrofit,
): TokenService {
return retrofit.create(TokenService::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.benenfeldt.remote.di
import android.util.Log
import com.benenfeldt.remote.BuildConfig
import com.benenfeldt.remote.token.AccessTokenInterceptor
import com.benenfeldt.remote.token.HY2Authenticator
import com.benenfeldt.remote.token.NeedAuthClient
import com.benenfeldt.remote.token.NeedAuthRetrofit
import com.benenfeldt.remote.token.PublicClient
Expand Down Expand Up @@ -70,10 +71,12 @@ object NetworkModule {
fun provideAuthOkHttpClient(
accessTokenInterceptor: AccessTokenInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor,
hY2Authenticator: HY2Authenticator,
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(accessTokenInterceptor)
.addInterceptor(httpLoggingInterceptor)
.authenticator(hY2Authenticator)
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.benenfeldt.remote.dto

import kotlinx.serialization.Serializable

@Serializable
data class TokenInformationResponse(
val role: String,
val validToken: Boolean,
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.benenfeldt.remote.dto

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

@Serializable
data class UserSignUpResponse(
val id: Int,
val department: String,
val nickname: String,
val username: String,
@SerialName("username")
val email: String,
val accessToken: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.benenfeldt.remote.mapper

import com.benenfeldt.remote.dto.TokenInformationResponse
import com.benenfeldt.remote.token.TokenInformation
import com.benenfeldt.remote.token.TokenRole

fun TokenInformationResponse.toTokenInformation(): TokenInformation {
return TokenInformation(
role = TokenRole.valueOf(role),
isValidToken = validToken,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.benenfeldt.remote.token

interface AuthCallback {
fun onAuthRequired()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,6 @@ class DefaultJwtManager
}.firstOrNull()
}

override suspend fun saveGoogleIdToken(token: String) {
dataStore.edit { preferences ->
preferences[googleIdTokenKey] = token
}

dataStore.data.map { preferences ->
preferences[googleIdTokenKey]
}.firstOrNull()
}

override suspend fun getGoogleIdToken(): String? {
return dataStore.data.map { preferences ->
preferences[googleIdTokenKey]
}.firstOrNull()
}

override suspend fun clearAllTokens() {
dataStore.edit { preferences ->
preferences.remove(accessJwtKey)
Expand All @@ -56,6 +40,5 @@ class DefaultJwtManager
private const val ACCESS_JWT_KEY_NAME = "access_jwt"
private const val GOOGLE_ID_TOKEN_KEY_NAME = "google_id_token"
val accessJwtKey = stringPreferencesKey(ACCESS_JWT_KEY_NAME)
val googleIdTokenKey = stringPreferencesKey(GOOGLE_ID_TOKEN_KEY_NAME)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.benenfeldt.remote.token

import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import javax.inject.Inject

class HY2Authenticator
@Inject
constructor(
private val authCallback: AuthCallback,
private val jwtManager: JwtManager,
) : Authenticator {
override fun authenticate(
route: Route?,
response: Response,
): Request? {
if (response.code == 401) {
runBlocking { jwtManager.clearAllTokens() }
authCallback.onAuthRequired()

// null을 반환하여 현재 요청을 중단
return null
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,5 @@ interface JwtManager {

suspend fun getAccessJwt(): String?

suspend fun saveGoogleIdToken(token: String)

suspend fun getGoogleIdToken(): String?

suspend fun clearAllTokens()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.benenfeldt.remote.token

data class TokenInformation(
val role: TokenRole,
val isValidToken: Boolean,
)
15 changes: 15 additions & 0 deletions core/remote/src/main/java/com/benenfeldt/remote/token/TokenRole.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.benenfeldt.remote.token

/**
* USER - 구글 로그인을 마치고, 회원가입을 한 유저
*
* GUEST - 구글 로그인을 했지만, 회원가입을 하지 않은 유저
*
* ADMIN - 관리자 계정, 하지만 앱으로 로그인 하는 경우는 없음
* */

enum class TokenRole {
Copy link
Contributor

Choose a reason for hiding this comment

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

카톡으로 이야기를 나누었지만 USER, GUEST, ADMIN의 의미가 혼동이 올 수 있을꺼 같았는데 주석을 작성해두는 것은 어떨까요

Copy link
Contributor

Choose a reason for hiding this comment

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

GUESTUNREGISTERED_USER로만 변경해도 혼동사항은 없을꺼 같기도 합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GUEST는 서버에서 사용한 네이밍임으로 통일성을 위해 일단은 그냥 두겠습니다
추후 게스트로그인 등을 구현시 처리하겠습니다!

USER,
GUEST,
ADMIN,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.benenfeldt.remote.token

import com.benenfeldt.remote.api.TokenService
import com.benenfeldt.remote.mapper.toResult
import com.benenfeldt.remote.mapper.toTokenInformation
import javax.inject.Inject

class TokenValidator
@Inject
constructor(
private val tokenService: TokenService,
) {
suspend fun validate(): Result<TokenInformation> {
return tokenService.getTokenInformation()
.toResult { it.data.toTokenInformation() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.teamhy2.designsystem.common.HY2CircularLoading
import com.teamhy2.designsystem.common.HY2Dialog
import com.teamhy2.designsystem.common.HY2TimePicker
import com.teamhy2.designsystem.ui.theme.Gray800
import com.teamhy2.designsystem.ui.theme.HY2Theme
import com.teamhy2.designsystem.util.compositionlocal.LocalShowSnackBar
import com.teamhy2.feature.home.component.InitTimerComponent
Expand Down Expand Up @@ -84,7 +83,7 @@ fun HomeRoute(
}
}

SetNavigationBarColor(Gray800)
SetNavigationBarColor(Color.Black)

when (homeUiState) {
is HomeUiState.Loading -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fun RunningTimerComponent(
HY2Button(
text = stringResource(R.string.main_extend_study_room),
onClick = onStudyRoomExtendClick,
modifier = Modifier.height(52.dp),
)
Spacer(modifier = Modifier.height(8.dp))
}
Expand All @@ -66,6 +67,7 @@ fun RunningTimerComponent(
backgroundColor = Gray600,
textColor = Gray100,
onClick = onStudyRoomEndClick,
modifier = Modifier.height(52.dp),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface InitialUiState {
data object NeedUpdate : InitialUiState

data class Success(
val startDestination: String = "",
val startDestination: String,
val urls: Map<UrlName, UrlValue> = emptyMap(),
) : InitialUiState
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ package com.teamhy2.feature.main
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.benenfeldt.remote.token.JwtManager
import com.benenfeldt.remote.token.TokenRole
import com.benenfeldt.remote.token.TokenValidator
import com.google.firebase.firestore.FirebaseFirestore
import com.teamhy2.feature.home.navigation.Home
import com.teamhy2.hongikyeolgong2.notification.NotificationHandler
import com.teamhy2.onboarding.domain.repository.WebViewRepository
import com.teamhy2.onboarding.navigation.Onboarding
import com.teamhy2.onboarding.navigation.SignUp
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
Expand All @@ -24,6 +31,8 @@ class InitialViewModel
@Inject
constructor(
private val webViewRepository: WebViewRepository,
private val jwtManager: JwtManager,
Copy link
Contributor

Choose a reason for hiding this comment

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

이전 PR에서 다루지 못해서 죄송하지만 JwtManager가 현재 GoogleIdToken까지 관리를 하고 있는데 그러면 JwtManager이라는 이름의 역할을 넘어서는 일을 하는 것 같습니다. TokenManager 정도로 변경하는 것은 어떤가요? 분리하기엔 너무 관리 주체가 나눠지는거 같아서

private val tokenValidator: TokenValidator,
val notificationHandler: NotificationHandler,
) : ViewModel() {
private val _initialUiState: MutableStateFlow<InitialUiState> =
Expand All @@ -34,40 +43,44 @@ class InitialViewModel
val errorFlow: SharedFlow<Throwable> = _errorFlow.asSharedFlow()

init {
fetchStartDestination()
fetchFirebaseUrls()
initInitialState()
}

private fun fetchStartDestination() {
fun setStartDestination(startDestination: String) {
if (_initialUiState.value is InitialUiState.Loading) {
_initialUiState.update { InitialUiState.Success(startDestination = startDestination) }
return
}
if (_initialUiState.value is InitialUiState.Success) {
_initialUiState.update {
(it as InitialUiState.Success).copy(startDestination = startDestination)
}
private fun initInitialState() {
viewModelScope.launch {
val startDestination: Deferred<String> = async { getStartDestination() }
val urls: Deferred<Map<String, String>> = async { webViewRepository.fetchFirebaseUrls() }

_initialUiState.update {
InitialUiState.Success(
startDestination = startDestination.await(),
urls = urls.await(),
)
}
}

// TODO: 자동 로그인 구현 시 변경
setStartDestination(Onboarding.ROUTE)
}

private fun fetchFirebaseUrls() {
viewModelScope.launch {
val urls = webViewRepository.fetchFirebaseUrls()
if (_initialUiState.value is InitialUiState.Loading) {
_initialUiState.update { InitialUiState.Success(urls = urls) }
return@launch
}
if (_initialUiState.value is InitialUiState.Success) {
_initialUiState.update { (it as InitialUiState.Success).copy(urls = urls) }
}
private suspend fun getStartDestination(): String {
jwtManager.getAccessJwt() ?: return resetToken()

val isValidToken = tokenValidator.validate().getOrNull()
if (isValidToken == null) {
_errorFlow.emit(Throwable("서버 연결에 이상이 있습니다."))
return resetToken()
}

return when (isValidToken.role) {
TokenRole.USER -> Home.ROUTE
TokenRole.GUEST -> SignUp.ROUTE
TokenRole.ADMIN -> resetToken()
}
}

private suspend fun resetToken(): String {
jwtManager.clearAllTokens()
return Onboarding.ROUTE
}

fun getMinVersion(currentVersion: Long) {
viewModelScope.launch {
val firebaseStore = FirebaseFirestore.getInstance()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.teamhy2.feature.main

import android.content.Context
import android.content.Intent
import com.benenfeldt.remote.token.AuthCallback
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class MainAuthCallback
@Inject
constructor(
@ApplicationContext private val context: Context,
) : AuthCallback {
override fun onAuthRequired() {
val intent =
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
context.startActivity(intent)
Comment on lines +15 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

컴포즈에서 Intent를 쓰니까 새롭군요 ㅋㅋㅋㅋ

}
}
Loading