273 lines
9.1 KiB
Kotlin
273 lines
9.1 KiB
Kotlin
package com.rosetta.messenger.providers
|
|
|
|
import android.content.Context
|
|
import androidx.compose.runtime.*
|
|
import com.rosetta.messenger.crypto.CryptoManager
|
|
import com.rosetta.messenger.database.DatabaseService
|
|
import com.rosetta.messenger.database.DecryptedAccountData
|
|
import com.rosetta.messenger.network.ProtocolManager
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.flow.*
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
|
|
/**
|
|
* Auth state management - matches React Native architecture
|
|
*/
|
|
sealed class AuthStatus {
|
|
object Loading : AuthStatus()
|
|
object Unauthenticated : AuthStatus()
|
|
data class Authenticated(val account: DecryptedAccountData) : AuthStatus()
|
|
data class Locked(val publicKey: String) : AuthStatus()
|
|
}
|
|
|
|
data class AuthStateData(
|
|
val status: AuthStatus = AuthStatus.Loading,
|
|
val hasExistingAccounts: Boolean = false,
|
|
val availableAccounts: List<String> = emptyList()
|
|
)
|
|
|
|
class AuthStateManager(
|
|
private val context: Context,
|
|
private val scope: CoroutineScope
|
|
) {
|
|
private val databaseService = DatabaseService.getInstance(context)
|
|
|
|
private val _state = MutableStateFlow(AuthStateData())
|
|
val state: StateFlow<AuthStateData> = _state.asStateFlow()
|
|
|
|
private var currentDecryptedAccount: DecryptedAccountData? = null
|
|
|
|
// 🚀 ОПТИМИЗАЦИЯ: Кэш списка аккаунтов для UI
|
|
private var accountsCache: List<String>? = null
|
|
private var lastAccountsLoadTime = 0L
|
|
private val accountsCacheTTL = 5000L // 5 секунд
|
|
|
|
companion object {
|
|
private const val TAG = "AuthStateManager"
|
|
}
|
|
|
|
init {
|
|
scope.launch {
|
|
loadAccounts()
|
|
checkAuthStatus()
|
|
}
|
|
}
|
|
|
|
private suspend fun loadAccounts() = withContext(Dispatchers.IO) {
|
|
try {
|
|
// 🚀 ОПТИМИЗАЦИЯ: Используем кэш если он свежий
|
|
val currentTime = System.currentTimeMillis()
|
|
if (accountsCache != null && (currentTime - lastAccountsLoadTime) < accountsCacheTTL) {
|
|
_state.update { it.copy(
|
|
hasExistingAccounts = accountsCache!!.isNotEmpty(),
|
|
availableAccounts = accountsCache!!
|
|
)}
|
|
return@withContext
|
|
}
|
|
|
|
val accounts = databaseService.getAllEncryptedAccounts()
|
|
val hasAccounts = accounts.isNotEmpty()
|
|
val accountKeys = accounts.map { it.publicKey }
|
|
|
|
// Обновляем кэш
|
|
accountsCache = accountKeys
|
|
lastAccountsLoadTime = currentTime
|
|
|
|
_state.update { it.copy(
|
|
hasExistingAccounts = hasAccounts,
|
|
availableAccounts = accountKeys
|
|
)}
|
|
} catch (e: Exception) {
|
|
}
|
|
}
|
|
|
|
private suspend fun checkAuthStatus() {
|
|
try {
|
|
val hasAccounts = databaseService.hasAccounts()
|
|
if (!hasAccounts) {
|
|
_state.update { it.copy(
|
|
status = AuthStatus.Unauthenticated
|
|
)}
|
|
} else {
|
|
_state.update { it.copy(
|
|
status = AuthStatus.Unauthenticated
|
|
)}
|
|
}
|
|
} catch (e: Exception) {
|
|
_state.update { it.copy(
|
|
status = AuthStatus.Unauthenticated
|
|
)}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create new account from seed phrase
|
|
* Matches createAccountFromSeedPhrase from React Native
|
|
* 🚀 ОПТИМИЗАЦИЯ: Dispatchers.Default для CPU-интенсивной криптографии
|
|
*/
|
|
suspend fun createAccount(
|
|
seedPhrase: List<String>,
|
|
password: String
|
|
): Result<DecryptedAccountData> = withContext(Dispatchers.Default) {
|
|
try {
|
|
// Step 1: Generate key pair from seed phrase (using BIP39)
|
|
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
|
|
|
// Step 2: Generate private key hash for protocol
|
|
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
|
|
|
// Step 3: Encrypt private key with password
|
|
val encryptedPrivateKey = CryptoManager.encryptWithPassword(
|
|
keyPair.privateKey, password
|
|
)
|
|
|
|
// Step 4: Encrypt seed phrase with password
|
|
val encryptedSeedPhrase = CryptoManager.encryptWithPassword(
|
|
seedPhrase.joinToString(" "), password
|
|
)
|
|
|
|
// Step 5: Save to database
|
|
val saved = withContext(Dispatchers.IO) {
|
|
databaseService.saveEncryptedAccount(
|
|
publicKey = keyPair.publicKey,
|
|
privateKeyEncrypted = encryptedPrivateKey,
|
|
seedPhraseEncrypted = encryptedSeedPhrase
|
|
)
|
|
}
|
|
|
|
if (!saved) {
|
|
return@withContext Result.failure(Exception("Failed to save account to database"))
|
|
}
|
|
|
|
// Step 6: Create decrypted account object
|
|
val decryptedAccount = DecryptedAccountData(
|
|
publicKey = keyPair.publicKey,
|
|
privateKey = keyPair.privateKey,
|
|
privateKeyHash = privateKeyHash,
|
|
seedPhrase = seedPhrase
|
|
)
|
|
|
|
// Step 7: Update state and reload accounts
|
|
currentDecryptedAccount = decryptedAccount
|
|
_state.update { it.copy(
|
|
status = AuthStatus.Authenticated(decryptedAccount)
|
|
)}
|
|
|
|
loadAccounts()
|
|
|
|
// Step 8: Connect and authenticate with protocol
|
|
ProtocolManager.connect()
|
|
|
|
// Give WebSocket time to connect before authenticating
|
|
kotlinx.coroutines.delay(500)
|
|
|
|
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
|
|
|
|
Result.success(decryptedAccount)
|
|
} catch (e: Exception) {
|
|
Result.failure(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unlock account with password
|
|
* Matches loginWithPassword from React Native
|
|
*/
|
|
suspend fun unlock(
|
|
publicKey: String,
|
|
password: String
|
|
): Result<DecryptedAccountData> = withContext(Dispatchers.Default) {
|
|
try {
|
|
// Decrypt account from database
|
|
val decryptedAccount = withContext(Dispatchers.IO) {
|
|
databaseService.decryptAccount(publicKey, password)
|
|
}
|
|
|
|
if (decryptedAccount == null) {
|
|
return@withContext Result.failure(Exception("Invalid password or account not found"))
|
|
}
|
|
|
|
// Update last used timestamp
|
|
withContext(Dispatchers.IO) {
|
|
databaseService.updateLastUsed(publicKey)
|
|
}
|
|
|
|
// Update state
|
|
currentDecryptedAccount = decryptedAccount
|
|
_state.update { it.copy(
|
|
status = AuthStatus.Authenticated(decryptedAccount)
|
|
)}
|
|
|
|
// Connect and authenticate with protocol
|
|
ProtocolManager.connect()
|
|
|
|
// Give WebSocket time to connect before authenticating
|
|
kotlinx.coroutines.delay(500)
|
|
|
|
ProtocolManager.authenticate(decryptedAccount.publicKey, decryptedAccount.privateKeyHash)
|
|
|
|
Result.success(decryptedAccount)
|
|
} catch (e: Exception) {
|
|
Result.failure(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout - clears decrypted account from memory
|
|
*/
|
|
fun logout() {
|
|
currentDecryptedAccount = null
|
|
_state.update { it.copy(
|
|
status = AuthStatus.Unauthenticated
|
|
)}
|
|
}
|
|
|
|
/**
|
|
* Delete account from database
|
|
*/
|
|
suspend fun deleteAccount(publicKey: String): Result<Unit> = withContext(Dispatchers.IO) {
|
|
try {
|
|
val success = databaseService.deleteAccount(publicKey)
|
|
if (!success) {
|
|
return@withContext Result.failure(Exception("Failed to delete account"))
|
|
}
|
|
|
|
// If deleting current account, logout
|
|
if (currentDecryptedAccount?.publicKey == publicKey) {
|
|
withContext(Dispatchers.Main) {
|
|
logout()
|
|
}
|
|
}
|
|
|
|
loadAccounts()
|
|
Result.success(Unit)
|
|
} catch (e: Exception) {
|
|
Result.failure(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current decrypted account (if authenticated)
|
|
*/
|
|
fun getCurrentAccount(): DecryptedAccountData? = currentDecryptedAccount
|
|
}
|
|
|
|
@Composable
|
|
fun rememberAuthState(context: Context): AuthStateManager {
|
|
val scope = rememberCoroutineScope()
|
|
return remember(context) {
|
|
AuthStateManager(context, scope)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ProvideAuthState(
|
|
authState: AuthStateManager,
|
|
content: @Composable (AuthStateData) -> Unit
|
|
) {
|
|
val state by authState.state.collectAsState()
|
|
content(state)
|
|
}
|