Нормальные токены
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ package auth.data.remote.api
|
|||||||
|
|
||||||
import auth.data.remote.dto.AuthRequest
|
import auth.data.remote.dto.AuthRequest
|
||||||
import auth.data.remote.dto.AuthResponse
|
import auth.data.remote.dto.AuthResponse
|
||||||
|
import auth.data.remote.dto.RefreshTokenRequest
|
||||||
import core.domain.model.ServerConfigModel
|
import core.domain.model.ServerConfigModel
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
@@ -10,13 +11,14 @@ import retrofit2.http.POST
|
|||||||
|
|
||||||
interface AuthApi {
|
interface AuthApi {
|
||||||
@POST("auth/login")
|
@POST("auth/login")
|
||||||
@Headers("Cache-Control: no-cache")
|
|
||||||
suspend fun login(@Body request: AuthRequest): AuthResponse
|
suspend fun login(@Body request: AuthRequest): AuthResponse
|
||||||
|
|
||||||
@POST("auth/register")
|
@POST("auth/register")
|
||||||
@Headers("Cache-Control: no-cache")
|
|
||||||
suspend fun register(@Body request: AuthRequest): AuthResponse
|
suspend fun register(@Body request: AuthRequest): AuthResponse
|
||||||
|
|
||||||
|
@POST("auth/refresh")
|
||||||
|
suspend fun refreshToken(@Body request: RefreshTokenRequest): AuthResponse
|
||||||
|
|
||||||
@GET("config")
|
@GET("config")
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
suspend fun getConfig(): ServerConfigModel
|
suspend fun getConfig(): ServerConfigModel
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class AuthRequest(
|
|||||||
|
|
||||||
data class AuthResponse(
|
data class AuthResponse(
|
||||||
@SerializedName("accessToken") val accessToken: String?,
|
@SerializedName("accessToken") val accessToken: String?,
|
||||||
|
@SerializedName("refreshToken") val refreshToken: String?,
|
||||||
@SerializedName("user") val user: UserDto?,
|
@SerializedName("user") val user: UserDto?,
|
||||||
@SerializedName("userId") val userId: String?,
|
@SerializedName("userId") val userId: String?,
|
||||||
@SerializedName("username") val username: String?,
|
@SerializedName("username") val username: String?,
|
||||||
@@ -21,3 +22,7 @@ data class UserDto(
|
|||||||
@SerializedName("displayName") val displayName: String?,
|
@SerializedName("displayName") val displayName: String?,
|
||||||
@SerializedName("avatarUrl") val avatarUrl: String?
|
@SerializedName("avatarUrl") val avatarUrl: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RefreshTokenRequest(
|
||||||
|
@SerializedName("refreshToken") val refreshToken: String
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package auth.data.repository
|
|||||||
|
|
||||||
import auth.data.remote.api.AuthApi
|
import auth.data.remote.api.AuthApi
|
||||||
import auth.data.remote.dto.AuthRequest
|
import auth.data.remote.dto.AuthRequest
|
||||||
|
import auth.data.remote.dto.RefreshTokenRequest
|
||||||
import auth.domain.model.AuthResult
|
import auth.domain.model.AuthResult
|
||||||
import auth.domain.repository.AuthRepository
|
import auth.domain.repository.AuthRepository
|
||||||
import core.network.ServerConfig
|
import core.network.ServerConfig
|
||||||
@@ -25,12 +26,13 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
val token = response.accessToken ?: return Result.failure(Exception("Token is null"))
|
val token = response.accessToken ?: return Result.failure(Exception("Token is null"))
|
||||||
val userId = response.userId ?: ""
|
val userId = response.userId ?: ""
|
||||||
|
|
||||||
tokenManager.saveToken(token, userId)
|
tokenManager.saveToken(token, userId, response.refreshToken)
|
||||||
_isAuthenticated.value = true
|
_isAuthenticated.value = true
|
||||||
fetchConfig()
|
fetchConfig()
|
||||||
Result.success(
|
Result.success(
|
||||||
AuthResult(
|
AuthResult(
|
||||||
token = token,
|
token = token,
|
||||||
|
refreshToken = response.refreshToken,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
userName = response.username ?: userName,
|
userName = response.username ?: userName,
|
||||||
displayName = response.displayName ?: 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 token = response.accessToken ?: return Result.failure(Exception("Token is null"))
|
||||||
val userId = response.userId ?: ""
|
val userId = response.userId ?: ""
|
||||||
|
|
||||||
tokenManager.saveToken(token, userId)
|
tokenManager.saveToken(token, userId, response.refreshToken)
|
||||||
_isAuthenticated.value = true
|
_isAuthenticated.value = true
|
||||||
fetchConfig()
|
fetchConfig()
|
||||||
Result.success(
|
Result.success(
|
||||||
AuthResult(
|
AuthResult(
|
||||||
token = token,
|
token = token,
|
||||||
|
refreshToken = response.refreshToken,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
userName = response.username ?: userName,
|
userName = response.username ?: userName,
|
||||||
displayName = response.displayName ?: response.username ?: userName,
|
displayName = response.displayName ?: response.username ?: userName,
|
||||||
@@ -95,4 +98,34 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
// Silent fail
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package auth.domain.model
|
|||||||
|
|
||||||
data class AuthResult(
|
data class AuthResult(
|
||||||
val token: String,
|
val token: String,
|
||||||
|
val refreshToken: String?,
|
||||||
val userId: String,
|
val userId: String,
|
||||||
val userName: String,
|
val userName: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ interface AuthRepository {
|
|||||||
fun isAuthenticated(): Boolean
|
fun isAuthenticated(): Boolean
|
||||||
fun isAuthenticatedFlow(): kotlinx.coroutines.flow.StateFlow<Boolean>
|
fun isAuthenticatedFlow(): kotlinx.coroutines.flow.StateFlow<Boolean>
|
||||||
suspend fun updatePushToken(token: String)
|
suspend fun updatePushToken(token: String)
|
||||||
|
suspend fun refreshToken(): Result<AuthResult>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
private fun updatePushToken() {
|
||||||
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
||||||
if (task.isSuccessful) {
|
if (task.isSuccessful) {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AuthInterceptor @Inject constructor(
|
class AuthInterceptor @Inject constructor(
|
||||||
@@ -15,6 +13,10 @@ class AuthInterceptor @Inject constructor(
|
|||||||
private val navigationManager: core.utils.NavigationManager,
|
private val navigationManager: core.utils.NavigationManager,
|
||||||
private val authRepositoryProvider: javax.inject.Provider<auth.domain.repository.AuthRepository>
|
private val authRepositoryProvider: javax.inject.Provider<auth.domain.repository.AuthRepository>
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isRefreshing = false
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val token = tokenManager.getToken()
|
val token = tokenManager.getToken()
|
||||||
val request = chain.request().newBuilder().apply {
|
val request = chain.request().newBuilder().apply {
|
||||||
@@ -23,16 +25,55 @@ class AuthInterceptor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val response = chain.proceed(request)
|
var response = chain.proceed(request)
|
||||||
|
|
||||||
if (response.code == 401) {
|
if (response.code == 401) {
|
||||||
// Global logout
|
// Attempt to refresh token synchronously
|
||||||
kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.Main) {
|
val refreshedToken = refreshAuthToken()
|
||||||
authRepositoryProvider.get().logout()
|
|
||||||
navigationManager.logout()
|
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
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ class TokenManager @Inject constructor(context: Context) {
|
|||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
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)
|
val editor = prefs.edit().putString("jwt_token", token)
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
editor.putString("user_id", userId)
|
editor.putString("user_id", userId)
|
||||||
}
|
}
|
||||||
|
if (refreshToken != null) {
|
||||||
|
editor.putString("refresh_token", refreshToken)
|
||||||
|
}
|
||||||
editor.apply()
|
editor.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,11 +35,15 @@ class TokenManager @Inject constructor(context: Context) {
|
|||||||
return prefs.getString("jwt_token", null)
|
return prefs.getString("jwt_token", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRefreshToken(): String? {
|
||||||
|
return prefs.getString("refresh_token", null)
|
||||||
|
}
|
||||||
|
|
||||||
fun getUserId(): String? {
|
fun getUserId(): String? {
|
||||||
return prefs.getString("user_id", null)
|
return prefs.getString("user_id", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteToken() {
|
fun deleteToken() {
|
||||||
prefs.edit().remove("jwt_token").remove("user_id").apply()
|
prefs.edit().remove("jwt_token").remove("user_id").remove("refresh_token").apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user