android: add auth network core, token store, and DI wiring

This commit is contained in:
Codex
2026-03-08 22:21:24 +03:00
parent acdb83e04e
commit 0ff838baf7
19 changed files with 663 additions and 43 deletions

View File

@@ -7,3 +7,10 @@
- Added app dependencies for Retrofit/OkHttp, DataStore, coroutines, Hilt, and unit testing. - Added app dependencies for Retrofit/OkHttp, DataStore, coroutines, Hilt, and unit testing.
- Enabled INTERNET permission and registered MessengerApplication in manifest. - Enabled INTERNET permission and registered MessengerApplication in manifest.
- Added MessengerApplication with HiltAndroidApp. - 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.

View File

@@ -1,23 +1,25 @@
plugins { plugins {
id(com.android.application) id("com.android.application")
id(org.jetbrains.kotlin.android) id("org.jetbrains.kotlin.android")
id(org.jetbrains.kotlin.kapt) id("org.jetbrains.kotlin.plugin.compose")
id(org.jetbrains.kotlin.plugin.serialization) id("org.jetbrains.kotlin.kapt")
id(com.google.dagger.hilt.android) id("org.jetbrains.kotlin.plugin.serialization")
id("com.google.dagger.hilt.android")
} }
android { android {
namespace = ru.daemonlord.messenger namespace = "ru.daemonlord.messenger"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = ru.daemonlord.messenger applicationId = "ru.daemonlord.messenger"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 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 { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }
@@ -27,8 +29,8 @@ android {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile(proguard-android-optimize.txt), getDefaultProguardFile("proguard-android-optimize.txt"),
proguard-rules.pro "proguard-rules.pro"
) )
} }
} }
@@ -39,7 +41,7 @@ android {
} }
kotlinOptions { kotlinOptions {
jvmTarget = 17 jvmTarget = "17"
} }
buildFeatures { buildFeatures {
@@ -48,48 +50,48 @@ android {
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = 1.5.15 kotlinCompilerExtensionVersion = "1.5.15"
} }
packaging { packaging {
resources { resources {
excludes += /META-INF/AL2.0 excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
} }
dependencies { dependencies {
implementation(androidx.core:core-ktx:1.15.0) implementation("androidx.core:core-ktx:1.15.0")
implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.8.7) implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation(androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7) implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation(androidx.activity:activity-compose:1.10.1) implementation("androidx.activity:activity-compose:1.10.1")
implementation(androidx.navigation:navigation-compose:2.8.5) implementation("androidx.navigation:navigation-compose:2.8.5")
implementation(androidx.compose.ui:ui:1.7.6) implementation("androidx.compose.ui:ui:1.7.6")
implementation(androidx.compose.ui:ui-tooling-preview:1.7.6) implementation("androidx.compose.ui:ui-tooling-preview:1.7.6")
implementation(androidx.compose.material3:material3:1.3.1) implementation("androidx.compose.material3:material3:1.3.1")
implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0) 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-serialization-json:1.7.3")
implementation(com.squareup.retrofit2:retrofit:2.11.0) implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation(com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0) implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation(com.squareup.okhttp3:okhttp:4.12.0) implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation(com.squareup.okhttp3:logging-interceptor: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) implementation("com.google.dagger:hilt-android:2.52")
kapt(com.google.dagger:hilt-compiler:2.52) kapt("com.google.dagger:hilt-compiler:2.52")
implementation(androidx.hilt:hilt-navigation-compose:1.2.0) implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
testImplementation(junit:junit:4.13.2) testImplementation("junit:junit:4.13.2")
testImplementation(org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0) testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
testImplementation(io.mockk:mockk:1.13.13) testImplementation("androidx.datastore:datastore-preferences-core:1.1.1")
testImplementation(com.squareup.okhttp3:mockwebserver:4.12.0) testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
debugImplementation(androidx.compose.ui:ui-tooling:1.7.6) debugImplementation("androidx.compose.ui:ui-tooling:1.7.6")
debugImplementation(androidx.compose.ui:ui-test-manifest:1.7.6) debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.6")
} }
kapt { kapt {

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,7 @@
package ru.daemonlord.messenger.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RefreshClient

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -1,7 +1,8 @@
plugins { plugins {
id(com.android.application) version 8.7.2 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.android") version "2.0.21" apply false
id(com.google.dagger.hilt.android) version 2.52 apply false id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
id(org.jetbrains.kotlin.plugin.serialization) version 2.0.21 apply false id("com.google.dagger.hilt.android") version "2.52" apply false
id(org.jetbrains.kotlin.kapt) version 2.0.21 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
} }