From 89da510a3f78100494369cf18800da755ec73b68 Mon Sep 17 00:00:00 2001 From: Eric85710 Date: Tue, 30 Jun 2026 16:04:17 +0800 Subject: [PATCH] dm-180 --- .../login_v3/data/api/Main_API_Service.kt | 9 +++ .../data/repository/basic/Token_Auth.kt | 68 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt diff --git a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt index dafbf85..eb1721a 100644 --- a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt +++ b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt @@ -1,6 +1,7 @@ package com.example.login_v3.data.api import com.example.login_v3.data.api.api_class.* +import com.squareup.moshi.JsonClass import okhttp3.MultipartBody import retrofit2.Response import retrofit2.http.Body @@ -15,6 +16,11 @@ import retrofit2.http.Part import retrofit2.http.Path import retrofit2.http.Query +@JsonClass(generateAdapter = true) +data class RefreshRequest( + val refresh_token: String +) + interface TecnologiaApi { @GET("api/health") @@ -28,6 +34,9 @@ interface TecnologiaApi { @POST("api/auth/login") suspend fun login(@Body body: LoginRequest): Response + @POST("api/auth/refresh") + suspend fun refreshToken(@Body body: RefreshRequest): Response + // 取得個人資料:通常需要 Token @GET("api/me") suspend fun getUserProfile(): Response diff --git a/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt b/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt new file mode 100644 index 0000000..1534640 --- /dev/null +++ b/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt @@ -0,0 +1,68 @@ +package com.example.login_v3.data.repository.basic + +import com.example.login_v3.data.api.RefreshRequest +import com.example.login_v3.data.api.TecnologiaApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Route +import okhttp3.Response +import javax.inject.Inject + +class TokenAuthenticator @Inject constructor( + private val tokenManager: TokenManager, + // 💡 關鍵:使用 dagger.Lazy 避免 Hilt 的循環依賴 (Circular Dependency) + private val apiService: dagger.Lazy +) : Authenticator { + + override fun authenticate(route: Route?, response: Response): Request? { + // 1. 取得 DataStore 中的當前帳號資訊 + val refreshToken = runBlocking { tokenManager.currentRefreshToken.first() } + val currentUserId = runBlocking { tokenManager.currentUserId.first() } + + if (refreshToken.isNullOrEmpty() || currentUserId.isNullOrEmpty()) { + return null // 沒有憑證資訊,不重試,直接拋出原 401 錯誤 + } + + // 2. 同步發送網路請求,拿 refresh_token 去換新 Token + val refreshResponse = runBlocking { + try { + // 用 apiService.get() 來取得真正需要用的實例 + apiService.get().refreshToken(RefreshRequest(refreshToken)) + } catch (e: Exception) { + null + } + } + + // 3. 判斷刷新是否成功 + if (refreshResponse != null && refreshResponse.isSuccessful) { + val newAuthData = refreshResponse.body() + if (newAuthData != null) { + + // 4. 成功:更新本地 DataStore 資料 + runBlocking { + tokenManager.saveAuthData( + userId = currentUserId, + accessToken = newAuthData.access_token, + refreshToken = newAuthData.refresh_token, + expiresInSec = newAuthData.expires_in + ) + } + + // 5. 成功:用新的 Access Token 重新建構剛才失敗的 API 請求 + return response.request.newBuilder() + .header("Authorization", "Bearer ${newAuthData.access_token}") + .build() + } + } + + // 6. 失敗:如果連 Refresh Token 都失效(例如過期被後端拒絕),則強制登出,導回登入頁 + runBlocking { + tokenManager.logout(currentUserId) + // 這裡可以透過 EventBus、SharedFlow 或 LiveData 貼出全域通知,要求 UI 跳轉到 LoginActivity/LoginScreen + } + + return null + } +} \ No newline at end of file