Compare commits

..

22 Commits

Author SHA1 Message Date
f20aa4a359 update dependencies 2022-07-08 23:53:35 +06:00
ff973e63fc start migration onto ktor-based serialization of data 2022-06-12 18:54:28 +06:00
399405a4fb update dependencies 2022-06-12 18:18:07 +06:00
b7e5e2745d update dependencies 2022-06-05 00:30:41 +06:00
92cd98b80f add client subfeature 2022-05-21 14:48:53 +06:00
b73df49925 fixes in auth 2022-05-20 00:15:49 +06:00
f835dc1557 update tgbotapi 2022-05-19 00:01:50 +06:00
5bbe0b8a0e fix of build 2022-05-18 23:41:07 +06:00
9216c013ec try to fix ktor update 2022-05-18 23:36:39 +06:00
c238f391a8 several small errors fix 2022-05-18 15:51:59 +06:00
dd369177c1 one more try to fix nosuchelement exception 2022-05-18 15:33:26 +06:00
6c094f3ad9 Изменил(а) на 'gradle/libs.versions.toml' 2022-05-16 04:30:58 +00:00
84aea630c3 update dependencies 2022-05-10 23:51:42 +06:00
55d0e1b55f a little updates 2022-05-07 21:54:19 +06:00
a3ff08af27 fixes according to updates 2022-05-07 20:48:17 +06:00
7551d7c2bb start migration onto new stack of dependencies versions (plus old changes) 2022-05-07 01:50:58 +06:00
b41a18a01e Update prepare client 2022-04-09 13:20:27 +00:00
a993459b97 update dependencies 2022-04-01 22:31:59 +06:00
37114f0ddb temporal potentially working variant (bot not building 2022-03-26 14:19:20 +06:00
51cdfb320b add posts list model 2022-03-26 11:53:53 +06:00
90293d7ffc add class for js repo of states and start adding of posts list mvvm 2022-03-26 10:19:11 +06:00
cb5de073fb almost complete rework of ui up to module ui 2022-03-23 23:21:02 +06:00
147 changed files with 1328 additions and 1138 deletions

View File

@ -1,12 +1,14 @@
## Структура проекта ## Структура проекта
* **Features** - набор **законченных** фич проекта. Считается, что любая фича, находящаяся в мастере может быть добавлена в * **Features** - набор **законченных** фич проекта. Считается, что любая фича, находящаяся в мастере может быть добавлена в
клиент и использована в нем. Исключением является `common` - это набор вещей, используемых везде. В подпунктах представлены клиент и использована в нем. Исключением является `common` - это набор вещей, используемых везде.
части, на которые *обычно* разделяется фича * Части, на которые *обычно* разделяется фича
* Common - общая для фичи часть. Тут, как правило, хранятся конвенции путей для сетевых соединений, общие типы и пр. * Common - общая для фичи часть. Тут, как правило, хранятся конвенции путей для сетевых соединений, общие типы и пр.
* Server - часть, включаемая в сервер для подключения фичи. Обычно содержит работу с бд, определение модулей сервера и пр. * Server - часть, включаемая в сервер для подключения фичи. Обычно содержит работу с бд, определение модулей сервера и пр.
* Client - часть с клиентским кодом. В большинстве своём включает работу с сервером, MVVM часть (View при этом должны * Client - часть с клиентским кодом. В большинстве своём включает работу с сервером, MVVM часть (View при этом должны
находиться в платформенной части, если их нельзя вынести в сommon часть клиента) находиться в платформенной части, если их нельзя вынести в сommon часть клиента)
* Также существует фича `client`, которая не является фичей самой по-себе. Фактически, это набор разных фич, который системно
отсутствуют в бэке и используются в основном для пробрасывания удобных `API` и `View` для клиента
* **Services** - модули, отвечающие за клиент-серверную работу фич с точки зрения их взаимодействия. Например, в рамках сервисов * **Services** - модули, отвечающие за клиент-серверную работу фич с точки зрения их взаимодействия. Например, в рамках сервисов
должен быть добавлен модуль для постов - именно через сервисы будет происходить создание поста, его редактирование и удаление должен быть добавлен модуль для постов - именно через сервисы будет происходить создание поста, его редактирование и удаление
* **Client** - итоговый клиент. На момент написания этой доки (`Пн окт 25 12:56:41 +06 2021`) предполагается два варианта: * **Client** - итоговый клиент. На момент написания этой доки (`Пн окт 25 12:56:41 +06 2021`) предполагается два варианта:

View File

@ -4,14 +4,16 @@ buildscript {
mavenCentral() mavenCentral()
mavenLocal() mavenLocal()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.4' classpath libs.buildscript.kt.gradle
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath libs.buildscript.kt.serialization
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath libs.buildscript.jb.dokka
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" classpath libs.buildscript.gh.release
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath libs.buildscript.android.gradle
classpath libs.buildscript.android.dexcount
} }
} }
@ -20,6 +22,7 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
} }
} }

View File

@ -32,9 +32,6 @@ kotlin {
api project(":postssystem.features.content.binary.client") api project(":postssystem.features.content.binary.client")
api project(":postssystem.services.posts.client") api project(":postssystem.services.posts.client")
api libs.microutils.fsm.common
api libs.microutils.fsm.repos.common
api libs.microutils.crypto api libs.microutils.crypto
implementation compose.runtime implementation compose.runtime
@ -43,7 +40,7 @@ kotlin {
jvmMain { jvmMain {
dependencies { dependencies {
api "io.ktor:ktor-client-apache:$ktor_version" api libs.ktor.client.apache
} }
} }

View File

@ -1,148 +0,0 @@
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.ClientReadFilesStorage
import dev.inmo.postssystem.features.files.common.storage.ReadFilesStorage
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage
import dev.inmo.postssystem.features.roles.client.ClientRolesStorage
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.repos.KeyValueRepo
import dev.inmo.postssystem.client.settings.DefaultSettings
import dev.inmo.postssystem.client.settings.Settings
import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings
import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.publicators.simple.client.SimplePublicatorService
import dev.inmo.postssystem.publicators.simple.client.SimplePublicatorServiceClient
import dev.inmo.postssystem.services.posts.client.ClientPostsService
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
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.binds
import org.koin.dsl.module
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
val SettingsQualifier = StringQualifier("Settings")
val RolesQualifier = StringQualifier("Roles")
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 {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) }
single {
Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
}
}
single<StringFormat> { get<Json>() }
single(SettingsQualifier) { settingsFactory() }
single { 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
)) } }
single<AuthSettings> { DefaultAuthSettings(get(SettingsQualifier), get(), koin, get()) }
single<Settings> { DefaultSettings(get()) }
AdditionalModules.modules.forEach {
it.apply { load() }
}
}
)
}.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> {
Cbor {
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
}
}
single { UnifiedRequester(get(), get()) }
single { StatusFeatureClient(get(serverUrlQualifier), get()) }
single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) }
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) }
single<PostsService> { ClientPostsService(get(serverUrlQualifier), get()) } binds arrayOf(
ReadPostsService::class,
WritePostsService::class
)
single<SimplePublicatorService> { SimplePublicatorServiceClient(get(serverUrlQualifier), get()) }
}
}

View File

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

View File

@ -1,12 +0,0 @@
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
}

View File

@ -1,27 +0,0 @@
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
}
}

View File

@ -1,30 +0,0 @@
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? = CreatePostUIFSMState(),
override val context: String = "main"
) : UIFSMState
val DefaultAuthUIFSMState = AuthUIFSMState()
@Serializable
data class CreatePostUIFSMState(
override val from: UIFSMState? = null,
override val context: String = "main"
) : UIFSMState

View File

@ -1,31 +1,24 @@
package dev.inmo.postssystem.client 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.postssystem.features.auth.common.AuthTokenInfo
import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler
import dev.inmo.micro_utils.fsm.common.CheckableHandlerHolder
import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.micro_utils.repos.mappers.withMapper import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import dev.inmo.postssystem.client.settings.auth.AuthSettings import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.common.common.baseKoin
import dev.inmo.postssystem.features.common.common.getAllDistinct import dev.inmo.postssystem.features.common.common.getAllDistinct
import dev.inmo.postssystem.services.posts.client.ui.create.* import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.services.posts.client.ui.list.PostsListUIFSMState
import dev.inmo.postssystem.services.posts.client.ui.list.PostsListUIState
import kotlinx.browser.* import kotlinx.browser.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.context.loadKoinModules import org.koin.core.context.loadKoinModules
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.Qualifier
import org.koin.dsl.module import org.koin.dsl.module
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import kotlin.reflect.KClass
val defaultTypedSerializer = TypedSerializer<Any>( val defaultTypedSerializer = TypedSerializer<Any>(
"AuthTokenInfo" to AuthTokenInfo.serializer(), "AuthTokenInfo" to AuthTokenInfo.serializer(),
@ -35,9 +28,11 @@ val defaultTypedSerializer = TypedSerializer<Any>(
"Short" to Short.serializer(), "Short" to Short.serializer(),
"Byte" to Byte.serializer(), "Byte" to Byte.serializer(),
"Float" to Float.serializer(), "Float" to Float.serializer(),
"Double" to Double.serializer(), "Double" to Double.serializer()
"UIFSMState" to UIFSMStateSerializer
) )
val defaultSerialFormat = Json {
ignoreUnknownKeys = true
}
fun baseKoin(): Koin { fun baseKoin(): Koin {
val anyToString: suspend Any.() -> String = { val anyToString: suspend Any.() -> String = {
@ -78,69 +73,22 @@ fun baseKoin(): Koin {
) )
}, },
{ {
JSUIFSMStatesRepo(window.history) JSUIFSMStatesRepo(window.history, AuthUIFSMState(PostsListUIFSMState()), getAllDistinct())
} }
) { ) {
val scope = first
first.apply { first.apply {
second.apply { second.apply {
loadKoinModules( loadKoinModules(
module { module {
factory { document.getElementById("main") as HTMLElement } factory { document.getElementById("main") as HTMLElement }
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
factory { AuthUIViewModel(get()) }
factory { AuthView(get(), get(UIScopeQualifier)) }
factory<PostCreateUIModel> { DefaultPostCreateUIModel(get(), get()) }
factory { PostCreateUIViewModel(get()) }
factory { PostCreateView(get(), getAllDistinct(), get(UIScopeQualifier)) }
} }
) )
strictlyOn<AuthUIFSMState>(get<AuthView>()) getAllDistinct<UIFSMHandler.Registrator>().forEach {
with(it) {
// Костыль, в JS на момент Пн дек 6 14:19:29 +06 2021 если использовать strictlyOn генерируются include()
// некорректные безымянные классы (у них отсутствует метод handleState)
class DefaultStateHandlerWrapper<T : UIFSMState>(
private val klass: KClass<out UIFSMHandler<T>>,
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 = get<AuthSettings>()
authSettings.loadingJob.join()
if (authSettings.authorizedDIModule.value == null) {
error("Can't perform state $state: Auth module was not initialized")
} else {
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> registerHandler(
klass: KClass<out UIFSMHandler<T>>,
qualifier: Qualifier? = null,
parameters: ((T) -> ParametersHolder)? = null
) = add(
DefaultStateHandlerWrapper<T>(
klass,
T::class,
qualifier,
parameters
)
)
registerHandler(PostCreateView::class)
} }
} }
} }

View File

@ -1,20 +1,20 @@
package dev.inmo.postssystem.client package dev.inmo.postssystem.client
import dev.inmo.postssystem.client.ui.fsm.*
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.serialization.StringFormat
import org.w3c.dom.* import org.w3c.dom.*
import org.w3c.dom.url.URL import org.w3c.dom.url.URL
private fun History.refreshHistory( private fun History.refreshHistory(
states: Iterable<UIFSMState>, states: Iterable<UIFSMState>,
fillers: List<UIFSMStateSearchParamsHandler>
) { ) {
val currentUrl = window.location.pathname val currentUrl = window.location.pathname
val params = states.mapNotNull<UIFSMState, Pair<String, String>> { val currentParams = (URL(window.location.href)).searchParams
when (it) { val params = states.flatMap<UIFSMState, Pair<String, String>> { state ->
is AuthUIFSMState -> null fillers.flatMap {
is CreatePostUIFSMState -> null it.takeParams(state, currentParams)
} }
} }
pushState( pushState(
@ -24,9 +24,11 @@ private fun History.refreshHistory(
) )
} }
private fun takeStates(initialState: UIFSMState): List<UIFSMState> { private fun takeStates(initialState: UIFSMState, fillers: List<UIFSMStateSearchParamsHandler>): List<UIFSMState> {
val params = (URL(window.location.href)).searchParams val params = (URL(window.location.href)).searchParams
val additionalStates = listOfNotNull<UIFSMState>() val additionalStates = fillers.mapNotNull {
it.takeState(params)
}
return additionalStates + listOfNotNull( return additionalStates + listOfNotNull(
if (additionalStates.isEmpty()) { if (additionalStates.isEmpty()) {
@ -41,12 +43,13 @@ private fun takeStates(initialState: UIFSMState): List<UIFSMState> {
class JSUIFSMStatesRepo( class JSUIFSMStatesRepo(
private val history: History, private val history: History,
private val initialState: UIFSMState = DefaultAuthUIFSMState private val initialState: UIFSMState,
private val fillers: List<UIFSMStateSearchParamsHandler>
) : DefaultStatesManagerRepo<UIFSMState> { ) : DefaultStatesManagerRepo<UIFSMState> {
private val statesMap = mutableMapOf<String, UIFSMState>() private val statesMap = mutableMapOf<String, UIFSMState>()
init { init {
val states = takeStates(initialState) val states = takeStates(initialState, fillers)
states.forEach { states.forEach {
statesMap[it.context] = it statesMap[it.context] = it
} }
@ -68,12 +71,12 @@ class JSUIFSMStatesRepo(
override suspend fun removeState(state: UIFSMState) { override suspend fun removeState(state: UIFSMState) {
statesMap.remove((state.context as? String) ?: return) statesMap.remove((state.context as? String) ?: return)
history.refreshHistory(statesMap.values) history.refreshHistory(statesMap.values, fillers)
} }
override suspend fun set(state: UIFSMState) { override suspend fun set(state: UIFSMState) {
console.log(state) console.log(state)
statesMap[state.context] = state statesMap[state.context] = state
history.refreshHistory(statesMap.values) history.refreshHistory(statesMap.values, fillers)
} }
} }

View File

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

View File

@ -0,0 +1,9 @@
package dev.inmo.postssystem.client
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
import org.w3c.dom.url.URLSearchParams
interface UIFSMStateSearchParamsHandler {
fun takeParams(state: UIFSMState, currentParams: URLSearchParams): List<Pair<String, String>>
fun takeState(params: URLSearchParams): UIFSMState?
}

View File

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

View File

@ -1,19 +0,0 @@
package dev.inmo.postssystem.client.utils
import dev.inmo.postssystem.features.files.common.FullFileInfo
import dev.inmo.micro_utils.common.toArrayBuffer
import io.ktor.utils.io.core.readBytes
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.inputProvider().readBytes().toArrayBuffer())))
hiddenElement.href = url
hiddenElement.target = "_blank"
hiddenElement.download = fullFileInfo.name.name
hiddenElement.click()
}

View File

@ -1,25 +0,0 @@
package dev.inmo.postssystem.client.utils
import androidx.compose.runtime.*
import kotlinx.coroutines.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
import kotlin.coroutines.CoroutineContext
fun Composition.linkWithJob(job: Job) {
job.invokeOnCompletion {
this@linkWithJob.dispose()
}
}
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
suspend fun <TElement : Element> renderComposableAndLinkToContext(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(
currentCoroutineContext()
)
}

View File

@ -1,77 +0,0 @@
package dev.inmo.postssystem.client.utils
import androidx.compose.runtime.MutableState
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 dev.inmo.postssystem.features.common.common.BytesBasedInputProvider
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.coroutines.flow.MutableStateFlow
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(
onSet: (FullFileInfo) -> Unit
): (Event) -> Unit = {
(it.target as? HTMLInputElement) ?.apply {
files ?.also { files ->
files[0] ?.also { file ->
val reader: FileReader = FileReader()
reader.onload = {
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
onSet(
FullFileInfo(
FileName(file.name),
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
BytesBasedInputProvider(bytes)
)
)
}
reader.readAsArrayBuffer(file)
}
}
}
}
fun fileCallbackForHTMLInputChange(
onSet: (MPPFile) -> Unit
): (Event) -> Unit = {
(it.target as? HTMLInputElement) ?.apply {
files ?.also { files ->
files[0] ?.also { file ->
onSet(file)
}
}
}
}
fun uploadFileCallbackForHTMLInputChange(
output: MutableState<FullFileInfo?>
): (Event) -> Unit = uploadFileCallbackForHTMLInputChange {
output.value = it
}
fun uploadFileCallbackForHTMLInputChange(
output: MutableStateFlow<FullFileInfo?>
): (Event) -> Unit = uploadFileCallbackForHTMLInputChange {
output.value = it
}
fun fileCallbackForHTMLInputChange(
output: MutableState<MPPFile?>
): (Event) -> Unit = fileCallbackForHTMLInputChange {
output.value = it
}
fun fileCallbackForHTMLInputChange(
output: MutableStateFlow<MPPFile?>
): (Event) -> Unit = fileCallbackForHTMLInputChange {
output.value = it
}

View File

@ -26,12 +26,12 @@ android {
} }
} }
compileSdkVersion "$android_compileSdkVersion".toInteger() compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
buildToolsVersion "$android_buildToolsVersion" buildToolsVersion libs.versions.android.props.buildTools.get()
defaultConfig { defaultConfig {
minSdkVersion "$android_minSdkVersion".toInteger() minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
targetSdkVersion "$android_compileSdkVersion".toInteger() targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger() versionCode "${android_code_version}".toInteger()
versionName "$version" versionName "$version"
} }

View File

@ -13,6 +13,21 @@ allprojects {
projectByName(name) projectByName(name)
} }
allTargetsConfiguration = { ->
kotlin {
targets.all {
compilations.all {
kotlinOptions {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
]
}
}
}
}
}
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppJsProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsProject.gradle" mppJsProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsProject.gradle"
@ -20,6 +35,6 @@ allprojects {
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle" defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle" publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
} }
} }

View File

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

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

@ -5,11 +5,11 @@ import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import io.ktor.client.HttpClientConfig import io.ktor.client.HttpClientConfig
import io.ktor.client.features.cookies.* import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage
import io.ktor.client.features.expectSuccess import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.HttpReceivePipeline import io.ktor.client.statement.*
import io.ktor.client.statement.HttpResponse
import io.ktor.http.* import io.ktor.http.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -25,10 +25,6 @@ fun HttpClientConfig<*>.installClientAuthenticator(
onUserRetrieved: suspend (User?) -> Unit, onUserRetrieved: suspend (User?) -> Unit,
onAuthKeyInvalidated: suspend () -> Unit onAuthKeyInvalidated: suspend () -> Unit
) { ) {
// install(Logging) {
// logger = Logger.DEFAULT
// level = LogLevel.HEADERS
// }
install(HttpCookies) { install(HttpCookies) {
// Will keep an in-memory map with all the cookies from previous requests. // Will keep an in-memory map with all the cookies from previous requests.
storage = AcceptAllCookiesStorage() storage = AcceptAllCookiesStorage()
@ -98,13 +94,13 @@ fun HttpClientConfig<*>.installClientAuthenticator(
receivePipeline.intercept(HttpReceivePipeline.Before) { receivePipeline.intercept(HttpReceivePipeline.Before) {
if ( if (
context.request.url.toString().startsWith(baseUrl) it.request.url.toString().startsWith(baseUrl)
&& context.response.status == HttpStatusCode.Unauthorized && it.status == HttpStatusCode.Unauthorized
) { ) {
authMutex.withLock { refreshToken() } authMutex.withLock { refreshToken() }
val newResponse = context.client ?.request<HttpResponse>{ val newResponse = it.call.client.request {
takeFrom(context.request) takeFrom(it.request)
} ?: return@intercept }
proceedWith(newResponse) proceedWith(newResponse)
} }
} }

View File

@ -0,0 +1,28 @@
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
import kotlin.js.JsExport
internal fun CommonAuthModuleLoader() = 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

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

View File

@ -1,19 +1,16 @@
package dev.inmo.postssystem.client.settings.auth package dev.inmo.postssystem.features.auth.client.settings
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.AuthUnavailableException
import dev.inmo.postssystem.features.auth.client.ui.* import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.postssystem.features.auth.common.* 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.status.client.StatusFeatureClient
import dev.inmo.postssystem.features.users.common.User 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.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.micro_utils.repos.*
import dev.inmo.postssystem.features.auth.client.createAuthorizedFeaturesDIModule
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError.AuthIncorrect
import dev.inmo.postssystem.features.common.common.DBDropper
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koin.core.Koin import org.koin.core.Koin
@ -29,8 +26,6 @@ data class DefaultAuthSettings(
override val authorizedDIModule: StateFlow<Module?> = _authorizedDIModule.asStateFlow() override val authorizedDIModule: StateFlow<Module?> = _authorizedDIModule.asStateFlow()
private val _user = MutableStateFlow<User?>(null) private val _user = MutableStateFlow<User?>(null)
override val user: StateFlow<User?> = _user.asStateFlow() 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 getCurrentServerURL() = repo.get(SERVER_URL_FIELD) as? String
private suspend fun getCurrentUsername() = repo.get(USERNAME_FIELD) as? String private suspend fun getCurrentUsername() = repo.get(USERNAME_FIELD) as? String
@ -42,18 +37,6 @@ data class DefaultAuthSettings(
updateModule(serverUrl, token.either()) 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? { override suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError? {
return runCatching { return runCatching {
if (getCurrentServerURL() != serverUrl || getCurrentUsername() != creds.username.string) { if (getCurrentServerURL() != serverUrl || getCurrentUsername() != creds.username.string) {
@ -73,7 +56,7 @@ data class DefaultAuthSettings(
initialAuthKey: Either<AuthKey, AuthTokenInfo>, initialAuthKey: Either<AuthKey, AuthTokenInfo>,
): AuthUIError? { ): AuthUIError? {
val currentModule = authorizedDIModule.value val currentModule = authorizedDIModule.value
val newModule = getAuthorizedFeaturesDIModule( val newModule = createAuthorizedFeaturesDIModule(
serverUrl, serverUrl,
initialAuthKey, initialAuthKey,
{ {
@ -101,8 +84,8 @@ data class DefaultAuthSettings(
currentModule ?.let { koin.loadModules(listOf(currentModule)) } currentModule ?.let { koin.loadModules(listOf(currentModule)) }
} }
return when { return when {
!serverAvailable -> ServerUnavailableAuthUIError !serverAvailable -> AuthUIError.ServerUnavailable
!authCorrect -> AuthIncorrectAuthUIError !authCorrect -> AuthIncorrect
else -> { else -> {
_authorizedDIModule.value = newModule _authorizedDIModule.value = newModule
null null

View File

@ -0,0 +1,8 @@
package dev.inmo.postssystem.features.auth.client.ui
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
data class AuthUIFSMState(
override val from: UIFSMState?,
override val context: String = "main"
) : UIFSMState

View File

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

View File

@ -3,18 +3,23 @@ package dev.inmo.postssystem.features.auth.client.ui
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
sealed class AuthUIError sealed interface AuthUIError {
@Serializable // @Serializable
object ServerUnavailableAuthUIError : AuthUIError() object ServerUnavailable : AuthUIError
@Serializable // @Serializable
object AuthIncorrectAuthUIError : AuthUIError() object AuthIncorrect : AuthUIError
}
@Serializable @Serializable
sealed class AuthUIState sealed interface AuthUIState {
@Serializable @Serializable
data class InitAuthUIState(val showError: AuthUIError? = null) : AuthUIState() data class Init(val showError: AuthUIError? = null) : AuthUIState
val DefaultInitAuthUIState = InitAuthUIState() // @Serializable
@Serializable object Loading : AuthUIState
object LoadingAuthUIState : AuthUIState() // @Serializable
@Serializable object Authorized : AuthUIState
object AuthorizedAuthUIState : AuthUIState()
companion object {
val DefaultInit = Init()
}
}

View File

@ -1,7 +1,7 @@
package dev.inmo.postssystem.features.auth.client.ui package dev.inmo.postssystem.features.auth.client.ui
import dev.inmo.postssystem.features.auth.common.AuthCreds 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 dev.inmo.postssystem.features.users.common.Username
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View File

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

View File

@ -1,28 +1,41 @@
package dev.inmo.postssystem.client.fsm.ui package dev.inmo.postssystem.features.auth.client
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import dev.inmo.jsuikit.elements.* import dev.inmo.jsuikit.elements.*
import dev.inmo.jsuikit.modifiers.* import dev.inmo.jsuikit.modifiers.*
import dev.inmo.jsuikit.utils.Attrs import dev.inmo.jsuikit.utils.Attrs
import dev.inmo.postssystem.client.ui.fsm.* import dev.inmo.micro_utils.coroutines.compose.renderComposableAndLinkToContextAndRoot
import dev.inmo.postssystem.features.auth.client.ui.* import dev.inmo.postssystem.features.auth.client.ui.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.StatesMachine import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext import dev.inmo.postssystem.features.auth.client.ui.AuthUIError.AuthIncorrect
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.browser.document
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.dom.* import kotlinx.dom.*
import org.jetbrains.compose.web.attributes.InputType import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.* import org.w3c.dom.*
@ExperimentalStdlibApi
@EagerInitialization
val loader = DefaultModuleLoader {
factory { AuthView(get(), get(DefaultQualifiers.UIScopeQualifier), getAllDistinct()) }
singleWithRandomQualifier {
UIFSMHandler.Registrator {
strictlyOn(get<AuthView>())
}
}
} + CommonAuthModuleLoader()
class AuthView( class AuthView(
private val viewModel: AuthUIViewModel, private val viewModel: AuthUIViewModel,
private val uiScope: CoroutineScope private val uiScope: CoroutineScope,
) : JSView<AuthUIFSMState>() { defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
) : JSView<AuthUIFSMState>(defaultExceptionsHandlers) {
override suspend fun StatesMachine<in UIFSMState>.safeHandleState( override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement, htmlElement: HTMLElement,
@ -36,7 +49,7 @@ class AuthView(
val errorText = mutableStateOf<String?>(null) val errorText = mutableStateOf<String?>(null)
val root = htmlElement.appendElement("div") {} val root = htmlElement.appendElement("div") {}
val composition = renderComposableAndLinkToContext(root) { val composition = renderComposableAndLinkToContextAndRoot(root) {
val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank() val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank()
Flex( Flex(
@ -54,13 +67,13 @@ class AuthView(
} }
} }
TextField( StandardInput(
InputType.Text, InputType.Text,
usernameState, usernameState,
disabled, disabled,
"Username", "Username",
) )
TextField( StandardInput(
InputType.Password, InputType.Password,
passwordState, passwordState,
disabled, disabled,
@ -80,23 +93,23 @@ class AuthView(
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) { val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) { when (it) {
is InitAuthUIState -> { is AuthUIState.Init -> {
disabled.value = false disabled.value = false
errorText.value = when (it.showError) { errorText.value = when (it.showError) {
ServerUnavailableAuthUIError -> "Server unavailable" AuthUIError.ServerUnavailable -> "Server unavailable"
AuthIncorrectAuthUIError -> { AuthIncorrect -> {
passwordState.value = "" passwordState.value = ""
"Username or password is incorrect" "Username or password is incorrect"
} }
null -> null null -> null
} }
} }
LoadingAuthUIState -> { AuthUIState.Loading -> {
disabled.value = true disabled.value = true
errorText.value = null errorText.value = null
} }
AuthorizedAuthUIState -> { AuthUIState.Authorized -> {
completion.complete(state.from) completion.complete(state.from)
} }
} }

View File

@ -3,10 +3,20 @@ package dev.inmo.postssystem.features.auth.common
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import dev.inmo.postssystem.features.users.common.Username import dev.inmo.postssystem.features.users.common.Username
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
sealed interface AuthKey sealed interface AuthKey
@Serializable
private data class AuthKeySurrogate(
val authCreds: AuthCreds?,
val token: AuthToken?,
val refreshToken: RefreshToken?
)
@Serializable @Serializable
@SerialName("authcreds") @SerialName("authcreds")
data class AuthCreds( data class AuthCreds(

View File

@ -2,17 +2,18 @@ package dev.inmo.postssystem.features.auth.server
import dev.inmo.postssystem.features.auth.common.* import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService
import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator import dev.inmo.postssystem.features.common.server.ApplicationAuthenticationConfigurator
import dev.inmo.postssystem.features.users.common.User import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.coroutines.safely import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.server.* import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.* import dev.inmo.micro_utils.ktor.server.configurators.*
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.response.respond import io.ktor.server.application.call
import io.ktor.routing.* import io.ktor.server.auth.*
import io.ktor.sessions.* import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.*
import io.ktor.server.sessions.sessions
import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.nullable
data class AuthUserPrincipal( data class AuthUserPrincipal(
@ -24,90 +25,78 @@ fun User.principal() = AuthUserPrincipal(this)
class AuthenticationRoutingConfigurator( class AuthenticationRoutingConfigurator(
private val authFeature: AuthFeature, private val authFeature: AuthFeature,
private val authTokensService: AuthTokensService, private val authTokensService: AuthTokensService
private val unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element, ApplicationAuthenticationConfigurator.Element { ) : ApplicationRoutingConfigurator.Element, ApplicationAuthenticationConfigurator.Element {
override fun Route.invoke() { override fun Route.invoke() {
unifiedRouter.apply { route(authRootPathPart) {
route(authRootPathPart) { post(authAuthPathPart) {
post(authAuthPathPart) { safely(
safely( {
{ // TODO:: add error info
// TODO:: add error info it.printStackTrace()
it.printStackTrace() call.respond(
call.respond( HttpStatusCode.InternalServerError,
HttpStatusCode.InternalServerError, "Something went wrong"
"Something went wrong"
)
}
) {
val creds = uniload(AuthCreds.serializer())
val tokenInfo = authFeature.auth(creds)
if (tokenInfo == null) {
if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
unianswer(
AuthTokenInfo.serializer().nullable,
tokenInfo
)
}
}
}
post(authRefreshPathPart) {
safely(
{
// TODO:: add error info
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
val refreshToken = uniload(RefreshToken.serializer())
val tokenInfo = authFeature.refresh(refreshToken)
if (tokenInfo == null) {
if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
unianswer(
AuthTokenInfo.serializer().nullable,
tokenInfo
)
}
}
}
post(authGetMePathPart) {
safely(
{
// TODO:: add error info
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
unianswer(
User.serializer().nullable,
authFeature.getMe(
uniload(AuthToken.serializer())
)
) )
} }
) {
val creds = call.receive<AuthCreds>()
val tokenInfo = authFeature.auth(creds)
if (tokenInfo == null) {
if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
call.respond(tokenInfo)
}
}
}
post (authRefreshPathPart) {
safely(
{
// TODO:: add error info
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
val refreshToken = call.receive<RefreshToken>()
val tokenInfo = authFeature.refresh(refreshToken)
if (tokenInfo == null) {
if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
call.respond(tokenInfo)
}
}
}
post(authGetMePathPart) {
safely(
{
// TODO:: add error info
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
call.respond(
authFeature.getMe(call.receive()) ?: HttpStatusCode.NoContent
)
} }
} }
} }
} }
override fun Authentication.Configuration.invoke() { override fun AuthenticationConfig.invoke() {
session<AuthToken> { session<AuthToken> {
validate { validate {
val result = authTokensService.getUserPrincipal(it) val result = authTokensService.getUserPrincipal(it)

View File

@ -4,14 +4,14 @@ import dev.inmo.postssystem.features.auth.common.AuthToken
import dev.inmo.postssystem.features.common.common.Milliseconds import dev.inmo.postssystem.features.common.common.Milliseconds
import dev.inmo.postssystem.features.auth.common.tokenSessionKey import dev.inmo.postssystem.features.auth.common.tokenSessionKey
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationSessionsConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationSessionsConfigurator
import io.ktor.sessions.* import io.ktor.server.sessions.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class SessionAuthenticationConfigurator( class SessionAuthenticationConfigurator(
private val maxAge: Milliseconds private val maxAge: Milliseconds
) : ApplicationSessionsConfigurator.Element { ) : ApplicationSessionsConfigurator.Element {
private val maxAgeInSeconds = TimeUnit.MILLISECONDS.toSeconds(maxAge) private val maxAgeInSeconds = TimeUnit.MILLISECONDS.toSeconds(maxAge)
override fun Sessions.Configuration.invoke() { override fun SessionsConfig.invoke() {
cookie<AuthToken>(tokenSessionKey) { cookie<AuthToken>(tokenSessionKey) {
cookie.maxAgeInSeconds = maxAgeInSeconds cookie.maxAgeInSeconds = maxAgeInSeconds
serializer = object : SessionSerializer<AuthToken> { serializer = object : SessionSerializer<AuthToken> {

View File

@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":postssystem.features.client.template.common")
api project(":postssystem.features.common.client")
}
}
}
}

View File

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

View File

@ -0,0 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":postssystem.features.common.common")
}
}
}
}

View File

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

View File

@ -0,0 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppJavaProjectPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":postssystem.features.client.template.common")
api project(":postssystem.features.common.server")
}
}
}
}

View File

@ -13,8 +13,13 @@ kotlin {
dependencies { dependencies {
api project(":postssystem.features.common.common") api project(":postssystem.features.common.common")
api libs.microutils.repos.ktor.client api libs.microutils.repos.ktor.client
api "io.ktor:ktor-client-auth:$ktor_version" api libs.ktor.client.auth
api "io.ktor:ktor-client-logging:$ktor_version" api libs.ktor.client.logging
api libs.microutils.common.compose
api libs.microutils.coroutines.compose
api libs.microutils.fsm.common
api libs.microutils.fsm.repos.common
api compose.runtime api compose.runtime
} }

View File

@ -1,6 +1,6 @@
package dev.inmo.postssystem.features.common.common package dev.inmo.postssystem.features.common.common
object AdditionalModules { class AdditionalModules {
private val additionalModules = mutableListOf<ModuleLoader>() private val additionalModules = mutableListOf<ModuleLoader>()
val modules: List<ModuleLoader> val modules: List<ModuleLoader>
get() = additionalModules.toList() get() = additionalModules.toList()
@ -8,4 +8,8 @@ object AdditionalModules {
fun addModule(moduleLoader: ModuleLoader): Boolean { fun addModule(moduleLoader: ModuleLoader): Boolean {
return additionalModules.add(moduleLoader) return additionalModules.add(moduleLoader)
} }
companion object {
val Default = AdditionalModules()
}
} }

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.client package dev.inmo.postssystem.features.common.common
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo

View File

@ -0,0 +1,71 @@
package dev.inmo.postssystem.features.common.common
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.repos.KeyValueRepo
import dev.inmo.postssystem.features.common.common.ui.fsm.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.koin.core.Koin
import org.koin.core.context.startKoin
import org.koin.core.scope.Scope
import org.koin.dsl.module
/**
* 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 {
Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
}
}
single<StringFormat> { get<Json>() }
single {
Cbor { serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } } }
}
single<BinaryFormat> { get<Cbor>() }
single(DefaultQualifiers.SettingsQualifier) { settingsFactory() }
single { DBDropper(get(DefaultQualifiers.SettingsQualifier)) }
single(DefaultQualifiers.FSMHandlersBuilderQualifier) { handlersSetter }
single { repoFactory() }
single { defaultScope }
single(DefaultQualifiers.UIScopeQualifier) { get<CoroutineScope>().LinkedSupervisorScope(Dispatchers.Main) }
single<StatesMachine<UIFSMState>>(DefaultQualifiers.UIFSMQualifier) {
UIFSM(get()) {
(this@single to this@UIFSM).apply(get(DefaultQualifiers.FSMHandlersBuilderQualifier))
}
}
} + AdditionalModules.Default.modules.map {
module {
with(it) {
load()
}
}
}
)
}.koin.apply {
loadModules(
listOf(
module { single<Koin> { this@apply } }
)
)
}

View File

@ -0,0 +1,10 @@
package dev.inmo.postssystem.features.common.common
import org.koin.core.qualifier.StringQualifier
object DefaultQualifiers {
val SettingsQualifier = StringQualifier("Settings")
val FSMHandlersBuilderQualifier = StringQualifier("FSMHandlersBuilder")
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
val UIFSMQualifier = StringQualifier("UIFSM")
}

View File

@ -2,6 +2,27 @@ package dev.inmo.postssystem.features.common.common
import org.koin.core.module.Module import org.koin.core.module.Module
fun interface ModuleLoader { interface ModuleLoader {
fun Module.load() fun Module.load()
class ByCallback(private val loadingBlock: Module.() -> Unit) : ModuleLoader {
override fun Module.load() {
loadingBlock()
}
}
}
operator fun ModuleLoader.plus(other: ModuleLoader) = ModuleLoader.ByCallback {
with(this@plus) {
load()
}
with (other) {
load()
}
}
fun DefaultModuleLoader(loadingBlock: Module.() -> Unit): ModuleLoader.ByCallback {
val newModuleLoader = ModuleLoader.ByCallback(loadingBlock)
AdditionalModules.Default.addModule(newModuleLoader)
return newModuleLoader
} }

View File

@ -1,4 +0,0 @@
package dev.inmo.postssystem.features.common.common
interface UIView {
}

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.features.common.common package dev.inmo.postssystem.features.common.common.ui
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.features.common.common package dev.inmo.postssystem.features.common.common.ui
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow

View File

@ -0,0 +1,4 @@
package dev.inmo.postssystem.features.common.common.ui
interface UIView {
}

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.features.common.common package dev.inmo.postssystem.features.common.common.ui
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -7,9 +7,7 @@ interface UIViewModel<StateType> {
val currentState: StateFlow<StateType> val currentState: StateFlow<StateType>
} }
abstract class AbstractUIViewModel<StateType> : UIViewModel<StateType> { abstract class AbstractUIViewModel<StateType>(initState: StateType) : UIViewModel<StateType> {
protected val _currentState = DefaultMVVMStateFlow(initState()) protected val _currentState = DefaultMVVMStateFlow(initState)
override val currentState: StateFlow<StateType> = _currentState.asStateFlow() override val currentState: StateFlow<StateType> = _currentState.asStateFlow()
abstract fun initState(): StateType
} }

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.client.ui.fsm package dev.inmo.postssystem.features.common.common.ui.fsm
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder 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.dsl.buildFSM
@ -6,8 +6,6 @@ import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import org.koin.core.qualifier.StringQualifier import org.koin.core.qualifier.StringQualifier
val UIFSMQualifier = StringQualifier("UIFSM")
fun UIFSM( fun UIFSM(
repo: DefaultStatesManagerRepo<UIFSMState>, repo: DefaultStatesManagerRepo<UIFSMState>,
handlersSetter: FSMBuilder<UIFSMState>.() -> Unit handlersSetter: FSMBuilder<UIFSMState>.() -> Unit

View File

@ -0,0 +1,7 @@
package dev.inmo.postssystem.features.common.common.ui.fsm
import dev.inmo.micro_utils.common.Optional
fun interface UIFSMExceptionHandler {
suspend fun handle(currentState: UIFSMState, exception: Throwable): Optional<UIFSMState?>
}

View File

@ -0,0 +1,34 @@
package dev.inmo.postssystem.features.common.common.ui.fsm
import dev.inmo.micro_utils.common.onPresented
import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
import org.koin.core.scope.Scope
interface UIFSMHandler<T : UIFSMState> : StatesHandler<T, UIFSMState> {
fun interface Registrator {
fun FSMBuilder<UIFSMState>.include()
}
val defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
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? {
defaultExceptionsHandlers.forEach {
it.handle(currentState, e).onPresented { state ->
return state
}
}
return null
}
}

View File

@ -0,0 +1,12 @@
package dev.inmo.postssystem.features.common.common.ui.fsm
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import kotlinx.serialization.*
interface UIFSMState : State {
val from: UIFSMState?
get() = null
override val context: String
get() = "main"
}

View File

@ -0,0 +1,18 @@
package dev.inmo.postssystem.features.common.common.ui
import androidx.compose.runtime.Composable
import com.soywiz.klock.DateTime
import com.soywiz.klock.ISO8601
import org.jetbrains.compose.web.dom.Text
object DateTimeView {
val simpleFormat = ISO8601.BaseIsoDateTimeFormat(
"DD.MM.YYYY, hh:mm"
)
@Composable
fun Simple(
dateTime: DateTime
) {
Text(dateTime.local.format(simpleFormat))
}
}

View File

@ -1,12 +1,13 @@
package dev.inmo.postssystem.client.fsm.ui package dev.inmo.postssystem.features.common.common.ui
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.micro_utils.fsm.common.StatesMachine import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.postssystem.features.common.common.ui.fsm.*
import kotlinx.browser.document import kotlinx.browser.document
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> { abstract class JSView<T : UIFSMState>(
override val defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
) : UIFSMHandler<T> {
open suspend fun StatesMachine<in UIFSMState>.safeHandleState( open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement, htmlElement: HTMLElement,
state: T state: T

View File

@ -14,19 +14,19 @@ kotlin {
api libs.microutils.serialization.typedserializer api libs.microutils.serialization.typedserializer
api libs.microutils.mimetypes api libs.microutils.mimetypes
api libs.klock api libs.klock
api "io.insert-koin:koin-core:$koin_version" api libs.koin.core
api "com.benasher44:uuid:$uuid_version" api libs.uuid
api "io.ktor:ktor-http:$ktor_version" api libs.ktor.http
} }
} }
jvmMain { jvmMain {
dependencies { dependencies {
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" api libs.kotlin.reflect
} }
} }
androidMain { androidMain {
dependencies { dependencies {
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" api libs.kotlin.reflect
} }
} }
} }

View File

@ -8,7 +8,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
@Serializer(DateTime::class)
object DateTimeSerializer : KSerializer<DateTime> { object DateTimeSerializer : KSerializer<DateTime> {
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = Double.serializer().descriptor get() = Double.serializer().descriptor

View File

@ -33,9 +33,10 @@ expect class FileBasedInputProvider : SimpleInputProvider {
} }
@Serializable(SimpleInputProviderSerializer::class) @Serializable(SimpleInputProviderSerializer::class)
class CustomInputProvider(private val provider: () -> Input) : SimpleInputProvider { class CustomInputProvider(
override val contentBytes: Long? override val contentBytes: Long? = null,
get() = null private val provider: () -> Input
) : SimpleInputProvider {
override fun invoke(): Input = provider() override fun invoke(): Input = provider()
} }

View File

@ -17,8 +17,8 @@ kotlin {
} }
jvmMain { jvmMain {
dependencies { dependencies {
api "io.ktor:ktor-auth:$ktor_version" api libs.ktor.server.auth
api "ch.qos.logback:logback-classic:$logback_version" api libs.logback
} }
} }
} }

View File

@ -1,14 +1,13 @@
package dev.inmo.postssystem.features.common.server.sessions package dev.inmo.postssystem.features.common.server
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.application.Application import io.ktor.server.application.Application
import io.ktor.auth.Authentication import io.ktor.server.auth.*
import io.ktor.auth.authentication
class ApplicationAuthenticationConfigurator( class ApplicationAuthenticationConfigurator(
private val elements: List<Element> private val elements: List<Element>
) : KtorApplicationConfigurator { ) : KtorApplicationConfigurator {
fun interface Element { operator fun Authentication.Configuration.invoke() } fun interface Element { operator fun AuthenticationConfig.invoke() }
override fun Application.configure() { override fun Application.configure() {
authentication { authentication {

View File

@ -1,8 +1,9 @@
package dev.inmo.postssystem.features.common.server.sessions package dev.inmo.postssystem.features.common.server
import org.koin.core.qualifier.StringQualifier import org.koin.core.qualifier.StringQualifier
object Qualifiers { object Qualifiers {
val filesFolderQualifier = StringQualifier("filesFolder") val filesFolderQualifier = StringQualifier("filesFolder")
val commonFilesFolderQualifier = StringQualifier("commonFilesFolder")
val usersRolesKeyValueFactoryQualifier = StringQualifier("usersRolesKeyValueFactory") val usersRolesKeyValueFactoryQualifier = StringQualifier("usersRolesKeyValueFactory")
} }

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.features.common.server.sessions package dev.inmo.postssystem.features.common.server
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.koin.core.module.Module import org.koin.core.module.Module

View File

@ -1,17 +1,20 @@
package dev.inmo.postssystem.features.content.binary.client package dev.inmo.postssystem.features.content.binary.client
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.jsuikit.elements.DefaultButton import dev.inmo.jsuikit.elements.*
import dev.inmo.jsuikit.modifiers.UIKitWidth import dev.inmo.jsuikit.modifiers.UIKitWidth
import dev.inmo.micro_utils.common.selectFile import dev.inmo.micro_utils.common.selectFile
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.postssystem.features.common.common.* import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.client.ContentClientProvider import dev.inmo.postssystem.features.content.client.ContentClientProvider
import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.common.*
import org.jetbrains.compose.web.dom.Img
import org.jetbrains.compose.web.dom.Text
import org.koin.core.module.Module import org.koin.core.module.Module
object LoadingClientModule : ModuleLoader { object LoadingClientModule : ModuleLoader {
init { init {
AdditionalModules.addModule(this) AdditionalModules.Default.addModule(this)
} }
override fun Module.load() { override fun Module.load() {
@ -41,4 +44,23 @@ object BinaryContentClientProvider : ContentClientProvider {
} }
} }
} }
@Composable
override fun renderPreview(content: Content): Boolean {
if (content is BinaryContent) {
Tile {
Card(
header = {
CardTitle {
Text(content.filename.name)
}
}
) {}
}
return true
}
return false
}
} }

View File

@ -3,8 +3,8 @@ package dev.inmo.postssystem.features.content.binary.server
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.postssystem.features.common.common.singleWithBinds import dev.inmo.postssystem.features.common.common.singleWithBinds
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
import dev.inmo.postssystem.features.common.server.sessions.Qualifiers import dev.inmo.postssystem.features.common.server.Qualifiers
import dev.inmo.postssystem.features.common.server.sessions.ServerModuleLoader import dev.inmo.postssystem.features.common.server.ServerModuleLoader
import dev.inmo.postssystem.features.content.common.BinaryContent import dev.inmo.postssystem.features.content.common.BinaryContent
import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper
import dev.inmo.postssystem.features.files.common.* import dev.inmo.postssystem.features.files.common.*

View File

@ -0,0 +1,11 @@
package dev.inmo.postssystem.features.content.client
import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
val loader = DefaultModuleLoader {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) }
}

View File

@ -10,4 +10,7 @@ interface ContentClientProvider {
@Composable @Composable
fun renderNewInstance(state: MutableState<Content?>) fun renderNewInstance(state: MutableState<Content?>)
@Composable
fun renderPreview(content: Content): Boolean
} }

View File

@ -1,7 +1,6 @@
package dev.inmo.postssystem.features.content.common package dev.inmo.postssystem.features.content.common
import dev.inmo.micro_utils.common.FileName import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.mime_types.MimeType import dev.inmo.micro_utils.mime_types.MimeType
import dev.inmo.postssystem.features.common.common.SimpleInputProvider import dev.inmo.postssystem.features.common.common.SimpleInputProvider
import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.PolymorphicSerializer
@ -30,6 +29,18 @@ data class BinaryContent(
) : Content ) : Content
val ContentSerializer = PolymorphicSerializer(Content::class) val ContentSerializer = PolymorphicSerializer(Content::class)
@Serializable
data class ContentWrapper(
val content: Content
)
@Serializable
data class ContentsWrapper(
val content: List<Content>
)
@Serializable
data class ContentsEithersWrapper(
val content: List<Either<ContentId, Content>>
)
/** /**
* Content which is already registered in database. Using its [id] you can retrieve all known * Content which is already registered in database. Using its [id] you can retrieve all known

View File

@ -1,18 +1,20 @@
package dev.inmo.postssystem.features.content.text.client package dev.inmo.postssystem.features.content.text.client
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.jsuikit.elements.Tile
import dev.inmo.jsuikit.modifiers.UIKitWidth import dev.inmo.jsuikit.modifiers.UIKitWidth
import dev.inmo.jsuikit.modifiers.include import dev.inmo.jsuikit.modifiers.include
import dev.inmo.postssystem.features.common.common.* import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.client.ContentClientProvider import dev.inmo.postssystem.features.content.client.ContentClientProvider
import dev.inmo.postssystem.features.content.common.Content import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.text.common.TextContent import dev.inmo.postssystem.features.content.text.common.TextContent
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.dom.TextArea import org.jetbrains.compose.web.dom.TextArea
import org.koin.core.module.Module import org.koin.core.module.Module
object LoadingClientModule : ModuleLoader { object LoadingClientModule : ModuleLoader {
init { init {
AdditionalModules.addModule(this) AdditionalModules.Default.addModule(this)
} }
override fun Module.load() { override fun Module.load() {
@ -36,4 +38,17 @@ object TextContentClientProvider : ContentClientProvider {
onInput { state.value = TextContent(it.value) } onInput { state.value = TextContent(it.value) }
} }
} }
@Composable
override fun renderPreview(content: Content): Boolean {
if (content is TextContent) {
Tile {
Text(content.text)
}
return true
}
return false
}
} }

View File

@ -1,7 +1,7 @@
package dev.inmo.postssystem.features.content.text.server package dev.inmo.postssystem.features.content.text.server
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
import dev.inmo.postssystem.features.common.server.sessions.ServerModuleLoader import dev.inmo.postssystem.features.common.server.ServerModuleLoader
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper
import dev.inmo.postssystem.features.content.text.common.TextContent import dev.inmo.postssystem.features.content.text.common.TextContent

View File

@ -12,6 +12,7 @@ kotlin {
dependencies { dependencies {
api project(":postssystem.features.files.common") api project(":postssystem.features.files.common")
api project(":postssystem.features.common.client") api project(":postssystem.features.common.client")
api project(":postssystem.features.auth.client")
} }
} }
} }

View File

@ -8,6 +8,7 @@ import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
import kotlinx.serialization.BinaryFormat import kotlinx.serialization.BinaryFormat
@ -24,15 +25,14 @@ class ClientReadFilesStorage(
MetaFileInfoStorageWrapper.serializer().nullable, MetaFileInfoStorageWrapper.serializer().nullable,
FileId.serializer() FileId.serializer()
) { ) {
private val unifiedRequester = UnifiedRequester(client, serialFormat)
private val fullFilesPath = buildStandardUrl(baseUrl, filesRootPathPart) private val fullFilesPath = buildStandardUrl(baseUrl, filesRootPathPart)
private val fullFilesGetBytesPath = buildStandardUrl( private val fullFilesGetBytesPath = buildStandardUrl(
fullFilesPath, fullFilesPath,
filesGetFilesPathPart filesGetFilesPathPart
) )
override suspend fun getBytes(id: FileId): ByteArray = client.post<HttpResponse>(fullFilesGetBytesPath) { override suspend fun getBytes(id: FileId): ByteArray = client.post(fullFilesGetBytesPath) {
body = serialFormat.encodeToByteArray(FileId.serializer(), id) setBody(serialFormat.encodeToByteArray(FileId.serializer(), id))
}.readBytes() }.readBytes()
override suspend fun getFullFileInfo( override suspend fun getFullFileInfo(

View File

@ -0,0 +1,9 @@
package dev.inmo.postssystem.features.files.client
import dev.inmo.postssystem.features.auth.client.AuthorizedModuleLoader
import dev.inmo.postssystem.features.auth.client.AuthorizedQualifiers
import dev.inmo.postssystem.features.files.common.storage.ReadFilesStorage
val loader = AuthorizedModuleLoader {
single<ReadFilesStorage> { ClientReadFilesStorage(get(AuthorizedQualifiers.ServerUrlQualifier), get(), get()) }
}

View File

@ -12,10 +12,6 @@ import kotlinx.serialization.Serializable
sealed interface FileInfo { sealed interface FileInfo {
val name: FileName val name: FileName
val mimeType: MimeType val mimeType: MimeType
companion object {
fun serializer(): KSerializer<FileInfo> = FileInfoSerializer
}
} }
object FileInfoSerializer : KSerializer<FileInfo> by TypedSerializer( object FileInfoSerializer : KSerializer<FileInfo> by TypedSerializer(

View File

@ -4,58 +4,47 @@ import dev.inmo.postssystem.features.files.common.*
import dev.inmo.postssystem.features.files.common.storage.* import dev.inmo.postssystem.features.files.common.storage.*
import dev.inmo.micro_utils.ktor.server.* import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes import dev.inmo.micro_utils.repos.ktor.server.crud.*
import dev.inmo.micro_utils.repos.ktor.server.crud.configureWriteStandardCrudRepoRoutes import io.ktor.http.HttpStatusCode
import io.ktor.application.call import io.ktor.http.decodeURLQueryComponent
import io.ktor.auth.authenticate import io.ktor.server.application.call
import io.ktor.response.respondBytes import io.ktor.server.auth.authenticate
import io.ktor.routing.* import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.*
import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.nullable
class FilesRoutingConfigurator( class FilesRoutingConfigurator(
private val filesStorage: ReadFilesStorage, private val filesStorage: ReadFilesStorage,
private val writeFilesStorage: WriteFilesStorage?, private val writeFilesStorage: WriteFilesStorage?,
private val unifierRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element { ) : ApplicationRoutingConfigurator.Element {
constructor(filesStorage: FilesStorage, unifierRouter: UnifiedRouter) : this(filesStorage, filesStorage, unifierRouter) constructor(filesStorage: FilesStorage) : this(filesStorage, filesStorage)
override fun Route.invoke() { override fun Route.invoke() {
authenticate { authenticate {
route(filesRootPathPart) { route(filesRootPathPart) {
configureReadStandardCrudRepoRoutes( configureReadCRUDRepoRoutes(
filesStorage, filesStorage,
MetaFileInfoStorageWrapper.serializer(), ::FileId
MetaFileInfoStorageWrapper.serializer().nullable,
FileId.serializer(),
unifierRouter
) )
writeFilesStorage ?.let { writeFilesStorage ?.let {
configureWriteStandardCrudRepoRoutes( configureWriteCRUDRepoRoutes(writeFilesStorage)
writeFilesStorage,
FullFileInfoStorageWrapper.serializer(),
FullFileInfoStorageWrapper.serializer().nullable,
FullFileInfo.serializer(),
FileId.serializer(),
unifierRouter
)
} }
unifierRouter.apply { post(filesGetFilesPathPart) {
post(filesGetFilesPathPart) { call.respondBytes(
call.respondBytes( filesStorage.getBytes(
filesStorage.getBytes( call.receive()
uniload(FileId.serializer())
)
) )
} )
get(filesGetFullFileInfoPathPart) { }
unianswer( get(filesGetFullFileInfoPathPart) {
FullFileInfoStorageWrapper.serializer().nullable, call.respond(
filesStorage.getFullFileInfo( filesStorage.getFullFileInfo(
decodeUrlQueryValueOrSendError(filesFileIdParameter, FileId.serializer()) ?: return@get FileId(call.getParameterOrSendError(filesFileIdParameter) ?.decodeURLQueryComponent() ?: return@get)
) ) ?: HttpStatusCode.NoContent
) )
}
} }
} }
} }

View File

@ -2,7 +2,9 @@ package dev.inmo.postssystem.features.posts.common
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import dev.inmo.postssystem.features.common.common.DateTimeSerializer import dev.inmo.postssystem.features.common.common.DateTimeSerializer
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId import dev.inmo.postssystem.features.content.common.ContentId
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
@ -23,8 +25,8 @@ typealias ContentIds = List<ContentId>
* @see RegisteredPost * @see RegisteredPost
*/ */
@Serializable @Serializable
sealed class Post { sealed interface Post {
abstract val content: ContentIds val content: ContentIds
} }
/** /**
@ -33,7 +35,7 @@ sealed class Post {
@Serializable @Serializable
data class NewPost( data class NewPost(
override val content: ContentIds override val content: ContentIds
) : Post() ) : Post
/** /**
* Registered [Post] * Registered [Post]
@ -44,4 +46,16 @@ data class RegisteredPost(
override val content: ContentIds, override val content: ContentIds,
@Serializable(DateTimeSerializer::class) @Serializable(DateTimeSerializer::class)
val creationDate: DateTime val creationDate: DateTime
) : Post() ) : Post
@Serializable
data class PostWithContent(
val post: Post,
val content: List<@Polymorphic Content>
)
@Serializable
data class RegisteredPostWithContent(
val post: RegisteredPost,
val content: List<@Polymorphic Content>
)

View File

@ -12,6 +12,7 @@ kotlin {
dependencies { dependencies {
api project(":postssystem.features.roles.common") api project(":postssystem.features.roles.common")
api project(":postssystem.features.common.client") api project(":postssystem.features.common.client")
api project(":postssystem.features.auth.client")
} }
} }
} }

View File

@ -6,12 +6,12 @@ import kotlinx.serialization.KSerializer
class ClientRolesStorage<T : Role>( class ClientRolesStorage<T : Role>(
private val baseUrl: String, private val baseUrl: String,
private val unifiedRequester: UnifiedRequester, private val client: HttpClient,
private val serializer: KSerializer<T> private val serializer: KSerializer<T>
) : RolesStorage<T>, ) : RolesStorage<T>,
ReadRolesStorage<T> by ReadClientRolesStorage( ReadRolesStorage<T> by ReadClientRolesStorage(
baseUrl, unifiedRequester, serializer baseUrl, client, serializer
), ),
WriteRolesStorage<T> by WriteClientRolesStorage( WriteRolesStorage<T> by WriteClientRolesStorage(
baseUrl, unifiedRequester, serializer baseUrl, client, serializer
) )

View File

@ -0,0 +1,31 @@
package dev.inmo.postssystem.features.roles.client
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import org.koin.core.Koin
class DefaultRolesSettings(
private val koin: Koin,
private val authSettings: AuthSettings,
private val scope: CoroutineScope
) : RolesSettings {
private val _userRoles = MutableStateFlow<List<Role>>(emptyList())
override val userRoles: StateFlow<List<Role>> = _userRoles.asStateFlow()
val rolesUpdatingJob = (authSettings.user + authSettings.authorizedDIModule).subscribeSafelyWithoutExceptions(scope) {
val user = authSettings.user.value
if (user == null || authSettings.authorizedDIModule.value == null) {
_userRoles.value = emptyList()
} else {
_userRoles.value = koin.get<RolesStorage<Role>>().getRoles(user.id)
}
println(user)
println(userRoles.value)
}
}

View File

@ -0,0 +1,11 @@
package dev.inmo.postssystem.features.roles.client
import dev.inmo.postssystem.features.auth.client.AuthorizedModuleLoader
import dev.inmo.postssystem.features.auth.client.AuthorizedQualifiers
import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage
val loader = AuthorizedModuleLoader {
single<RolesSettings> { DefaultRolesSettings(getKoin(), get(), get()) }
single<RolesStorage<Role>> { ClientRolesStorage(get(AuthorizedQualifiers.ServerUrlQualifier), get(), Role.serializer()) }
}

View File

@ -3,13 +3,15 @@ package dev.inmo.postssystem.features.roles.client
import dev.inmo.postssystem.features.roles.common.* import dev.inmo.postssystem.features.roles.common.*
import dev.inmo.micro_utils.ktor.client.UnifiedRequester import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
class ReadClientRolesStorage<T : Role>( class ReadClientRolesStorage<T : Role>(
private val baseUrl: String, private val baseUrl: String,
private val unifiedRequester: UnifiedRequester, private val client: HttpClient,
private val serializer: KSerializer<T> private val serializer: KSerializer<T>
) : ReadRolesStorage<T> { ) : ReadRolesStorage<T> {
private val userRolesSerializer = ListSerializer(serializer) private val userRolesSerializer = ListSerializer(serializer)
@ -21,14 +23,13 @@ class ReadClientRolesStorage<T : Role>(
override suspend fun getSubjects( override suspend fun getSubjects(
role: T role: T
): List<RoleSubject> = unifiedRequester.uniget( ): List<RoleSubject> = client.get(
buildStandardUrl( buildStandardUrl(
userRolesFullUrl, userRolesFullUrl,
usersRolesGetSubjectsPathPart, usersRolesGetSubjectsPathPart,
usersRolesRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, role) usersRolesRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, role)
), )
RoleSubjectsSerializer ).body()
)
override suspend fun getRoles( override suspend fun getRoles(
subject: RoleSubject subject: RoleSubject

View File

@ -0,0 +1,8 @@
package dev.inmo.postssystem.features.roles.client
import dev.inmo.postssystem.features.roles.common.Role
import kotlinx.coroutines.flow.StateFlow
interface RolesSettings {
val userRoles: StateFlow<List<Role>>
}

View File

@ -8,7 +8,7 @@ import kotlinx.serialization.builtins.serializer
class WriteClientRolesStorage<T : Role>( class WriteClientRolesStorage<T : Role>(
private val baseUrl: String, private val baseUrl: String,
private val unifiedRequester: UnifiedRequester, private val client: HttpClient,
private val serializer: KSerializer<T> private val serializer: KSerializer<T>
) : WriteRolesStorage<T> { ) : WriteRolesStorage<T> {
private val wrapperSerializer = RolesStorageIncludeExcludeWrapper.serializer( private val wrapperSerializer = RolesStorageIncludeExcludeWrapper.serializer(

View File

@ -6,17 +6,16 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.* import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@Serializable(RoleSerializer::class) @Polymorphic
interface Role { interface Role {
companion object { companion object {
fun serializer() = RoleSerializer fun serializer(): KSerializer<Role> = RoleSerializer
} }
} }
@Serializable @Serializable
data class UnknownRole(val originalJson: JsonElement) : Role data class UnknownRole(val originalJson: JsonElement) : Role
@Serializer(Role::class)
object RoleSerializer : KSerializer<Role> { object RoleSerializer : KSerializer<Role> {
private val userRoleFormat = Json { ignoreUnknownKeys = true } private val userRoleFormat = Json { ignoreUnknownKeys = true }
private const val keyField = "key" private const val keyField = "key"

View File

@ -3,30 +3,16 @@ package dev.inmo.postssystem.features.roles.manager.common
import dev.inmo.postssystem.features.roles.common.Role import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RoleSerializer import dev.inmo.postssystem.features.roles.common.RoleSerializer
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable(RolesManagerRoleSerializer::class) @Serializable
interface RolesManagerRole : Role { sealed interface RolesManagerRole : Role
companion object {
fun serializer() = RolesManagerRoleSerializer
}
}
@Serializable @Serializable
@SerialName("GeneralRolesManagerRole")
object GeneralRolesManagerRole : RolesManagerRole { object GeneralRolesManagerRole : RolesManagerRole {
override fun toString(): String = "GeneralRolesManagerRole" override fun toString(): String = "GeneralRolesManagerRole"
} }
private const val KEY = "roles_manager" private val justForLoading = GeneralRolesManagerRole.serializer()
object RolesManagerRoleSerializer : TypedSerializer<RolesManagerRole>(
RolesManagerRole::class,
mapOf(
"${KEY}_general" to GeneralRolesManagerRole.serializer()
)
) {
init {
RoleSerializer.includeSerializer(KEY, RolesManagerRoleSerializer)
serializers.forEach { (k, v) -> RoleSerializer.includeSerializer(k, v) }
}
}

View File

@ -4,7 +4,7 @@ import dev.inmo.postssystem.features.roles.common.*
import dev.inmo.postssystem.features.roles.manager.common.GeneralRolesManagerRole import dev.inmo.postssystem.features.roles.manager.common.GeneralRolesManagerRole
import dev.inmo.postssystem.features.roles.server.RolesChecker import dev.inmo.postssystem.features.roles.server.RolesChecker
import dev.inmo.postssystem.features.users.common.User import dev.inmo.postssystem.features.users.common.User
import io.ktor.application.ApplicationCall import io.ktor.server.application.ApplicationCall
object RolesManagerRolesChecker : RolesChecker<Role> { object RolesManagerRolesChecker : RolesChecker<Role> {
override val key: String override val key: String

View File

@ -1,18 +1,14 @@
package dev.inmo.postssystem.features.roles.manager.server package dev.inmo.postssystem.features.roles.manager.server
import dev.inmo.micro_utils.ktor.server.UnifiedRouter
import dev.inmo.postssystem.features.roles.common.RolesStorage import dev.inmo.postssystem.features.roles.common.RolesStorage
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRole import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRole
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer
import dev.inmo.postssystem.features.roles.server.RolesStorageWriteServerRoutesConfigurator import dev.inmo.postssystem.features.roles.server.RolesStorageWriteServerRoutesConfigurator
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
class RolesManagerRolesStorageServerRoutesConfigurator( class RolesManagerRolesStorageServerRoutesConfigurator(
storage: RolesStorage<RolesManagerRole>, storage: RolesStorage<RolesManagerRole>
unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element by RolesStorageWriteServerRoutesConfigurator( ) : ApplicationRoutingConfigurator.Element by RolesStorageWriteServerRoutesConfigurator(
storage, storage,
RolesManagerRoleSerializer, RolesManagerRole.serializer(),
RolesManagerRolesChecker.key, RolesManagerRolesChecker.key
unifiedRouter = unifiedRouter
) )

View File

@ -2,7 +2,7 @@ package dev.inmo.postssystem.features.roles.server
import dev.inmo.postssystem.features.roles.common.* import dev.inmo.postssystem.features.roles.common.*
import dev.inmo.postssystem.features.users.common.User import dev.inmo.postssystem.features.users.common.User
import io.ktor.application.ApplicationCall import io.ktor.server.application.ApplicationCall
interface RolesChecker<T : Role> { interface RolesChecker<T : Role> {
val key: String val key: String

View File

@ -3,9 +3,8 @@ package dev.inmo.postssystem.features.roles.server
import dev.inmo.postssystem.features.roles.common.* import dev.inmo.postssystem.features.roles.common.*
import dev.inmo.micro_utils.ktor.server.* import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.application.call import io.ktor.server.auth.authenticate
import io.ktor.auth.authenticate import io.ktor.server.routing.*
import io.ktor.routing.*
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
@ -13,8 +12,7 @@ class RolesStorageWriteServerRoutesConfigurator<T : Role>(
private val storage: WriteRolesStorage<T>, private val storage: WriteRolesStorage<T>,
private val serializer: KSerializer<T>, private val serializer: KSerializer<T>,
private val includeAuthKey: String, private val includeAuthKey: String,
private val excludeAuthKey: String = includeAuthKey, private val excludeAuthKey: String = includeAuthKey
private val unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element { ) : ApplicationRoutingConfigurator.Element {
override fun Route.invoke() { override fun Route.invoke() {
route(usersRolesRootPathPart) { route(usersRolesRootPathPart) {

View File

@ -3,21 +3,19 @@ package dev.inmo.postssystem.features.roles.server
import dev.inmo.postssystem.features.auth.common.AuthToken import dev.inmo.postssystem.features.auth.common.AuthToken
import dev.inmo.postssystem.features.auth.server.principal import dev.inmo.postssystem.features.auth.server.principal
import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService
import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator import dev.inmo.postssystem.features.common.server.ApplicationAuthenticationConfigurator
import dev.inmo.postssystem.features.roles.common.Role import dev.inmo.postssystem.features.roles.common.Role
import dev.inmo.postssystem.features.roles.common.RolesStorage import dev.inmo.postssystem.features.roles.common.RolesStorage
import io.ktor.application.call
import io.ktor.auth.Authentication
import io.ktor.auth.session
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.response.respond import io.ktor.server.auth.*
import io.ktor.server.response.respond
class RolesAuthenticationConfigurator<T : Role>( class RolesAuthenticationConfigurator<T : Role>(
private val usersRolesStorage: RolesStorage<T>, private val usersRolesStorage: RolesStorage<T>,
private val authTokensService: AuthTokensService, private val authTokensService: AuthTokensService,
private val rolesCheckers: List<RolesChecker<T>> private val rolesCheckers: List<RolesChecker<T>>
) : ApplicationAuthenticationConfigurator.Element { ) : ApplicationAuthenticationConfigurator.Element {
override fun Authentication.Configuration.invoke() { override fun AuthenticationConfig.invoke() {
rolesCheckers.forEach { checker -> rolesCheckers.forEach { checker ->
session<AuthToken>(checker.key) { session<AuthToken>(checker.key) {
validate { validate {

View File

@ -3,16 +3,15 @@ package dev.inmo.postssystem.features.roles.server
import dev.inmo.postssystem.features.roles.common.* import dev.inmo.postssystem.features.roles.common.*
import dev.inmo.micro_utils.ktor.server.* import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.auth.authenticate import io.ktor.server.auth.authenticate
import io.ktor.routing.* import io.ktor.server.routing.*
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
class RolesStorageReadServerRoutesConfigurator<T : Role>( class RolesStorageReadServerRoutesConfigurator<T : Role>(
private val storage: ReadRolesStorage<T>, private val storage: ReadRolesStorage<T>,
private val serializer: KSerializer<T>, private val serializer: KSerializer<T>
private val unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element { ) : ApplicationRoutingConfigurator.Element {
private val userRolesSerializer = ListSerializer(serializer) private val userRolesSerializer = ListSerializer(serializer)
override fun Route.invoke() { override fun Route.invoke() {

View File

@ -22,6 +22,6 @@ class StatusFeatureClient(
statusAuthorisedPathPart statusAuthorisedPathPart
) )
suspend fun checkServerStatus() = client.get<HttpResponse>(fullStatusUrl).status == HttpStatusCode.OK suspend fun checkServerStatus() = client.get(fullStatusUrl).status == HttpStatusCode.OK
suspend fun checkServerStatusWithAuth() = client.get<HttpResponse>(fullAuthorisedStatusUrl).status == HttpStatusCode.OK suspend fun checkServerStatusWithAuth() = client.get(fullAuthorisedStatusUrl).status == HttpStatusCode.OK
} }

View File

@ -3,11 +3,11 @@ package dev.inmo.postssystem.features.status.server
import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart
import dev.inmo.postssystem.features.status.common.statusRootPart import dev.inmo.postssystem.features.status.common.statusRootPart
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.response.respond import io.ktor.server.application.call
import io.ktor.routing.* import io.ktor.server.auth.authenticate
import io.ktor.server.response.respond
import io.ktor.server.routing.*
object StatusRoutingConfigurator : ApplicationRoutingConfigurator.Element { object StatusRoutingConfigurator : ApplicationRoutingConfigurator.Element {
override fun Route.invoke() { override fun Route.invoke() {

View File

@ -12,6 +12,7 @@ kotlin {
dependencies { dependencies {
api project(":postssystem.features.users.common") api project(":postssystem.features.users.common")
api project(":postssystem.features.common.client") api project(":postssystem.features.common.client")
api project(":postssystem.features.auth.client")
} }
} }
} }

View File

@ -0,0 +1,9 @@
package dev.inmo.postssystem.features.users.client
import dev.inmo.postssystem.features.auth.client.AuthorizedModuleLoader
import dev.inmo.postssystem.features.auth.client.AuthorizedQualifiers
import dev.inmo.postssystem.features.users.common.ReadUsersStorage
val loader = AuthorizedModuleLoader {
single<ReadUsersStorage> { UsersStorageKtorClient(get(AuthorizedQualifiers.ServerUrlQualifier), get()) }
}

Some files were not shown because too many files have changed in this diff Show More