Compare commits
23 Commits
72578f6b58
...
reborn/2
| Author | SHA1 | Date | |
|---|---|---|---|
| 546736690c | |||
| e98a484c4d | |||
| ff973e63fc | |||
| 399405a4fb | |||
| b7e5e2745d | |||
| 92cd98b80f | |||
| b73df49925 | |||
| f835dc1557 | |||
| 5bbe0b8a0e | |||
| 9216c013ec | |||
| c238f391a8 | |||
| dd369177c1 | |||
| 6c094f3ad9 | |||
| 84aea630c3 | |||
| 55d0e1b55f | |||
| a3ff08af27 | |||
| 7551d7c2bb | |||
| b41a18a01e | |||
| a993459b97 | |||
| 37114f0ddb | |||
| 51cdfb320b | |||
| 90293d7ffc | |||
| cb5de073fb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.idea
|
||||
.kotlin
|
||||
out/*
|
||||
*.iml
|
||||
target
|
||||
|
||||
14
README.md
14
README.md
@@ -1,12 +1,14 @@
|
||||
## Структура проекта
|
||||
|
||||
* **Features** - набор **законченных** фич проекта. Считается, что любая фича, находящаяся в мастере может быть добавлена в
|
||||
клиент и использована в нем. Исключением является `common` - это набор вещей, используемых везде. В подпунктах представлены
|
||||
части, на которые *обычно* разделяется фича
|
||||
* Common - общая для фичи часть. Тут, как правило, хранятся конвенции путей для сетевых соединений, общие типы и пр.
|
||||
* Server - часть, включаемая в сервер для подключения фичи. Обычно содержит работу с бд, определение модулей сервера и пр.
|
||||
* Client - часть с клиентским кодом. В большинстве своём включает работу с сервером, MVVM часть (View при этом должны
|
||||
находиться в платформенной части, если их нельзя вынести в сommon часть клиента)
|
||||
клиент и использована в нем. Исключением является `common` - это набор вещей, используемых везде.
|
||||
* Части, на которые *обычно* разделяется фича
|
||||
* Common - общая для фичи часть. Тут, как правило, хранятся конвенции путей для сетевых соединений, общие типы и пр.
|
||||
* Server - часть, включаемая в сервер для подключения фичи. Обычно содержит работу с бд, определение модулей сервера и пр.
|
||||
* Client - часть с клиентским кодом. В большинстве своём включает работу с сервером, MVVM часть (View при этом должны
|
||||
находиться в платформенной части, если их нельзя вынести в сommon часть клиента)
|
||||
* Также существует фича `client`, которая не является фичей самой по-себе. Фактически, это набор разных фич, который системно
|
||||
отсутствуют в бэке и используются в основном для пробрасывания удобных `API` и `View` для клиента
|
||||
* **Services** - модули, отвечающие за клиент-серверную работу фич с точки зрения их взаимодействия. Например, в рамках сервисов
|
||||
должен быть добавлен модуль для постов - именно через сервисы будет происходить создание поста, его редактирование и удаление
|
||||
* **Client** - итоговый клиент. На момент написания этой доки (`Пн окт 25 12:56:41 +06 2021`) предполагается два варианта:
|
||||
|
||||
14
build.gradle
14
build.gradle
@@ -4,14 +4,16 @@ buildscript {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
classpath libs.buildscript.jb.dokka
|
||||
classpath libs.buildscript.gh.release
|
||||
classpath libs.buildscript.android.gradle
|
||||
classpath libs.buildscript.android.dexcount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +22,8 @@ allprojects {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.kt.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
@@ -32,9 +33,6 @@ kotlin {
|
||||
api project(":postssystem.features.content.binary.client")
|
||||
|
||||
api project(":postssystem.services.posts.client")
|
||||
|
||||
api libs.microutils.fsm.common
|
||||
api libs.microutils.fsm.repos.common
|
||||
api libs.microutils.crypto
|
||||
|
||||
implementation compose.runtime
|
||||
@@ -43,7 +41,7 @@ kotlin {
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-client-apache:$ktor_version"
|
||||
api libs.ktor.client.apache
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.postssystem.features.auth.client.installClientAuthenticator
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.files.client.ClientReadFilesStorage
|
||||
import dev.inmo.postssystem.features.files.common.storage.ReadFilesStorage
|
||||
import dev.inmo.postssystem.features.roles.common.Role
|
||||
import dev.inmo.postssystem.features.roles.common.RolesStorage
|
||||
import dev.inmo.postssystem.features.roles.client.ClientRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer
|
||||
import dev.inmo.postssystem.features.users.client.UsersStorageKtorClient
|
||||
import dev.inmo.postssystem.features.users.common.ReadUsersStorage
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import dev.inmo.micro_utils.common.Either
|
||||
import dev.inmo.micro_utils.coroutines.LinkedSupervisorScope
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.postssystem.client.settings.DefaultSettings
|
||||
import dev.inmo.postssystem.client.settings.Settings
|
||||
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
||||
import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings
|
||||
import dev.inmo.postssystem.features.common.common.*
|
||||
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
|
||||
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
|
||||
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
|
||||
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
|
||||
import dev.inmo.postssystem.publicators.simple.client.SimplePublicatorService
|
||||
import dev.inmo.postssystem.publicators.simple.client.SimplePublicatorServiceClient
|
||||
import dev.inmo.postssystem.services.posts.client.ClientPostsService
|
||||
import dev.inmo.postssystem.services.posts.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.StringFormat
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.*
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.binds
|
||||
import org.koin.dsl.module
|
||||
|
||||
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
|
||||
val SettingsQualifier = StringQualifier("Settings")
|
||||
val RolesQualifier = StringQualifier("Roles")
|
||||
private val FSMHandlersBuilderQualifier = StringQualifier("FSMHandlersBuilder")
|
||||
|
||||
val defaultSerialFormat = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint for getting [org.koin.core.Koin] DI for the client
|
||||
*
|
||||
* @param repoFactory Factory for creating of [DefaultStatesManagerRepo] for [dev.inmo.postssystem.client.ui.fsm.UIFSM]
|
||||
*/
|
||||
fun baseKoin(
|
||||
defaultScope: CoroutineScope,
|
||||
settingsFactory: Scope.() -> KeyValueRepo<String, Any>,
|
||||
repoFactory: Scope.() -> DefaultStatesManagerRepo<UIFSMState>,
|
||||
handlersSetter: Pair<Scope, FSMBuilder<UIFSMState>>.() -> Unit
|
||||
): Koin = startKoin {
|
||||
modules(
|
||||
module {
|
||||
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
|
||||
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
|
||||
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
|
||||
single { SerializersModuleConfigurator(getAll()) }
|
||||
|
||||
single {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
|
||||
}
|
||||
}
|
||||
single<StringFormat> { get<Json>() }
|
||||
|
||||
single(SettingsQualifier) { settingsFactory() }
|
||||
single { DBDropper(get(SettingsQualifier)) }
|
||||
single(FSMHandlersBuilderQualifier) { handlersSetter }
|
||||
single { repoFactory() }
|
||||
single { defaultScope }
|
||||
single(UIScopeQualifier) { get<CoroutineScope>().LinkedSupervisorScope(Dispatchers.Main) }
|
||||
single<StatesMachine<UIFSMState>>(UIFSMQualifier) { UIFSM(get()) { (this@single to this@UIFSM).apply(get(
|
||||
FSMHandlersBuilderQualifier
|
||||
)) } }
|
||||
single<AuthSettings> { DefaultAuthSettings(get(SettingsQualifier), get(), koin, get()) }
|
||||
single<Settings> { DefaultSettings(get()) }
|
||||
|
||||
AdditionalModules.modules.forEach {
|
||||
it.apply { load() }
|
||||
}
|
||||
}
|
||||
)
|
||||
}.koin.apply {
|
||||
loadModules(
|
||||
listOf(
|
||||
module { single<Koin> { this@apply } }
|
||||
)
|
||||
)
|
||||
RolesManagerRoleSerializer // Just to activate it in JS client
|
||||
}
|
||||
|
||||
fun getAuthorizedFeaturesDIModule(
|
||||
serverUrl: String,
|
||||
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
|
||||
onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit,
|
||||
onUserRetrieved: suspend (User?) -> Unit,
|
||||
onAuthKeyInvalidated: suspend () -> Unit
|
||||
): Module {
|
||||
val serverUrlQualifier = StringQualifier("serverUrl")
|
||||
val credsQualifier = StringQualifier("creds")
|
||||
|
||||
return module {
|
||||
single(createdAtStart = true) {
|
||||
HttpClient {
|
||||
installClientAuthenticator(serverUrl, get(), get(credsQualifier), onAuthKeyUpdated, onUserRetrieved, onAuthKeyInvalidated)
|
||||
}
|
||||
}
|
||||
single(credsQualifier) { initialAuthKey }
|
||||
single(serverUrlQualifier) { serverUrl }
|
||||
single<BinaryFormat> {
|
||||
Cbor {
|
||||
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
|
||||
}
|
||||
}
|
||||
single { UnifiedRequester(get(), get()) }
|
||||
|
||||
single { StatusFeatureClient(get(serverUrlQualifier), get()) }
|
||||
|
||||
single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) }
|
||||
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
|
||||
single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) }
|
||||
single<PostsService> { ClientPostsService(get(serverUrlQualifier), get()) } binds arrayOf(
|
||||
ReadPostsService::class,
|
||||
WritePostsService::class
|
||||
)
|
||||
single<SimplePublicatorService> { SimplePublicatorServiceClient(get(serverUrlQualifier), get()) }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package dev.inmo.postssystem.client.settings
|
||||
|
||||
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
||||
|
||||
|
||||
data class DefaultSettings(
|
||||
override val authSettings: AuthSettings
|
||||
) : Settings
|
||||
@@ -1,12 +0,0 @@
|
||||
package dev.inmo.postssystem.client.settings
|
||||
|
||||
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koin.core.module.Module
|
||||
|
||||
interface Settings {
|
||||
val authSettings: AuthSettings
|
||||
|
||||
val authorizedDIModule: StateFlow<Module?>
|
||||
get() = authSettings.authorizedDIModule
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package dev.inmo.postssystem.client.ui.fsm
|
||||
|
||||
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
|
||||
interface UIFSMHandler<T : UIFSMState> : StatesHandler<T, UIFSMState> {
|
||||
suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState?
|
||||
override suspend fun StatesMachine<in UIFSMState>.handleState(state: T): UIFSMState? {
|
||||
return runCatching {
|
||||
safeHandleState(state).also(::println)
|
||||
}.getOrElse {
|
||||
errorToNextStep(state, it) ?.let { return it } ?: throw it
|
||||
}.also(::println)
|
||||
}
|
||||
|
||||
suspend fun errorToNextStep(
|
||||
currentState: T,
|
||||
e: Throwable
|
||||
): UIFSMState? = when (e) {
|
||||
is AuthUnavailableException -> if (currentState is AuthUIFSMState) {
|
||||
currentState
|
||||
} else {
|
||||
AuthUIFSMState(currentState)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package dev.inmo.postssystem.client.ui.fsm
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.serialization.*
|
||||
|
||||
@Serializable(UIFSMStateSerializer::class)
|
||||
sealed interface UIFSMState : State {
|
||||
val from: UIFSMState?
|
||||
get() = null
|
||||
override val context: String
|
||||
get() = "main"
|
||||
}
|
||||
|
||||
object UIFSMStateSerializer : KSerializer<UIFSMState> by TypedSerializer(
|
||||
"auth" to AuthUIFSMState.serializer(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthUIFSMState(
|
||||
override val from: UIFSMState? = CreatePostUIFSMState(),
|
||||
override val context: String = "main"
|
||||
) : UIFSMState
|
||||
val DefaultAuthUIFSMState = AuthUIFSMState()
|
||||
|
||||
@Serializable
|
||||
data class CreatePostUIFSMState(
|
||||
override val from: UIFSMState? = null,
|
||||
override val context: String = "main"
|
||||
) : UIFSMState
|
||||
@@ -1,31 +1,24 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.fsm.ui.*
|
||||
import dev.inmo.postssystem.client.ui.*
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMStateSerializer
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIModel
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIViewModel
|
||||
import dev.inmo.postssystem.features.auth.common.AuthTokenInfo
|
||||
import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler
|
||||
import dev.inmo.micro_utils.fsm.common.CheckableHandlerHolder
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||
import dev.inmo.postssystem.features.common.common.baseKoin
|
||||
import dev.inmo.postssystem.features.common.common.getAllDistinct
|
||||
import dev.inmo.postssystem.services.posts.client.ui.create.*
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMHandler
|
||||
import dev.inmo.postssystem.services.posts.client.ui.list.PostsListUIFSMState
|
||||
import dev.inmo.postssystem.services.posts.client.ui.list.PostsListUIState
|
||||
import kotlinx.browser.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import org.koin.core.Koin
|
||||
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.w3c.dom.HTMLElement
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val defaultTypedSerializer = TypedSerializer<Any>(
|
||||
"AuthTokenInfo" to AuthTokenInfo.serializer(),
|
||||
@@ -35,9 +28,11 @@ val defaultTypedSerializer = TypedSerializer<Any>(
|
||||
"Short" to Short.serializer(),
|
||||
"Byte" to Byte.serializer(),
|
||||
"Float" to Float.serializer(),
|
||||
"Double" to Double.serializer(),
|
||||
"UIFSMState" to UIFSMStateSerializer
|
||||
"Double" to Double.serializer()
|
||||
)
|
||||
val defaultSerialFormat = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
fun baseKoin(): Koin {
|
||||
val anyToString: suspend Any.() -> String = {
|
||||
@@ -78,69 +73,22 @@ fun baseKoin(): Koin {
|
||||
)
|
||||
},
|
||||
{
|
||||
JSUIFSMStatesRepo(window.history)
|
||||
JSUIFSMStatesRepo(window.history, AuthUIFSMState(PostsListUIFSMState()), getAllDistinct())
|
||||
}
|
||||
) {
|
||||
val scope = first
|
||||
first.apply {
|
||||
second.apply {
|
||||
loadKoinModules(
|
||||
module {
|
||||
factory { document.getElementById("main") as HTMLElement }
|
||||
|
||||
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
|
||||
factory { AuthUIViewModel(get()) }
|
||||
factory { AuthView(get(), get(UIScopeQualifier)) }
|
||||
|
||||
factory<PostCreateUIModel> { DefaultPostCreateUIModel(get(), get()) }
|
||||
factory { PostCreateUIViewModel(get()) }
|
||||
factory { PostCreateView(get(), getAllDistinct(), get(UIScopeQualifier)) }
|
||||
}
|
||||
)
|
||||
strictlyOn<AuthUIFSMState>(get<AuthView>())
|
||||
|
||||
// Костыль, в 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)
|
||||
}
|
||||
getAllDistinct<UIFSMHandler.Registrator>().forEach {
|
||||
with(it) {
|
||||
include()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
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.serialization.StringFormat
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.url.URL
|
||||
|
||||
private fun History.refreshHistory(
|
||||
states: Iterable<UIFSMState>,
|
||||
fillers: List<UIFSMStateSearchParamsHandler>
|
||||
) {
|
||||
val currentUrl = window.location.pathname
|
||||
val params = states.mapNotNull<UIFSMState, Pair<String, String>> {
|
||||
when (it) {
|
||||
is AuthUIFSMState -> null
|
||||
is CreatePostUIFSMState -> null
|
||||
val currentParams = (URL(window.location.href)).searchParams
|
||||
val params = states.flatMap<UIFSMState, Pair<String, String>> { state ->
|
||||
fillers.flatMap {
|
||||
it.takeParams(state, currentParams)
|
||||
}
|
||||
}
|
||||
pushState(
|
||||
@@ -24,9 +24,11 @@ private fun History.refreshHistory(
|
||||
)
|
||||
}
|
||||
|
||||
private fun takeStates(initialState: UIFSMState): List<UIFSMState> {
|
||||
private fun takeStates(initialState: UIFSMState, fillers: List<UIFSMStateSearchParamsHandler>): List<UIFSMState> {
|
||||
val params = (URL(window.location.href)).searchParams
|
||||
val additionalStates = listOfNotNull<UIFSMState>()
|
||||
val additionalStates = fillers.mapNotNull {
|
||||
it.takeState(params)
|
||||
}
|
||||
|
||||
return additionalStates + listOfNotNull(
|
||||
if (additionalStates.isEmpty()) {
|
||||
@@ -41,12 +43,13 @@ private fun takeStates(initialState: UIFSMState): List<UIFSMState> {
|
||||
|
||||
class JSUIFSMStatesRepo(
|
||||
private val history: History,
|
||||
private val initialState: UIFSMState = DefaultAuthUIFSMState
|
||||
private val initialState: UIFSMState,
|
||||
private val fillers: List<UIFSMStateSearchParamsHandler>
|
||||
) : DefaultStatesManagerRepo<UIFSMState> {
|
||||
private val statesMap = mutableMapOf<String, UIFSMState>()
|
||||
|
||||
init {
|
||||
val states = takeStates(initialState)
|
||||
val states = takeStates(initialState, fillers)
|
||||
states.forEach {
|
||||
statesMap[it.context] = it
|
||||
}
|
||||
@@ -68,12 +71,12 @@ class JSUIFSMStatesRepo(
|
||||
|
||||
override suspend fun removeState(state: UIFSMState) {
|
||||
statesMap.remove((state.context as? String) ?: return)
|
||||
history.refreshHistory(statesMap.values)
|
||||
history.refreshHistory(statesMap.values, fillers)
|
||||
}
|
||||
|
||||
override suspend fun set(state: UIFSMState) {
|
||||
console.log(state)
|
||||
statesMap[state.context] = state
|
||||
history.refreshHistory(statesMap.values)
|
||||
history.refreshHistory(statesMap.values, fillers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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.postssystem.features.common.common.DefaultQualifiers
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun main() {
|
||||
window.addEventListener("load", {
|
||||
val koin = baseKoin()
|
||||
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(UIFSMQualifier)
|
||||
val uiStatesMachine = koin.get<StatesMachine<UIFSMState>>(DefaultQualifiers.UIFSMQualifier)
|
||||
uiStatesMachine.start(koin.get())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.client
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
|
||||
import org.w3c.dom.url.URLSearchParams
|
||||
|
||||
interface UIFSMStateSearchParamsHandler {
|
||||
fun takeParams(state: UIFSMState, currentParams: URLSearchParams): List<Pair<String, String>>
|
||||
fun takeState(params: URLSearchParams): UIFSMState?
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.Element
|
||||
|
||||
val mainContainer: Element
|
||||
get() = document.getElementById("main")!!
|
||||
@@ -1,19 +0,0 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.FullFileInfo
|
||||
import dev.inmo.micro_utils.common.toArrayBuffer
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
import org.w3c.dom.url.URL
|
||||
import org.w3c.files.Blob
|
||||
|
||||
fun triggerDownloadFile(fullFileInfo: FullFileInfo) {
|
||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||
|
||||
val url = URL.createObjectURL(Blob(arrayOf(fullFileInfo.inputProvider().readBytes().toArrayBuffer())))
|
||||
hiddenElement.href = url
|
||||
hiddenElement.target = "_blank"
|
||||
hiddenElement.download = fullFileInfo.name.name
|
||||
hiddenElement.click()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun Composition.linkWithJob(job: Job) {
|
||||
job.invokeOnCompletion {
|
||||
this@linkWithJob.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(
|
||||
currentCoroutineContext()
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package dev.inmo.postssystem.client.utils
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import dev.inmo.postssystem.features.files.common.FullFileInfo
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
|
||||
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
|
||||
import dev.inmo.postssystem.features.common.common.BytesBasedInputProvider
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.get
|
||||
|
||||
fun uploadFileCallbackForHTMLInputChange(
|
||||
onSet: (FullFileInfo) -> Unit
|
||||
): (Event) -> Unit = {
|
||||
(it.target as? HTMLInputElement) ?.apply {
|
||||
files ?.also { files ->
|
||||
files[0] ?.also { file ->
|
||||
val reader: FileReader = FileReader()
|
||||
|
||||
reader.onload = {
|
||||
val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray()
|
||||
onSet(
|
||||
FullFileInfo(
|
||||
FileName(file.name),
|
||||
findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any,
|
||||
BytesBasedInputProvider(bytes)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fileCallbackForHTMLInputChange(
|
||||
onSet: (MPPFile) -> Unit
|
||||
): (Event) -> Unit = {
|
||||
(it.target as? HTMLInputElement) ?.apply {
|
||||
files ?.also { files ->
|
||||
files[0] ?.also { file ->
|
||||
onSet(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadFileCallbackForHTMLInputChange(
|
||||
output: MutableState<FullFileInfo?>
|
||||
): (Event) -> Unit = uploadFileCallbackForHTMLInputChange {
|
||||
output.value = it
|
||||
}
|
||||
|
||||
fun uploadFileCallbackForHTMLInputChange(
|
||||
output: MutableStateFlow<FullFileInfo?>
|
||||
): (Event) -> Unit = uploadFileCallbackForHTMLInputChange {
|
||||
output.value = it
|
||||
}
|
||||
|
||||
fun fileCallbackForHTMLInputChange(
|
||||
output: MutableState<MPPFile?>
|
||||
): (Event) -> Unit = fileCallbackForHTMLInputChange {
|
||||
output.value = it
|
||||
}
|
||||
|
||||
fun fileCallbackForHTMLInputChange(
|
||||
output: MutableStateFlow<MPPFile?>
|
||||
): (Event) -> Unit = fileCallbackForHTMLInputChange {
|
||||
output.value = it
|
||||
}
|
||||
|
||||
@@ -58,5 +58,6 @@
|
||||
"semver": "^7.3.2",
|
||||
"svgo": "^2.8.0",
|
||||
"watch-run": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
|
||||
}
|
||||
|
||||
@@ -1,39 +1,16 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
android {
|
||||
ext {
|
||||
jvmKotlinFolderFile = {
|
||||
String sep = File.separator
|
||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
if (jvmKotlinFolder.exists()) {
|
||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||
android.sourceSets.main.java.srcDirs = []
|
||||
for (oldDir in oldDirs) {
|
||||
if (oldDir != jvmKotlinFolder.path) {
|
||||
android.sourceSets.main.java.srcDirs += oldDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
namespace "${project.group}.${project.name}"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -51,18 +28,7 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sep = File.separator
|
||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ allprojects {
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.kt.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
@@ -11,6 +13,7 @@ kotlin {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.client")
|
||||
api project(":postssystem.features.status.client")
|
||||
api project(":postssystem.features.auth.common")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import dev.inmo.micro_utils.common.Either
|
||||
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 { StatusFeatureClient(get(AuthorizedQualifiers.ServerUrlQualifier), get()) }
|
||||
|
||||
AdditionalModules.Authorized.modules.forEach {
|
||||
with(it) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.AdditionalModules
|
||||
import dev.inmo.postssystem.features.common.common.ModuleLoader
|
||||
import org.koin.core.module.Module
|
||||
|
||||
private val AuthorizedAdditionalModules = AdditionalModules()
|
||||
val AdditionalModules.Companion.Authorized: AdditionalModules
|
||||
get() = AuthorizedAdditionalModules
|
||||
|
||||
fun AuthorizedModuleLoader(loadingBlock: Module.() -> Unit): ModuleLoader.ByCallback {
|
||||
val newModuleLoader = ModuleLoader.ByCallback(loadingBlock)
|
||||
AdditionalModules.Authorized.addModule(newModuleLoader)
|
||||
return newModuleLoader
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
|
||||
object AuthorizedQualifiers {
|
||||
val CredsQualifier = StringQualifier("creds")
|
||||
val ServerUrlQualifier = StringQualifier("serverUrl")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.CheckableHandlerHolder
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
|
||||
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIFSMState
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMHandler
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.parameter.ParametersHolder
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
import org.koin.core.scope.Scope
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
// Костыль, в JS на момент Пн дек 6 14:19:29 +06 2021 если использовать strictlyOn генерируются
|
||||
// некорректные безымянные классы (у них отсутствует метод handleState)
|
||||
class DefaultStateHandlerWrapper<T : UIFSMState>(
|
||||
private val klass: KClass<out UIFSMHandler<T>>,
|
||||
private val koin: Koin,
|
||||
private val stateKlass: KClass<T>,
|
||||
private val qualifier: Qualifier? = null,
|
||||
private val parameters: ((T) -> ParametersHolder)? = null
|
||||
) : CheckableHandlerHolder<UIFSMState, UIFSMState> {
|
||||
override suspend fun StatesMachine<in UIFSMState>.handleState(state: UIFSMState): UIFSMState? {
|
||||
@Suppress("UNCHECKED_CAST", "NAME_SHADOWING")
|
||||
val state = state as T
|
||||
return runCatching {
|
||||
val authSettings = koin.get<AuthSettings>()
|
||||
authSettings.loadingJob.join()
|
||||
if (authSettings.authorizedDIModule.value == null) {
|
||||
error("Can't perform state $state: Auth module was not initialized")
|
||||
} else {
|
||||
koin.get<UIFSMHandler<T>>(klass, qualifier, parameters ?.let { { it(state) } }).run {
|
||||
handleState(state)
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
e.printStackTrace()
|
||||
AuthUIFSMState(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkHandleable(state: UIFSMState): Boolean = stateKlass.isInstance(state)
|
||||
}
|
||||
inline fun <reified T : UIFSMState> FSMBuilder<UIFSMState>.registerAfterAuthHandler(
|
||||
koin: Koin,
|
||||
klass: KClass<out UIFSMHandler<T>>,
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ((T) -> ParametersHolder)? = null
|
||||
) = add(
|
||||
DefaultStateHandlerWrapper<T>(
|
||||
klass,
|
||||
koin,
|
||||
T::class,
|
||||
qualifier,
|
||||
parameters
|
||||
)
|
||||
)
|
||||
@@ -2,14 +2,14 @@ package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.client.bodyOrNull
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class ClientAuthFeature(
|
||||
private val requester: UnifiedRequester,
|
||||
private val client: HttpClient,
|
||||
baseUrl: String
|
||||
) : AuthFeature {
|
||||
private val rootUrl = buildStandardUrl(baseUrl.dropLastWhile { it == '/' }, authRootPathPart)
|
||||
@@ -26,28 +26,23 @@ class ClientAuthFeature(
|
||||
authGetMePathPart
|
||||
)
|
||||
|
||||
constructor(client: HttpClient, rootUrl: String): this(
|
||||
UnifiedRequester(client),
|
||||
rootUrl
|
||||
)
|
||||
override suspend fun auth(creds: AuthCreds): AuthTokenInfo? = client.post(
|
||||
fullAuthPath
|
||||
) {
|
||||
setBody(creds)
|
||||
}.bodyOrNull()
|
||||
|
||||
override suspend fun auth(creds: AuthCreds): AuthTokenInfo? = requester.unipost(
|
||||
fullAuthPath,
|
||||
AuthCreds.serializer() to creds,
|
||||
AuthTokenInfo.serializer().nullable
|
||||
)
|
||||
override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? = client.post(
|
||||
fullRefreshPath
|
||||
) {
|
||||
setBody(refresh)
|
||||
}.bodyOrNull()
|
||||
|
||||
override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? = requester.unipost(
|
||||
fullRefreshPath,
|
||||
RefreshToken.serializer() to refresh,
|
||||
AuthTokenInfo.serializer().nullable
|
||||
)
|
||||
|
||||
override suspend fun getMe(authToken: AuthToken): User? = requester.unipost(
|
||||
fullGetMePath,
|
||||
AuthToken.serializer() to authToken,
|
||||
User.serializer().nullable
|
||||
)
|
||||
override suspend fun getMe(authToken: AuthToken): User? = client.post(
|
||||
fullGetMePath
|
||||
) {
|
||||
setBody(authToken)
|
||||
}.bodyOrNull()
|
||||
|
||||
fun isAuthRequest(builder: HttpRequestBuilder): Boolean = builder.url.buildString().let {
|
||||
it == fullAuthPath || it == fullRefreshPath
|
||||
|
||||
@@ -5,11 +5,11 @@ import dev.inmo.postssystem.features.users.common.User
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.features.cookies.*
|
||||
import io.ktor.client.features.expectSuccess
|
||||
import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage
|
||||
import io.ktor.client.plugins.cookies.HttpCookies
|
||||
import io.ktor.client.plugins.expectSuccess
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.HttpReceivePipeline
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -25,10 +25,6 @@ fun HttpClientConfig<*>.installClientAuthenticator(
|
||||
onUserRetrieved: suspend (User?) -> Unit,
|
||||
onAuthKeyInvalidated: suspend () -> Unit
|
||||
) {
|
||||
// install(Logging) {
|
||||
// logger = Logger.DEFAULT
|
||||
// level = LogLevel.HEADERS
|
||||
// }
|
||||
install(HttpCookies) {
|
||||
// Will keep an in-memory map with all the cookies from previous requests.
|
||||
storage = AcceptAllCookiesStorage()
|
||||
@@ -41,7 +37,7 @@ fun HttpClientConfig<*>.installClientAuthenticator(
|
||||
}.onSecond {
|
||||
currentRefreshToken = it.refresh
|
||||
}
|
||||
val creds = initialAuthKey.t1 as? AuthCreds
|
||||
val creds = initialAuthKey.t1OrNull as? AuthCreds
|
||||
var userRefreshJob: Job? = null
|
||||
|
||||
install("Auth Token Refresher") {
|
||||
@@ -98,13 +94,13 @@ fun HttpClientConfig<*>.installClientAuthenticator(
|
||||
|
||||
receivePipeline.intercept(HttpReceivePipeline.Before) {
|
||||
if (
|
||||
context.request.url.toString().startsWith(baseUrl)
|
||||
&& context.response.status == HttpStatusCode.Unauthorized
|
||||
it.request.url.toString().startsWith(baseUrl)
|
||||
&& it.status == HttpStatusCode.Unauthorized
|
||||
) {
|
||||
authMutex.withLock { refreshToken() }
|
||||
val newResponse = context.client ?.request<HttpResponse>{
|
||||
takeFrom(context.request)
|
||||
} ?: return@intercept
|
||||
val newResponse = it.call.client.request {
|
||||
takeFrom(it.request)
|
||||
}
|
||||
proceedWith(newResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import dev.inmo.micro_utils.common.Optional
|
||||
import dev.inmo.micro_utils.common.optional
|
||||
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
|
||||
import dev.inmo.postssystem.features.auth.client.settings.DefaultAuthSettings
|
||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||
import dev.inmo.postssystem.features.common.common.*
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMExceptionHandler
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
|
||||
import kotlin.js.JsExport
|
||||
|
||||
internal fun CommonAuthModuleLoader() = DefaultModuleLoader {
|
||||
single<AuthSettings> { DefaultAuthSettings(get(DefaultQualifiers.SettingsQualifier), get(), getKoin(), get()) }
|
||||
|
||||
singleWithRandomQualifier {
|
||||
UIFSMExceptionHandler { currentState, exception ->
|
||||
if (exception is AuthUnavailableException) {
|
||||
Optional.presented(AuthUIFSMState(currentState))
|
||||
} else {
|
||||
Optional.absent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factory<AuthUIModel> { DefaultAuthUIModel(get(), get()) }
|
||||
factory { AuthUIViewModel(get()) }
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package dev.inmo.postssystem.client.settings.auth
|
||||
package dev.inmo.postssystem.features.auth.client.settings
|
||||
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.roles.common.Role
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -11,7 +10,6 @@ import org.koin.core.module.Module
|
||||
interface AuthSettings {
|
||||
val authorizedDIModule: StateFlow<Module?>
|
||||
val user: StateFlow<User?>
|
||||
val userRoles: StateFlow<List<Role>>
|
||||
val loadingJob: Job
|
||||
|
||||
suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError?
|
||||
@@ -1,19 +1,16 @@
|
||||
package dev.inmo.postssystem.client.settings.auth
|
||||
package dev.inmo.postssystem.features.auth.client.settings
|
||||
|
||||
import dev.inmo.postssystem.client.DBDropper
|
||||
import dev.inmo.postssystem.client.getAuthorizedFeaturesDIModule
|
||||
import dev.inmo.postssystem.features.auth.client.AuthUnavailableException
|
||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.roles.common.Role
|
||||
import dev.inmo.postssystem.features.roles.common.RolesStorage
|
||||
import dev.inmo.postssystem.features.status.client.StatusFeatureClient
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import dev.inmo.micro_utils.common.Either
|
||||
import dev.inmo.micro_utils.common.either
|
||||
import dev.inmo.micro_utils.coroutines.plus
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.postssystem.features.auth.client.createAuthorizedFeaturesDIModule
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError.AuthIncorrect
|
||||
import dev.inmo.postssystem.features.common.common.DBDropper
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koin.core.Koin
|
||||
@@ -29,8 +26,6 @@ data class DefaultAuthSettings(
|
||||
override val authorizedDIModule: StateFlow<Module?> = _authorizedDIModule.asStateFlow()
|
||||
private val _user = MutableStateFlow<User?>(null)
|
||||
override val user: StateFlow<User?> = _user.asStateFlow()
|
||||
private val _userRoles = MutableStateFlow<List<Role>>(emptyList())
|
||||
override val userRoles: StateFlow<List<Role>> = _userRoles.asStateFlow()
|
||||
|
||||
private suspend fun getCurrentServerURL() = repo.get(SERVER_URL_FIELD) as? String
|
||||
private suspend fun getCurrentUsername() = repo.get(USERNAME_FIELD) as? String
|
||||
@@ -42,18 +37,6 @@ data class DefaultAuthSettings(
|
||||
updateModule(serverUrl, token.either())
|
||||
}
|
||||
|
||||
val rolesUpdatingJob = (user + authorizedDIModule).subscribeSafelyWithoutExceptions(scope) {
|
||||
val user = user.value
|
||||
|
||||
if (user == null || authorizedDIModule.value == null) {
|
||||
_userRoles.value = emptyList()
|
||||
} else {
|
||||
_userRoles.value = koin.get<RolesStorage<Role>>().getRoles(user.id)
|
||||
}
|
||||
println(user)
|
||||
println(userRoles.value)
|
||||
}
|
||||
|
||||
override suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError? {
|
||||
return runCatching {
|
||||
if (getCurrentServerURL() != serverUrl || getCurrentUsername() != creds.username.string) {
|
||||
@@ -73,7 +56,7 @@ data class DefaultAuthSettings(
|
||||
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
|
||||
): AuthUIError? {
|
||||
val currentModule = authorizedDIModule.value
|
||||
val newModule = getAuthorizedFeaturesDIModule(
|
||||
val newModule = createAuthorizedFeaturesDIModule(
|
||||
serverUrl,
|
||||
initialAuthKey,
|
||||
{
|
||||
@@ -101,8 +84,8 @@ data class DefaultAuthSettings(
|
||||
currentModule ?.let { koin.loadModules(listOf(currentModule)) }
|
||||
}
|
||||
return when {
|
||||
!serverAvailable -> ServerUnavailableAuthUIError
|
||||
!authCorrect -> AuthIncorrectAuthUIError
|
||||
!serverAvailable -> AuthUIError.ServerUnavailable
|
||||
!authCorrect -> AuthIncorrect
|
||||
else -> {
|
||||
_authorizedDIModule.value = newModule
|
||||
null
|
||||
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.postssystem.features.auth.client.ui
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.UIFSMState
|
||||
|
||||
data class AuthUIFSMState(
|
||||
override val from: UIFSMState?,
|
||||
override val context: String = "main"
|
||||
) : UIFSMState
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.postssystem.features.auth.client.ui
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.common.common.UIModel
|
||||
import dev.inmo.postssystem.features.common.common.ui.UIModel
|
||||
|
||||
interface AuthUIModel : UIModel<AuthUIState> {
|
||||
suspend fun initAuth(serverUrl: String, creds: AuthCreds)
|
||||
|
||||
@@ -3,18 +3,23 @@ package dev.inmo.postssystem.features.auth.client.ui
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class AuthUIError
|
||||
@Serializable
|
||||
object ServerUnavailableAuthUIError : AuthUIError()
|
||||
@Serializable
|
||||
object AuthIncorrectAuthUIError : AuthUIError()
|
||||
sealed interface AuthUIError {
|
||||
// @Serializable
|
||||
object ServerUnavailable : AuthUIError
|
||||
// @Serializable
|
||||
object AuthIncorrect : AuthUIError
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class AuthUIState
|
||||
@Serializable
|
||||
data class InitAuthUIState(val showError: AuthUIError? = null) : AuthUIState()
|
||||
val DefaultInitAuthUIState = InitAuthUIState()
|
||||
@Serializable
|
||||
object LoadingAuthUIState : AuthUIState()
|
||||
@Serializable
|
||||
object AuthorizedAuthUIState : AuthUIState()
|
||||
sealed interface AuthUIState {
|
||||
@Serializable
|
||||
data class Init(val showError: AuthUIError? = null) : AuthUIState
|
||||
// @Serializable
|
||||
object Loading : AuthUIState
|
||||
// @Serializable
|
||||
object Authorized : AuthUIState
|
||||
|
||||
companion object {
|
||||
val DefaultInit = Init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.postssystem.features.auth.client.ui
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.common.common.UIViewModel
|
||||
import dev.inmo.postssystem.features.common.common.ui.UIViewModel
|
||||
import dev.inmo.postssystem.features.users.common.Username
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
package dev.inmo.postssystem.client.ui
|
||||
package dev.inmo.postssystem.features.auth.client.ui
|
||||
|
||||
import dev.inmo.postssystem.client.settings.auth.AuthSettings
|
||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||
import dev.inmo.postssystem.features.auth.client.settings.AuthSettings
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.common.common.AbstractUIModel
|
||||
import dev.inmo.postssystem.features.common.common.ui.AbstractUIModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DefaultAuthUIModel(
|
||||
private val scope: CoroutineScope,
|
||||
private val authSettings: AuthSettings
|
||||
) : AbstractUIModel<AuthUIState>(LoadingAuthUIState), AuthUIModel {
|
||||
) : AbstractUIModel<AuthUIState>(AuthUIState.Loading), AuthUIModel {
|
||||
init {
|
||||
scope.launch {
|
||||
_currentState.value = LoadingAuthUIState
|
||||
_currentState.value = AuthUIState.Loading
|
||||
authSettings.loadingJob.join()
|
||||
if (authSettings.authorizedDIModule.value == null) {
|
||||
_currentState.value = DefaultInitAuthUIState
|
||||
_currentState.value = AuthUIState.DefaultInit
|
||||
} else {
|
||||
_currentState.value = AuthorizedAuthUIState
|
||||
_currentState.value = AuthUIState.Authorized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun initAuth(serverUrl: String, creds: AuthCreds) {
|
||||
_currentState.value = LoadingAuthUIState
|
||||
_currentState.value = AuthUIState.Loading
|
||||
val authError = authSettings.auth(serverUrl, creds)
|
||||
if (authError == null) {
|
||||
_currentState.value = AuthorizedAuthUIState
|
||||
_currentState.value = AuthUIState.Authorized
|
||||
} else {
|
||||
_currentState.value = InitAuthUIState(authError)
|
||||
_currentState.value = AuthUIState.Init(authError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
package dev.inmo.postssystem.features.auth.client
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.inmo.jsuikit.elements.*
|
||||
import dev.inmo.jsuikit.modifiers.*
|
||||
import dev.inmo.jsuikit.utils.Attrs
|
||||
import dev.inmo.postssystem.client.ui.fsm.*
|
||||
import dev.inmo.micro_utils.coroutines.compose.renderComposableAndLinkToContextAndRoot
|
||||
import dev.inmo.postssystem.features.auth.client.ui.*
|
||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import dev.inmo.postssystem.client.utils.renderComposableAndLinkToContext
|
||||
import dev.inmo.postssystem.features.auth.client.ui.AuthUIError.AuthIncorrect
|
||||
import dev.inmo.postssystem.features.common.common.*
|
||||
import dev.inmo.postssystem.features.common.common.ui.JSView
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.*
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.dom.*
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
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(
|
||||
private val viewModel: AuthUIViewModel,
|
||||
private val uiScope: CoroutineScope
|
||||
) : JSView<AuthUIFSMState>() {
|
||||
private val uiScope: CoroutineScope,
|
||||
defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
|
||||
) : JSView<AuthUIFSMState>(defaultExceptionsHandlers) {
|
||||
|
||||
override suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||
htmlElement: HTMLElement,
|
||||
@@ -36,7 +49,7 @@ class AuthView(
|
||||
val errorText = mutableStateOf<String?>(null)
|
||||
|
||||
val root = htmlElement.appendElement("div") {}
|
||||
val composition = renderComposableAndLinkToContext(root) {
|
||||
val composition = renderComposableAndLinkToContextAndRoot(root) {
|
||||
val authBtnDisabled = usernameState.value.isBlank() || passwordState.value.isBlank()
|
||||
|
||||
Flex(
|
||||
@@ -54,13 +67,13 @@ class AuthView(
|
||||
}
|
||||
}
|
||||
|
||||
TextField(
|
||||
StandardInput(
|
||||
InputType.Text,
|
||||
usernameState,
|
||||
disabled,
|
||||
"Username",
|
||||
)
|
||||
TextField(
|
||||
StandardInput(
|
||||
InputType.Password,
|
||||
passwordState,
|
||||
disabled,
|
||||
@@ -80,23 +93,23 @@ class AuthView(
|
||||
|
||||
val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) {
|
||||
when (it) {
|
||||
is InitAuthUIState -> {
|
||||
is AuthUIState.Init -> {
|
||||
disabled.value = false
|
||||
|
||||
errorText.value = when (it.showError) {
|
||||
ServerUnavailableAuthUIError -> "Server unavailable"
|
||||
AuthIncorrectAuthUIError -> {
|
||||
AuthUIError.ServerUnavailable -> "Server unavailable"
|
||||
AuthIncorrect -> {
|
||||
passwordState.value = ""
|
||||
"Username or password is incorrect"
|
||||
}
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
LoadingAuthUIState -> {
|
||||
AuthUIState.Loading -> {
|
||||
disabled.value = true
|
||||
errorText.value = null
|
||||
}
|
||||
AuthorizedAuthUIState -> {
|
||||
AuthUIState.Authorized -> {
|
||||
completion.complete(state.from)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.auth.client"/>
|
||||
@@ -3,10 +3,20 @@ package dev.inmo.postssystem.features.auth.common
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.postssystem.features.users.common.Username
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
sealed interface AuthKey
|
||||
|
||||
@Serializable
|
||||
private data class AuthKeySurrogate(
|
||||
val authCreds: AuthCreds?,
|
||||
val token: AuthToken?,
|
||||
val refreshToken: RefreshToken?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@SerialName("authcreds")
|
||||
data class AuthCreds(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.auth.common"/>
|
||||
@@ -2,17 +2,18 @@ package dev.inmo.postssystem.features.auth.server
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService
|
||||
import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator
|
||||
import dev.inmo.postssystem.features.common.server.ApplicationAuthenticationConfigurator
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.server.*
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.*
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.sessions.*
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.sessions.sessions
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
data class AuthUserPrincipal(
|
||||
@@ -24,90 +25,78 @@ fun User.principal() = AuthUserPrincipal(this)
|
||||
|
||||
class AuthenticationRoutingConfigurator(
|
||||
private val authFeature: AuthFeature,
|
||||
private val authTokensService: AuthTokensService,
|
||||
private val unifiedRouter: UnifiedRouter
|
||||
private val authTokensService: AuthTokensService
|
||||
) : ApplicationRoutingConfigurator.Element, ApplicationAuthenticationConfigurator.Element {
|
||||
override fun Route.invoke() {
|
||||
unifiedRouter.apply {
|
||||
route(authRootPathPart) {
|
||||
post(authAuthPathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
it.printStackTrace()
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
val creds = uniload(AuthCreds.serializer())
|
||||
|
||||
val tokenInfo = authFeature.auth(creds)
|
||||
|
||||
if (tokenInfo == null) {
|
||||
if (call.response.status() == null) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
} else {
|
||||
call.sessions.set(tokenSessionKey, tokenInfo.token)
|
||||
unianswer(
|
||||
AuthTokenInfo.serializer().nullable,
|
||||
tokenInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
post(authRefreshPathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
val refreshToken = uniload(RefreshToken.serializer())
|
||||
|
||||
val tokenInfo = authFeature.refresh(refreshToken)
|
||||
|
||||
if (tokenInfo == null) {
|
||||
if (call.response.status() == null) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
} else {
|
||||
call.sessions.set(tokenSessionKey, tokenInfo.token)
|
||||
unianswer(
|
||||
AuthTokenInfo.serializer().nullable,
|
||||
tokenInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
post(authGetMePathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
unianswer(
|
||||
User.serializer().nullable,
|
||||
authFeature.getMe(
|
||||
uniload(AuthToken.serializer())
|
||||
)
|
||||
route(authRootPathPart) {
|
||||
post(authAuthPathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
it.printStackTrace()
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
val creds = call.receive<AuthCreds>()
|
||||
|
||||
val tokenInfo = authFeature.auth(creds)
|
||||
|
||||
if (tokenInfo == null) {
|
||||
if (call.response.status() == null) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
} else {
|
||||
call.sessions.set(tokenSessionKey, tokenInfo.token)
|
||||
call.respond(tokenInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
post (authRefreshPathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
val refreshToken = call.receive<RefreshToken>()
|
||||
|
||||
val tokenInfo = authFeature.refresh(refreshToken)
|
||||
|
||||
if (tokenInfo == null) {
|
||||
if (call.response.status() == null) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
} else {
|
||||
call.sessions.set(tokenSessionKey, tokenInfo.token)
|
||||
call.respond(tokenInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
post(authGetMePathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
call.respond(
|
||||
authFeature.getMe(call.receive()) ?: HttpStatusCode.NoContent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun Authentication.Configuration.invoke() {
|
||||
override fun AuthenticationConfig.invoke() {
|
||||
session<AuthToken> {
|
||||
validate {
|
||||
val result = authTokensService.getUserPrincipal(it)
|
||||
|
||||
@@ -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.auth.common.tokenSessionKey
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationSessionsConfigurator
|
||||
import io.ktor.sessions.*
|
||||
import io.ktor.server.sessions.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SessionAuthenticationConfigurator(
|
||||
private val maxAge: Milliseconds
|
||||
) : ApplicationSessionsConfigurator.Element {
|
||||
private val maxAgeInSeconds = TimeUnit.MILLISECONDS.toSeconds(maxAge)
|
||||
override fun Sessions.Configuration.invoke() {
|
||||
override fun SessionsConfig.invoke() {
|
||||
cookie<AuthToken>(tokenSessionKey) {
|
||||
cookie.maxAgeInSeconds = maxAgeInSeconds
|
||||
serializer = object : SessionSerializer<AuthToken> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.postssystem.features.auth.common.AuthToken
|
||||
import dev.inmo.postssystem.features.auth.common.RefreshToken
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import com.soywiz.klock.milliseconds
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.milliseconds
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.common.common.Milliseconds
|
||||
import dev.inmo.postssystem.features.users.common.*
|
||||
|
||||
18
features/client/template/client/build.gradle
Normal file
18
features/client/template/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.client.template.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
features/client/template/common/build.gradle
Normal file
17
features/client/template/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
features/client/template/server/build.gradle
Normal file
17
features/client/template/server/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.client.template.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.kt.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
@@ -13,8 +14,13 @@ kotlin {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api libs.microutils.repos.ktor.client
|
||||
api "io.ktor:ktor-client-auth:$ktor_version"
|
||||
api "io.ktor:ktor-client-logging:$ktor_version"
|
||||
api libs.ktor.client.auth
|
||||
api libs.ktor.client.logging
|
||||
|
||||
api libs.microutils.common.compose
|
||||
api libs.microutils.coroutines.compose
|
||||
api libs.microutils.fsm.common
|
||||
api libs.microutils.fsm.repos.common
|
||||
|
||||
api compose.runtime
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
object AdditionalModules {
|
||||
class AdditionalModules {
|
||||
private val additionalModules = mutableListOf<ModuleLoader>()
|
||||
val modules: List<ModuleLoader>
|
||||
get() = additionalModules.toList()
|
||||
@@ -8,4 +8,8 @@ object AdditionalModules {
|
||||
fun addModule(moduleLoader: ModuleLoader): Boolean {
|
||||
return additionalModules.add(moduleLoader)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Default = AdditionalModules()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.inmo.postssystem.client
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
@@ -0,0 +1,71 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.LinkedSupervisorScope
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.StringFormat
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
* Entrypoint for getting [org.koin.core.Koin] DI for the client
|
||||
*
|
||||
* @param repoFactory Factory for creating of [DefaultStatesManagerRepo] for [dev.inmo.postssystem.client.ui.fsm.UIFSM]
|
||||
*/
|
||||
fun baseKoin(
|
||||
defaultScope: CoroutineScope,
|
||||
settingsFactory: Scope.() -> KeyValueRepo<String, Any>,
|
||||
repoFactory: Scope.() -> DefaultStatesManagerRepo<UIFSMState>,
|
||||
handlersSetter: Pair<Scope, FSMBuilder<UIFSMState>>.() -> Unit
|
||||
): Koin = startKoin {
|
||||
modules(
|
||||
module {
|
||||
single {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } }
|
||||
}
|
||||
}
|
||||
single<StringFormat> { get<Json>() }
|
||||
single {
|
||||
Cbor { serializersModule = SerializersModule { get<SerializersModuleConfigurator>().apply { invoke() } } }
|
||||
}
|
||||
single<BinaryFormat> { get<Cbor>() }
|
||||
|
||||
single(DefaultQualifiers.SettingsQualifier) { settingsFactory() }
|
||||
single { DBDropper(get(DefaultQualifiers.SettingsQualifier)) }
|
||||
single(DefaultQualifiers.FSMHandlersBuilderQualifier) { handlersSetter }
|
||||
single { repoFactory() }
|
||||
single { defaultScope }
|
||||
single(DefaultQualifiers.UIScopeQualifier) { get<CoroutineScope>().LinkedSupervisorScope(Dispatchers.Main) }
|
||||
single<StatesMachine<UIFSMState>>(DefaultQualifiers.UIFSMQualifier) {
|
||||
UIFSM(get()) {
|
||||
(this@single to this@UIFSM).apply(get(DefaultQualifiers.FSMHandlersBuilderQualifier))
|
||||
}
|
||||
}
|
||||
} + AdditionalModules.Default.modules.map {
|
||||
module {
|
||||
with(it) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}.koin.apply {
|
||||
loadModules(
|
||||
listOf(
|
||||
module { single<Koin> { this@apply } }
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
|
||||
object DefaultQualifiers {
|
||||
val SettingsQualifier = StringQualifier("Settings")
|
||||
val FSMHandlersBuilderQualifier = StringQualifier("FSMHandlersBuilder")
|
||||
val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
|
||||
val UIFSMQualifier = StringQualifier("UIFSM")
|
||||
}
|
||||
@@ -2,6 +2,27 @@ package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import org.koin.core.module.Module
|
||||
|
||||
fun interface ModuleLoader {
|
||||
interface ModuleLoader {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
interface UIView {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
package dev.inmo.postssystem.features.common.common.ui
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
package dev.inmo.postssystem.features.common.common.ui
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -0,0 +1,4 @@
|
||||
package dev.inmo.postssystem.features.common.common.ui
|
||||
|
||||
interface UIView {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
package dev.inmo.postssystem.features.common.common.ui
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -7,9 +7,7 @@ interface UIViewModel<StateType> {
|
||||
val currentState: StateFlow<StateType>
|
||||
}
|
||||
|
||||
abstract class AbstractUIViewModel<StateType> : UIViewModel<StateType> {
|
||||
protected val _currentState = DefaultMVVMStateFlow(initState())
|
||||
abstract class AbstractUIViewModel<StateType>(initState: StateType) : UIViewModel<StateType> {
|
||||
protected val _currentState = DefaultMVVMStateFlow(initState)
|
||||
override val currentState: StateFlow<StateType> = _currentState.asStateFlow()
|
||||
|
||||
abstract fun initState(): StateType
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.inmo.postssystem.client.ui.fsm
|
||||
package dev.inmo.postssystem.features.common.common.ui.fsm
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
|
||||
@@ -6,8 +6,6 @@ import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
|
||||
val UIFSMQualifier = StringQualifier("UIFSM")
|
||||
|
||||
fun UIFSM(
|
||||
repo: DefaultStatesManagerRepo<UIFSMState>,
|
||||
handlersSetter: FSMBuilder<UIFSMState>.() -> Unit
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.postssystem.features.common.common.ui.fsm
|
||||
|
||||
import dev.inmo.micro_utils.common.Optional
|
||||
|
||||
fun interface UIFSMExceptionHandler {
|
||||
suspend fun handle(currentState: UIFSMState, exception: Throwable): Optional<UIFSMState?>
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dev.inmo.postssystem.features.common.common.ui.fsm
|
||||
|
||||
import dev.inmo.micro_utils.common.onPresented
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
interface UIFSMHandler<T : UIFSMState> : StatesHandler<T, UIFSMState> {
|
||||
fun interface Registrator {
|
||||
fun FSMBuilder<UIFSMState>.include()
|
||||
}
|
||||
val defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
|
||||
|
||||
suspend fun StatesMachine<in UIFSMState>.safeHandleState(state: T): UIFSMState?
|
||||
override suspend fun StatesMachine<in UIFSMState>.handleState(state: T): UIFSMState? {
|
||||
return runCatching {
|
||||
safeHandleState(state).also(::println)
|
||||
}.getOrElse {
|
||||
errorToNextStep(state, it) ?.let { return it } ?: throw it
|
||||
}.also(::println)
|
||||
}
|
||||
|
||||
suspend fun errorToNextStep(
|
||||
currentState: T,
|
||||
e: Throwable
|
||||
): UIFSMState? {
|
||||
defaultExceptionsHandlers.forEach {
|
||||
it.handle(currentState, e).onPresented { state ->
|
||||
return state
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.postssystem.features.common.common.ui.fsm
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.serialization.*
|
||||
|
||||
interface UIFSMState : State {
|
||||
val from: UIFSMState?
|
||||
get() = null
|
||||
override val context: String
|
||||
get() = "main"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.features.common.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.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))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package dev.inmo.postssystem.client.fsm.ui
|
||||
package dev.inmo.postssystem.features.common.common.ui
|
||||
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler
|
||||
import dev.inmo.postssystem.client.ui.fsm.UIFSMState
|
||||
import dev.inmo.micro_utils.fsm.common.StatesMachine
|
||||
import dev.inmo.postssystem.features.common.common.ui.fsm.*
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
abstract class JSView<T : UIFSMState> : UIFSMHandler<T> {
|
||||
abstract class JSView<T : UIFSMState>(
|
||||
override val defaultExceptionsHandlers: Iterable<UIFSMExceptionHandler>
|
||||
) : UIFSMHandler<T> {
|
||||
open suspend fun StatesMachine<in UIFSMState>.safeHandleState(
|
||||
htmlElement: HTMLElement,
|
||||
state: T
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.common.client"/>
|
||||
@@ -14,19 +14,19 @@ kotlin {
|
||||
api libs.microutils.serialization.typedserializer
|
||||
api libs.microutils.mimetypes
|
||||
api libs.klock
|
||||
api "io.insert-koin:koin-core:$koin_version"
|
||||
api "com.benasher44:uuid:$uuid_version"
|
||||
api "io.ktor:ktor-http:$ktor_version"
|
||||
api libs.koin.core
|
||||
api libs.uuid
|
||||
api libs.ktor.http
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
api libs.kotlin.reflect
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
api libs.kotlin.reflect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
@@ -8,7 +8,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
@Serializer(DateTime::class)
|
||||
object DateTimeSerializer : KSerializer<DateTime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = Double.serializer().descriptor
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filesize
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ByteArraySerializer
|
||||
@@ -27,15 +28,25 @@ class BytesBasedInputProvider(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(SimpleInputProviderSerializer::class)
|
||||
expect class FileBasedInputProvider : SimpleInputProvider {
|
||||
internal interface FileInputProvider : SimpleInputProvider {
|
||||
val file: MPPFile
|
||||
|
||||
override val contentBytes: Long?
|
||||
get() = file.filesize
|
||||
}
|
||||
|
||||
@Serializable(SimpleInputProviderSerializer::class)
|
||||
class CustomInputProvider(private val provider: () -> Input) : SimpleInputProvider {
|
||||
override val contentBytes: Long?
|
||||
get() = null
|
||||
expect class FileBasedInputProvider : FileInputProvider {
|
||||
override val file: MPPFile
|
||||
|
||||
override fun invoke(): Input
|
||||
}
|
||||
|
||||
@Serializable(SimpleInputProviderSerializer::class)
|
||||
class CustomInputProvider(
|
||||
override val contentBytes: Long? = null,
|
||||
private val provider: () -> Input
|
||||
) : SimpleInputProvider {
|
||||
override fun invoke(): Input = provider()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,9 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(SimpleInputProviderSerializer::class)
|
||||
actual class FileBasedInputProvider internal constructor(
|
||||
actual val file: MPPFile
|
||||
) : SimpleInputProvider {
|
||||
override val contentBytes: Long
|
||||
get() = file.filesize
|
||||
|
||||
override fun invoke(): Input = error("Files inputs must not be used directly")
|
||||
actual override val file: MPPFile
|
||||
) : FileInputProvider {
|
||||
actual override fun invoke(): Input = error("Files inputs must not be used directly")
|
||||
}
|
||||
|
||||
fun MPPFile.inputProvider() = FileBasedInputProvider(this)
|
||||
|
||||
@@ -8,10 +8,8 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(SimpleInputProviderSerializer::class)
|
||||
actual class FileBasedInputProvider(
|
||||
actual val file: MPPFile
|
||||
) : SimpleInputProvider {
|
||||
override val contentBytes: Long?
|
||||
get() = file.filesize
|
||||
actual override val file: MPPFile
|
||||
) : FileInputProvider {
|
||||
|
||||
override fun invoke(): Input = file.inputStream().asInput()
|
||||
actual override fun invoke(): Input = file.inputStream().asInput()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import org.koin.core.definition.Definition
|
||||
import org.koin.core.definition.KoinDefinition
|
||||
import org.koin.core.instance.InstanceFactory
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
@@ -11,6 +12,6 @@ inline fun <reified T : Any> Module.singleWithBinds(
|
||||
qualifier: Qualifier? = null,
|
||||
createdAtStart: Boolean = false,
|
||||
noinline definition: Definition<T>
|
||||
): Pair<Module, InstanceFactory<*>> {
|
||||
): KoinDefinition<*> {
|
||||
return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.toTypedArray())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.mime_types.*
|
||||
|
||||
actual val MPPFile.mimeType: MimeType
|
||||
get() {
|
||||
return getMimeTypeOrAny(filename.extension)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.common.common"/>
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.mime_types.MimeType
|
||||
import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny
|
||||
|
||||
actual val MPPFile.mimeType: MimeType
|
||||
get() {
|
||||
return getMimeTypeOrAny(filename.extension)
|
||||
}
|
||||
@@ -17,8 +17,8 @@ kotlin {
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-auth:$ktor_version"
|
||||
api "ch.qos.logback:logback-classic:$logback_version"
|
||||
api libs.ktor.server.auth
|
||||
api libs.logback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package dev.inmo.postssystem.features.common.server.sessions
|
||||
package dev.inmo.postssystem.features.common.server
|
||||
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.authentication
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.auth.*
|
||||
|
||||
class ApplicationAuthenticationConfigurator(
|
||||
private val elements: List<Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun Authentication.Configuration.invoke() }
|
||||
fun interface Element { operator fun AuthenticationConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
authentication {
|
||||
@@ -1,8 +1,9 @@
|
||||
package dev.inmo.postssystem.features.common.server.sessions
|
||||
package dev.inmo.postssystem.features.common.server
|
||||
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
|
||||
object Qualifiers {
|
||||
val filesFolderQualifier = StringQualifier("filesFolder")
|
||||
val commonFilesFolderQualifier = StringQualifier("commonFilesFolder")
|
||||
val usersRolesKeyValueFactoryQualifier = StringQualifier("usersRolesKeyValueFactory")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.inmo.postssystem.features.common.server.sessions
|
||||
package dev.inmo.postssystem.features.common.server
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.koin.core.module.Module
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.kt.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package dev.inmo.postssystem.features.content.binary.client
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.jsuikit.elements.DefaultButton
|
||||
import dev.inmo.jsuikit.elements.*
|
||||
import dev.inmo.jsuikit.modifiers.UIKitWidth
|
||||
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.content.client.ContentClientProvider
|
||||
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
|
||||
|
||||
object LoadingClientModule : ModuleLoader {
|
||||
init {
|
||||
AdditionalModules.addModule(this)
|
||||
AdditionalModules.Default.addModule(this)
|
||||
}
|
||||
|
||||
override fun Module.load() {
|
||||
@@ -41,4 +44,23 @@ object BinaryContentClientProvider : ContentClientProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun renderPreview(content: Content): Boolean {
|
||||
if (content is BinaryContent) {
|
||||
Tile {
|
||||
Card(
|
||||
header = {
|
||||
CardTitle {
|
||||
Text(content.filename.name)
|
||||
}
|
||||
}
|
||||
) {}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.content.binary.client"/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.content.binary.common"/>
|
||||
@@ -3,8 +3,8 @@ package dev.inmo.postssystem.features.content.binary.server
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
|
||||
import dev.inmo.postssystem.features.common.common.singleWithBinds
|
||||
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
|
||||
import dev.inmo.postssystem.features.common.server.sessions.Qualifiers
|
||||
import dev.inmo.postssystem.features.common.server.sessions.ServerModuleLoader
|
||||
import dev.inmo.postssystem.features.common.server.Qualifiers
|
||||
import dev.inmo.postssystem.features.common.server.ServerModuleLoader
|
||||
import dev.inmo.postssystem.features.content.common.BinaryContent
|
||||
import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
|
||||
@@ -6,7 +6,6 @@ import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
|
||||
import dev.inmo.postssystem.features.content.common.*
|
||||
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.kt.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.postssystem.features.content.client
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.*
|
||||
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
|
||||
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
|
||||
|
||||
val loader = DefaultModuleLoader {
|
||||
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
|
||||
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
|
||||
single { SerializersModuleConfigurator(getAll()) }
|
||||
}
|
||||
@@ -10,4 +10,7 @@ interface ContentClientProvider {
|
||||
|
||||
@Composable
|
||||
fun renderNewInstance(state: MutableState<Content?>)
|
||||
|
||||
@Composable
|
||||
fun renderPreview(content: Content): Boolean
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.content.client"/>
|
||||
@@ -1,7 +1,6 @@
|
||||
package dev.inmo.postssystem.features.content.common
|
||||
|
||||
import dev.inmo.micro_utils.common.FileName
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.mime_types.MimeType
|
||||
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||
import kotlinx.serialization.PolymorphicSerializer
|
||||
@@ -30,6 +29,18 @@ data class BinaryContent(
|
||||
) : Content
|
||||
|
||||
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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.content.common"/>
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.kt.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package dev.inmo.postssystem.features.content.text.client
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.jsuikit.elements.Tile
|
||||
import dev.inmo.jsuikit.modifiers.UIKitWidth
|
||||
import dev.inmo.jsuikit.modifiers.include
|
||||
import dev.inmo.postssystem.features.common.common.*
|
||||
import dev.inmo.postssystem.features.content.client.ContentClientProvider
|
||||
import dev.inmo.postssystem.features.content.common.Content
|
||||
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.koin.core.module.Module
|
||||
|
||||
object LoadingClientModule : ModuleLoader {
|
||||
init {
|
||||
AdditionalModules.addModule(this)
|
||||
AdditionalModules.Default.addModule(this)
|
||||
}
|
||||
|
||||
override fun Module.load() {
|
||||
@@ -36,4 +38,17 @@ object TextContentClientProvider : ContentClientProvider {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.content.content.text.client"/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.postssystem.features.content.content.text.common"/>
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.postssystem.features.content.text.server
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
|
||||
import dev.inmo.postssystem.features.common.server.sessions.ServerModuleLoader
|
||||
import dev.inmo.postssystem.features.common.server.ServerModuleLoader
|
||||
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
|
||||
import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper
|
||||
import dev.inmo.postssystem.features.content.text.common.TextContent
|
||||
|
||||
@@ -12,6 +12,7 @@ kotlin {
|
||||
dependencies {
|
||||
api project(":postssystem.features.files.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
api project(":postssystem.features.auth.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,46 @@ package dev.inmo.postssystem.features.files.client
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.ReadFilesStorage
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.client.bodyOrNull
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
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.KtorReadCRUDRepoClient
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.readBytes
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.encodeURLQueryComponent
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class ClientReadFilesStorage(
|
||||
baseUrl: String,
|
||||
private val client: HttpClient,
|
||||
private val serialFormat: BinaryFormat
|
||||
) : ReadFilesStorage, ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> by KtorReadStandardCrudRepo(
|
||||
private val serialFormat: BinaryFormat,
|
||||
contentType: ContentType
|
||||
) : ReadFilesStorage, ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> by KtorReadCRUDRepoClient<MetaFileInfoStorageWrapper, FileId>(
|
||||
buildStandardUrl(baseUrl, filesRootPathPart),
|
||||
UnifiedRequester(client, serialFormat),
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
client,
|
||||
contentType,
|
||||
{ it.string.encodeURLQueryComponent() }
|
||||
) {
|
||||
private val unifiedRequester = UnifiedRequester(client, serialFormat)
|
||||
private val fullFilesPath = buildStandardUrl(baseUrl, filesRootPathPart)
|
||||
private val fullFilesGetBytesPath = buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFilesPathPart
|
||||
)
|
||||
|
||||
override suspend fun getBytes(id: FileId): ByteArray = client.post<HttpResponse>(fullFilesGetBytesPath) {
|
||||
body = serialFormat.encodeToByteArray(FileId.serializer(), id)
|
||||
override suspend fun getBytes(id: FileId): ByteArray = client.post(fullFilesGetBytesPath) {
|
||||
setBody(serialFormat.encodeToByteArray(FileId.serializer(), id))
|
||||
}.readBytes()
|
||||
|
||||
override suspend fun getFullFileInfo(
|
||||
id: FileId
|
||||
): FullFileInfoStorageWrapper? = unifiedRequester.uniget(
|
||||
): FullFileInfoStorageWrapper? = client.get(
|
||||
buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFullFileInfoPathPart,
|
||||
filesFileIdParameter to unifiedRequester.encodeUrlQueryValue(FileId.serializer(), id)
|
||||
),
|
||||
FullFileInfoStorageWrapper.serializer().nullable
|
||||
)
|
||||
filesFileIdParameter to id.string.encodeURLQueryComponent()
|
||||
)
|
||||
).bodyOrNull()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user