Добавлен Yandex

This commit is contained in:
2026-04-19 00:22:05 +03:00
parent 586e2b61fd
commit b3c00b1719
24 changed files with 710 additions and 49 deletions

View File

@@ -4,16 +4,24 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyMapDao
import com.github.nullptroma.wallenc.data.db.app.dao.StorageMetaInfoDao
import com.github.nullptroma.wallenc.data.db.app.dao.YandexAccountDao
import com.github.nullptroma.wallenc.data.db.app.model.DbStorageKeyMap
import com.github.nullptroma.wallenc.data.db.app.model.DbStorageMetaInfo
import com.github.nullptroma.wallenc.data.db.app.model.DbYandexAccount
interface IAppDb {
val storageKeyMapDao: StorageKeyMapDao
val storageMetaInfoDao: StorageMetaInfoDao
val yandexAccountDao: YandexAccountDao
}
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 3, exportSchema = false)
@Database(
entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class, DbYandexAccount::class],
version = 4,
exportSchema = false,
)
abstract class AppDb : IAppDb, RoomDatabase() {
abstract override val storageKeyMapDao: StorageKeyMapDao
abstract override val storageMetaInfoDao: StorageMetaInfoDao
abstract override val yandexAccountDao: YandexAccountDao
}

View File

@@ -0,0 +1,27 @@
package com.github.nullptroma.wallenc.data.db.app.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.github.nullptroma.wallenc.data.db.app.model.DbYandexAccount
import kotlinx.coroutines.flow.Flow
@Dao
interface YandexAccountDao {
@Query("SELECT * FROM yandex_accounts WHERE yandexUserId = :id LIMIT 1")
suspend fun getByYandexUserId(id: String): DbYandexAccount?
@Insert
suspend fun insert(account: DbYandexAccount)
@Query(
"UPDATE yandex_accounts SET oauthToken = :token, email = :email WHERE vaultUuid = :vaultUuid",
)
suspend fun updateCredentials(vaultUuid: String, email: String, token: String)
@Query("SELECT * FROM yandex_accounts ORDER BY email COLLATE NOCASE ASC")
fun observeAll(): Flow<List<DbYandexAccount>>
@Query("DELETE FROM yandex_accounts WHERE vaultUuid = :vaultUuid")
suspend fun deleteByVaultUuid(vaultUuid: String)
}

View File

@@ -0,0 +1,16 @@
package com.github.nullptroma.wallenc.data.db.app.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "yandex_accounts",
indices = [Index(value = ["yandexUserId"], unique = true)],
)
data class DbYandexAccount(
@PrimaryKey val vaultUuid: String,
val yandexUserId: String,
val email: String,
val oauthToken: String,
)

View File

@@ -0,0 +1,13 @@
package com.github.nullptroma.wallenc.data.network
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface YandexUserInfoApi {
@GET("info")
suspend fun userInfo(
@Query("format") format: String,
@Header("Authorization") authorization: String,
): YandexUserInfoDto
}

View File

@@ -0,0 +1,17 @@
package com.github.nullptroma.wallenc.data.network
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
object YandexUserInfoApiFactory {
fun create(): YandexUserInfoApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://login.yandex.ru/")
.addConverterFactory(
JacksonConverterFactory.create(jacksonObjectMapper().findAndRegisterModules()),
)
.build()
return retrofit.create(YandexUserInfoApi::class.java)
}
}

View File

@@ -0,0 +1,11 @@
package com.github.nullptroma.wallenc.data.network
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
@JsonIgnoreProperties(ignoreUnknown = true)
data class YandexUserInfoDto(
val id: String,
val login: String,
@JsonProperty("default_email") val defaultEmail: String? = null,
)

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -33,7 +33,7 @@ class UnlockManager(
init {
CoroutineScope(ioDispatcher).launch {
vaultsManager.allStorages.collectLatest {
vaultsManager.allStorages.collect {
mutex.withLock {
val allKeys = keymapRepository.getAll()
val keysToRemove = mutableListOf<StorageKeyMap>()

View File

@@ -1,33 +1,84 @@
package com.github.nullptroma.wallenc.data.vaults
import android.content.Context
import com.github.nullptroma.wallenc.data.db.app.dao.YandexAccountDao
import com.github.nullptroma.wallenc.data.db.app.model.DbYandexAccount
import com.github.nullptroma.wallenc.data.db.app.repository.StorageKeyMapRepository
import com.github.nullptroma.wallenc.data.network.YandexUserInfoApi
import com.github.nullptroma.wallenc.data.storages.UnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVault
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import java.util.UUID
class VaultsManager(
private val ioDispatcher: CoroutineDispatcher,
context: Context,
keyRepo: StorageKeyMapRepository,
private val yandexAccountDao: YandexAccountDao,
private val yandexUserInfoApi: YandexUserInfoApi,
) : IVaultsManager {
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
class VaultsManager(ioDispatcher: CoroutineDispatcher, context: Context, keyRepo: StorageKeyMapRepository) : IVaultsManager {
override val localVault = LocalVault(ioDispatcher, context)
// До unlockManager: UnlockManager в init подписывается на allStorages.
override val allStorages: StateFlow<List<IStorage>> = localVault.storages
override val unlockManager: IUnlockManager = UnlockManager(
keymapRepository = keyRepo,
ioDispatcher = ioDispatcher,
vaultsManager = this
)
override val remoteVaults: StateFlow<List<IVault>>
get() = TODO("Not yet implemented")
override val allStorages: StateFlow<List<IStorage>>
get() = localVault.storages
override val allVaults: StateFlow<List<IVault>>
get() = MutableStateFlow(listOf(localVault))
private val _remoteVaults = yandexAccountDao.observeAll()
.map { rows ->
rows.map { row ->
YandexVault(
uuid = UUID.fromString(row.vaultUuid),
accountEmail = row.email,
oauthToken = row.oauthToken,
)
}
}
.stateIn(scope, SharingStarted.Eagerly, emptyList())
override fun addYandexVault(email: String, token: String) {
TODO("Not yet implemented")
override val remoteVaults: StateFlow<List<IVault>> = _remoteVaults
override val allVaults: StateFlow<List<IVault>> = _remoteVaults
.map { listOf(localVault) + it }
.stateIn(scope, SharingStarted.Eagerly, listOf(localVault))
override suspend fun addYandexVault(accessToken: String) = withContext(ioDispatcher) {
val info = yandexUserInfoApi.userInfo("json", "OAuth $accessToken")
val email = info.defaultEmail?.takeIf { it.isNotBlank() }
?: "${info.login}@yandex.ru"
val existing = yandexAccountDao.getByYandexUserId(info.id)
val vaultUuid = existing?.vaultUuid ?: UUID.randomUUID().toString()
if (existing != null) {
yandexAccountDao.updateCredentials(vaultUuid, email, accessToken)
} else {
yandexAccountDao.insert(
DbYandexAccount(
vaultUuid = vaultUuid,
yandexUserId = info.id,
email = email,
oauthToken = accessToken,
)
)
}
}
}
override suspend fun removeRemoteVault(vaultUuid: UUID) = withContext(ioDispatcher) {
yandexAccountDao.deleteByVaultUuid(vaultUuid.toString())
}
}

View File

@@ -0,0 +1,44 @@
package com.github.nullptroma.wallenc.data.vaults
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.enums.VaultType
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IYandexVault
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
/**
* Удалённый vault Яндекс: OAuth и профиль есть; Yandex.Disk и хранилища — заглушки.
*/
class YandexVault(
override val uuid: UUID,
override val accountEmail: String,
@Suppress("unused") val oauthToken: String,
) : IYandexVault {
override val type: VaultType = VaultType.YANDEX
private val _storages = MutableStateFlow<List<IStorage>>(emptyList())
override val storages: StateFlow<List<IStorage>?> = _storages
private val _isAvailable = MutableStateFlow(true)
override val isAvailable: StateFlow<Boolean> = _isAvailable
private val _totalSpace = MutableStateFlow<Int?>(null)
override val totalSpace: StateFlow<Int?> = _totalSpace
private val _availableSpace = MutableStateFlow<Int?>(null)
override val availableSpace: StateFlow<Int?> = _availableSpace
override suspend fun createStorage(): IStorage {
throw UnsupportedOperationException("Yandex.Disk ещё не подключён")
}
override suspend fun createStorage(enc: StorageEncryptionInfo): IStorage {
throw UnsupportedOperationException("Yandex.Disk ещё не подключён")
}
override suspend fun remove(storage: IStorage) {
// заглушка до интеграции API Диска
}
}