feat: add provider login flow
This commit is contained in:
@@ -15,6 +15,41 @@ interface SessionGateway {
|
||||
suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit>
|
||||
}
|
||||
|
||||
interface ProviderProfileStore {
|
||||
fun observeProfiles(): Flow<List<ProviderProfile>>
|
||||
|
||||
fun observeActiveProfile(): Flow<ProviderProfile?>
|
||||
|
||||
fun observeLoginPreferences(profileId: ProviderProfileId): Flow<LoginPreferences>
|
||||
|
||||
suspend fun profiles(): List<ProviderProfile>
|
||||
|
||||
suspend fun activeProfile(): ProviderProfile?
|
||||
|
||||
suspend fun loginPreferences(profileId: ProviderProfileId): LoginPreferences
|
||||
|
||||
suspend fun saveProfile(profile: ProviderProfile): DomainResult<Unit>
|
||||
|
||||
suspend fun setActiveProfile(profileId: ProviderProfileId?): DomainResult<Unit>
|
||||
|
||||
suspend fun saveLoginPreferences(preferences: LoginPreferences): DomainResult<Unit>
|
||||
}
|
||||
|
||||
interface CredentialStore {
|
||||
suspend fun saveCredentials(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
): DomainResult<Unit>
|
||||
|
||||
suspend fun loadCredentials(
|
||||
profileId: ProviderProfileId,
|
||||
): DomainResult<LoginCredentials?>
|
||||
|
||||
suspend fun clearCredentials(profileId: ProviderProfileId): DomainResult<Unit>
|
||||
}
|
||||
|
||||
interface LocalSessionGateway : SessionGateway
|
||||
|
||||
interface DeviceCatalog {
|
||||
fun observeDevices(
|
||||
profileId: ProviderProfileId,
|
||||
|
||||
@@ -53,6 +53,13 @@ data class LoginCredentials(
|
||||
val password: Password,
|
||||
)
|
||||
|
||||
data class LoginPreferences(
|
||||
val profileId: ProviderProfileId,
|
||||
val rememberedUsername: Username? = null,
|
||||
val rememberUsername: Boolean = false,
|
||||
val autoLoginEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
data class Session(
|
||||
val profileId: ProviderProfileId,
|
||||
val username: Username,
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.flagship.abox.manager.domain
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SessionCoordinator(
|
||||
private val profileStore: ProviderProfileStore,
|
||||
private val credentialStore: CredentialStore,
|
||||
private val remoteGateway: SessionGateway,
|
||||
private val localGateway: LocalSessionGateway,
|
||||
) {
|
||||
fun observeActiveSession(): Flow<Session?> = profileStore.observeActiveProfile()
|
||||
.flatMapLatest { profile ->
|
||||
profile?.gateway()?.observeSession(profile.id) ?: flowOf(null)
|
||||
}
|
||||
|
||||
suspend fun bootstrap(): DomainResult<Session?> = storeOperation {
|
||||
val profile = profileStore.activeProfile() ?: return@storeOperation DomainResult.Success(null)
|
||||
if (profile.type == ProviderType.LOCAL_EXPERIENCE) {
|
||||
return@storeOperation localGateway.login(profile.id, localCredentials())
|
||||
.mapSuccess { it }
|
||||
}
|
||||
val preferences = profileStore.loginPreferences(profile.id)
|
||||
if (!preferences.autoLoginEnabled) return@storeOperation DomainResult.Success(null)
|
||||
when (val credentials = credentialStore.loadCredentials(profile.id)) {
|
||||
is DomainResult.Failure -> credentials
|
||||
is DomainResult.Success -> {
|
||||
val savedCredentials = credentials.value ?: return@storeOperation DomainResult.Success(null)
|
||||
if (profileStore.activeProfile()?.id != profile.id) {
|
||||
return@storeOperation DomainResult.Success(null)
|
||||
}
|
||||
profile.gateway().login(profile.id, savedCredentials).mapSuccess { it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun selectProvider(profile: ProviderProfile): DomainResult<Session?> = storeOperation {
|
||||
val current = profileStore.activeProfile()
|
||||
if (current?.id != profile.id && current != null) {
|
||||
when (val switched = switchProvider()) {
|
||||
is DomainResult.Failure -> return@storeOperation switched
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
}
|
||||
when (val saved = profileStore.saveProfile(profile)) {
|
||||
is DomainResult.Failure -> return@storeOperation saved
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
when (val activated = profileStore.setActiveProfile(profile.id)) {
|
||||
is DomainResult.Failure -> return@storeOperation activated
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
if (profile.type == ProviderType.LOCAL_EXPERIENCE) {
|
||||
localGateway.login(profile.id, localCredentials()).mapSuccess { it }
|
||||
} else {
|
||||
DomainResult.Success(null)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
rememberUsername: Boolean,
|
||||
autoLogin: Boolean,
|
||||
): DomainResult<Session> = storeOperation {
|
||||
val profile = profileStore.activeProfile()
|
||||
?: return@storeOperation DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "No active provider profile"),
|
||||
)
|
||||
if (profile.id != profileId || profile.type == ProviderType.LOCAL_EXPERIENCE) {
|
||||
return@storeOperation DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "The active provider does not require this login"),
|
||||
)
|
||||
}
|
||||
when (val result = remoteGateway.login(profileId, credentials)) {
|
||||
is DomainResult.Failure -> result
|
||||
is DomainResult.Success -> {
|
||||
val previousPreferences = profileStore.loginPreferences(profileId)
|
||||
val previousCredentials = credentialStore.loadCredentials(profileId)
|
||||
if (previousCredentials is DomainResult.Failure) {
|
||||
remoteGateway.logout(profileId)
|
||||
return@storeOperation previousCredentials
|
||||
}
|
||||
val preferences = LoginPreferences(
|
||||
profileId = profileId,
|
||||
rememberedUsername = credentials.username.takeIf { rememberUsername },
|
||||
rememberUsername = rememberUsername,
|
||||
autoLoginEnabled = autoLogin,
|
||||
)
|
||||
val credentialResult = if (autoLogin) {
|
||||
credentialStore.saveCredentials(profileId, credentials)
|
||||
} else {
|
||||
credentialStore.clearCredentials(profileId)
|
||||
}
|
||||
if (credentialResult is DomainResult.Failure) {
|
||||
remoteGateway.logout(profileId)
|
||||
return@storeOperation credentialResult
|
||||
}
|
||||
when (val preferencesResult = profileStore.saveLoginPreferences(preferences)) {
|
||||
is DomainResult.Success -> result
|
||||
is DomainResult.Failure -> {
|
||||
when (previousCredentials) {
|
||||
is DomainResult.Success -> previousCredentials.value?.let {
|
||||
credentialStore.saveCredentials(profileId, it)
|
||||
} ?: credentialStore.clearCredentials(profileId)
|
||||
is DomainResult.Failure -> Unit
|
||||
}
|
||||
profileStore.saveLoginPreferences(previousPreferences)
|
||||
remoteGateway.logout(profileId)
|
||||
preferencesResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun recoverSession(profileId: ProviderProfileId): DomainResult<Session?> = storeOperation {
|
||||
val profile = profileStore.activeProfile()
|
||||
if (profile?.id != profileId || profile.type == ProviderType.LOCAL_EXPERIENCE) {
|
||||
return@storeOperation DomainResult.Success(null)
|
||||
}
|
||||
val preferences = profileStore.loginPreferences(profileId)
|
||||
if (!preferences.autoLoginEnabled) return@storeOperation DomainResult.Success(null)
|
||||
when (val credentials = credentialStore.loadCredentials(profileId)) {
|
||||
is DomainResult.Failure -> credentials
|
||||
is DomainResult.Success -> {
|
||||
val savedCredentials = credentials.value ?: return@storeOperation DomainResult.Success(null)
|
||||
when (val result = remoteGateway.login(profileId, savedCredentials)) {
|
||||
is DomainResult.Success -> result.mapSuccess { it }
|
||||
is DomainResult.Failure -> {
|
||||
if (result.error is DomainError.Authentication ||
|
||||
result.error is DomainError.TokenInvalid
|
||||
) {
|
||||
credentialStore.clearCredentials(profileId)
|
||||
profileStore.saveLoginPreferences(
|
||||
preferences.copy(autoLoginEnabled = false),
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout(): DomainResult<Unit> = storeOperation {
|
||||
val profile = profileStore.activeProfile() ?: return@storeOperation DomainResult.Success(Unit)
|
||||
when (val credentialResult = credentialStore.clearCredentials(profile.id)) {
|
||||
is DomainResult.Failure -> return@storeOperation credentialResult
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
val preferences = profileStore.loginPreferences(profile.id)
|
||||
when (
|
||||
val preferencesResult = profileStore.saveLoginPreferences(
|
||||
preferences.copy(autoLoginEnabled = false),
|
||||
)
|
||||
) {
|
||||
is DomainResult.Failure -> return@storeOperation preferencesResult
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
when (val logoutResult = profile.gateway().logout(profile.id)) {
|
||||
is DomainResult.Failure -> return@storeOperation logoutResult
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
if (profile.type == ProviderType.LOCAL_EXPERIENCE) {
|
||||
profileStore.setActiveProfile(null)
|
||||
} else {
|
||||
DomainResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun switchProvider(): DomainResult<Unit> = storeOperation {
|
||||
when (val logoutResult = logout()) {
|
||||
is DomainResult.Failure -> logoutResult
|
||||
is DomainResult.Success -> profileStore.setActiveProfile(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ProviderProfile.gateway(): SessionGateway =
|
||||
if (type == ProviderType.LOCAL_EXPERIENCE) localGateway else remoteGateway
|
||||
|
||||
private fun localCredentials(): LoginCredentials = LoginCredentials(
|
||||
username = Username("local"),
|
||||
password = Password(""),
|
||||
)
|
||||
|
||||
private suspend fun <T> storeOperation(block: suspend () -> DomainResult<T>): DomainResult<T> =
|
||||
try {
|
||||
block()
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
DomainResult.Failure(
|
||||
DomainError.Unknown(detail = error.message ?: error::class.java.simpleName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T, R> DomainResult<T>.mapSuccess(transform: (T) -> R): DomainResult<R> = when (this) {
|
||||
is DomainResult.Failure -> this
|
||||
is DomainResult.Success -> DomainResult.Success(transform(value))
|
||||
}
|
||||
Reference in New Issue
Block a user