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
client
features
auth
common
content
binary
client
src
jsMain
kotlin
dev
inmo
postssystem
features
client
src
commonMain
kotlin
dev
inmo
postssystem
features
content
text
client
src
jsMain
kotlin
dev
inmo
postssystem
features
files
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
files
roles
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
roles
manager
common
src
commonMain
kotlin
dev
inmo
postssystem
features
roles
manager
users
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
users
gradle
publicators/simple/client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
publicators
simple
services/posts/client
build.gradle
src
commonMain
jsMain
kotlin
dev
inmo
postssystem
services
targets/telegram/loader/client/src/commonMain/kotlin/dev/inmo/postssystem/targets/telegram/loader/client

@ -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 {
}
}
}
}

@ -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()
}
}
}
}

@ -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
}

@ -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")
}

@ -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
)
)

@ -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()) }
}

@ -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?
}

@ -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"
}
}

@ -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)
}
}

@ -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)

@ -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

@ -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)
}
}
}

@ -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()
}
}
}