Нормальные токены

This commit is contained in:
Халимов Рустам
2026-04-19 22:55:38 +03:00
parent f6400ce3ac
commit 945134f029
15 changed files with 116 additions and 14 deletions

View File

@@ -2,6 +2,7 @@ package auth.data.remote.api
import auth.data.remote.dto.AuthRequest
import auth.data.remote.dto.AuthResponse
import auth.data.remote.dto.RefreshTokenRequest
import core.domain.model.ServerConfigModel
import retrofit2.http.Body
import retrofit2.http.GET
@@ -10,13 +11,14 @@ import retrofit2.http.POST
interface AuthApi {
@POST("auth/login")
@Headers("Cache-Control: no-cache")
suspend fun login(@Body request: AuthRequest): AuthResponse
@POST("auth/register")
@Headers("Cache-Control: no-cache")
suspend fun register(@Body request: AuthRequest): AuthResponse
@POST("auth/refresh")
suspend fun refreshToken(@Body request: RefreshTokenRequest): AuthResponse
@GET("config")
@Headers("Cache-Control: no-cache")
suspend fun getConfig(): ServerConfigModel

View File

@@ -9,6 +9,7 @@ data class AuthRequest(
data class AuthResponse(
@SerializedName("accessToken") val accessToken: String?,
@SerializedName("refreshToken") val refreshToken: String?,
@SerializedName("user") val user: UserDto?,
@SerializedName("userId") val userId: String?,
@SerializedName("username") val username: String?,
@@ -21,3 +22,7 @@ data class UserDto(
@SerializedName("displayName") val displayName: String?,
@SerializedName("avatarUrl") val avatarUrl: String?
)
data class RefreshTokenRequest(
@SerializedName("refreshToken") val refreshToken: String
)

View File

@@ -2,6 +2,7 @@ package auth.data.repository
import auth.data.remote.api.AuthApi
import auth.data.remote.dto.AuthRequest
import auth.data.remote.dto.RefreshTokenRequest
import auth.domain.model.AuthResult
import auth.domain.repository.AuthRepository
import core.network.ServerConfig
@@ -25,12 +26,13 @@ class AuthRepositoryImpl @Inject constructor(
val token = response.accessToken ?: return Result.failure(Exception("Token is null"))
val userId = response.userId ?: ""
tokenManager.saveToken(token, userId)
tokenManager.saveToken(token, userId, response.refreshToken)
_isAuthenticated.value = true
fetchConfig()
Result.success(
AuthResult(
token = token,
refreshToken = response.refreshToken,
userId = userId,
userName = response.username ?: userName,
displayName = response.displayName ?: response.username ?: userName,
@@ -48,12 +50,13 @@ class AuthRepositoryImpl @Inject constructor(
val token = response.accessToken ?: return Result.failure(Exception("Token is null"))
val userId = response.userId ?: ""
tokenManager.saveToken(token, userId)
tokenManager.saveToken(token, userId, response.refreshToken)
_isAuthenticated.value = true
fetchConfig()
Result.success(
AuthResult(
token = token,
refreshToken = response.refreshToken,
userId = userId,
userName = response.username ?: userName,
displayName = response.displayName ?: response.username ?: userName,
@@ -95,4 +98,34 @@ class AuthRepositoryImpl @Inject constructor(
// Silent fail
}
}
override suspend fun refreshToken(): Result<AuthResult> {
val currentRefreshToken = tokenManager.getRefreshToken()
if (currentRefreshToken == null) {
return Result.failure(Exception("Refresh token is null"))
}
return try {
val response = api.refreshToken(RefreshTokenRequest(currentRefreshToken))
val newAccessToken = response.accessToken ?: return Result.failure(Exception("New access token is null"))
val newRefreshToken = response.refreshToken
val userId = response.userId ?: ""
tokenManager.saveToken(newAccessToken, userId, newRefreshToken)
_isAuthenticated.value = true
Result.success(
AuthResult(
token = newAccessToken,
refreshToken = newRefreshToken,
userId = userId,
userName = response.username ?: "",
displayName = response.displayName ?: "",
avatarUrl = null
)
)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -2,6 +2,7 @@ package auth.domain.model
data class AuthResult(
val token: String,
val refreshToken: String?,
val userId: String,
val userName: String,
val displayName: String,

View File

@@ -10,4 +10,5 @@ interface AuthRepository {
fun isAuthenticated(): Boolean
fun isAuthenticatedFlow(): kotlinx.coroutines.flow.StateFlow<Boolean>
suspend fun updatePushToken(token: String)
suspend fun refreshToken(): Result<AuthResult>
}

View File

@@ -69,6 +69,18 @@ class AuthViewModel @Inject constructor(
}
}
fun refreshToken() {
viewModelScope.launch {
repository.refreshToken()
.onSuccess {
// Token refreshed successfully
}
.onFailure {
// Refresh failed, will trigger logout via AuthInterceptor
}
}
}
private fun updatePushToken() {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {

View File

@@ -5,9 +5,7 @@ import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
@Singleton
class AuthInterceptor @Inject constructor(
@@ -15,6 +13,10 @@ class AuthInterceptor @Inject constructor(
private val navigationManager: core.utils.NavigationManager,
private val authRepositoryProvider: javax.inject.Provider<auth.domain.repository.AuthRepository>
) : Interceptor {
@Volatile
private var isRefreshing = false
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenManager.getToken()
val request = chain.request().newBuilder().apply {
@@ -23,16 +25,55 @@ class AuthInterceptor @Inject constructor(
}
}.build()
val response = chain.proceed(request)
var response = chain.proceed(request)
if (response.code == 401) {
// Global logout
kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.Main) {
authRepositoryProvider.get().logout()
navigationManager.logout()
// Attempt to refresh token synchronously
val refreshedToken = refreshAuthToken()
if (refreshedToken != null) {
// Close the old response before retrying
response.close()
// Retry the original request with the new token
val retryRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $refreshedToken")
.build()
response = chain.proceed(retryRequest)
} else {
// Refresh failed, logout
logoutUser()
}
}
return response
}
private fun refreshAuthToken(): String? {
if (isRefreshing) {
// Already refreshing, wait and return current token
return tokenManager.getToken()
}
return runBlocking {
isRefreshing = true
try {
val result = authRepositoryProvider.get().refreshToken()
if (result.isSuccess) {
result.getOrNull()?.token
} else {
null
}
} finally {
isRefreshing = false
}
}
}
private fun logoutUser() {
runBlocking {
authRepositoryProvider.get().logout()
}
navigationManager.logout()
}
}

View File

@@ -20,11 +20,14 @@ class TokenManager @Inject constructor(context: Context) {
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(token: String, userId: String? = null) {
fun saveToken(token: String, userId: String? = null, refreshToken: String? = null) {
val editor = prefs.edit().putString("jwt_token", token)
if (userId != null) {
editor.putString("user_id", userId)
}
if (refreshToken != null) {
editor.putString("refresh_token", refreshToken)
}
editor.apply()
}
@@ -32,11 +35,15 @@ class TokenManager @Inject constructor(context: Context) {
return prefs.getString("jwt_token", null)
}
fun getRefreshToken(): String? {
return prefs.getString("refresh_token", null)
}
fun getUserId(): String? {
return prefs.getString("user_id", null)
}
fun deleteToken() {
prefs.edit().remove("jwt_token").remove("user_id").apply()
prefs.edit().remove("jwt_token").remove("user_id").remove("refresh_token").apply()
}
}