diff --git a/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt b/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt index 4db0ae4..1e526db 100644 --- a/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt +++ b/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt @@ -45,25 +45,40 @@ class AuthApiClient(private val context: Context) { loadTokens { val accessToken = tokenManager.getAccessToken() val refreshToken = tokenManager.getRefreshToken() + android.util.Log.d("AuthApiClient", "loadTokens: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}") if (accessToken != null && refreshToken != null) { + android.util.Log.d("AuthApiClient", "loadTokens: Returning BearerTokens") BearerTokens(accessToken, refreshToken) } else { + android.util.Log.d("AuthApiClient", "loadTokens: No tokens available, returning null") null } } refreshTokens { - val refreshToken = tokenManager.getRefreshToken() ?: return@refreshTokens null + android.util.Log.d("AuthApiClient", "refreshTokens: Starting token refresh") + val refreshToken = tokenManager.getRefreshToken() ?: run { + android.util.Log.e("AuthApiClient", "refreshTokens: No refresh token found!") + return@refreshTokens null + } + + android.util.Log.d("AuthApiClient", "refreshTokens: Calling /auth/refresh endpoint") + try { + val response: RefreshResponse = client.post("http://10.0.2.2:3000/auth/refresh") { + markAsRefreshTokenRequest() + contentType(ContentType.Application.Json) + setBody(RefreshRequest(refreshToken)) + }.body() - val response: RefreshResponse = client.post("http://10.0.2.2:3000/auth/refresh") { - markAsRefreshTokenRequest() - contentType(ContentType.Application.Json) - setBody(RefreshRequest(refreshToken)) - }.body() + android.util.Log.d("AuthApiClient", "refreshTokens: Refresh successful, saving new tokens") + tokenManager.saveTokens(response.accessToken, response.refreshToken) + android.util.Log.d("AuthApiClient", "refreshTokens: New tokens saved successfully") - tokenManager.saveTokens(response.accessToken, response.refreshToken) - - BearerTokens(response.accessToken, response.refreshToken) + BearerTokens(response.accessToken, response.refreshToken) + } catch (e: Exception) { + android.util.Log.e("AuthApiClient", "refreshTokens: Refresh failed: ${e.message}", e) + throw e + } } } } @@ -155,21 +170,40 @@ class AuthApiClient(private val context: Context) { } suspend fun getUserDetails(): Result = runCatching { - client.get("users/me").body() + android.util.Log.d("AuthApiClient", "getUserDetails: Calling /users/me endpoint") + try { + val response = client.get("users/me") + android.util.Log.d("AuthApiClient", "getUserDetails: Response status=${response.status}") + val userDetails = response.body() + android.util.Log.d("AuthApiClient", "getUserDetails: Success - user id=${userDetails.id}") + userDetails + } catch (e: Exception) { + android.util.Log.e("AuthApiClient", "getUserDetails: Error - ${e.message}", e) + throw e + } } suspend fun refreshToken(): Result = runCatching { + android.util.Log.d("AuthApiClient", "refreshToken: Starting manual token refresh") val refreshToken = tokenManager.getRefreshToken() ?: throw IllegalStateException("No refresh token found") - val response: RefreshResponse = client.post("auth/refresh") { - contentType(ContentType.Application.Json) - setBody(RefreshRequest(refreshToken)) - }.body() + android.util.Log.d("AuthApiClient", "refreshToken: Calling /auth/refresh endpoint") + try { + val response: RefreshResponse = client.post("auth/refresh") { + contentType(ContentType.Application.Json) + setBody(RefreshRequest(refreshToken)) + }.body() - // Save the new tokens (refresh token rotates) - tokenManager.saveTokens(response.accessToken, response.refreshToken) - response + android.util.Log.d("AuthApiClient", "refreshToken: Refresh successful, saving new tokens") + // Save the new tokens (refresh token rotates) + tokenManager.saveTokens(response.accessToken, response.refreshToken) + android.util.Log.d("AuthApiClient", "refreshToken: New tokens saved successfully") + response + } catch (e: Exception) { + android.util.Log.e("AuthApiClient", "refreshToken: Refresh failed: ${e.message}", e) + throw e + } } suspend fun logout(): Result = runCatching { diff --git a/app/src/main/java/com/example/livingai_lg/api/TokenManager.kt b/app/src/main/java/com/example/livingai_lg/api/TokenManager.kt index 4267933..de980c9 100644 --- a/app/src/main/java/com/example/livingai_lg/api/TokenManager.kt +++ b/app/src/main/java/com/example/livingai_lg/api/TokenManager.kt @@ -2,6 +2,7 @@ package com.example.livingai_lg.api import android.content.Context import android.content.SharedPreferences +import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey @@ -20,25 +21,47 @@ class TokenManager(context: Context) { ) companion object { + private const val TAG = "TokenManager" private const val KEY_ACCESS_TOKEN = "access_token" private const val KEY_REFRESH_TOKEN = "refresh_token" } fun saveTokens(accessToken: String, refreshToken: String) { - prefs.edit() + Log.d(TAG, "saveTokens: Saving tokens (accessToken length=${accessToken.length}, refreshToken length=${refreshToken.length})") + val success = prefs.edit() .putString(KEY_ACCESS_TOKEN, accessToken) .putString(KEY_REFRESH_TOKEN, refreshToken) .commit() // Use commit() instead of apply() to ensure tokens are saved synchronously + + if (success) { + Log.d(TAG, "saveTokens: Tokens saved successfully") + // Verify tokens were saved + val savedAccess = prefs.getString(KEY_ACCESS_TOKEN, null) + val savedRefresh = prefs.getString(KEY_REFRESH_TOKEN, null) + Log.d(TAG, "saveTokens: Verification - accessToken saved=${savedAccess != null}, refreshToken saved=${savedRefresh != null}") + } else { + Log.e(TAG, "saveTokens: FAILED to save tokens!") + } } - fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null) + fun getAccessToken(): String? { + val token = prefs.getString(KEY_ACCESS_TOKEN, null) + Log.d(TAG, "getAccessToken: token=${token != null}, length=${token?.length ?: 0}") + return token + } - fun getRefreshToken(): String? = prefs.getString(KEY_REFRESH_TOKEN, null) + fun getRefreshToken(): String? { + val token = prefs.getString(KEY_REFRESH_TOKEN, null) + Log.d(TAG, "getRefreshToken: token=${token != null}, length=${token?.length ?: 0}") + return token + } fun clearTokens() { + Log.d(TAG, "clearTokens: Clearing all tokens") prefs.edit() .remove(KEY_ACCESS_TOKEN) .remove(KEY_REFRESH_TOKEN) .apply() + Log.d(TAG, "clearTokens: Tokens cleared") } } diff --git a/app/src/main/java/com/example/livingai_lg/ui/MainViewModel.kt b/app/src/main/java/com/example/livingai_lg/ui/MainViewModel.kt index f153dfc..9625712 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/MainViewModel.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/MainViewModel.kt @@ -38,12 +38,22 @@ class MainViewModel(context: Context) : ViewModel() { init { // Immediately check if tokens exist (synchronous check) - val hasTokens = tokenManager.getAccessToken() != null && tokenManager.getRefreshToken() != null + val accessToken = tokenManager.getAccessToken() + val refreshToken = tokenManager.getRefreshToken() + val hasTokens = accessToken != null && refreshToken != null + + Log.d(TAG, "MainViewModel.init: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}, hasTokens=$hasTokens") + if (hasTokens) { - // Tokens exist, validate them asynchronously + // Tokens exist - optimistically set to Authenticated for immediate navigation + // Then validate in background (this prevents redirect to landing page on app restart) + Log.d(TAG, "MainViewModel.init: Tokens found, setting authState to Authenticated (optimistic)") + _authState.value = AuthState.Authenticated + // Validate tokens in background (this will only revert if there's a clear auth failure) checkAuthStatus() } else { // No tokens, immediately set to unauthenticated + Log.d(TAG, "MainViewModel.init: No tokens found, setting authState to Unauthenticated") _authState.value = AuthState.Unauthenticated } } @@ -86,15 +96,17 @@ class MainViewModel(context: Context) : ViewModel() { */ private fun validateTokensOptimistic() { viewModelScope.launch { + Log.d(TAG, "validateTokensOptimistic: Starting token validation") // Try to fetch user details first - Ktor's Auth plugin will auto-refresh if access token is expired authApiClient.getUserDetails() .onSuccess { userDetails -> // Tokens are valid, user is authenticated - Log.d(TAG, "Token validation successful - user authenticated") + Log.d(TAG, "validateTokensOptimistic: Token validation successful - user authenticated, userId=${userDetails.id}") _authState.value = AuthState.Authenticated _userState.value = UserState.Success(userDetails) } .onFailure { error -> + Log.w(TAG, "validateTokensOptimistic: getUserDetails failed - ${error.message}") // Check if this is a network error or authentication error val isNetworkError = error.message?.contains("Unable to resolve host", ignoreCase = true) == true || error.message?.contains("timeout", ignoreCase = true) == true @@ -104,12 +116,15 @@ class MainViewModel(context: Context) : ViewModel() { || error.message?.contains("ConnectException", ignoreCase = true) == true || error.message?.contains("UnknownHostException", ignoreCase = true) == true + Log.d(TAG, "validateTokensOptimistic: isNetworkError=$isNetworkError") + if (isNetworkError) { // Network error - keep optimistic authentication state // User might be offline, tokens are still valid - Log.w(TAG, "Network error during token validation (keeping optimistic auth): ${error.message}") + Log.w(TAG, "validateTokensOptimistic: Network error detected (keeping optimistic auth): ${error.message}") _userState.value = UserState.Error("Network error. Please check your connection.") // Keep authState as Authenticated - don't revert on network errors + Log.d(TAG, "validateTokensOptimistic: Keeping authState as Authenticated despite network error") return@launch } @@ -190,12 +205,16 @@ class MainViewModel(context: Context) : ViewModel() { val accessToken = tokenManager.getAccessToken() val refreshToken = tokenManager.getRefreshToken() + Log.d(TAG, "checkAuthStatus: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}") + if (accessToken != null && refreshToken != null) { - // Tokens exist, validate them by fetching user details - // The Ktor Auth plugin will automatically refresh if access token is expired - validateTokens() + // Tokens exist, validate them using optimistic validation + // This keeps authState as Authenticated unless there's a clear auth failure + Log.d(TAG, "checkAuthStatus: Validating tokens optimistically") + validateTokensOptimistic() } else { // No tokens, user is not authenticated + Log.d(TAG, "checkAuthStatus: No tokens found, setting authState to Unauthenticated") _authState.value = AuthState.Unauthenticated } }