full reborn

This commit is contained in:
2021-11-24 13:52:27 +06:00
parent 0ac6b0a4df
commit 6a6a197041
246 changed files with 4327 additions and 6952 deletions
.github/workflows
LICENSEREADME.mdbuild.gradle
business_cases/post_creating
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
business_cases
post_creating
main
common
src
commonMain
kotlin
dev
inmo
postssystem
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
business_cases
post_creating
client
core
defaultAndroidSettingsdefaultAndroidSettings.gradleextensions.gradle
features
auth
common
client
build.gradle
src
commonMain
kotlin
dev
inmo
main
common
build.gradle
src
commonMain
kotlin
dev
inmo
jvmMain
kotlin
dev
inmo
postssystem
features
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
features
files
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
main
common
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
features
roles
status
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
main
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
status
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
features
template
users
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
main
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
jvmMain
kotlin
dev
inmo
postssystem
features
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
gradle.properties
gradle/wrapper
gradlewgradlew.bat
mimes_generator
mppAndroidProject.gradlemppJavaProject.gradlemppJsProject.gradlemppProjectWithSerialization.gradlepubconf.kpsbpublish.gradlepublish.kpsb
publishing
server
settings.gradle

41
client/build.gradle Normal file

@ -0,0 +1,41 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
js(IR) {
binaries.executable()
}
sourceSets {
commonMain {
dependencies {
api project(":postssystem.features.common.client")
api project(":postssystem.features.status.client")
api project(":postssystem.features.files.client")
api project(":postssystem.features.users.client")
api project(":postssystem.features.auth.client")
api project(":postssystem.features.roles.client")
api project(":postssystem.features.roles.manager.client")
api "dev.inmo:micro_utils.fsm.common:$microutils_version"
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
api "dev.inmo:micro_utils.crypto:$microutils_version"
}
}
jvmMain {
dependencies {
api "io.ktor:ktor-client-apache:$ktor_version"
}
}
jsMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-html:$kotlinx_html_version"
}
}
}
}

@ -0,0 +1,12 @@
package dev.inmo.postssystem.client
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.KeyValueRepo
class DBDropper(
private val repo: KeyValueRepo<String, Any>
) {
suspend operator fun invoke() {
repo.unset(repo.getAllByWithNextPaging { keys(it) })
}
}

@ -0,0 +1,105 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.features.auth.client.installClientAuthenticator
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.files.client.ClientFilesStorage
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
import dev.inmo.postssystem.features.roles.common.UserRole
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
import dev.inmo.postssystem.features.roles.client.ClientUsersRolesStorage
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer
import dev.inmo.postssystem.features.users.client.UsersStorageKtorClient
import dev.inmo.postssystem.features.users.common.ReadUsersStorage
import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.common.Either
import dev.inmo.micro_utils.coroutines.LinkedSupervisorScope
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import dev.inmo.micro_utils.repos.KeyValueRepo
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.json.Json
import org.koin.core.Koin
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.core.qualifier.*
import org.koin.core.scope.Scope
import org.koin.dsl.module
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
val SettingsQualifier = StringQualifier("Settings")
val UserRolesQualifier = StringQualifier("UserRoles")
private val DBDropperQualifier = StringQualifier("DBDropper")
private val FSMHandlersBuilderQualifier = StringQualifier("FSMHandlersBuilder")
val defaultSerialFormat = Json {
ignoreUnknownKeys = true
}
/**
* Entrypoint for getting [org.koin.core.Koin] DI for the client
*
* @param repoFactory Factory for creating of [DefaultStatesManagerRepo] for [dev.inmo.postssystem.client.ui.fsm.UIFSM]
*/
fun baseKoin(
defaultScope: CoroutineScope,
settingsFactory: Scope.() -> KeyValueRepo<String, Any>,
repoFactory: Scope.() -> DefaultStatesManagerRepo<UIFSMState>,
handlersSetter: Pair<Scope, FSMBuilder<UIFSMState>>.() -> Unit
): Koin = startKoin {
modules(
module {
single<StringFormat> { defaultSerialFormat }
single(SettingsQualifier) { settingsFactory() }
single<DBDropper>(DBDropperQualifier) { DBDropper(get(SettingsQualifier)) }
single(FSMHandlersBuilderQualifier) { handlersSetter }
single { repoFactory() }
single { defaultScope }
single(UIScopeQualifier) { get<CoroutineScope>().LinkedSupervisorScope(Dispatchers.Main) }
single<StatesMachine<UIFSMState>>(UIFSMQualifier) { UIFSM(get()) { (this@single to this@UIFSM).apply(get(
FSMHandlersBuilderQualifier
)) } }
}
)
}.koin.apply {
loadModules(
listOf(
module { single<Koin> { this@apply } }
)
)
RolesManagerRoleSerializer // Just to activate it in JS client
}
fun getAuthorizedFeaturesDIModule(
serverUrl: String,
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit,
onUserRetrieved: suspend (User?) -> Unit,
onAuthKeyInvalidated: suspend () -> Unit
): Module {
val serverUrlQualifier = StringQualifier("serverUrl")
val credsQualifier = StringQualifier("creds")
return module {
single(createdAtStart = true) {
HttpClient {
installClientAuthenticator(serverUrl, get(), get(credsQualifier), onAuthKeyUpdated, onUserRetrieved, onAuthKeyInvalidated)
}
}
single(credsQualifier) { initialAuthKey }
single(serverUrlQualifier) { serverUrl }
single<BinaryFormat> { standardKtorSerialFormat }
single { UnifiedRequester(get(), get()) }
single<FilesStorage> { ClientFilesStorage(get(serverUrlQualifier), get(), get()) }
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
single<UsersRolesStorage<UserRole>> { ClientUsersRolesStorage(get(serverUrlQualifier), get(), UserRole.serializer()) }
}
}

@ -0,0 +1,8 @@
package dev.inmo.postssystem.client.settings
import dev.inmo.postssystem.client.settings.auth.AuthSettings
data class DefaultSettings(
override val authSettings: AuthSettings
) : Settings

@ -0,0 +1,12 @@
package dev.inmo.postssystem.client.settings
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.module.Module
interface Settings {
val authSettings: AuthSettings
val authorizedDIModule: StateFlow<Module?>
get() = authSettings.authorizedDIModule
}

@ -0,0 +1,18 @@
package dev.inmo.postssystem.client.settings.auth
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.roles.common.UserRole
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<UserRole>>
val loadingJob: Job
suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError?
}

@ -0,0 +1,118 @@
package dev.inmo.postssystem.client.settings.auth
import dev.inmo.postssystem.client.DBDropper
import dev.inmo.postssystem.client.getAuthorizedFeaturesDIModule
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.UserRole
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
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 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<UserRole>>(emptyList())
override val userRoles: StateFlow<List<UserRole>> = _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<UsersRolesStorage<UserRole>>().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 = getAuthorizedFeaturesDIModule(
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,36 @@
package dev.inmo.postssystem.client.ui
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.common.common.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,17 @@
package dev.inmo.postssystem.client.ui.fsm
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import org.koin.core.qualifier.StringQualifier
val UIFSMQualifier = StringQualifier("UIFSM")
fun UIFSM(
repo: DefaultStatesManagerRepo<UIFSMState>,
handlersSetter: FSMBuilder<UIFSMState>.() -> Unit
) = buildFSM<UIFSMState> {
statesManager = DefaultStatesManager(repo)
handlersSetter()
}

@ -0,0 +1,27 @@
package dev.inmo.postssystem.client.ui.fsm
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException
import dev.inmo.micro_utils.fsm.common.*
interface UIFSMHandler<T : UIFSMState> : StatesHandler<T, UIFSMState> {
suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState?
override suspend fun StatesMachine<in UIFSMState>.handleState(state: T): UIFSMState? {
return runCatching {
safeHandleState(state).also(::println)
}.getOrElse {
errorToNextStep(state, it) ?.let { return it } ?: throw it
}.also(::println)
}
suspend fun errorToNextStep(
currentState: T,
e: Throwable
): UIFSMState? = when (e) {
is AuthUnavailableException -> if (currentState is AuthUIFSMState) {
currentState
} else {
AuthUIFSMState(currentState)
}
else -> null
}
}

@ -0,0 +1,24 @@
package dev.inmo.postssystem.client.ui.fsm
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import kotlinx.serialization.*
@Serializable(UIFSMStateSerializer::class)
sealed interface UIFSMState : State {
val from: UIFSMState?
get() = null
override val context: String
get() = "main"
}
object UIFSMStateSerializer : KSerializer<UIFSMState> by TypedSerializer(
"auth" to AuthUIFSMState.serializer(),
)
@Serializable
data class AuthUIFSMState(
override val from: UIFSMState? = null,
override val context: String = "main"
) : UIFSMState
val DefaultAuthUIFSMState = AuthUIFSMState()

@ -0,0 +1,92 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.fsm.ui.*
import dev.inmo.postssystem.client.ui.*
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.client.ui.fsm.UIFSMStateSerializer
import dev.inmo.postssystem.features.auth.client.ui.AuthUIModel
import dev.inmo.postssystem.features.auth.client.ui.AuthUIViewModel
import dev.inmo.postssystem.features.auth.common.AuthTokenInfo
import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import kotlinx.browser.document
import kotlinx.browser.localStorage
import kotlinx.coroutines.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.serializer
import org.koin.core.Koin
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
import org.w3c.dom.HTMLElement
val defaultTypedSerializer = TypedSerializer<Any>(
"AuthTokenInfo" to AuthTokenInfo.serializer(),
"String" to String.serializer(),
"Int" to Int.serializer(),
"Long" to Long.serializer(),
"Short" to Short.serializer(),
"Byte" to Byte.serializer(),
"Float" to Float.serializer(),
"Double" to Double.serializer(),
"UIFSMState" to UIFSMStateSerializer
)
fun baseKoin(): Koin {
val anyToString: suspend Any.() -> String = {
defaultSerialFormat.encodeToString(
defaultTypedSerializer,
this
)
}
return baseKoin(
CoroutineScope(
Dispatchers.Default +
ContextSafelyExceptionHandler { it.printStackTrace() } +
CoroutineExceptionHandler { _, it -> it.printStackTrace() }
),
{
CookiesKeyValueRepo.withMapper<String, Any, String, String>(
{ this },
{
runCatching {
anyToString()
}.getOrElse {
if (it is NoSuchElementException) {
val name = this::class.simpleName!!
defaultTypedSerializer.include(name, serializer())
anyToString()
} else {
throw it
}
}
},
{ this },
{
defaultSerialFormat.decodeFromString(
defaultTypedSerializer,
this
)
}
)
},
{
OneStateUIFSMStatesRepo(get(), localStorage)
}
) {
first.apply {
second.apply {
loadKoinModules(
module {
factory { document.getElementById("main") as HTMLElement }
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
factory { AuthUIViewModel(get()) }
factory { AuthView(get(), get(UIScopeQualifier)) }
}
)
strictlyOn<AuthUIFSMState>(get<AuthView>())
}
}
}
}

@ -0,0 +1,14 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.UIFSMQualifier
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.micro_utils.fsm.common.StatesMachine
import kotlinx.browser.window
fun main() {
window.addEventListener("load", {
val koin = baseKoin()
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
uiStatesMachine.start(koin.get())
})
}

@ -0,0 +1,63 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import kotlinx.serialization.StringFormat
import org.w3c.dom.*
class OneStateUIFSMStatesRepo(
private val serialFormat: StringFormat,
private val storage: Storage,
private val initialState: UIFSMState = DefaultAuthUIFSMState
) : DefaultStatesManagerRepo<UIFSMState> {
private val String.storageKey
get() = "${FSMStateSettingsFieldPrefix}$this"
private val String.UIFSMState
get() = runCatching {
serialFormat.decodeFromString(UIFSMStateSerializer, this)
}.onFailure { it.printStackTrace() }.getOrNull()
init {
if (states().isEmpty()) {
setState(initialState)
}
}
private fun setState(state: UIFSMState) {
storage[state.context.storageKey] = serialFormat.encodeToString(UIFSMStateSerializer, state)
}
override suspend fun getContextState(context: Any): UIFSMState? {
return when (context) {
is String -> storage[context.storageKey] ?.UIFSMState ?: return DefaultAuthUIFSMState
else -> null
}
}
override suspend fun contains(context: Any): Boolean = when (context) {
is String -> storage.get(context) ?.UIFSMState != null
else -> super.contains(context)
}
private fun states(): List<UIFSMState> = storage.iterator().asSequence().mapNotNull { (k, v) ->
if (k.startsWith(FSMStateSettingsFieldPrefix)) {
v.UIFSMState
} else {
null
}
}.toList()
override suspend fun getStates(): List<UIFSMState> = states()
override suspend fun removeState(state: UIFSMState) {
storage.removeItem((state.context as? String) ?.storageKey ?: return)
}
override suspend fun set(state: UIFSMState) {
setState(state)
}
companion object {
private const val FSMStateSettingsFieldPrefix = "UIFSMState_"
}
}

@ -0,0 +1,82 @@
package dev.inmo.postssystem.client
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.pagination.utils.reverse
import dev.inmo.micro_utils.repos.KeyValueRepo
import kotlinx.browser.localStorage
import kotlinx.coroutines.flow.*
import org.w3c.dom.get
import org.w3c.dom.set
object CookiesKeyValueRepo : KeyValueRepo<String, String> {
private val _onNewValue = MutableSharedFlow<Pair<String, String>>()
private val _onValueRemoved = MutableSharedFlow<String>()
override val onNewValue: Flow<Pair<String, String>> = _onNewValue.asSharedFlow()
override val onValueRemoved: Flow<String> = _onValueRemoved.asSharedFlow()
override suspend fun contains(key: String): Boolean = localStorage.iterator().asSequence().any { it.first == key }
override suspend fun count(): Long = localStorage.length.toLong()
override suspend fun get(k: String): String? = localStorage[k]
override suspend fun keys(
v: String,
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = localStorage.iterator().asSequence().mapNotNull {
if (it.second == v) it.first else null
}.toList().let {
it.paginate(
if (reversed) {
pagination.reverse(it.count())
} else {
pagination
}
)
}
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.first }.toList().let {
it.paginate(
if (reversed) {
pagination.reverse(it.count())
} else {
pagination
}
)
}
override suspend fun values(
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = localStorage.iterator().asSequence().map { it.second }.toList().let {
it.paginate(
if (reversed) {
pagination.reverse(it.count())
} else {
pagination
}
)
}
override suspend fun set(toSet: Map<String, String>) {
toSet.forEach { (k, v) ->
localStorage[k] = v
_onNewValue.emit(k to v)
}
}
override suspend fun unset(toUnset: List<String>) {
toUnset.forEach {
localStorage[it] ?.let { _ ->
localStorage.removeItem(it)
_onValueRemoved.emit(it)
}
}
}
}

@ -0,0 +1,19 @@
package dev.inmo.postssystem.client
import org.w3c.dom.Storage
import org.w3c.dom.get
class StorageIterator(private val storage: Storage) : Iterator<Pair<String, String>> {
private var index = 0
override fun hasNext(): Boolean = index < storage.length
override fun next(): Pair<String, String> {
val k = storage.key(index) ?: error("Key for index $index was not found")
val v = storage[k] ?: error("Key for index $index was not found")
index++
return k to v
}
}
operator fun Storage.iterator() = StorageIterator(this)

@ -0,0 +1,120 @@
package dev.inmo.postssystem.client.fsm.ui
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.postssystem.client.utils.HTMLViewContainer
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 kotlinx.browser.document
import kotlinx.coroutines.*
import kotlinx.dom.clear
import kotlinx.html.*
import kotlinx.html.dom.append
import kotlinx.html.js.form
import kotlinx.html.js.onClickFunction
import org.w3c.dom.*
class AuthView(
private val viewModel: AuthUIViewModel,
private val uiScope: CoroutineScope
) : JSView<AuthUIFSMState>() {
private val usernameInput
get() = document.getElementById("authUsername") as? HTMLInputElement
private val passwordInput
get() = document.getElementById("authPassword") as? HTMLInputElement
private val authButton
get() = document.getElementById("authButton")
private val errorBadge
get() = document.getElementById("errorBadge") as? HTMLElement
private val progressBarDiv
get() = document.getElementById("progressBar") as? HTMLDivElement
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
container: HTMLViewContainer,
state: AuthUIFSMState
): UIFSMState? {
val completion = CompletableDeferred<UIFSMState?>()
htmlElement.clear()
htmlElement.append {
form(classes = "vertical_container") {
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(type = InputType.text, classes = "mdl-textfield__input") {
id = "authUsername"
}
label(classes = "mdl-textfield__label") {
+"Имя пользователя"
}
}
div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(type = InputType.password, classes = "mdl-textfield__input") {
id = "authPassword"
}
label(classes = "mdl-textfield__label") {
+"Пароль"
}
}
div(classes = "mdl-progress mdl-js-progress") {
id = "progressBar"
}
span(classes = "material-icons mdl-badge mdl-badge--overlap gone") {
id = "errorBadge"
attributes["data-badge"] = "!"
}
button(classes = "mdl-button mdl-js-button mdl-button--raised") {
+"Авторизоваться"
id = "authButton"
onClickFunction = {
it.preventDefault()
val serverUrl = document.location ?.run { "$hostname:$port" }
val username = usernameInput ?.value
val password = passwordInput ?.value
if (serverUrl != null && username != null && password != null) {
uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, username, password) }
}
}
}
}
}
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) {
is InitAuthUIState -> {
usernameInput ?.removeAttribute("disabled")
passwordInput ?.removeAttribute("disabled")
authButton ?.removeAttribute("disabled")
errorBadge ?.apply {
when (it.showError) {
ServerUnavailableAuthUIError -> {
classList.remove("gone")
innerText = "Сервер недоступен"
}
AuthIncorrectAuthUIError -> {
classList.remove("gone")
innerText = "Данные некорректны"
}
null -> classList.add("gone")
}
}
progressBarDiv ?.classList ?.add("gone")
}
LoadingAuthUIState -> {
usernameInput ?.setAttribute("disabled", "")
passwordInput ?.setAttribute("disabled", "")
authButton ?.setAttribute("disabled", "")
errorBadge ?.classList ?.add("gone")
progressBarDiv ?.classList ?.remove("gone")
}
AuthorizedAuthUIState -> {
htmlElement.clear()
completion.complete(state.from)
}
}
}
return completion.await().also {
viewJob.cancel()
}
}
}

@ -0,0 +1,7 @@
package dev.inmo.postssystem.client.fsm.ui
import kotlinx.browser.document
import org.w3c.dom.Element
val mainContainer: Element
get() = document.getElementById("main")!!

@ -0,0 +1,21 @@
package dev.inmo.postssystem.client.fsm.ui
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.postssystem.client.utils.HTMLViewContainer
import dev.inmo.micro_utils.fsm.common.StatesMachine
import org.w3c.dom.HTMLElement
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement,
container: HTMLViewContainer,
state: T
): UIFSMState? = null
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState? {
return HTMLViewContainer.from(state.context) ?.let {
safeHandleState(it.htmlElement ?: return null, it, state)
}
}
}

@ -0,0 +1,20 @@
package dev.inmo.postssystem.client.fsm.ui.defaults
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import kotlinx.coroutines.CompletableDeferred
import kotlinx.html.TagConsumer
import kotlinx.html.js.button
import kotlinx.html.js.onClickFunction
import org.w3c.dom.HTMLElement
fun TagConsumer<HTMLElement>.addBackButton(
completableDeferred: CompletableDeferred<UIFSMState>,
stateToBack: UIFSMState
) {
button {
+"Назад"
onClickFunction = {
completableDeferred.complete(stateToBack)
}
}
}

@ -0,0 +1,64 @@
package dev.inmo.postssystem.client.utils
import com.benasher44.uuid.uuid4
import kotlinx.browser.document
import kotlinx.html.*
import kotlinx.html.dom.append
import kotlinx.html.js.*
import org.w3c.dom.*
object DialogHelper {
fun createOneFieldDialog(
title: String,
hint: String,
doneButtonText: String,
closeButtonText: String,
onClose: () -> Unit,
onSubmit: (String) -> Unit
): HTMLDialogElement {
lateinit var dialogElement: HTMLDialogElement
(document.getElementsByTagName("body").item(0) as? HTMLBodyElement) ?.append {
dialogElement = dialog("mdl-dialog") {
h4("mdl-dialog__title") {
+title
}
val id = "form_${uuid4()}_text"
div(classes = "mdl-dialog__content") {
form("#") {
div("mdl-textfield mdl-js-textfield mdl-textfield--floating-label") {
input(InputType.text, classes = "mdl-textfield__input") {
this.id = id
}
label(classes = "mdl-textfield__label") {
+hint
attributes["for"] = id
}
}
}
}
div(classes = "mdl-dialog__actions mdl-dialog__actions--full-width") {
button(classes = "mdl-button", type = ButtonType.button) {
+doneButtonText
onClickFunction = {
it.preventDefault()
val input = document.getElementById(id) as? HTMLInputElement
input ?.value ?.let {
onSubmit(it)
}
}
}
button(classes = "mdl-button", type = ButtonType.button) {
+closeButtonText
onClickFunction = {
it.preventDefault()
onClose()
}
}
}
}
}
return dialogElement
}
}

@ -0,0 +1,18 @@
package dev.inmo.postssystem.client.utils
import dev.inmo.postssystem.features.files.common.FullFileInfo
import dev.inmo.micro_utils.common.toArrayBuffer
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.url.URL
import org.w3c.files.Blob
fun triggerDownloadFile(fullFileInfo: FullFileInfo) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
val url = URL.createObjectURL(Blob(arrayOf(fullFileInfo.byteArrayAllocator().toArrayBuffer())))
hiddenElement.href = url
hiddenElement.target = "_blank"
hiddenElement.download = fullFileInfo.name.name
hiddenElement.click()
}

@ -0,0 +1,134 @@
package dev.inmo.postssystem.client.utils
import kotlinx.browser.document
import kotlinx.dom.clear
import kotlinx.html.dom.append
import kotlinx.html.id
import kotlinx.html.js.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
object HTMLViewsConstants {
val mainContainer: MainHTMLViewContainer = MainHTMLViewContainer
val processesContainer: DrawerHTMLViewContainer = DrawerHTMLViewContainer
val toolsContainerId: ToolsHTMLViewContainer = ToolsHTMLViewContainer
}
sealed interface HTMLViewContainer {
val htmlElement: HTMLElement?
get() = document.getElementById(id) as? HTMLElement
val id: String
fun setIsLoading() {
htmlElement ?.apply {
clear()
append {
div("mdl-spinner mdl-js-spinner is-active")
}
}
}
companion object {
fun from(elementId: String) = when (elementId) {
MainHTMLViewContainer.id -> MainHTMLViewContainer
DrawerHTMLViewContainer.id -> DrawerHTMLViewContainer
ToolsHTMLViewContainer.id -> ToolsHTMLViewContainer
else -> null
}
}
}
object MainHTMLViewContainer : HTMLViewContainer {
override val id: String
get() = "main"
}
object DrawerHTMLViewContainer : HTMLViewContainer {
data class DrawerAddButtonInfo(
val text: String,
val onAddButtonClick: (Event) -> Unit,
)
override val id: String
get() = "drawer"
private val titleElement: HTMLElement?
get() = (document.getElementById("drawerTitle") ?:let {
htmlElement ?.append {
span("mdl-layout-title") {
id = "drawerTitle"
}
} ?.first()
}) as? HTMLElement
var title: String?
get() = titleElement ?.textContent
set(value) {
if (value == null) {
titleElement ?.classList ?.add("gone")
} else {
val element = titleElement ?: return
element.textContent = value
element.classList.remove("gone")
}
}
private val contentElement
get() = (document.getElementById("drawerContent") ?:let {
htmlElement ?.append {
nav("mdl-navigation") {
id = "drawerContent"
}
} ?.first()
}) as? HTMLElement
fun <T> setListContent(
title: String?,
data: Iterable<T>,
getText: (T) -> String?,
addButtonInfo: DrawerAddButtonInfo? = null,
onClick: (T) -> Unit
) {
this.title = title
val contentElement = contentElement ?: return
contentElement.clear()
contentElement.append {
fun hideDrawer() {
// Emulate clicking for hiding of drawer
(document.getElementsByClassName("mdl-layout__obfuscator").item(0) as? HTMLElement) ?.click()
}
data.forEach {
val elementTitle = getText(it) ?: return@forEach
div("mdl-navigation__link") {
+elementTitle
onClickFunction = { _ ->
onClick(it)
hideDrawer()
}
}
}
if (addButtonInfo != null) {
button(classes = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent") {
+addButtonInfo.text
onClickFunction = {
addButtonInfo.onAddButtonClick(it)
hideDrawer()
}
}
}
}
}
override fun setIsLoading() {
contentElement ?.apply {
clear()
append {
div("mdl-spinner mdl-js-spinner is-active")
}
}
}
}
object ToolsHTMLViewContainer : HTMLViewContainer {
override val id: String
get() = "tools"
}

@ -0,0 +1,41 @@
package dev.inmo.postssystem.client.utils
import dev.inmo.postssystem.features.files.common.FullFileInfo
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.files.FileReader
import org.w3c.files.get
fun uploadFileCallbackForHTMLInputChange(
output: MutableStateFlow<FullFileInfo?>,
scope: CoroutineScope
): (Event) -> Unit = {
(it.target as? HTMLInputElement) ?.apply {
files ?.also { files ->
files[0] ?.also { file ->
scope.launch {
val reader: FileReader = FileReader()
reader.onload = {
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
output.value = FullFileInfo(
FileName(file.name),
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
bytes.asAllocator
)
Unit
}
reader.readAsArrayBuffer(file)
}
}
}
}
}

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PostsSystem</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
<link rel="stylesheet" href="styles/material.min.css" type="text/css">
<link rel="stylesheet" href="styles/containers.css" type="text/css">
<link rel="stylesheet" href="styles/visibility.css" type="text/css">
</head>
<body>
<!-- Always shows a header, even in smaller screens. -->
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
<header class="mdl-layout__header">
<div class="mdl-layout__header-row">
<!-- Title -->
<span class="mdl-layout-title">Posts System</span>
<!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div>
<!-- Navigation. We hide it in small screens. -->
<nav id="tools" class="mdl-navigation mdl-layout--large-screen-only"></nav>
</div>
</header>
<main class="mdl-layout__content">
<div id="main" class="page-content"></div>
</main>
</div>
<script type="application/javascript" defer src="js/material.min.js"></script>
<script type="application/javascript" src="postssystem.client.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
.vertical_container {
display: flex;
flex-direction: column;
align-items: center;
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,3 @@
.gone {
display: none;
}

@ -0,0 +1,35 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.features.users.common.ReadUsersStorage
import dev.inmo.micro_utils.repos.pagination.getAll
import org.koin.core.context.startKoin
fun readLine(suggestionText: String): String {
while (true) {
println(suggestionText)
readLine() ?.let { return it }
}
}
suspend fun main(args: Array<String>) {
val serverUrl = readLine("Server url:")
val login = readLine("Username:")
val password = readLine("Password:")
val koin = startKoin {
// modules(getAuthorizedFeaturesDIModule(serverUrl, AuthCreds(Username(login), password)))
}.koin
while (true) {
val chosen = readLine(
"""
Choose action:
1. Show server users
""".trimIndent()
).toIntOrNull()
when (chosen) {
1 -> println(koin.get<ReadUsersStorage>().getAll { getByPagination(it) })
else -> println("Sorry, I didn't understand")
}
}
}

@ -0,0 +1 @@
<manifest package="dev.inmo.postssystem.client"/>