Compare commits

..

No commits in common. "master" and "72578f6b58a540601b8c11f2f0324ca7443fcdb7" have entirely different histories.

147 changed files with 1134 additions and 1324 deletions

View File

@ -1,14 +1,12 @@
## Структура проекта ## Структура проекта
* **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,16 +4,14 @@ 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 libs.buildscript.kt.gradle classpath 'com.android.tools.build:gradle:7.0.4'
classpath libs.buildscript.kt.serialization classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath libs.buildscript.jb.dokka classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath libs.buildscript.gh.release classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath libs.buildscript.android.gradle classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath libs.buildscript.android.dexcount
} }
} }
@ -22,7 +20,6 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
} }
} }

View File

@ -32,6 +32,9 @@ 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
@ -40,7 +43,7 @@ kotlin {
jvmMain { jvmMain {
dependencies { dependencies {
api libs.ktor.client.apache api "io.ktor:ktor-client-apache:$ktor_version"
} }
} }

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.features.common.common package dev.inmo.postssystem.client
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,148 @@
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

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

View File

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

View File

@ -1,7 +1,8 @@
package dev.inmo.postssystem.features.auth.client.settings package dev.inmo.postssystem.client.settings.auth
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
@ -10,6 +11,7 @@ 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,16 +1,19 @@
package dev.inmo.postssystem.features.auth.client.settings package dev.inmo.postssystem.client.settings.auth
import dev.inmo.postssystem.client.DBDropper
import dev.inmo.postssystem.client.getAuthorizedFeaturesDIModule
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException import dev.inmo.postssystem.features.auth.client.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
@ -26,6 +29,8 @@ 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
@ -37,6 +42,18 @@ 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) {
@ -56,7 +73,7 @@ data class DefaultAuthSettings(
initialAuthKey: Either<AuthKey, AuthTokenInfo>, initialAuthKey: Either<AuthKey, AuthTokenInfo>,
): AuthUIError? { ): AuthUIError? {
val currentModule = authorizedDIModule.value val currentModule = authorizedDIModule.value
val newModule = createAuthorizedFeaturesDIModule( val newModule = getAuthorizedFeaturesDIModule(
serverUrl, serverUrl,
initialAuthKey, initialAuthKey,
{ {
@ -84,8 +101,8 @@ data class DefaultAuthSettings(
currentModule ?.let { koin.loadModules(listOf(currentModule)) } currentModule ?.let { koin.loadModules(listOf(currentModule)) }
} }
return when { return when {
!serverAvailable -> AuthUIError.ServerUnavailable !serverAvailable -> ServerUnavailableAuthUIError
!authCorrect -> AuthIncorrect !authCorrect -> AuthIncorrectAuthUIError
else -> { else -> {
_authorizedDIModule.value = newModule _authorizedDIModule.value = newModule
null null

View File

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

View File

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

View File

@ -0,0 +1,30 @@
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,24 +1,31 @@
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.features.auth.client.ui.* import dev.inmo.postssystem.client.settings.auth.AuthSettings
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.features.common.common.ui.fsm.UIFSMHandler import dev.inmo.postssystem.services.posts.client.ui.create.*
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(),
@ -28,11 +35,9 @@ 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 = {
@ -73,22 +78,69 @@ fun baseKoin(): Koin {
) )
}, },
{ {
JSUIFSMStatesRepo(window.history, AuthUIFSMState(PostsListUIFSMState()), getAllDistinct()) JSUIFSMStatesRepo(window.history)
} }
) { ) {
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)) }
} }
) )
getAllDistinct<UIFSMHandler.Registrator>().forEach { strictlyOn<AuthUIFSMState>(get<AuthView>())
with(it) {
include() // Костыль, в JS на момент Пн дек 6 14:19:29 +06 2021 если использовать strictlyOn генерируются
// некорректные безымянные классы (у них отсутствует метод 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 currentParams = (URL(window.location.href)).searchParams val params = states.mapNotNull<UIFSMState, Pair<String, String>> {
val params = states.flatMap<UIFSMState, Pair<String, String>> { state -> when (it) {
fillers.flatMap { is AuthUIFSMState -> null
it.takeParams(state, currentParams) is CreatePostUIFSMState -> null
} }
} }
pushState( pushState(
@ -24,11 +24,9 @@ private fun History.refreshHistory(
) )
} }
private fun takeStates(initialState: UIFSMState, fillers: List<UIFSMStateSearchParamsHandler>): List<UIFSMState> { private fun takeStates(initialState: UIFSMState): List<UIFSMState> {
val params = (URL(window.location.href)).searchParams val params = (URL(window.location.href)).searchParams
val additionalStates = fillers.mapNotNull { val additionalStates = listOfNotNull<UIFSMState>()
it.takeState(params)
}
return additionalStates + listOfNotNull( return additionalStates + listOfNotNull(
if (additionalStates.isEmpty()) { if (additionalStates.isEmpty()) {
@ -43,13 +41,12 @@ private fun takeStates(initialState: UIFSMState, fillers: List<UIFSMStateSearchP
class JSUIFSMStatesRepo( class JSUIFSMStatesRepo(
private val history: History, private val history: History,
private val initialState: UIFSMState, private val initialState: UIFSMState = DefaultAuthUIFSMState
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, fillers) val states = takeStates(initialState)
states.forEach { states.forEach {
statesMap[it.context] = it statesMap[it.context] = it
} }
@ -71,12 +68,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, fillers) history.refreshHistory(statesMap.values)
} }
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, fillers) history.refreshHistory(statesMap.values)
} }
} }

View File

@ -1,13 +1,14 @@
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>>(DefaultQualifiers.UIFSMQualifier) val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
uiStatesMachine.start(koin.get()) uiStatesMachine.start(koin.get())
}) })
} }

View File

@ -1,9 +0,0 @@
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,41 +1,28 @@
package dev.inmo.postssystem.features.auth.client package dev.inmo.postssystem.client.fsm.ui
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.micro_utils.coroutines.compose.renderComposableAndLinkToContextAndRoot import dev.inmo.postssystem.client.ui.fsm.*
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.features.auth.client.ui.AuthUIError.AuthIncorrect import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext
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
defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler> ) : JSView<AuthUIFSMState>() {
) : JSView<AuthUIFSMState>(defaultExceptionsHandlers) {
override suspend fun StatesMachine<in UIFSMState>.safeHandleState( override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement, htmlElement: HTMLElement,
@ -49,7 +36,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 = renderComposableAndLinkToContextAndRoot(root) { val composition = renderComposableAndLinkToContext(root) {
val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank() val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank()
Flex( Flex(
@ -67,13 +54,13 @@ class AuthView(
} }
} }
StandardInput( TextField(
InputType.Text, InputType.Text,
usernameState, usernameState,
disabled, disabled,
"Username", "Username",
) )
StandardInput( TextField(
InputType.Password, InputType.Password,
passwordState, passwordState,
disabled, disabled,
@ -93,23 +80,23 @@ class AuthView(
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) { val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
when (it) { when (it) {
is AuthUIState.Init -> { is InitAuthUIState -> {
disabled.value = false disabled.value = false
errorText.value = when (it.showError) { errorText.value = when (it.showError) {
AuthUIError.ServerUnavailable -> "Server unavailable" ServerUnavailableAuthUIError -> "Server unavailable"
AuthIncorrect -> { AuthIncorrectAuthUIError -> {
passwordState.value = "" passwordState.value = ""
"Username or password is incorrect" "Username or password is incorrect"
} }
null -> null null -> null
} }
} }
AuthUIState.Loading -> { LoadingAuthUIState -> {
disabled.value = true disabled.value = true
errorText.value = null errorText.value = null
} }
AuthUIState.Authorized -> { AuthorizedAuthUIState -> {
completion.complete(state.from) completion.complete(state.from)
} }
} }

View File

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

View File

@ -1,13 +1,12 @@
package dev.inmo.postssystem.features.common.common.ui package dev.inmo.postssystem.client.fsm.ui
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.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>( abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
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

@ -1,19 +1,16 @@
package dev.inmo.postssystem.services.posts.client package dev.inmo.postssystem.client.fsm.ui
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.micro_utils.coroutines.compose.renderComposableAndLinkToContextAndRoot
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.StatesMachine import dev.inmo.micro_utils.fsm.common.StatesMachine
import dev.inmo.postssystem.features.auth.client.registerAfterAuthHandler import dev.inmo.postssystem.client.ui.fsm.CreatePostUIFSMState
import dev.inmo.postssystem.features.common.common.* import dev.inmo.postssystem.client.ui.fsm.UIFSMState
import dev.inmo.postssystem.features.common.common.ui.JSView import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext
import dev.inmo.postssystem.features.common.common.ui.fsm.*
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.services.posts.client.ui.create.PostsCreateUIFSMState
import dev.inmo.postssystem.services.posts.client.ui.create.PostCreateUIViewModel import dev.inmo.postssystem.services.posts.client.ui.create.PostCreateUIViewModel
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -21,31 +18,20 @@ import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
val jsLoader = DefaultModuleLoader {
factory { PostCreateView(get(), getAllDistinct(), get(DefaultQualifiers.UIScopeQualifier), getAllDistinct()) }
singleWithRandomQualifier<UIFSMHandler.Registrator> {
UIFSMHandler.Registrator {
registerAfterAuthHandler(getKoin(), PostCreateView::class)
}
}
}
class PostCreateView( class PostCreateView(
private val createPostCreateUIModel: PostCreateUIViewModel, private val createPostCreateUIModel: PostCreateUIViewModel,
private val contentClientProviders: List<ContentClientProvider>, private val contentClientProviders: List<ContentClientProvider>,
private val uiScope: CoroutineScope, private val uiScope: CoroutineScope
defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler> ) : JSView<CreatePostUIFSMState>() {
) : JSView<PostsCreateUIFSMState>(defaultExceptionsHandlers) {
override suspend fun StatesMachine<in UIFSMState>.safeHandleState( override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
htmlElement: HTMLElement, htmlElement: HTMLElement,
state: PostsCreateUIFSMState state: CreatePostUIFSMState
): UIFSMState? { ): UIFSMState? {
val result = CompletableDeferred<UIFSMState?>() val result = CompletableDeferred<UIFSMState?>()
val contentProvidersList = mutableStateListOf<Pair<ContentClientProvider, MutableState<Content?>>>() val contentProvidersList = mutableStateListOf<Pair<ContentClientProvider, MutableState<Content?>>>()
renderComposableAndLinkToContextAndRoot(htmlElement) { renderComposableAndLinkToContext(htmlElement) {
Flex( Flex(
UIKitFlex.Alignment.Horizontal.Center UIKitFlex.Alignment.Horizontal.Center
) { ) {

View File

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

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

@ -0,0 +1,77 @@
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 libs.versions.android.props.compileSdk.get().toInteger() compileSdkVersion "$android_compileSdkVersion".toInteger()
buildToolsVersion libs.versions.android.props.buildTools.get() buildToolsVersion "$android_buildToolsVersion"
defaultConfig { defaultConfig {
minSdkVersion libs.versions.android.props.minSdk.get().toInteger() minSdkVersion "$android_minSdkVersion".toInteger()
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger() targetSdkVersion "$android_compileSdkVersion".toInteger()
versionCode "${android_code_version}".toInteger() versionCode "${android_code_version}".toInteger()
versionName "$version" versionName "$version"
} }

View File

@ -13,21 +13,6 @@ 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"
@ -35,6 +20,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,7 +2,6 @@ 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"
@ -12,7 +11,6 @@ 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

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

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

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

@ -1,59 +0,0 @@
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.plugins.cookies.AcceptAllCookiesStorage import io.ktor.client.features.cookies.*
import io.ktor.client.plugins.cookies.HttpCookies import io.ktor.client.features.expectSuccess
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.HttpReceivePipeline
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,6 +25,10 @@ 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()
@ -94,13 +98,13 @@ fun HttpClientConfig<*>.installClientAuthenticator(
receivePipeline.intercept(HttpReceivePipeline.Before) { receivePipeline.intercept(HttpReceivePipeline.Before) {
if ( if (
it.request.url.toString().startsWith(baseUrl) context.request.url.toString().startsWith(baseUrl)
&& it.status == HttpStatusCode.Unauthorized && context.response.status == HttpStatusCode.Unauthorized
) { ) {
authMutex.withLock { refreshToken() } authMutex.withLock { refreshToken() }
val newResponse = it.call.client.request { val newResponse = context.client ?.request<HttpResponse>{
takeFrom(it.request) takeFrom(context.request)
} } ?: return@intercept
proceedWith(newResponse) proceedWith(newResponse)
} }
} }

View File

@ -1,28 +0,0 @@
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 +0,0 @@
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.ui.UIModel import dev.inmo.postssystem.features.common.common.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,23 +3,18 @@ package dev.inmo.postssystem.features.auth.client.ui
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
sealed interface AuthUIError { sealed class AuthUIError
// @Serializable @Serializable
object ServerUnavailable : AuthUIError object ServerUnavailableAuthUIError : AuthUIError()
// @Serializable @Serializable
object AuthIncorrect : AuthUIError object AuthIncorrectAuthUIError : AuthUIError()
}
@Serializable @Serializable
sealed interface AuthUIState { sealed class AuthUIState
@Serializable @Serializable
data class Init(val showError: AuthUIError? = null) : AuthUIState data class InitAuthUIState(val showError: AuthUIError? = null) : AuthUIState()
// @Serializable val DefaultInitAuthUIState = InitAuthUIState()
object Loading : AuthUIState @Serializable
// @Serializable object LoadingAuthUIState : AuthUIState()
object Authorized : AuthUIState @Serializable
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.ui.UIViewModel import dev.inmo.postssystem.features.common.common.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

@ -3,20 +3,10 @@ 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,18 +2,17 @@ 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.ApplicationAuthenticationConfigurator import dev.inmo.postssystem.features.common.server.sessions.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.server.application.call import io.ktor.response.respond
import io.ktor.server.auth.* import io.ktor.routing.*
import io.ktor.server.request.receive import io.ktor.sessions.*
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(
@ -25,78 +24,90 @@ 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() {
route(authRootPathPart) { unifiedRouter.apply {
post(authAuthPathPart) { route(authRootPathPart) {
safely( post(authAuthPathPart) {
{ safely(
// TODO:: add error info {
it.printStackTrace() // TODO:: add error info
call.respond( it.printStackTrace()
HttpStatusCode.InternalServerError, call.respond(
"Something went wrong" HttpStatusCode.InternalServerError,
) "Something went wrong"
} )
) { }
val creds = call.receive<AuthCreds>() ) {
val creds = uniload(AuthCreds.serializer())
val tokenInfo = authFeature.auth(creds)
val tokenInfo = authFeature.auth(creds)
if (tokenInfo == null) {
if (call.response.status() == null) { if (tokenInfo == null) {
call.respond(HttpStatusCode.Forbidden) if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
unianswer(
AuthTokenInfo.serializer().nullable,
tokenInfo
)
} }
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
call.respond(tokenInfo)
} }
} }
} post(authRefreshPathPart) {
post (authRefreshPathPart) { safely(
safely( {
{ // TODO:: add error info
// TODO:: add error info call.respond(
call.respond( HttpStatusCode.InternalServerError,
HttpStatusCode.InternalServerError, "Something went wrong"
"Something went wrong" )
) }
} ) {
) { val refreshToken = uniload(RefreshToken.serializer())
val refreshToken = call.receive<RefreshToken>()
val tokenInfo = authFeature.refresh(refreshToken)
val tokenInfo = authFeature.refresh(refreshToken)
if (tokenInfo == null) {
if (tokenInfo == null) { if (call.response.status() == null) {
if (call.response.status() == null) { call.respond(HttpStatusCode.Forbidden)
call.respond(HttpStatusCode.Forbidden) }
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
unianswer(
AuthTokenInfo.serializer().nullable,
tokenInfo
)
} }
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
call.respond(tokenInfo)
} }
} }
} post(authGetMePathPart) {
post(authGetMePathPart) { safely(
safely( {
{ // TODO:: add error info
// TODO:: add error info call.respond(
call.respond( HttpStatusCode.InternalServerError,
HttpStatusCode.InternalServerError, "Something went wrong"
"Something went wrong" )
}
) {
unianswer(
User.serializer().nullable,
authFeature.getMe(
uniload(AuthToken.serializer())
)
) )
} }
) {
call.respond(
authFeature.getMe(call.receive()) ?: HttpStatusCode.NoContent
)
} }
} }
} }
} }
override fun AuthenticationConfig.invoke() { override fun Authentication.Configuration.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.server.sessions.* import io.ktor.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 SessionsConfig.invoke() { override fun Sessions.Configuration.invoke() {
cookie<AuthToken>(tokenSessionKey) { cookie<AuthToken>(tokenSessionKey) {
cookie.maxAgeInSeconds = maxAgeInSeconds cookie.maxAgeInSeconds = maxAgeInSeconds
serializer = object : SessionSerializer<AuthToken> { serializer = object : SessionSerializer<AuthToken> {

View File

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

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

View File

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

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

View File

@ -1,17 +0,0 @@
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,13 +13,8 @@ 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 libs.ktor.client.auth api "io.ktor:ktor-client-auth:$ktor_version"
api libs.ktor.client.logging api "io.ktor:ktor-client-logging:$ktor_version"
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
class AdditionalModules { object 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,8 +8,4 @@ class 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,71 +0,0 @@
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

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

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

View File

@ -2,27 +2,6 @@ package dev.inmo.postssystem.features.common.common
import org.koin.core.module.Module import org.koin.core.module.Module
interface ModuleLoader { fun 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 +1,4 @@
package dev.inmo.postssystem.features.common.common.ui package dev.inmo.postssystem.features.common.common
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
interface UIView {
}

View File

@ -1,4 +1,4 @@
package dev.inmo.postssystem.features.common.common.ui package dev.inmo.postssystem.features.common.common
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -7,7 +7,9 @@ interface UIViewModel<StateType> {
val currentState: StateFlow<StateType> val currentState: StateFlow<StateType>
} }
abstract class AbstractUIViewModel<StateType>(initState: StateType) : UIViewModel<StateType> { abstract class AbstractUIViewModel<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 +0,0 @@
package dev.inmo.postssystem.features.common.common.ui
interface UIView {
}

View File

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

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

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

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

@ -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 libs.koin.core api "io.insert-koin:koin-core:$koin_version"
api libs.uuid api "com.benasher44:uuid:$uuid_version"
api libs.ktor.http api "io.ktor:ktor-http:$ktor_version"
} }
} }
jvmMain { jvmMain {
dependencies { dependencies {
api libs.kotlin.reflect api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
} }
} }
androidMain { androidMain {
dependencies { dependencies {
api libs.kotlin.reflect api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
} }
} }
} }

View File

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

View File

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

View File

@ -1,13 +1,14 @@
package dev.inmo.postssystem.features.common.server package dev.inmo.postssystem.features.common.server.sessions
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.server.application.Application import io.ktor.application.Application
import io.ktor.server.auth.* import io.ktor.auth.Authentication
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 AuthenticationConfig.invoke() } fun interface Element { operator fun Authentication.Configuration.invoke() }
override fun Application.configure() { override fun Application.configure() {
authentication { authentication {

View File

@ -1,9 +1,8 @@
package dev.inmo.postssystem.features.common.server package dev.inmo.postssystem.features.common.server.sessions
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 package dev.inmo.postssystem.features.common.server.sessions
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,20 +1,17 @@
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.* import dev.inmo.jsuikit.elements.DefaultButton
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.Default.addModule(this) AdditionalModules.addModule(this)
} }
override fun Module.load() { override fun Module.load() {
@ -44,23 +41,4 @@ 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.Qualifiers import dev.inmo.postssystem.features.common.server.sessions.Qualifiers
import dev.inmo.postssystem.features.common.server.ServerModuleLoader import dev.inmo.postssystem.features.common.server.sessions.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

@ -1,11 +0,0 @@
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,7 +10,4 @@ interface ContentClientProvider {
@Composable @Composable
fun renderNewInstance(state: MutableState<Content?>) fun renderNewInstance(state: MutableState<Content?>)
@Composable
fun renderPreview(content: Content): Boolean
} }

View File

@ -1,6 +1,7 @@
package dev.inmo.postssystem.features.content.common package dev.inmo.postssystem.features.content.common
import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.common.FileName
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
@ -29,18 +30,6 @@ 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,20 +1,18 @@
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.Default.addModule(this) AdditionalModules.addModule(this)
} }
override fun Module.load() { override fun Module.load() {
@ -38,17 +36,4 @@ 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.ServerModuleLoader import dev.inmo.postssystem.features.common.server.sessions.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,7 +12,6 @@ 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,7 +8,6 @@ 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
@ -25,14 +24,15 @@ 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(fullFilesGetBytesPath) { override suspend fun getBytes(id: FileId): ByteArray = client.post<HttpResponse>(fullFilesGetBytesPath) {
setBody(serialFormat.encodeToByteArray(FileId.serializer(), id)) body = serialFormat.encodeToByteArray(FileId.serializer(), id)
}.readBytes() }.readBytes()
override suspend fun getFullFileInfo( override suspend fun getFullFileInfo(

View File

@ -1,9 +0,0 @@
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,6 +12,10 @@ 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,47 +4,58 @@ 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.* import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
import io.ktor.http.HttpStatusCode import dev.inmo.micro_utils.repos.ktor.server.crud.configureWriteStandardCrudRepoRoutes
import io.ktor.http.decodeURLQueryComponent import io.ktor.application.call
import io.ktor.server.application.call import io.ktor.auth.authenticate
import io.ktor.server.auth.authenticate import io.ktor.response.respondBytes
import io.ktor.server.request.receive import io.ktor.routing.*
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) : this(filesStorage, filesStorage) constructor(filesStorage: FilesStorage, unifierRouter: UnifiedRouter) : this(filesStorage, filesStorage, unifierRouter)
override fun Route.invoke() { override fun Route.invoke() {
authenticate { authenticate {
route(filesRootPathPart) { route(filesRootPathPart) {
configureReadCRUDRepoRoutes( configureReadStandardCrudRepoRoutes(
filesStorage, filesStorage,
::FileId MetaFileInfoStorageWrapper.serializer(),
MetaFileInfoStorageWrapper.serializer().nullable,
FileId.serializer(),
unifierRouter
) )
writeFilesStorage ?.let { writeFilesStorage ?.let {
configureWriteCRUDRepoRoutes(writeFilesStorage) configureWriteStandardCrudRepoRoutes(
writeFilesStorage,
FullFileInfoStorageWrapper.serializer(),
FullFileInfoStorageWrapper.serializer().nullable,
FullFileInfo.serializer(),
FileId.serializer(),
unifierRouter
)
} }
post(filesGetFilesPathPart) { unifierRouter.apply {
call.respondBytes( post(filesGetFilesPathPart) {
filesStorage.getBytes( call.respondBytes(
call.receive() filesStorage.getBytes(
uniload(FileId.serializer())
)
) )
) }
} get(filesGetFullFileInfoPathPart) {
get(filesGetFullFileInfoPathPart) { unianswer(
call.respond( FullFileInfoStorageWrapper.serializer().nullable,
filesStorage.getFullFileInfo( filesStorage.getFullFileInfo(
FileId(call.getParameterOrSendError(filesFileIdParameter) ?.decodeURLQueryComponent() ?: return@get) decodeUrlQueryValueOrSendError(filesFileIdParameter, FileId.serializer()) ?: return@get
) ?: HttpStatusCode.NoContent )
) )
}
} }
} }
} }

View File

@ -2,9 +2,7 @@ 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
@ -25,8 +23,8 @@ typealias ContentIds = List<ContentId>
* @see RegisteredPost * @see RegisteredPost
*/ */
@Serializable @Serializable
sealed interface Post { sealed class Post {
val content: ContentIds abstract val content: ContentIds
} }
/** /**
@ -35,7 +33,7 @@ sealed interface Post {
@Serializable @Serializable
data class NewPost( data class NewPost(
override val content: ContentIds override val content: ContentIds
) : Post ) : Post()
/** /**
* Registered [Post] * Registered [Post]
@ -46,16 +44,4 @@ 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,7 +12,6 @@ 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 client: HttpClient, private val unifiedRequester: UnifiedRequester,
private val serializer: KSerializer<T> private val serializer: KSerializer<T>
) : RolesStorage<T>, ) : RolesStorage<T>,
ReadRolesStorage<T> by ReadClientRolesStorage( ReadRolesStorage<T> by ReadClientRolesStorage(
baseUrl, client, serializer baseUrl, unifiedRequester, serializer
), ),
WriteRolesStorage<T> by WriteClientRolesStorage( WriteRolesStorage<T> by WriteClientRolesStorage(
baseUrl, client, serializer baseUrl, unifiedRequester, serializer
) )

View File

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

@ -1,11 +0,0 @@
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,15 +3,13 @@ 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 client: HttpClient, private val unifiedRequester: UnifiedRequester,
private val serializer: KSerializer<T> private val serializer: KSerializer<T>
) : ReadRolesStorage<T> { ) : ReadRolesStorage<T> {
private val userRolesSerializer = ListSerializer(serializer) private val userRolesSerializer = ListSerializer(serializer)
@ -23,13 +21,14 @@ class ReadClientRolesStorage<T : Role>(
override suspend fun getSubjects( override suspend fun getSubjects(
role: T role: T
): List<RoleSubject> = client.get( ): List<RoleSubject> = unifiedRequester.uniget(
buildStandardUrl( buildStandardUrl(
userRolesFullUrl, userRolesFullUrl,
usersRolesGetSubjectsPathPart, usersRolesGetSubjectsPathPart,
usersRolesRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, role) usersRolesRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, role)
) ),
).body() RoleSubjectsSerializer
)
override suspend fun getRoles( override suspend fun getRoles(
subject: RoleSubject subject: RoleSubject

View File

@ -1,8 +0,0 @@
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 client: HttpClient, private val unifiedRequester: UnifiedRequester,
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,16 +6,17 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.* import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@Polymorphic @Serializable(RoleSerializer::class)
interface Role { interface Role {
companion object { companion object {
fun serializer(): KSerializer<Role> = RoleSerializer fun serializer() = 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,16 +3,30 @@ 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 @Serializable(RolesManagerRoleSerializer::class)
sealed interface RolesManagerRole : Role 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 val justForLoading = GeneralRolesManagerRole.serializer() private const val KEY = "roles_manager"
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.server.application.ApplicationCall import io.ktor.application.ApplicationCall
object RolesManagerRolesChecker : RolesChecker<Role> { object RolesManagerRolesChecker : RolesChecker<Role> {
override val key: String override val key: String

View File

@ -1,14 +1,18 @@
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,
RolesManagerRole.serializer(), RolesManagerRoleSerializer,
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.server.application.ApplicationCall import io.ktor.application.ApplicationCall
interface RolesChecker<T : Role> { interface RolesChecker<T : Role> {
val key: String val key: String

View File

@ -3,8 +3,9 @@ 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.server.auth.authenticate import io.ktor.application.call
import io.ktor.server.routing.* import io.ktor.auth.authenticate
import io.ktor.routing.*
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
@ -12,7 +13,8 @@ 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,19 +3,21 @@ 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.ApplicationAuthenticationConfigurator import dev.inmo.postssystem.features.common.server.sessions.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.server.auth.* import io.ktor.response.respond
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 AuthenticationConfig.invoke() { override fun Authentication.Configuration.invoke() {
rolesCheckers.forEach { checker -> rolesCheckers.forEach { checker ->
session<AuthToken>(checker.key) { session<AuthToken>(checker.key) {
validate { validate {

View File

@ -3,15 +3,16 @@ 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.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.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(fullStatusUrl).status == HttpStatusCode.OK suspend fun checkServerStatus() = client.get<HttpResponse>(fullStatusUrl).status == HttpStatusCode.OK
suspend fun checkServerStatusWithAuth() = client.get(fullAuthorisedStatusUrl).status == HttpStatusCode.OK suspend fun checkServerStatusWithAuth() = client.get<HttpResponse>(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.server.application.call import io.ktor.response.respond
import io.ktor.server.auth.authenticate import io.ktor.routing.*
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,7 +12,6 @@ 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")
} }
} }
} }

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