diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 207c2966..60939a41 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(project(":core:navigation-api")) implementation(project(":core:ui")) implementation(project(":core:navigation-impl")) + implementation(project(":core:datastore-impl")) //Timber implementation(libs.timber) diff --git a/app/src/main/java/ru/yeahub/Application.kt b/app/src/main/java/ru/yeahub/Application.kt index 792c54b3..303b420d 100644 --- a/app/src/main/java/ru/yeahub/Application.kt +++ b/app/src/main/java/ru/yeahub/Application.kt @@ -3,6 +3,7 @@ package ru.yeahub import android.app.Application import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import ru.yeahub.datastore_impl.di.datastoreModule import ru.yeahub.detail_question.impl.di.detailQuestionFeatureModule import ru.yeahub.example_details.impl.detailsFeatureModule import ru.yeahub.example_home.impl.data.di.questionsMainFeatureModule @@ -45,6 +46,7 @@ class Application : Application() { modules( networkModule, navigationPathModule, + datastoreModule, questionsModule, profileFeatureModule, questionsMainFeatureModule, diff --git a/core/datastore-api/build.gradle.kts b/core/datastore-api/build.gradle.kts new file mode 100644 index 00000000..a4899639 --- /dev/null +++ b/core/datastore-api/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ru.yeahub.datastore_api" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/core/datastore-api/consumer-rules.pro b/core/datastore-api/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/datastore-api/proguard-rules.pro b/core/datastore-api/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/datastore-api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/datastore-api/src/main/AndroidManifest.xml b/core/datastore-api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/core/datastore-api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenDataStore.kt b/core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenDataStore.kt new file mode 100644 index 00000000..b9ede4be --- /dev/null +++ b/core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenDataStore.kt @@ -0,0 +1,16 @@ +package ru.yeahub.datastore_api + +/** + * Контракт локального хранения токена авторизации: + * - saveAccessToken - сохраняет access token + * - getAccessToken - возвращает сохранённый access token + * - clearTokens - очищает токены авторизации + */ +interface TokenDataStore { + + suspend fun saveAccessToken(accessToken: String): TokenStorageResult + + suspend fun getAccessToken(): String? + + suspend fun clearTokens(): TokenStorageResult +} \ No newline at end of file diff --git a/core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenStorageResult.kt b/core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenStorageResult.kt new file mode 100644 index 00000000..2f4be8aa --- /dev/null +++ b/core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenStorageResult.kt @@ -0,0 +1,13 @@ +package ru.yeahub.datastore_api + +/** + * Результат операции локального хранения токена: + * - Success - операция выполнена успешно + * - Error - операция завершилась ошибкой + */ +sealed interface TokenStorageResult { + + data object Success : TokenStorageResult + + data object Error : TokenStorageResult +} \ No newline at end of file diff --git a/core/datastore-impl/build.gradle.kts b/core/datastore-impl/build.gradle.kts new file mode 100644 index 00000000..608884f6 --- /dev/null +++ b/core/datastore-impl/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ru.yeahub.datastore_impl" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(project(":core:datastore-api")) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.datastore.preferences) + + implementation(libs.koin.core) + implementation(libs.koin.android) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/core/datastore-impl/consumer-rules.pro b/core/datastore-impl/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/datastore-impl/proguard-rules.pro b/core/datastore-impl/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/datastore-impl/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/datastore-impl/src/main/AndroidManifest.xml b/core/datastore-impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/core/datastore-impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt new file mode 100644 index 00000000..7370c4a1 --- /dev/null +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt @@ -0,0 +1,76 @@ +package ru.yeahub.datastore_impl + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.first +import ru.yeahub.datastore_api.TokenDataStore +import ru.yeahub.datastore_api.TokenStorageResult +import ru.yeahub.datastore_impl.crypto.CryptoManager + +/** + * Реализация локального хранения токена через Preferences DataStore: + * - шифрует access token перед сохранением + * - читает и расшифровывает access token + * - очищает токены авторизации + */ +class TokenDataStoreImpl( + private val dataStore: DataStore, + private val cryptoManager: CryptoManager, +) : TokenDataStore { + + override suspend fun saveAccessToken(accessToken: String): TokenStorageResult { + return runCatching { + dataStore.edit { preferences -> + val encryptedToken = cryptoManager.encrypt(accessToken) + preferences[ACCESS_TOKEN_KEY] = encryptedToken + } + }.fold( + onSuccess = { + TokenStorageResult.Success + }, + onFailure = { exception -> + if (exception is CancellationException) { + throw exception + } + + TokenStorageResult.Error + }, + ) + } + + override suspend fun getAccessToken(): String? { + val encryptedToken = + dataStore.data.first()[ACCESS_TOKEN_KEY] + ?: return null + + return runCatching { + cryptoManager.decrypt(encryptedToken) + }.getOrNull() + } + + override suspend fun clearTokens(): TokenStorageResult { + return runCatching { + dataStore.edit { preferences -> + preferences.remove(ACCESS_TOKEN_KEY) + } + }.fold( + onSuccess = { + TokenStorageResult.Success + }, + onFailure = { exception -> + if (exception is CancellationException) { + throw exception + } + + TokenStorageResult.Error + }, + ) + } + + private companion object { + private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token") + } +} \ No newline at end of file diff --git a/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManager.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManager.kt new file mode 100644 index 00000000..92307b98 --- /dev/null +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManager.kt @@ -0,0 +1,13 @@ +package ru.yeahub.datastore_impl.crypto + +/** + * Контракт шифрования данных: + * - encrypt - шифрует строку + * - decrypt - расшифровывает строку + */ +interface CryptoManager { + + fun encrypt(value: String): String + + fun decrypt(value: String): String +} \ No newline at end of file diff --git a/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManagerImpl.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManagerImpl.kt new file mode 100644 index 00000000..cd8adfd7 --- /dev/null +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManagerImpl.kt @@ -0,0 +1,105 @@ +package ru.yeahub.datastore_impl.crypto + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.nio.charset.StandardCharsets +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +/** + * Реализация шифрования через Android Keystore: + * - использует AES/GCM + * - ключ хранится в Android Keystore + */ +class CryptoManagerImpl : CryptoManager { + + override fun encrypt(value: String): String { + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) + + val encryptedBytes = cipher.doFinal( + value.toByteArray(StandardCharsets.UTF_8), + ) + + val combined = cipher.iv + encryptedBytes + + return Base64.encodeToString( + combined, + Base64.NO_WRAP, + ) + } + + override fun decrypt(value: String): String { + val decoded = Base64.decode( + value, + Base64.NO_WRAP, + ) + + val iv = decoded.copyOfRange( + fromIndex = 0, + toIndex = IV_SIZE, + ) + + val encryptedBytes = decoded.copyOfRange( + fromIndex = IV_SIZE, + toIndex = decoded.size, + ) + + val cipher = Cipher.getInstance(TRANSFORMATION) + + cipher.init( + Cipher.DECRYPT_MODE, + getSecretKey(), + GCMParameterSpec( + TAG_LENGTH, + iv, + ), + ) + + return String( + cipher.doFinal(encryptedBytes), + StandardCharsets.UTF_8, + ) + } + + private fun getSecretKey(): SecretKey { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { + load(null) + } + + keyStore.getKey(KEY_ALIAS, null)?.let { + return it as SecretKey + } + + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + ANDROID_KEYSTORE, + ) + + keyGenerator.init( + KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or + KeyProperties.PURPOSE_DECRYPT, + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build(), + ) + + return keyGenerator.generateKey() + } + + private companion object { + private const val KEY_ALIAS = "auth_token_key" + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + private const val TRANSFORMATION = "AES/GCM/NoPadding" + + private const val IV_SIZE = 12 + private const val TAG_LENGTH = 128 + } +} \ No newline at end of file diff --git a/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/di/DatastoreModule.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/di/DatastoreModule.kt new file mode 100644 index 00000000..8ad45dbc --- /dev/null +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/di/DatastoreModule.kt @@ -0,0 +1,49 @@ +package ru.yeahub.datastore_impl.di + +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module +import ru.yeahub.datastore_api.TokenDataStore +import ru.yeahub.datastore_impl.TokenDataStoreImpl +import ru.yeahub.datastore_impl.crypto.CryptoManager +import ru.yeahub.datastore_impl.crypto.CryptoManagerImpl + +/** + * DI модуль локального хранения: + * - создаёт Preferences DataStore + * - регистрирует CryptoManager для шифрования токена + * - регистрирует TokenDataStore + */ +val datastoreModule = module { + single { + CryptoManagerImpl() + } + + single> { + PreferenceDataStoreFactory.create( + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { + emptyPreferences() + }, + ), + produceFile = { + androidContext().preferencesDataStoreFile( + name = DATASTORE_FILE_NAME, + ) + }, + ) + } + + singleOf(::TokenDataStoreImpl) { + bind() + } +} + +private const val DATASTORE_FILE_NAME = "auth_preferences" \ No newline at end of file diff --git a/feature/authentication/impl/build.gradle.kts b/feature/authentication/impl/build.gradle.kts index 3c03380e..bb24d15e 100644 --- a/feature/authentication/impl/build.gradle.kts +++ b/feature/authentication/impl/build.gradle.kts @@ -46,6 +46,7 @@ android { } dependencies { + implementation(project(":core:datastore-api")) implementation(project(":core:navigation-api")) implementation(project(":core:network-api")) implementation(libs.androidx.compose.material.icons.extended) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/AuthSessionRepositoryImpl.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/AuthSessionRepositoryImpl.kt new file mode 100644 index 00000000..6b816d36 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/AuthSessionRepositoryImpl.kt @@ -0,0 +1,28 @@ +package ru.yeahub.authentication.impl.login.data.repository + +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository +import ru.yeahub.datastore_api.TokenDataStore +import ru.yeahub.datastore_api.TokenStorageResult + +/** + * Реализация репозитория auth-сессии: + * - делегирует сохранение access token в TokenDataStore + * - делегирует чтение access token в TokenDataStore + * - делегирует очистку токенов в TokenDataStore + */ +class AuthSessionRepositoryImpl( + private val tokenDataStore: TokenDataStore, +) : AuthSessionRepository { + + override suspend fun saveAccessToken(accessToken: String): TokenStorageResult { + return tokenDataStore.saveAccessToken(accessToken) + } + + override suspend fun getAccessToken(): String? { + return tokenDataStore.getAccessToken() + } + + override suspend fun clearTokens(): TokenStorageResult { + return tokenDataStore.clearTokens() + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/LoginRepositoryImpl.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/LoginRepositoryImpl.kt index bb34b6c4..fd7c224e 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/LoginRepositoryImpl.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/LoginRepositoryImpl.kt @@ -29,10 +29,17 @@ class LoginRepositoryImpl( private val gson: Gson, ) : LoginRepositoryApi { + /** + * Выполняет авторизацию пользователя: + * - преобразует LoginModel в request DTO + * - вызывает backend + * - преобразует response DTO в AuthResult + */ override suspend fun login(loginModel: LoginModel): AuthResult { return try { val request = domainToDataMapper.map(loginModel) val response = remoteDataSourceApi.login(request) + responseToDomainMapper.map(response) } catch (exception: CancellationException) { throw exception diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/di/LoginFeatureModule.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/di/LoginFeatureModule.kt index 61acadbe..3e4b9091 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/di/LoginFeatureModule.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/di/LoginFeatureModule.kt @@ -8,11 +8,15 @@ import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import ru.yeahub.authentication.impl.login.data.mapper.LoginDomainToDataMapper import ru.yeahub.authentication.impl.login.data.mapper.LoginResponseToDomainMapper -import ru.yeahub.authentication.impl.login.data.repository.remote.LoginRemoteDataSourceApi +import ru.yeahub.authentication.impl.login.data.repository.AuthSessionRepositoryImpl import ru.yeahub.authentication.impl.login.data.repository.LoginRepositoryImpl +import ru.yeahub.authentication.impl.login.data.repository.remote.LoginRemoteDataSourceApi import ru.yeahub.authentication.impl.login.data.repository.remote.LoginRemoteDataSourceImpl +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository import ru.yeahub.authentication.impl.login.domain.repository.LoginRepositoryApi +import ru.yeahub.authentication.impl.login.domain.usecase.CheckAuthStateUseCase import ru.yeahub.authentication.impl.login.domain.usecase.LoginUseCase +import ru.yeahub.authentication.impl.login.domain.usecase.LogoutUseCase import ru.yeahub.authentication.impl.login.presentation.mapper.LoginStateMapper import ru.yeahub.authentication.impl.login.presentation.viewmodel.LoginViewModel @@ -21,6 +25,7 @@ import ru.yeahub.authentication.impl.login.presentation.viewmodel.LoginViewModel * - регистрирует mapper'ы * - регистрирует remote data source * - регистрирует repository + * - регистрирует auth session repository * - регистрирует use case * - регистрирует ViewModel */ @@ -38,7 +43,13 @@ val loginFeatureModule = module { bind() } + singleOf(::AuthSessionRepositoryImpl) { + bind() + } + factoryOf(::LoginUseCase) + factoryOf(::CheckAuthStateUseCase) + factoryOf(::LogoutUseCase) singleOf(::LoginStateMapper) viewModelOf(::LoginViewModel) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/entity/LoginError.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/entity/LoginError.kt index f24bb38b..e11613b8 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/entity/LoginError.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/entity/LoginError.kt @@ -8,6 +8,7 @@ package ru.yeahub.authentication.impl.login.domain.entity * - AccountBlocked - аккаунт заблокирован * - TooManyAttempts - слишком много попыток входа * - EmailNotConfirmed - email не подтверждён + * - TokenSaveFailed - не удалось сохранить токен авторизации * - Network - ошибка сети * - Server - ошибка сервера * - Unknown - неизвестная ошибка @@ -20,7 +21,8 @@ sealed interface LoginError { data object AccountBlocked : LoginError data object TooManyAttempts : LoginError data object EmailNotConfirmed : LoginError + data object TokenSaveFailed : LoginError data object Network : LoginError data object Server : LoginError data object Unknown : LoginError -} +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/repository/AuthSessionRepository.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/repository/AuthSessionRepository.kt new file mode 100644 index 00000000..c664f2f0 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/repository/AuthSessionRepository.kt @@ -0,0 +1,18 @@ +package ru.yeahub.authentication.impl.login.domain.repository + +import ru.yeahub.datastore_api.TokenStorageResult + +/** + * Контракт репозитория auth-сессии: + * - saveAccessToken - сохраняет access token пользователя + * - getAccessToken - возвращает сохранённый access token + * - clearTokens - очищает сохранённые токены авторизации + */ +interface AuthSessionRepository { + + suspend fun saveAccessToken(accessToken: String): TokenStorageResult + + suspend fun getAccessToken(): String? + + suspend fun clearTokens(): TokenStorageResult +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/CheckAuthStateUseCase.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/CheckAuthStateUseCase.kt new file mode 100644 index 00000000..942cf4f7 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/CheckAuthStateUseCase.kt @@ -0,0 +1,16 @@ +package ru.yeahub.authentication.impl.login.domain.usecase + +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository + +/** + * UseCase проверки авторизованного состояния: + * - проверяет наличие сохранённого access token + */ +class CheckAuthStateUseCase( + private val authSessionRepository: AuthSessionRepository, +) { + + suspend operator fun invoke(): Boolean { + return authSessionRepository.getAccessToken().isNullOrBlank().not() + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LoginUseCase.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LoginUseCase.kt index 3af79955..6913e92a 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LoginUseCase.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LoginUseCase.kt @@ -1,18 +1,40 @@ package ru.yeahub.authentication.impl.login.domain.usecase import ru.yeahub.authentication.impl.login.domain.entity.AuthResult +import ru.yeahub.authentication.impl.login.domain.entity.Failure +import ru.yeahub.authentication.impl.login.domain.entity.LoginError +import ru.yeahub.authentication.impl.login.domain.entity.LoginException import ru.yeahub.authentication.impl.login.domain.entity.LoginModel +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository import ru.yeahub.authentication.impl.login.domain.repository.LoginRepositoryApi +import ru.yeahub.datastore_api.TokenStorageResult /** * UseCase авторизации: - * - login - запускает вход через репозиторий + * - выполняет login через LoginRepository + * - сохраняет access token через AuthSessionRepository + * - возвращает результат успешной авторизации + * - маппит ошибку локального хранения в LoginError.TokenSaveFailed */ class LoginUseCase( - private val repository: LoginRepositoryApi, + private val loginRepository: LoginRepositoryApi, + private val authSessionRepository: AuthSessionRepository, ) { suspend operator fun invoke(loginModel: LoginModel): AuthResult { - return repository.login(loginModel) + val authResult = loginRepository.login(loginModel) + + return when ( + authSessionRepository.saveAccessToken( + accessToken = authResult.tokens.accessToken, + ) + ) { + TokenStorageResult.Success -> authResult + + TokenStorageResult.Error -> throw LoginException( + error = LoginError.TokenSaveFailed, + failure = Failure(), + ) + } } } \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LogoutUseCase.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LogoutUseCase.kt new file mode 100644 index 00000000..5c59ca50 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LogoutUseCase.kt @@ -0,0 +1,17 @@ +package ru.yeahub.authentication.impl.login.domain.usecase + +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository +import ru.yeahub.datastore_api.TokenStorageResult + +/** + * UseCase выхода из аккаунта: + * - очищает сохранённые токены авторизации + */ +class LogoutUseCase( + private val authSessionRepository: AuthSessionRepository, +) { + + suspend operator fun invoke(): TokenStorageResult { + return authSessionRepository.clearTokens() + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/presentation/viewmodel/LoginViewModel.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/presentation/viewmodel/LoginViewModel.kt index 1cb80a6d..9b39767d 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/presentation/viewmodel/LoginViewModel.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/presentation/viewmodel/LoginViewModel.kt @@ -229,6 +229,16 @@ class LoginViewModel( ) } + is LoginError.TokenSaveFailed -> { + sendCommand( + command = LoginCommand.ShowSnackbar( + message = TextOrResource.Resource( + R.string.login_token_save_error, + ), + ), + ) + } + else -> { sendCommand( command = LoginCommand.ShowSnackbar( diff --git a/feature/authentication/impl/src/main/res/values/strings.xml b/feature/authentication/impl/src/main/res/values/strings.xml index bc397f6c..2972c407 100644 --- a/feature/authentication/impl/src/main/res/values/strings.xml +++ b/feature/authentication/impl/src/main/res/values/strings.xml @@ -29,4 +29,5 @@ Произошла непредвиденная ошибка Показать пароль Скрыть пароль + Не удалось сохранить сессию. Попробуйте войти ещё раз \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f5055b6..317c1201 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ runtimeVersion = "1.10.2" ui = "1.10.2" foundation = "1.10.2" material3 = "1.4.0" +datastorePreferences = "1.2.1" [libraries] androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } @@ -59,6 +60,9 @@ androidx-icons = { group = "androidx.compose.material", name = "material-icons-e compose-markdown = { group = "com.github.jeziellago", name = "compose-markdown", version = "0.5.7" } #androidx-material = { module = "androidx.compose.material:material", version.ref = "composeBom" } +#DATASTORE +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } + #RETROFIT retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-gsonConverter = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 002e2400..a8d55eb7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,8 @@ include(":core:common") include(":core:utils") include(":core:navigation-api") include(":core:navigation-impl") +include(":core:datastore-api") +include(":core:datastore-impl") include(":feature") include(":feature:example-profile") include(":feature:example-profile:api") @@ -65,3 +67,4 @@ include(":feature:enter-and-registration:impl") //include(":feature:forgot-password") //include(":feature:forgot-password:api") //include(":feature:forgot-password:impl") +