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")
+