android: add auth network core, token store, and DI wiring
This commit is contained in:
@@ -7,3 +7,10 @@
|
||||
- Added app dependencies for Retrofit/OkHttp, DataStore, coroutines, Hilt, and unit testing.
|
||||
- Enabled INTERNET permission and registered MessengerApplication in manifest.
|
||||
- Added MessengerApplication with HiltAndroidApp.
|
||||
|
||||
### Step 2 - Network/data core + DI
|
||||
- Fixed DTO/Auth API serialization annotations and endpoint declarations for `/api/v1/auth/login`, `/api/v1/auth/refresh`, `/api/v1/auth/me`.
|
||||
- Implemented DataStore-based token persistence with a corrected `getTokens()` read path.
|
||||
- Added auth network stack: bearer interceptor, 401 authenticator with refresh flow and retry guard.
|
||||
- Added clean-layer contracts and implementations: `domain/common`, `domain/auth`, `data/auth/repository`.
|
||||
- Wired dependencies with Hilt modules for DataStore, OkHttp/Retrofit, and repository bindings.
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
plugins {
|
||||
id(com.android.application)
|
||||
id(org.jetbrains.kotlin.android)
|
||||
id(org.jetbrains.kotlin.kapt)
|
||||
id(org.jetbrains.kotlin.plugin.serialization)
|
||||
id(com.google.dagger.hilt.android)
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("org.jetbrains.kotlin.kapt")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("com.google.dagger.hilt.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = ru.daemonlord.messenger
|
||||
namespace = "ru.daemonlord.messenger"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = ru.daemonlord.messenger
|
||||
applicationId = "ru.daemonlord.messenger"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = 0.1.0
|
||||
versionName = "0.1.0"
|
||||
buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:8000/\"")
|
||||
|
||||
testInstrumentationRunner = androidx.test.runner.AndroidJUnitRunner
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
@@ -27,8 +29,8 @@ android {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile(proguard-android-optimize.txt),
|
||||
proguard-rules.pro
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +41,7 @@ android {
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 17
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -48,48 +50,48 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = 1.5.15
|
||||
kotlinCompilerExtensionVersion = "1.5.15"
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += /META-INF/AL2.0
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(androidx.core:core-ktx:1.15.0)
|
||||
implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.8.7)
|
||||
implementation(androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7)
|
||||
implementation(androidx.activity:activity-compose:1.10.1)
|
||||
implementation(androidx.navigation:navigation-compose:2.8.5)
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
||||
implementation("androidx.activity:activity-compose:1.10.1")
|
||||
implementation("androidx.navigation:navigation-compose:2.8.5")
|
||||
|
||||
implementation(androidx.compose.ui:ui:1.7.6)
|
||||
implementation(androidx.compose.ui:ui-tooling-preview:1.7.6)
|
||||
implementation(androidx.compose.material3:material3:1.3.1)
|
||||
implementation("androidx.compose.ui:ui:1.7.6")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.7.6")
|
||||
implementation("androidx.compose.material3:material3:1.3.1")
|
||||
|
||||
implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0)
|
||||
implementation(org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3)
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||
|
||||
implementation(com.squareup.retrofit2:retrofit:2.11.0)
|
||||
implementation(com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0)
|
||||
implementation(com.squareup.okhttp3:okhttp:4.12.0)
|
||||
implementation(com.squareup.okhttp3:logging-interceptor:4.12.0)
|
||||
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
|
||||
implementation(androidx.datastore:datastore-preferences:1.1.1)
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
|
||||
implementation(com.google.dagger:hilt-android:2.52)
|
||||
kapt(com.google.dagger:hilt-compiler:2.52)
|
||||
implementation(androidx.hilt:hilt-navigation-compose:1.2.0)
|
||||
implementation("com.google.dagger:hilt-android:2.52")
|
||||
kapt("com.google.dagger:hilt-compiler:2.52")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||
|
||||
testImplementation(junit:junit:4.13.2)
|
||||
testImplementation(org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0)
|
||||
testImplementation(io.mockk:mockk:1.13.13)
|
||||
testImplementation(com.squareup.okhttp3:mockwebserver:4.12.0)
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
|
||||
testImplementation("androidx.datastore:datastore-preferences-core:1.1.1")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||
|
||||
debugImplementation(androidx.compose.ui:ui-tooling:1.7.6)
|
||||
debugImplementation(androidx.compose.ui:ui-test-manifest:1.7.6)
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:1.7.6")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.6")
|
||||
}
|
||||
|
||||
kapt {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package ru.daemonlord.messenger.core.network
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthHeaderInterceptor @Inject constructor(
|
||||
private val tokenRepository: TokenRepository,
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val noAuthHeader = originalRequest.header(NO_AUTH_HEADER)
|
||||
|
||||
if (noAuthHeader == "true") {
|
||||
val requestWithoutMarker = originalRequest.newBuilder()
|
||||
.removeHeader(NO_AUTH_HEADER)
|
||||
.build()
|
||||
return chain.proceed(requestWithoutMarker)
|
||||
}
|
||||
|
||||
val accessToken = runBlocking { tokenRepository.getTokens()?.accessToken }
|
||||
val requestBuilder = originalRequest.newBuilder()
|
||||
if (!accessToken.isNullOrBlank()) {
|
||||
requestBuilder.header("Authorization", "Bearer $accessToken")
|
||||
}
|
||||
return chain.proceed(requestBuilder.build())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val NO_AUTH_HEADER = "No-Auth"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package ru.daemonlord.messenger.core.network
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import ru.daemonlord.messenger.core.token.TokenBundle
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class TokenRefreshAuthenticator @Inject constructor(
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val refreshAuthApiService: AuthApiService,
|
||||
) : Authenticator {
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (responseCount(response) >= MAX_RETRIES) {
|
||||
return null
|
||||
}
|
||||
|
||||
val marker = response.request.header(NO_AUTH_HEADER)
|
||||
if (marker == "true") {
|
||||
return null
|
||||
}
|
||||
|
||||
val refreshedAccessToken = synchronized(this) {
|
||||
runBlocking {
|
||||
val currentTokens = tokenRepository.getTokens() ?: return@runBlocking null
|
||||
tryRefreshTokens(currentTokens)
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
return response.request.newBuilder()
|
||||
.header("Authorization", "Bearer $refreshedAccessToken")
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun tryRefreshTokens(tokens: TokenBundle): String? {
|
||||
return try {
|
||||
val refreshed = refreshAuthApiService.refresh(
|
||||
request = RefreshTokenRequestDto(refreshToken = tokens.refreshToken)
|
||||
)
|
||||
tokenRepository.saveTokens(
|
||||
TokenBundle(
|
||||
accessToken = refreshed.accessToken,
|
||||
refreshToken = refreshed.refreshToken,
|
||||
savedAtMillis = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
refreshed.accessToken
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
} catch (_: Exception) {
|
||||
tokenRepository.clearTokens()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun responseCount(response: Response): Int {
|
||||
var current: Response? = response
|
||||
var count = 1
|
||||
while (current?.priorResponse != null) {
|
||||
count++
|
||||
current = current.priorResponse
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_RETRIES = 2
|
||||
const val NO_AUTH_HEADER = "No-Auth"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package ru.daemonlord.messenger.core.token
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DataStoreTokenRepository @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : TokenRepository {
|
||||
|
||||
override fun observeTokens(): Flow<TokenBundle?> = dataStore.data.map { preferences ->
|
||||
preferences.toTokenBundleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getTokens(): TokenBundle? {
|
||||
return observeTokens().first()
|
||||
}
|
||||
|
||||
override suspend fun saveTokens(tokens: TokenBundle) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[ACCESS_TOKEN_KEY] = tokens.accessToken
|
||||
preferences[REFRESH_TOKEN_KEY] = tokens.refreshToken
|
||||
preferences[SAVED_AT_KEY] = tokens.savedAtMillis
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearTokens() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(ACCESS_TOKEN_KEY)
|
||||
preferences.remove(REFRESH_TOKEN_KEY)
|
||||
preferences.remove(SAVED_AT_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Preferences.toTokenBundleOrNull(): TokenBundle? {
|
||||
val access = this[ACCESS_TOKEN_KEY]
|
||||
val refresh = this[REFRESH_TOKEN_KEY]
|
||||
val savedAt = this[SAVED_AT_KEY]
|
||||
|
||||
if (access.isNullOrBlank() || refresh.isNullOrBlank() || savedAt == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return TokenBundle(
|
||||
accessToken = access,
|
||||
refreshToken = refresh,
|
||||
savedAtMillis = savedAt,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token")
|
||||
val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
|
||||
val SAVED_AT_KEY = longPreferencesKey("tokens_saved_at")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ru.daemonlord.messenger.data.auth.api
|
||||
|
||||
import ru.daemonlord.messenger.data.auth.dto.AuthUserDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.TokenResponseDto
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AuthApiService {
|
||||
@Headers("No-Auth: true")
|
||||
@POST("/api/v1/auth/login")
|
||||
suspend fun login(@Body request: LoginRequestDto): TokenResponseDto
|
||||
|
||||
@Headers("No-Auth: true")
|
||||
@POST("/api/v1/auth/refresh")
|
||||
suspend fun refresh(@Body request: RefreshTokenRequestDto): TokenResponseDto
|
||||
|
||||
@GET("/api/v1/auth/me")
|
||||
suspend fun me(): AuthUserDto
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ru.daemonlord.messenger.data.auth.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LoginRequestDto(
|
||||
val email: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RefreshTokenRequestDto(
|
||||
@SerialName("refresh_token")
|
||||
val refreshToken: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TokenResponseDto(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
@SerialName("refresh_token")
|
||||
val refreshToken: String,
|
||||
@SerialName("token_type")
|
||||
val tokenType: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthUserDto(
|
||||
val id: Long,
|
||||
val email: String,
|
||||
val name: String,
|
||||
val username: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String? = null,
|
||||
@SerialName("email_verified")
|
||||
val emailVerified: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponseDto(
|
||||
val detail: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
package ru.daemonlord.messenger.data.auth.repository
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
import ru.daemonlord.messenger.core.token.TokenBundle
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||
import ru.daemonlord.messenger.data.auth.dto.AuthUserDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NetworkAuthRepository @Inject constructor(
|
||||
private val authApiService: AuthApiService,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : AuthRepository {
|
||||
|
||||
override suspend fun login(email: String, password: String): AppResult<AuthUser> = withContext(ioDispatcher) {
|
||||
try {
|
||||
val tokenResponse = authApiService.login(
|
||||
request = LoginRequestDto(
|
||||
email = email,
|
||||
password = password,
|
||||
)
|
||||
)
|
||||
tokenRepository.saveTokens(
|
||||
TokenBundle(
|
||||
accessToken = tokenResponse.accessToken,
|
||||
refreshToken = tokenResponse.refreshToken,
|
||||
savedAtMillis = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
getMe()
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError(forLogin = true))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refreshTokens(): AppResult<Unit> = withContext(ioDispatcher) {
|
||||
val tokens = tokenRepository.getTokens()
|
||||
?: return@withContext AppResult.Error(AppError.Unauthorized)
|
||||
try {
|
||||
val refreshed = authApiService.refresh(
|
||||
request = RefreshTokenRequestDto(refreshToken = tokens.refreshToken)
|
||||
)
|
||||
tokenRepository.saveTokens(
|
||||
TokenBundle(
|
||||
accessToken = refreshed.accessToken,
|
||||
refreshToken = refreshed.refreshToken,
|
||||
savedAtMillis = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
AppResult.Success(Unit)
|
||||
} catch (error: Throwable) {
|
||||
tokenRepository.clearTokens()
|
||||
AppResult.Error(error.toAppError(forLogin = false))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMe(): AppResult<AuthUser> = withContext(ioDispatcher) {
|
||||
try {
|
||||
val user = authApiService.me().toDomain()
|
||||
AppResult.Success(user)
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError(forLogin = false))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun restoreSession(): AppResult<AuthUser> = withContext(ioDispatcher) {
|
||||
val tokens = tokenRepository.getTokens()
|
||||
?: return@withContext AppResult.Error(AppError.Unauthorized)
|
||||
|
||||
if (tokens.accessToken.isBlank() || tokens.refreshToken.isBlank()) {
|
||||
tokenRepository.clearTokens()
|
||||
return@withContext AppResult.Error(AppError.Unauthorized)
|
||||
}
|
||||
|
||||
when (val meResult = getMe()) {
|
||||
is AppResult.Success -> meResult
|
||||
is AppResult.Error -> {
|
||||
if (meResult.reason is AppError.Unauthorized) {
|
||||
tokenRepository.clearTokens()
|
||||
}
|
||||
meResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
tokenRepository.clearTokens()
|
||||
}
|
||||
|
||||
private fun AuthUserDto.toDomain(): AuthUser {
|
||||
return AuthUser(
|
||||
id = id,
|
||||
email = email,
|
||||
name = name,
|
||||
username = username,
|
||||
avatarUrl = avatarUrl,
|
||||
emailVerified = emailVerified,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Throwable.toAppError(forLogin: Boolean): AppError {
|
||||
return when (this) {
|
||||
is IOException -> AppError.Network
|
||||
is HttpException -> when (code()) {
|
||||
400 -> if (forLogin) AppError.InvalidCredentials else AppError.Server(message = message())
|
||||
401, 403 -> if (forLogin) AppError.InvalidCredentials else AppError.Unauthorized
|
||||
else -> AppError.Server(message = message())
|
||||
}
|
||||
else -> AppError.Unknown(cause = this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package ru.daemonlord.messenger.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import ru.daemonlord.messenger.BuildConfig
|
||||
import ru.daemonlord.messenger.core.network.AuthHeaderInterceptor
|
||||
import ru.daemonlord.messenger.core.network.TokenRefreshAuthenticator
|
||||
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideJson(): Json {
|
||||
return Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
isLenient = true
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
return HttpLoggingInterceptor().apply {
|
||||
level = if (BuildConfig.DEBUG) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@RefreshClient
|
||||
fun provideRefreshClient(
|
||||
loggingInterceptor: HttpLoggingInterceptor,
|
||||
): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.writeTimeout(20, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRefreshApiService(
|
||||
@RefreshClient refreshClient: OkHttpClient,
|
||||
json: Json,
|
||||
): AuthApiService {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.client(refreshClient)
|
||||
.build()
|
||||
.create(AuthApiService::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiClient(
|
||||
loggingInterceptor: HttpLoggingInterceptor,
|
||||
authHeaderInterceptor: AuthHeaderInterceptor,
|
||||
tokenRefreshAuthenticator: TokenRefreshAuthenticator,
|
||||
): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.addInterceptor(authHeaderInterceptor)
|
||||
.authenticator(tokenRefreshAuthenticator)
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.writeTimeout(20, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(
|
||||
client: OkHttpClient,
|
||||
json: Json,
|
||||
): Retrofit {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.client(client)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthApiService(retrofit: Retrofit): AuthApiService {
|
||||
return retrofit.create(AuthApiService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.daemonlord.messenger.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class RefreshClient
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.daemonlord.messenger.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import ru.daemonlord.messenger.data.auth.repository.NetworkAuthRepository
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAuthRepository(
|
||||
repository: NetworkAuthRepository,
|
||||
): AuthRepository
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ru.daemonlord.messenger.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import ru.daemonlord.messenger.core.token.DataStoreTokenRepository
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePreferenceDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
produceFile = { context.preferencesDataStoreFile("messenger_tokens.preferences_pb") }
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenRepository(
|
||||
repository: DataStoreTokenRepository,
|
||||
): TokenRepository = repository
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.daemonlord.messenger.domain.auth.model
|
||||
|
||||
data class AuthUser(
|
||||
val id: Long,
|
||||
val email: String,
|
||||
val name: String,
|
||||
val username: String,
|
||||
val avatarUrl: String?,
|
||||
val emailVerified: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.daemonlord.messenger.domain.auth.repository
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
|
||||
interface AuthRepository {
|
||||
suspend fun login(email: String, password: String): AppResult<AuthUser>
|
||||
suspend fun refreshTokens(): AppResult<Unit>
|
||||
suspend fun getMe(): AppResult<AuthUser>
|
||||
suspend fun restoreSession(): AppResult<AuthUser>
|
||||
suspend fun logout()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ru.daemonlord.messenger.domain.auth.usecase
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginUseCase @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
) {
|
||||
suspend operator fun invoke(email: String, password: String): AppResult<AuthUser> {
|
||||
return authRepository.login(email = email, password = password)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ru.daemonlord.messenger.domain.auth.usecase
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import javax.inject.Inject
|
||||
|
||||
class RestoreSessionUseCase @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
) {
|
||||
suspend operator fun invoke(): AppResult<AuthUser> {
|
||||
return authRepository.restoreSession()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.daemonlord.messenger.domain.common
|
||||
|
||||
sealed interface AppError {
|
||||
data object InvalidCredentials : AppError
|
||||
data object Unauthorized : AppError
|
||||
data object Network : AppError
|
||||
data class Server(val message: String?) : AppError
|
||||
data class Unknown(val cause: Throwable?) : AppError
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.daemonlord.messenger.domain.common
|
||||
|
||||
sealed interface AppResult<out T> {
|
||||
data class Success<T>(val data: T) : AppResult<T>
|
||||
data class Error(val reason: AppError) : AppResult<Nothing>
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
plugins {
|
||||
id(com.android.application) version 8.7.2 apply false
|
||||
id(org.jetbrains.kotlin.android) version 2.0.21 apply false
|
||||
id(com.google.dagger.hilt.android) version 2.52 apply false
|
||||
id(org.jetbrains.kotlin.plugin.serialization) version 2.0.21 apply false
|
||||
id(org.jetbrains.kotlin.kapt) version 2.0.21 apply false
|
||||
id("com.android.application") version "8.7.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
|
||||
id("com.google.dagger.hilt.android") version "2.52" apply false
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21" apply false
|
||||
id("org.jetbrains.kotlin.kapt") version "2.0.21" apply false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user