From 38f913da2489ab688caa46c806128dec1063e8f8 Mon Sep 17 00:00:00 2001 From: Gaileks Date: Thu, 28 May 2026 13:30:41 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены core-модули datastore-api и datastore-impl - Добавлен TokenDataStore для работы с accessToken - Реализовано сохранение accessToken через Preferences DataStore - Добавлено чтение сохранённого accessToken - Добавлена очистка accessToken при logout - Добавлен corruptionHandler для DataStore - Добавлена DI-регистрация DataStore и TokenDataStore - datastoreModule подключён в Application - TokenDataStore подключён в LoginRepositoryImpl - После успешной авторизации accessToken сохраняется локально - Добавлен CheckAuthStateUseCase для проверки авторизованного состояния - Добавлен LogoutUseCase для очистки токена --- app/build.gradle.kts | 1 + app/src/main/java/ru/yeahub/Application.kt | 2 + core/datastore-api/build.gradle.kts | 43 +++++++++++++++++ core/datastore-api/consumer-rules.pro | 0 core/datastore-api/proguard-rules.pro | 21 +++++++++ .../src/main/AndroidManifest.xml | 4 ++ .../ru/yeahub/datastore_api/TokenDataStore.kt | 16 +++++++ core/datastore-impl/build.gradle.kts | 46 +++++++++++++++++++ core/datastore-impl/consumer-rules.pro | 0 core/datastore-impl/proguard-rules.pro | 21 +++++++++ .../src/main/AndroidManifest.xml | 4 ++ .../datastore_api/TokenDataStoreImpl.kt | 38 +++++++++++++++ .../datastore_api/di/DatastoreModule.kt | 42 +++++++++++++++++ feature/authentication/impl/build.gradle.kts | 1 + .../data/repository/LoginRepositoryImpl.kt | 17 ++++++- .../impl/login/di/LoginFeatureModule.kt | 4 ++ .../domain/usecase/CheckAuthStateUseCase.kt | 16 +++++++ .../login/domain/usecase/LogoutUseCase.kt | 16 +++++++ gradle/libs.versions.toml | 4 ++ settings.gradle.kts | 3 ++ 20 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 core/datastore-api/build.gradle.kts create mode 100644 core/datastore-api/consumer-rules.pro create mode 100644 core/datastore-api/proguard-rules.pro create mode 100644 core/datastore-api/src/main/AndroidManifest.xml create mode 100644 core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenDataStore.kt create mode 100644 core/datastore-impl/build.gradle.kts create mode 100644 core/datastore-impl/consumer-rules.pro create mode 100644 core/datastore-impl/proguard-rules.pro create mode 100644 core/datastore-impl/src/main/AndroidManifest.xml create mode 100644 core/datastore-impl/src/main/java/ru/yeahub/datastore_api/TokenDataStoreImpl.kt create mode 100644 core/datastore-impl/src/main/java/ru/yeahub/datastore_api/di/DatastoreModule.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/CheckAuthStateUseCase.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LogoutUseCase.kt 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..717fb180 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_api.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..b01fb2c0 --- /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) + + suspend fun getAccessToken(): String? + + suspend fun clearTokens() +} \ 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..410c9c9a --- /dev/null +++ b/core/datastore-impl/build.gradle.kts @@ -0,0 +1,46 @@ +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.datastore.preferences) + implementation(libs.koin.core) + implementation(libs.koin.android) + 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-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_api/TokenDataStoreImpl.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/TokenDataStoreImpl.kt new file mode 100644 index 00000000..3171d13a --- /dev/null +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/TokenDataStoreImpl.kt @@ -0,0 +1,38 @@ +package ru.yeahub.datastore_api + +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.flow.first + +/** + * Реализация локального хранения токена через Preferences DataStore: + * - сохраняет access token + * - читает access token + * - очищает токены авторизации + */ +class TokenDataStoreImpl( + private val dataStore: DataStore, +) : TokenDataStore { + + override suspend fun saveAccessToken(accessToken: String) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN_KEY] = accessToken + } + } + + override suspend fun getAccessToken(): String? { + return dataStore.data.first()[ACCESS_TOKEN_KEY] + } + + override suspend fun clearTokens() { + dataStore.edit { preferences -> + preferences.remove(ACCESS_TOKEN_KEY) + } + } + + 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_api/di/DatastoreModule.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/di/DatastoreModule.kt new file mode 100644 index 00000000..4b8da495 --- /dev/null +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/di/DatastoreModule.kt @@ -0,0 +1,42 @@ +package ru.yeahub.datastore_api.di + +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +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_api.TokenDataStoreImpl + +/** + * DI модуль локального хранения: + * - создаёт Preferences DataStore + * - регистрирует TokenDataStore + */ +val datastoreModule = module { + 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/LoginRepositoryImpl.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/LoginRepositoryImpl.kt index bb34b6c4..145b9138 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 @@ -12,6 +12,7 @@ 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.LoginRepositoryApi +import ru.yeahub.datastore_api.TokenDataStore import ru.yeahub.network_api.models.ErrorResponseDto import java.io.IOException @@ -26,14 +27,28 @@ class LoginRepositoryImpl( private val remoteDataSourceApi: LoginRemoteDataSourceApi, private val domainToDataMapper: LoginDomainToDataMapper, private val responseToDomainMapper: LoginResponseToDomainMapper, + private val tokenDataStore: TokenDataStore, private val gson: Gson, ) : LoginRepositoryApi { + /** + * Выполняет авторизацию пользователя: + * - преобразует LoginModel в request DTO + * - вызывает backend + * - преобразует response DTO в AuthResult + * - сохраняет access token в локальное хранилище + */ override suspend fun login(loginModel: LoginModel): AuthResult { return try { val request = domainToDataMapper.map(loginModel) val response = remoteDataSourceApi.login(request) - responseToDomainMapper.map(response) + val authResult = responseToDomainMapper.map(response) + + tokenDataStore.saveAccessToken( + accessToken = authResult.tokens.accessToken, + ) + + authResult } catch (exception: CancellationException) { throw exception } catch (exception: IOException) { 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..16e764cd 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 @@ -12,7 +12,9 @@ import ru.yeahub.authentication.impl.login.data.repository.remote.LoginRemoteDat import ru.yeahub.authentication.impl.login.data.repository.LoginRepositoryImpl import ru.yeahub.authentication.impl.login.data.repository.remote.LoginRemoteDataSourceImpl 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 @@ -38,7 +40,9 @@ val loginFeatureModule = module { bind() } + factoryOf(::CheckAuthStateUseCase) factoryOf(::LoginUseCase) + factoryOf(::LogoutUseCase) singleOf(::LoginStateMapper) viewModelOf(::LoginViewModel) 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..a07be23e --- /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.datastore_api.TokenDataStore + +/** + * UseCase проверки авторизации пользователя: + * - возвращает true, если access token сохранён + */ +class CheckAuthStateUseCase( + private val tokenDataStore: TokenDataStore, +) { + + suspend operator fun invoke(): Boolean { + return tokenDataStore.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/LogoutUseCase.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LogoutUseCase.kt new file mode 100644 index 00000000..b9dbd6b9 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/usecase/LogoutUseCase.kt @@ -0,0 +1,16 @@ +package ru.yeahub.authentication.impl.login.domain.usecase + +import ru.yeahub.datastore_api.TokenDataStore + +/** + * UseCase выхода из аккаунта: + * - очищает сохранённые токены авторизации + */ +class LogoutUseCase( + private val tokenDataStore: TokenDataStore, +) { + + suspend operator fun invoke() { + tokenDataStore.clearTokens() + } +} \ 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") + From 26a17a4c688c4dc639029e490bcd2f8ca228a184 Mon Sep 17 00:00:00 2001 From: Gaileks Date: Wed, 3 Jun 2026 17:10:46 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=85?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20accessToken:=20?= =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=20=D1=88=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20accessToken=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20Android?= =?UTF-8?q?=20Keystore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/ru/yeahub/Application.kt | 2 +- core/datastore-impl/build.gradle.kts | 7 +- .../TokenDataStoreImpl.kt | 16 ++- .../datastore_impl/crypto/CryptoManager.kt | 13 +++ .../crypto/CryptoManagerImpl.kt | 105 ++++++++++++++++++ .../di/DatastoreModule.kt | 13 ++- 6 files changed, 146 insertions(+), 10 deletions(-) rename core/datastore-impl/src/main/java/ru/yeahub/{datastore_api => datastore_impl}/TokenDataStoreImpl.kt (66%) create mode 100644 core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManager.kt create mode 100644 core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/crypto/CryptoManagerImpl.kt rename core/datastore-impl/src/main/java/ru/yeahub/{datastore_api => datastore_impl}/di/DatastoreModule.kt (79%) diff --git a/app/src/main/java/ru/yeahub/Application.kt b/app/src/main/java/ru/yeahub/Application.kt index 717fb180..303b420d 100644 --- a/app/src/main/java/ru/yeahub/Application.kt +++ b/app/src/main/java/ru/yeahub/Application.kt @@ -3,7 +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_api.di.datastoreModule +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 diff --git a/core/datastore-impl/build.gradle.kts b/core/datastore-impl/build.gradle.kts index 410c9c9a..608884f6 100644 --- a/core/datastore-impl/build.gradle.kts +++ b/core/datastore-impl/build.gradle.kts @@ -34,12 +34,13 @@ android { dependencies { implementation(project(":core:datastore-api")) + + implementation(libs.androidx.core.ktx) implementation(libs.androidx.datastore.preferences) + implementation(libs.koin.core) implementation(libs.koin.android) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/TokenDataStoreImpl.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt similarity index 66% rename from core/datastore-impl/src/main/java/ru/yeahub/datastore_api/TokenDataStoreImpl.kt rename to core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt index 3171d13a..2513fb0d 100644 --- a/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/TokenDataStoreImpl.kt +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt @@ -1,10 +1,12 @@ -package ru.yeahub.datastore_api +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.flow.first +import ru.yeahub.datastore_api.TokenDataStore +import ru.yeahub.datastore_impl.crypto.CryptoManager /** * Реализация локального хранения токена через Preferences DataStore: @@ -14,16 +16,24 @@ import kotlinx.coroutines.flow.first */ class TokenDataStoreImpl( private val dataStore: DataStore, + private val cryptoManager: CryptoManager, ) : TokenDataStore { override suspend fun saveAccessToken(accessToken: String) { dataStore.edit { preferences -> - preferences[ACCESS_TOKEN_KEY] = accessToken + val encryptedToken = cryptoManager.encrypt(accessToken) + preferences[ACCESS_TOKEN_KEY] = encryptedToken } } override suspend fun getAccessToken(): String? { - return dataStore.data.first()[ACCESS_TOKEN_KEY] + val encryptedToken = + dataStore.data.first()[ACCESS_TOKEN_KEY] + ?: return null + + return runCatching { + cryptoManager.decrypt(encryptedToken) + }.getOrNull() } override suspend fun clearTokens() { 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_api/di/DatastoreModule.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/di/DatastoreModule.kt similarity index 79% rename from core/datastore-impl/src/main/java/ru/yeahub/datastore_api/di/DatastoreModule.kt rename to core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/di/DatastoreModule.kt index 4b8da495..8ad45dbc 100644 --- a/core/datastore-impl/src/main/java/ru/yeahub/datastore_api/di/DatastoreModule.kt +++ b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/di/DatastoreModule.kt @@ -1,24 +1,31 @@ -package ru.yeahub.datastore_api.di +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 androidx.datastore.preferences.core.PreferenceDataStoreFactory 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_api.TokenDataStoreImpl +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( From 62afcdb8dbc8084c357ce2990e67275f4968ad7d Mon Sep 17 00:00:00 2001 From: Gaileks Date: Sat, 13 Jun 2026 12:57:01 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20auth=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D1=81=D0=BE=D1=85?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/yeahub/datastore_api/TokenDataStore.kt | 4 +- .../datastore_api/TokenStorageResult.kt | 13 +++++ .../datastore_impl/TokenDataStoreImpl.kt | 50 +++++++++++++++---- .../repository/AuthSessionRepositoryImpl.kt | 28 +++++++++++ .../data/repository/LoginRepositoryImpl.kt | 10 +--- .../impl/login/di/LoginFeatureModule.kt | 11 +++- .../impl/login/domain/entity/LoginError.kt | 4 +- .../repository/AuthSessionRepository.kt | 18 +++++++ .../domain/usecase/CheckAuthStateUseCase.kt | 10 ++-- .../impl/login/domain/usecase/LoginUseCase.kt | 28 +++++++++-- .../login/domain/usecase/LogoutUseCase.kt | 9 ++-- .../presentation/viewmodel/LoginViewModel.kt | 10 ++++ .../impl/src/main/res/values/strings.xml | 1 + 13 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 core/datastore-api/src/main/java/ru/yeahub/datastore_api/TokenStorageResult.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/data/repository/AuthSessionRepositoryImpl.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/login/domain/repository/AuthSessionRepository.kt 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 index b01fb2c0..b9ede4be 100644 --- 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 @@ -8,9 +8,9 @@ package ru.yeahub.datastore_api */ interface TokenDataStore { - suspend fun saveAccessToken(accessToken: String) + suspend fun saveAccessToken(accessToken: String): TokenStorageResult suspend fun getAccessToken(): String? - suspend fun clearTokens() + 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/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt b/core/datastore-impl/src/main/java/ru/yeahub/datastore_impl/TokenDataStoreImpl.kt index 2513fb0d..7370c4a1 100644 --- 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 @@ -4,14 +4,16 @@ 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 + * - шифрует access token перед сохранением + * - читает и расшифровывает access token * - очищает токены авторизации */ class TokenDataStoreImpl( @@ -19,11 +21,24 @@ class TokenDataStoreImpl( private val cryptoManager: CryptoManager, ) : TokenDataStore { - override suspend fun saveAccessToken(accessToken: String) { - dataStore.edit { preferences -> - val encryptedToken = cryptoManager.encrypt(accessToken) - preferences[ACCESS_TOKEN_KEY] = encryptedToken - } + 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? { @@ -36,10 +51,23 @@ class TokenDataStoreImpl( }.getOrNull() } - override suspend fun clearTokens() { - dataStore.edit { preferences -> - preferences.remove(ACCESS_TOKEN_KEY) - } + 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 { 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 145b9138..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 @@ -12,7 +12,6 @@ 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.LoginRepositoryApi -import ru.yeahub.datastore_api.TokenDataStore import ru.yeahub.network_api.models.ErrorResponseDto import java.io.IOException @@ -27,7 +26,6 @@ class LoginRepositoryImpl( private val remoteDataSourceApi: LoginRemoteDataSourceApi, private val domainToDataMapper: LoginDomainToDataMapper, private val responseToDomainMapper: LoginResponseToDomainMapper, - private val tokenDataStore: TokenDataStore, private val gson: Gson, ) : LoginRepositoryApi { @@ -36,19 +34,13 @@ class LoginRepositoryImpl( * - преобразует LoginModel в request DTO * - вызывает backend * - преобразует response DTO в AuthResult - * - сохраняет access token в локальное хранилище */ override suspend fun login(loginModel: LoginModel): AuthResult { return try { val request = domainToDataMapper.map(loginModel) val response = remoteDataSourceApi.login(request) - val authResult = responseToDomainMapper.map(response) - tokenDataStore.saveAccessToken( - accessToken = authResult.tokens.accessToken, - ) - - authResult + responseToDomainMapper.map(response) } catch (exception: CancellationException) { throw exception } catch (exception: IOException) { 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 16e764cd..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,9 +8,11 @@ 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 @@ -23,6 +25,7 @@ import ru.yeahub.authentication.impl.login.presentation.viewmodel.LoginViewModel * - регистрирует mapper'ы * - регистрирует remote data source * - регистрирует repository + * - регистрирует auth session repository * - регистрирует use case * - регистрирует ViewModel */ @@ -40,8 +43,12 @@ val loginFeatureModule = module { bind() } - factoryOf(::CheckAuthStateUseCase) + singleOf(::AuthSessionRepositoryImpl) { + bind() + } + factoryOf(::LoginUseCase) + factoryOf(::CheckAuthStateUseCase) factoryOf(::LogoutUseCase) singleOf(::LoginStateMapper) 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 index a07be23e..942cf4f7 100644 --- 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 @@ -1,16 +1,16 @@ package ru.yeahub.authentication.impl.login.domain.usecase -import ru.yeahub.datastore_api.TokenDataStore +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository /** - * UseCase проверки авторизации пользователя: - * - возвращает true, если access token сохранён + * UseCase проверки авторизованного состояния: + * - проверяет наличие сохранённого access token */ class CheckAuthStateUseCase( - private val tokenDataStore: TokenDataStore, + private val authSessionRepository: AuthSessionRepository, ) { suspend operator fun invoke(): Boolean { - return tokenDataStore.getAccessToken().isNullOrBlank().not() + 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 index b9dbd6b9..5c59ca50 100644 --- 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 @@ -1,16 +1,17 @@ package ru.yeahub.authentication.impl.login.domain.usecase -import ru.yeahub.datastore_api.TokenDataStore +import ru.yeahub.authentication.impl.login.domain.repository.AuthSessionRepository +import ru.yeahub.datastore_api.TokenStorageResult /** * UseCase выхода из аккаунта: * - очищает сохранённые токены авторизации */ class LogoutUseCase( - private val tokenDataStore: TokenDataStore, + private val authSessionRepository: AuthSessionRepository, ) { - suspend operator fun invoke() { - tokenDataStore.clearTokens() + 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