almost complete rework of ui up to module ui

This commit is contained in:
2022-03-23 23:21:02 +06:00
parent 72578f6b58
commit cb5de073fb
62 changed files with 499 additions and 471 deletions

View File

@@ -2,6 +2,7 @@ plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.compose)
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -11,8 +12,14 @@ kotlin {
commonMain {
dependencies {
api project(":postssystem.features.common.client")
api project(":postssystem.features.roles.client")
api project(":postssystem.features.status.client")
api project(":postssystem.features.auth.common")
}
}
clientMain {
dependencies {
}
}
}
}

View File

@@ -0,0 +1,39 @@
package dev.inmo.postssystem.features.auth.client
import dev.inmo.micro_utils.common.Either
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.postssystem.features.auth.common.AuthKey
import dev.inmo.postssystem.features.auth.common.AuthTokenInfo
import dev.inmo.postssystem.features.common.common.AdditionalModules
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.features.users.common.User
import io.ktor.client.HttpClient
import org.koin.core.module.Module
import org.koin.dsl.module
fun createAuthorizedFeaturesDIModule(
serverUrl: String,
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit,
onUserRetrieved: suspend (User?) -> Unit,
onAuthKeyInvalidated: suspend () -> Unit
): Module {
return module {
single(AuthorizedQualifiers.CredsQualifier) { initialAuthKey }
single(AuthorizedQualifiers.ServerUrlQualifier) { serverUrl }
single (createdAtStart = true) {
HttpClient {
installClientAuthenticator(serverUrl, get(), get(AuthorizedQualifiers.CredsQualifier), onAuthKeyUpdated, onUserRetrieved, onAuthKeyInvalidated)
}
}
single { UnifiedRequester(get(), get()) }
single { StatusFeatureClient(get(AuthorizedQualifiers.ServerUrlQualifier), get()) }
AdditionalModules.Authorized.modules.forEach {
with(it) {
load()
}
}
}
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.postssystem.features.auth.client
import dev.inmo.postssystem.features.common.common.AdditionalModules
import dev.inmo.postssystem.features.common.common.ModuleLoader
import org.koin.core.module.Module
private val AuthorizedAdditionalModules = AdditionalModules()
val AdditionalModules.Companion.Authorized: AdditionalModules
get() = AuthorizedAdditionalModules
fun AuthorizedModuleLoader(loadingBlock: Module.() -> Unit): ModuleLoader.ByCallback {
val newModuleLoader = ModuleLoader.ByCallback(loadingBlock)
AdditionalModules.Authorized.addModule(newModuleLoader)
return newModuleLoader
}

View File

@@ -0,0 +1,8 @@
package dev.inmo.postssystem.features.auth.client
import org.koin.core.qualifier.StringQualifier
object AuthorizedQualifiers {
val CredsQualifier = StringQualifier("creds")
val ServerUrlQualifier = StringQualifier("serverUrl")
}

View File

@@ -0,0 +1,59 @@
package dev.inmo.postssystem.features.auth.client
import dev.inmo.micro_utils.fsm.common.CheckableHandlerHolder
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
import dev.inmo.postssystem.features.auth.client.ui.AuthUIFSMState
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
import org.koin.core.Koin
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.Qualifier
import org.koin.core.scope.Scope
import kotlin.reflect.KClass
// Костыль, в JS на момент Пн дек 6 14:19:29 +06 2021 если использовать strictlyOn генерируются
// некорректные безымянные классы (у них отсутствует метод handleState)
class DefaultStateHandlerWrapper<T : UIFSMState>(
private val klass: KClass<out UIFSMHandler<T>>,
private val koin: Koin,
private val stateKlass: KClass<T>,
private val qualifier: Qualifier? = null,
private val parameters: ((T) -> ParametersHolder)? = null
) : CheckableHandlerHolder<UIFSMState, UIFSMState> {
override suspend fun StatesMachine<in UIFSMState>.handleState(state: UIFSMState): UIFSMState? {
@Suppress("UNCHECKED_CAST", "NAME_SHADOWING")
val state = state as T
return runCatching {
val authSettings = koin.get<AuthSettings>()
authSettings.loadingJob.join()
if (authSettings.authorizedDIModule.value == null) {
error("Can't perform state $state: Auth module was not initialized")
} else {
koin.get<UIFSMHandler<T>>(klass, qualifier, parameters ?.let { { it(state) } }).run {
handleState(state)
}
}
}.getOrElse { e ->
e.printStackTrace()
AuthUIFSMState(state)
}
}
override suspend fun checkHandleable(state: UIFSMState): Boolean = stateKlass.isInstance(state)
}
inline fun <reified T : UIFSMState> FSMBuilder<UIFSMState>.registerAfterAuthHandler(
koin: Koin,
klass: KClass<out UIFSMHandler<T>>,
qualifier: Qualifier? = null,
noinline parameters: ((T) -> ParametersHolder)? = null
) = add(
DefaultStateHandlerWrapper<T>(
klass,
koin,
T::class,
qualifier,
parameters
)
)

View File

@@ -0,0 +1,27 @@
package dev.inmo.postssystem.features.auth.client
import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.common.optional
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
import dev.inmo.postssystem.features.auth.client.settings.DefaultAuthSettings
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMExceptionHandler
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
val defaultModuleLoader = DefaultModuleLoader {
single<AuthSettings> { DefaultAuthSettings(get(DefaultQualifiers.SettingsQualifier), get(), getKoin(), get()) }
singleWithRandomQualifier {
UIFSMExceptionHandler { currentState, exception ->
if (exception is AuthUnavailableException) {
Optional.presented(AuthUIFSMState(currentState))
} else {
Optional.absent()
}
}
}
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
factory { AuthUIViewModel(get()) }
}

View File

@@ -0,0 +1,18 @@
package dev.inmo.postssystem.features.auth.client.settings
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.users.common.User
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.module.Module
interface AuthSettings {
val authorizedDIModule: StateFlow<Module?>
val user: StateFlow<User?>
val userRoles: StateFlow<List<Role>>
val loadingJob: Job
suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError?
}

View File

@@ -0,0 +1,118 @@
package dev.inmo.postssystem.features.auth.client.settings
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.common.Either
import dev.inmo.micro_utils.common.either
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.*
import dev.inmo.postssystem.features.auth.client.createAuthorizedFeaturesDIModule
import dev.inmo.postssystem.features.common.common.DBDropper
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.Koin
import org.koin.core.module.Module
data class DefaultAuthSettings(
private val repo: KeyValueRepo<String, Any>,
private val scope: CoroutineScope,
private val koin: Koin,
private val dbDropper: DBDropper
) : AuthSettings {
private val _authorizedDIModule = MutableStateFlow<Module?>(null)
override val authorizedDIModule: StateFlow<Module?> = _authorizedDIModule.asStateFlow()
private val _user = MutableStateFlow<User?>(null)
override val user: StateFlow<User?> = _user.asStateFlow()
private val _userRoles = MutableStateFlow<List<Role>>(emptyList())
override val userRoles: StateFlow<List<Role>> = _userRoles.asStateFlow()
private suspend fun getCurrentServerURL() = repo.get(SERVER_URL_FIELD) as? String
private suspend fun getCurrentUsername() = repo.get(USERNAME_FIELD) as? String
private suspend fun getCurrentToken() = repo.get(TOKEN_FIELD) as? AuthTokenInfo
override val loadingJob: Job = scope.launch {
val serverUrl = getCurrentServerURL() ?: return@launch
val token = getCurrentToken() ?: return@launch
updateModule(serverUrl, token.either())
}
val rolesUpdatingJob = (user + authorizedDIModule).subscribeSafelyWithoutExceptions(scope) {
val user = user.value
if (user == null || authorizedDIModule.value == null) {
_userRoles.value = emptyList()
} else {
_userRoles.value = koin.get<RolesStorage<Role>>().getRoles(user.id)
}
println(user)
println(userRoles.value)
}
override suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError? {
return runCatching {
if (getCurrentServerURL() != serverUrl || getCurrentUsername() != creds.username.string) {
dbDropper()
}
repo.set(SERVER_URL_FIELD, serverUrl)
repo.set(USERNAME_FIELD, creds.username.string)
repo.unset(TOKEN_FIELD)
updateModule(serverUrl, creds.either())
}.onFailure {
it.printStackTrace()
}.getOrThrow()
}
private suspend fun updateModule(
serverUrl: String,
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
): AuthUIError? {
val currentModule = authorizedDIModule.value
val newModule = createAuthorizedFeaturesDIModule(
serverUrl,
initialAuthKey,
{
repo.set(TOKEN_FIELD, it)
},
{
_user.value = it
}
) {
repo.unset(SERVER_URL_FIELD, USERNAME_FIELD, TOKEN_FIELD)
_authorizedDIModule.value = null
_user.value = null
throw AuthUnavailableException
}
currentModule ?.let { koin.unloadModules(listOf(currentModule)) }
koin.loadModules(listOf(newModule))
val statusFeature = koin.get<StatusFeatureClient>()
val serverAvailable = statusFeature.checkServerStatus()
val authCorrect = serverAvailable && runCatching {
statusFeature.checkServerStatusWithAuth()
}.getOrElse { false }
if (!serverAvailable && !authCorrect) {
koin.unloadModules(listOf(newModule))
currentModule ?.let { koin.loadModules(listOf(currentModule)) }
}
return when {
!serverAvailable -> ServerUnavailableAuthUIError
!authCorrect -> AuthIncorrectAuthUIError
else -> {
_authorizedDIModule.value = newModule
null
}
}
}
companion object {
private const val SERVER_URL_FIELD = "AuthServerURL"
private const val USERNAME_FIELD = "AuthUsername"
private const val TOKEN_FIELD = "AuthToken"
}
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.postssystem.features.auth.client.ui
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
import kotlinx.serialization.Serializable
@Serializable
data class AuthUIFSMState(
override val from: UIFSMState?,
override val context: String = "main"
) : UIFSMState {
companion object {
val default = AuthUIFSMState(null)
}
}

View File

@@ -1,7 +1,7 @@
package dev.inmo.postssystem.features.auth.client.ui
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.common.common.UIModel
import dev.inmo.postssystem.features.common.common.ui.UIModel
interface AuthUIModel : UIModel<AuthUIState> {
suspend fun initAuth(serverUrl: String, creds: AuthCreds)

View File

@@ -1,7 +1,7 @@
package dev.inmo.postssystem.features.auth.client.ui
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.common.common.UIViewModel
import dev.inmo.postssystem.features.common.common.ui.UIViewModel
import dev.inmo.postssystem.features.users.common.Username
import kotlinx.coroutines.flow.StateFlow

View File

@@ -0,0 +1,35 @@
package dev.inmo.postssystem.features.auth.client.ui
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.common.common.ui.AbstractUIModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class DefaultAuthUIModel(
private val scope: CoroutineScope,
private val authSettings: AuthSettings
) : AbstractUIModel<AuthUIState>(LoadingAuthUIState), AuthUIModel {
init {
scope.launch {
_currentState.value = LoadingAuthUIState
authSettings.loadingJob.join()
if (authSettings.authorizedDIModule.value == null) {
_currentState.value = DefaultInitAuthUIState
} else {
_currentState.value = AuthorizedAuthUIState
}
}
}
override suspend fun initAuth(serverUrl: String, creds: AuthCreds) {
_currentState.value = LoadingAuthUIState
val authError = authSettings.auth(serverUrl, creds)
if (authError == null) {
_currentState.value = AuthorizedAuthUIState
} else {
_currentState.value = InitAuthUIState(authError)
}
}
}

View File

@@ -0,0 +1,119 @@
package dev.inmo.postssystem.features.auth.client
import androidx.compose.runtime.mutableStateOf
import dev.inmo.jsuikit.elements.*
import dev.inmo.jsuikit.modifiers.*
import dev.inmo.jsuikit.utils.Attrs
import dev.inmo.micro_utils.coroutines.compose.renderComposableAndLinkToContextAndRoot
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.common.common.ui.JSView
import dev.inmo.postssystem.features.common.common.ui.fsm.*
import kotlinx.browser.document
import kotlinx.coroutines.*
import kotlinx.dom.*
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.*
val loader = DefaultModuleLoader {
factory { AuthView(get(), get(DefaultQualifiers.UIScopeQualifier), getAllDistinct()) }
singleWithRandomQualifier<UIFSMHandler.Registrator> {
UIFSMHandler.Registrator {
strictlyOn(get<AuthView>())
}
}
}
class AuthView(
private val viewModel: AuthUIViewModel,
private val uiScope: CoroutineScope,
defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
) : JSView<AuthUIFSMState>(defaultExceptionsHandlers) {
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
state: AuthUIFSMState
): UIFSMState? {
val completion = CompletableDeferred<UIFSMState?>()
val usernameState = mutableStateOf("")
val passwordState = mutableStateOf("")
val disabled = mutableStateOf(true)
val errorText = mutableStateOf<String?>(null)
val root = htmlElement.appendElement("div") {}
val composition = renderComposableAndLinkToContextAndRoot(root) {
val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank()
Flex(
UIKitFlex.Alignment.Horizontal.Center
) {
Card(
Attrs(UIKitText.Alignment.Horizontal.Center),
bodyAttrs = Attrs(UIKitWidth.Fixed.Medium),
) {
CardTitle { Text("Log in") }
if (errorText.value != null) {
CardBadge(Attrs(UIKitLabel.Error)) {
Text(errorText.value.toString())
}
}
TextField(
InputType.Text,
usernameState,
disabled,
"Username",
)
TextField(
InputType.Password,
passwordState,
disabled,
"Password"
)
DefaultButton("Authorise", UIKitButton.Type.Primary, UIKitMargin.Small, disabled = authBtnDisabled) {
it.nativeEvent.preventDefault()
val serverUrl = document.location ?.run { "$hostname:$port" }
if (serverUrl != null) {
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, usernameState.value, passwordState.value) }
}
}
}
}
}
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) {
is InitAuthUIState -> {
disabled.value = false
errorText.value = when (it.showError) {
ServerUnavailableAuthUIError -> "Server unavailable"
AuthIncorrectAuthUIError -> {
passwordState.value = ""
"Username or password is incorrect"
}
null -> null
}
}
LoadingAuthUIState -> {
disabled.value = true
errorText.value = null
}
AuthorizedAuthUIState -> {
completion.complete(state.from)
}
}
}
return completion.await().also {
composition.dispose()
viewJob.cancel()
}
}
}