full reborn
This commit is contained in:
.github/workflows
LICENSEREADME.mdbuild.gradlebusiness_cases/post_creating
client
common
src
commonMain
kotlin
dev
inmo
postssystem
business_cases
post_creating
main
server
client
build.gradle
src
commonMain
kotlin
dev
inmo
jsMain
kotlin
dev
resources
jvmMain
kotlin
dev
inmo
postssystem
client
main
core
api
src
commonMain
kotlin
dev
inmo
postssystem
core
commonTest
kotlin
dev
inmo
postssystem
core
jvmMain
kotlin
dev
inmo
postssystem
core
content
api
business
content_adapters
binary
main
exposed
build.gradlegradle.properties
src
jvmMain
kotlin
dev
inmo
postssystem
core
exposed
jvmTest
kotlin
dev
inmo
postssystem
ktor
client
common
src
commonMain
kotlin
dev
inmo
postssystem
main
server
features
auth
client
common
server
common
client
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
common
jvmMain
kotlin
dev
inmo
postssystem
features
common
common
main
server
files
client
build.gradle
src
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
jvmMain
kotlin
dev
inmo
postssystem
features
main
server
roles
client
common
manager
client
common
server
server
status
template
client
common
server
users
client
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
users
jvmMain
kotlin
dev
inmo
postssystem
features
users
common
main
server
gradle/wrapper
gradlewgradlew.batmimes_generator
mppAndroidProject.gradlemppJavaProject.gradlemppJsProject.gradlemppProjectWithSerialization.gradlepubconf.kpsbpublish.gradlepublish.kpsbpublishing
api
src
commonMain
kotlin
main
exposed
ktor
client
src
commonMain
kotlin
com
insanusmokrassar
postssystem
main
common
src
commonMain
kotlin
com
insanusmokrassar
main
server
server
settings.gradle
18
features/auth/client/build.gradle
Normal file
18
features/auth/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.common.client")
|
||||
api project(":postssystem.features.auth.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
55
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientAuthFeature.kt
Normal file
55
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientAuthFeature.kt
Normal file
@ -0,0 +1,55 @@
|
||||
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.common.buildStandardUrl
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class ClientAuthFeature(
|
||||
private val requester: UnifiedRequester,
|
||||
baseUrl: String
|
||||
) : AuthFeature {
|
||||
private val rootUrl = buildStandardUrl(baseUrl.dropLastWhile { it == '/' }, authRootPathPart)
|
||||
private val fullAuthPath = buildStandardUrl(
|
||||
rootUrl,
|
||||
authAuthPathPart
|
||||
)
|
||||
private val fullRefreshPath = buildStandardUrl(
|
||||
rootUrl,
|
||||
authRefreshPathPart
|
||||
)
|
||||
private val fullGetMePath = buildStandardUrl(
|
||||
rootUrl,
|
||||
authGetMePathPart
|
||||
)
|
||||
|
||||
constructor(client: HttpClient, rootUrl: String): this(
|
||||
UnifiedRequester(client),
|
||||
rootUrl
|
||||
)
|
||||
|
||||
override suspend fun auth(creds: AuthCreds): AuthTokenInfo? = requester.unipost(
|
||||
fullAuthPath,
|
||||
AuthCreds.serializer() to creds,
|
||||
AuthTokenInfo.serializer().nullable
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
fun isAuthRequest(builder: HttpRequestBuilder): Boolean = builder.url.buildString().let {
|
||||
it == fullAuthPath || it == fullRefreshPath
|
||||
}
|
||||
}
|
112
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientCookiesConfigurator.kt
Normal file
112
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientCookiesConfigurator.kt
Normal file
@ -0,0 +1,112 @@
|
||||
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.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.request.*
|
||||
import io.ktor.client.statement.HttpReceivePipeline
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
object AuthUnavailableException : Exception()
|
||||
|
||||
fun HttpClientConfig<*>.installClientAuthenticator(
|
||||
baseUrl: String,
|
||||
scope: CoroutineScope,
|
||||
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
|
||||
onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit,
|
||||
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()
|
||||
}
|
||||
|
||||
val authMutex = Mutex()
|
||||
var currentRefreshToken: RefreshToken? = null
|
||||
initialAuthKey.onFirst {
|
||||
currentRefreshToken = it as? RefreshToken
|
||||
}.onSecond {
|
||||
currentRefreshToken = it.refresh
|
||||
}
|
||||
val creds = initialAuthKey.t1 as? AuthCreds
|
||||
var userRefreshJob: Job? = null
|
||||
|
||||
install("Auth Token Refresher") {
|
||||
val clientAuthFeature = ClientAuthFeature(this, baseUrl)
|
||||
fun refreshUser(newTokenInfo: AuthTokenInfo) {
|
||||
userRefreshJob ?.cancel()
|
||||
userRefreshJob = scope.launchSafelyWithoutExceptions {
|
||||
onUserRetrieved(clientAuthFeature.getMe(newTokenInfo.token))
|
||||
}
|
||||
}
|
||||
initialAuthKey.onSecond { refreshUser(it) }
|
||||
|
||||
suspend fun refreshToken() {
|
||||
val capturedRefresh = currentRefreshToken
|
||||
|
||||
runCatching {
|
||||
when {
|
||||
capturedRefresh == null && creds == null -> throw AuthUnavailableException
|
||||
capturedRefresh != null -> {
|
||||
currentRefreshToken = null
|
||||
val newTokenInfo = clientAuthFeature.refresh(capturedRefresh)
|
||||
currentRefreshToken = newTokenInfo ?.refresh
|
||||
if (newTokenInfo == null) {
|
||||
refreshToken()
|
||||
} else {
|
||||
onAuthKeyUpdated(newTokenInfo)
|
||||
refreshUser(newTokenInfo)
|
||||
}
|
||||
}
|
||||
creds != null -> {
|
||||
val newAuthTokenInfo = clientAuthFeature.auth(creds)
|
||||
|
||||
if (newAuthTokenInfo != null) {
|
||||
onAuthKeyUpdated(newAuthTokenInfo)
|
||||
refreshUser(newAuthTokenInfo)
|
||||
currentRefreshToken = newAuthTokenInfo.refresh
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
onAuthKeyInvalidated()
|
||||
}
|
||||
}
|
||||
|
||||
sendPipeline.intercept(HttpSendPipeline.State) {
|
||||
if (!context.url.buildString().startsWith(baseUrl) || clientAuthFeature.isAuthRequest(context)) {
|
||||
return@intercept
|
||||
}
|
||||
context.expectSuccess = false
|
||||
if (authMutex.isLocked) {
|
||||
authMutex.withLock { /* do nothing, just wait while mutex will be freed */ }
|
||||
}
|
||||
}
|
||||
|
||||
receivePipeline.intercept(HttpReceivePipeline.Before) {
|
||||
if (
|
||||
context.request.url.toString().startsWith(baseUrl)
|
||||
&& context.response.status == HttpStatusCode.Unauthorized
|
||||
) {
|
||||
authMutex.withLock { refreshToken() }
|
||||
val newResponse = context.client ?.request<HttpResponse>{
|
||||
takeFrom(context.request)
|
||||
} ?: return@intercept
|
||||
proceedWith(newResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIModel.kt
Normal file
8
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIModel.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package dev.inmo.postssystem.features.auth.client.ui
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.common.common.UIModel
|
||||
|
||||
interface AuthUIModel : UIModel<AuthUIState> {
|
||||
suspend fun initAuth(serverUrl: String, creds: AuthCreds)
|
||||
}
|
20
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIState.kt
Normal file
20
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIState.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package dev.inmo.postssystem.features.auth.client.ui
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class AuthUIError
|
||||
@Serializable
|
||||
object ServerUnavailableAuthUIError : AuthUIError()
|
||||
@Serializable
|
||||
object AuthIncorrectAuthUIError : 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()
|
34
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIViewModel.kt
Normal file
34
features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIViewModel.kt
Normal file
@ -0,0 +1,34 @@
|
||||
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.users.common.Username
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class AuthUIViewModel(
|
||||
private val model: AuthUIModel
|
||||
) : UIViewModel<AuthUIState> {
|
||||
override val currentState: StateFlow<AuthUIState>
|
||||
get() = model.currentState
|
||||
|
||||
private fun checkIncomingData(
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String
|
||||
): Boolean {
|
||||
return serverUrl.isNotBlank() && username.isNotBlank() && password.isNotBlank()
|
||||
}
|
||||
|
||||
suspend fun initAuth(
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String
|
||||
) {
|
||||
if (checkIncomingData(serverUrl, username, password)) {
|
||||
model.initAuth(
|
||||
serverUrl.takeIf { it.startsWith("http") } ?: "http://$serverUrl",
|
||||
AuthCreds(Username(username), password)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
1
features/auth/client/src/main/AndroidManifest.xml
Normal file
1
features/auth/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.auth.client"/>
|
18
features/auth/common/build.gradle
Normal file
18
features/auth/common/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.common.common")
|
||||
api project(":postssystem.features.users.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthFeature.kt
Normal file
9
features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthFeature.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.features.auth.common
|
||||
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
|
||||
interface AuthFeature {
|
||||
suspend fun auth(creds: AuthCreds): AuthTokenInfo?
|
||||
suspend fun refresh(refresh: RefreshToken): AuthTokenInfo?
|
||||
suspend fun getMe(authToken: AuthToken): User?
|
||||
}
|
36
features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthModels.kt
Normal file
36
features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthModels.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package dev.inmo.postssystem.features.auth.common
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.postssystem.features.users.common.Username
|
||||
import kotlinx.serialization.*
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
sealed interface AuthKey
|
||||
|
||||
@Serializable
|
||||
@SerialName("authcreds")
|
||||
data class AuthCreds(
|
||||
val username: Username,
|
||||
val password: String
|
||||
): AuthKey
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("token")
|
||||
@JvmInline
|
||||
value class AuthToken(val string: String = uuid4().toString()): AuthKey {
|
||||
override fun toString(): String = string
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("refresh")
|
||||
@JvmInline
|
||||
value class RefreshToken(val string: String = uuid4().toString()): AuthKey {
|
||||
override fun toString(): String = string
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AuthTokenInfo(
|
||||
val token: AuthToken,
|
||||
val refresh: RefreshToken
|
||||
)
|
8
features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/Constants.kt
Normal file
8
features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/Constants.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package dev.inmo.postssystem.features.auth.common
|
||||
|
||||
const val tokenSessionKey = "token"
|
||||
|
||||
const val authRootPathPart = "auth"
|
||||
const val authAuthPathPart = "auth"
|
||||
const val authRefreshPathPart = "refresh"
|
||||
const val authGetMePathPart = "getMe"
|
1
features/auth/common/src/main/AndroidManifest.xml
Normal file
1
features/auth/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.auth.common"/>
|
17
features/auth/server/build.gradle
Normal file
17
features/auth/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.auth.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/AuthenticationRoutingConfigurator.kt
Normal file
121
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/AuthenticationRoutingConfigurator.kt
Normal file
@ -0,0 +1,121 @@
|
||||
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.users.common.User
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.*
|
||||
import dev.inmo.micro_utils.ktor.server.unianswer
|
||||
import dev.inmo.micro_utils.ktor.server.uniload
|
||||
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 kotlinx.serialization.builtins.nullable
|
||||
|
||||
data class AuthUserPrincipal(
|
||||
val user: User
|
||||
) : Principal
|
||||
|
||||
fun User.principal() = AuthUserPrincipal(this)
|
||||
|
||||
|
||||
class AuthenticationRoutingConfigurator(
|
||||
private val authFeature: AuthFeature,
|
||||
private val authTokensService: AuthTokensService
|
||||
) : ApplicationRoutingConfigurator.Element, ApplicationAuthenticationConfigurator.Element {
|
||||
override fun Route.invoke() {
|
||||
route(authRootPathPart) {
|
||||
post(authAuthPathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
it.printStackTrace()
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
val creds = call.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)
|
||||
call.unianswer(
|
||||
AuthTokenInfo.serializer().nullable,
|
||||
tokenInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
post(authRefreshPathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
val refreshToken = call.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)
|
||||
call.unianswer(
|
||||
AuthTokenInfo.serializer().nullable,
|
||||
tokenInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
post(authGetMePathPart) {
|
||||
safely(
|
||||
{
|
||||
// TODO:: add error info
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
"Something went wrong"
|
||||
)
|
||||
}
|
||||
) {
|
||||
call.unianswer(
|
||||
User.serializer().nullable,
|
||||
authFeature.getMe(
|
||||
call.uniload(AuthToken.serializer())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun Authentication.Configuration.invoke() {
|
||||
session<AuthToken> {
|
||||
validate {
|
||||
val result = authTokensService.getUserPrincipal(it)
|
||||
if (result.isSuccess) {
|
||||
result.getOrThrow().principal()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { call.respond(HttpStatusCode.Unauthorized) }
|
||||
}
|
||||
}
|
||||
}
|
23
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/SessionAuthenticationConfigurator.kt
Normal file
23
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/SessionAuthenticationConfigurator.kt
Normal file
@ -0,0 +1,23 @@
|
||||
package dev.inmo.postssystem.features.auth.server
|
||||
|
||||
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 java.util.concurrent.TimeUnit
|
||||
|
||||
class SessionAuthenticationConfigurator(
|
||||
private val maxAge: Milliseconds
|
||||
) : ApplicationSessionsConfigurator.Element {
|
||||
private val maxAgeInSeconds = TimeUnit.MILLISECONDS.toSeconds(maxAge)
|
||||
override fun Sessions.Configuration.invoke() {
|
||||
cookie<AuthToken>(tokenSessionKey) {
|
||||
cookie.maxAgeInSeconds = maxAgeInSeconds
|
||||
serializer = object : SessionSerializer<AuthToken> {
|
||||
override fun deserialize(text: String): AuthToken = AuthToken(text)
|
||||
override fun serialize(session: AuthToken): String = session.string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensRepo.kt
Normal file
22
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensRepo.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import com.soywiz.klock.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
|
||||
import dev.inmo.micro_utils.repos.CRUDRepo
|
||||
|
||||
data class AuthTokenModel(
|
||||
val token: AuthToken,
|
||||
val refreshToken: RefreshToken,
|
||||
val userId: UserId,
|
||||
val expiring: DateTime,
|
||||
val die: DateTime
|
||||
)
|
||||
|
||||
interface AuthTokensRepo : CRUDRepo<AuthTokenModel, AuthToken, AuthTokenModel> {
|
||||
suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel?
|
||||
suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean
|
||||
suspend fun deleteDied(now: DateTime = DateTime.now())
|
||||
}
|
||||
|
18
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensService.kt
Normal file
18
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensService.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
|
||||
sealed class AuthTokenException : Exception()
|
||||
|
||||
object AuthTokenExpiredException : AuthTokenException()
|
||||
object AuthTokenNotFoundException : AuthTokenException()
|
||||
object UserNotFoundException : AuthTokenException()
|
||||
|
||||
|
||||
interface AuthTokensService {
|
||||
/**
|
||||
* @return [User] or one of failure exceptions: [AuthTokenException]
|
||||
*/
|
||||
suspend fun getUserPrincipal(authToken: AuthToken): Result<User>
|
||||
}
|
184
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/DefaultAuthTokensService.kt
Normal file
184
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/DefaultAuthTokensService.kt
Normal file
@ -0,0 +1,184 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import com.soywiz.klock.milliseconds
|
||||
import dev.inmo.postssystem.features.auth.common.*
|
||||
import dev.inmo.postssystem.features.common.common.Milliseconds
|
||||
import dev.inmo.postssystem.features.users.common.*
|
||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.repos.create
|
||||
import dev.inmo.micro_utils.repos.deleteById
|
||||
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
|
||||
import dev.inmo.micro_utils.repos.exposed.initTable
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||
import org.jetbrains.exposed.sql.statements.UpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
private class ExposedAuthTokensRepo(
|
||||
override val database: Database
|
||||
) : AuthTokensRepo, AbstractExposedCRUDRepo<AuthTokenModel, AuthToken, AuthTokenModel>(
|
||||
tableName = "ExposedAuthTokensRepo"
|
||||
) {
|
||||
private val tokenColumn = text("token")
|
||||
private val refreshTokenColumn = text("refreshToken")
|
||||
private val userIdColumn = long("userId")
|
||||
private val expiringColumn = double("expiring")
|
||||
private val dieColumn = double("die")
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(tokenColumn)
|
||||
|
||||
override val selectByIds: SqlExpressionBuilder.(List<AuthToken>) -> Op<Boolean> = {
|
||||
tokenColumn.inList(it.map { it.string })
|
||||
}
|
||||
|
||||
override val selectById: SqlExpressionBuilder.(AuthToken) -> Op<Boolean> = {
|
||||
tokenColumn.eq(it.string)
|
||||
}
|
||||
override val ResultRow.asObject: AuthTokenModel
|
||||
get() = AuthTokenModel(
|
||||
AuthToken(get(tokenColumn)),
|
||||
RefreshToken(get(refreshTokenColumn)),
|
||||
UserId(get(userIdColumn)),
|
||||
DateTime(get(expiringColumn)),
|
||||
DateTime(get(dieColumn))
|
||||
)
|
||||
|
||||
init {
|
||||
initTable()
|
||||
}
|
||||
|
||||
override fun insert(value: AuthTokenModel, it: InsertStatement<Number>) {
|
||||
it[tokenColumn] = value.token.string
|
||||
it[refreshTokenColumn] = value.refreshToken.string
|
||||
it[userIdColumn] = value.userId.long
|
||||
it[expiringColumn] = value.expiring.unixMillisDouble
|
||||
it[dieColumn] = value.die.unixMillisDouble
|
||||
}
|
||||
|
||||
override fun update(id: AuthToken, value: AuthTokenModel, it: UpdateStatement) {
|
||||
it[tokenColumn] = value.token.string
|
||||
it[refreshTokenColumn] = value.refreshToken.string
|
||||
it[userIdColumn] = value.userId.long
|
||||
it[expiringColumn] = value.expiring.unixMillisDouble
|
||||
it[dieColumn] = value.die.unixMillisDouble
|
||||
}
|
||||
|
||||
override fun InsertStatement<Number>.asObject(value: AuthTokenModel): AuthTokenModel = AuthTokenModel(
|
||||
AuthToken(get(tokenColumn)),
|
||||
RefreshToken(get(refreshTokenColumn)),
|
||||
UserId(get(userIdColumn)),
|
||||
DateTime(get(expiringColumn)),
|
||||
DateTime(get(dieColumn))
|
||||
)
|
||||
|
||||
override suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel? = transaction(database) {
|
||||
select { refreshTokenColumn.eq(refreshToken.string) }.limit(1).firstOrNull() ?.asObject
|
||||
}
|
||||
|
||||
override suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean = transaction {
|
||||
deleteWhere { tokenColumn.eq(toRemove.string) } > 0 && insert {
|
||||
insert(toInsert, it)
|
||||
}.insertedCount > 0
|
||||
}
|
||||
|
||||
override suspend fun deleteDied(
|
||||
now: DateTime
|
||||
) = transaction(database) {
|
||||
val nowAsDouble = now.unixMillisDouble
|
||||
val tokens = select { dieColumn.less(nowAsDouble) }.map { it[tokenColumn] }
|
||||
deleteWhere { dieColumn.less(nowAsDouble) }
|
||||
tokens
|
||||
}.forEach {
|
||||
deleteObjectsIdsChannel.emit(AuthToken(it))
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultAuthTokensService(
|
||||
private val authTokensRepo: AuthTokensRepo,
|
||||
private val usersRepo: ReadUsersStorage,
|
||||
private val userAuthenticator: UserAuthenticator,
|
||||
private val tokenLifetime: Milliseconds,
|
||||
private val cleaningScope: CoroutineScope
|
||||
) : AuthTokensService, AuthFeature {
|
||||
private val tokenDieLifetime = tokenLifetime * 2
|
||||
|
||||
constructor(
|
||||
database: Database,
|
||||
usersRepo: ReadUsersStorage,
|
||||
userAuthenticator: UserAuthenticator,
|
||||
tokenLifetime: Milliseconds,
|
||||
cleaningScope: CoroutineScope
|
||||
): this(
|
||||
ExposedAuthTokensRepo(database),
|
||||
usersRepo,
|
||||
userAuthenticator,
|
||||
tokenLifetime,
|
||||
cleaningScope
|
||||
)
|
||||
|
||||
init {
|
||||
cleaningScope.launchSafelyWithoutExceptions {
|
||||
while (isActive) {
|
||||
authTokensRepo.deleteDied()
|
||||
delay(tokenLifetime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserPrincipal(authToken: AuthToken): Result<User> {
|
||||
val authTokenModel = authTokensRepo.getById(authToken) ?: return Result.failure(AuthTokenNotFoundException)
|
||||
return if (authTokenModel.expiring < DateTime.now()) {
|
||||
Result.failure(AuthTokenExpiredException)
|
||||
} else {
|
||||
val user = usersRepo.getById(authTokenModel.userId) ?: return Result.failure(UserNotFoundException)
|
||||
Result.success(user)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun auth(creds: AuthCreds): AuthTokenInfo? {
|
||||
val user = userAuthenticator(creds) ?: return null
|
||||
val now = DateTime.now()
|
||||
val preAuthTokenModel = AuthTokenModel(
|
||||
AuthToken(),
|
||||
RefreshToken(),
|
||||
user.id,
|
||||
now + tokenLifetime.milliseconds,
|
||||
now + tokenDieLifetime.milliseconds
|
||||
)
|
||||
val tokenModel = authTokensRepo.create(preAuthTokenModel).firstOrNull() ?: return null
|
||||
return AuthTokenInfo(
|
||||
tokenModel.token,
|
||||
tokenModel.refreshToken
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? {
|
||||
val previousAuthTokenModel = authTokensRepo.getByRefreshToken(refresh) ?: return null
|
||||
val now = DateTime.now()
|
||||
|
||||
if (previousAuthTokenModel.die < now) {
|
||||
authTokensRepo.deleteById(previousAuthTokenModel.token)
|
||||
return null
|
||||
}
|
||||
|
||||
val newAuthTokenModel = AuthTokenModel(
|
||||
AuthToken(),
|
||||
RefreshToken(),
|
||||
previousAuthTokenModel.userId,
|
||||
now + tokenLifetime.milliseconds,
|
||||
now + tokenDieLifetime.milliseconds
|
||||
)
|
||||
return if (authTokensRepo.replaceToken(previousAuthTokenModel.token, newAuthTokenModel)) {
|
||||
AuthTokenInfo(newAuthTokenModel.token, newAuthTokenModel.refreshToken)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMe(authToken: AuthToken): User? {
|
||||
return usersRepo.getById(
|
||||
authTokensRepo.getById(authToken) ?.userId ?: return null
|
||||
)
|
||||
}
|
||||
}
|
9
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UserAuthenticator.kt
Normal file
9
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UserAuthenticator.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
|
||||
fun interface UserAuthenticator {
|
||||
suspend operator fun invoke(authCreds: AuthCreds): User?
|
||||
}
|
||||
|
44
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UsersAuths.kt
Normal file
44
features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UsersAuths.kt
Normal file
@ -0,0 +1,44 @@
|
||||
package dev.inmo.postssystem.features.auth.server.tokens
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.AuthCreds
|
||||
import dev.inmo.postssystem.features.users.common.*
|
||||
import dev.inmo.micro_utils.repos.exposed.AbstractExposedReadCRUDRepo
|
||||
import dev.inmo.micro_utils.repos.exposed.initTable
|
||||
import org.jetbrains.exposed.sql.*
|
||||
|
||||
private class ExposedUsersAuthenticationRepo(
|
||||
override val database: Database,
|
||||
private val usersRepo: ExposedUsersStorage
|
||||
) : AbstractExposedReadCRUDRepo<UserId, AuthCreds>("UsersAuthentications") {
|
||||
private val passwordColumn = text("password")
|
||||
private val userIdColumn = long("userid").uniqueIndex() references usersRepo.userIdColumn
|
||||
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(userIdColumn)
|
||||
|
||||
override val ResultRow.asObject: UserId
|
||||
get() = UserId(get(userIdColumn))
|
||||
override val selectById: SqlExpressionBuilder.(AuthCreds) -> Op<Boolean> = {
|
||||
usersRepo.select {
|
||||
usersRepo.usernameColumn.eq(it.username.string)
|
||||
}.firstOrNull() ?.get(usersRepo.userIdColumn) ?.let { userId ->
|
||||
userIdColumn.eq(userId).and(passwordColumn.eq(it.password))
|
||||
} ?: Op.FALSE
|
||||
}
|
||||
|
||||
init {
|
||||
initTable()
|
||||
uniqueIndex("${tableName}_user_password", userIdColumn, passwordColumn)
|
||||
}
|
||||
}
|
||||
|
||||
fun exposedUsersAuthenticator(
|
||||
database: Database,
|
||||
usersRepo: ExposedUsersStorage
|
||||
): UserAuthenticator {
|
||||
val usersAuthenticatorRepo = ExposedUsersAuthenticationRepo(database, usersRepo)
|
||||
return UserAuthenticator {
|
||||
val userId = usersAuthenticatorRepo.getById(it) ?: return@UserAuthenticator null
|
||||
usersRepo.getById(userId)
|
||||
}
|
||||
}
|
||||
|
20
features/common/client/build.gradle
Normal file
20
features/common/client/build.gradle
Normal file
@ -0,0 +1,20 @@
|
||||
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")
|
||||
api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version"
|
||||
api "io.ktor:ktor-client-auth:$ktor_version"
|
||||
api "io.ktor:ktor-client-logging:$ktor_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/DefaultUIFlow.kt
Normal file
7
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/DefaultUIFlow.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
inline fun <T> DefaultMVVMStateFlow(
|
||||
state: T
|
||||
) = MutableStateFlow<T>(state)
|
15
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIModel.kt
Normal file
15
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIModel.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
interface UIModel<StateType> {
|
||||
val currentState: StateFlow<StateType>
|
||||
}
|
||||
|
||||
abstract class AbstractUIModel<StateType>(
|
||||
initState: StateType
|
||||
) : UIModel<StateType> {
|
||||
protected val _currentState = DefaultMVVMStateFlow(initState)
|
||||
override val currentState: StateFlow<StateType> = _currentState.asStateFlow()
|
||||
}
|
4
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIView.kt
Normal file
4
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIView.kt
Normal file
@ -0,0 +1,4 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
interface UIView {
|
||||
}
|
15
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIViewModel.kt
Normal file
15
features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIViewModel.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
interface UIViewModel<StateType> {
|
||||
val currentState: StateFlow<StateType>
|
||||
}
|
||||
|
||||
abstract class AbstractUIViewModel<StateType> : UIViewModel<StateType> {
|
||||
protected val _currentState = DefaultMVVMStateFlow(initState())
|
||||
override val currentState: StateFlow<StateType> = _currentState.asStateFlow()
|
||||
|
||||
abstract fun initState(): StateType
|
||||
}
|
1
features/common/client/src/main/AndroidManifest.xml
Normal file
1
features/common/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.common.client"/>
|
30
features/common/common/build.gradle
Normal file
30
features/common/common/build.gradle
Normal file
@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "dev.inmo:micro_utils.common:$microutils_version"
|
||||
api "dev.inmo:micro_utils.serialization.typed_serializer:$microutils_version"
|
||||
api "io.insert-koin:koin-core:$koin_version"
|
||||
api "com.benasher44:uuid:$uuid_version"
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/CommonJson.kt
Normal file
11
features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/CommonJson.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
val DefaultJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
|
||||
}
|
||||
|
||||
val Json.default
|
||||
get() = DefaultJson
|
5
features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/KoinGetAllDistinct.kt
Normal file
5
features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/KoinGetAllDistinct.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
inline fun <reified T : Any> Scope.getAllDistinct() = getAll<T>().distinct()
|
3
features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/Typealiases.kt
Normal file
3
features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/Typealiases.kt
Normal file
@ -0,0 +1,3 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
typealias Milliseconds = Long
|
16
features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/KoinSingleWithBinds.kt
Normal file
16
features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/KoinSingleWithBinds.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package dev.inmo.postssystem.features.common.common
|
||||
|
||||
import org.koin.core.definition.Definition
|
||||
import org.koin.core.instance.InstanceFactory
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
import org.koin.dsl.binds
|
||||
import kotlin.reflect.full.allSuperclasses
|
||||
|
||||
inline fun <reified T : Any> Module.singleWithBinds(
|
||||
qualifier: Qualifier? = null,
|
||||
createdAtStart: Boolean = false,
|
||||
noinline definition: Definition<T>
|
||||
): Pair<Module, InstanceFactory<*>> {
|
||||
return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.toTypedArray())
|
||||
}
|
1
features/common/common/src/main/AndroidManifest.xml
Normal file
1
features/common/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.common.common"/>
|
25
features/common/server/build.gradle
Normal file
25
features/common/server/build.gradle
Normal file
@ -0,0 +1,25 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api "dev.inmo:micro_utils.repos.exposed:$microutils_version"
|
||||
api "dev.inmo:micro_utils.repos.ktor.server:$microutils_version"
|
||||
api "dev.inmo:micro_utils.ktor.server:$microutils_version"
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-auth:$ktor_version"
|
||||
api "ch.qos.logback:logback-classic:$logback_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/common/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/server/sessions/ApplicationAuthenticationConfigurator.kt
Normal file
18
features/common/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/server/sessions/ApplicationAuthenticationConfigurator.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.features.common.server.sessions
|
||||
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.authentication
|
||||
|
||||
class ApplicationAuthenticationConfigurator(
|
||||
private val elements: List<Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun Authentication.Configuration.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
authentication {
|
||||
elements.forEach { it.apply { invoke() } }
|
||||
}
|
||||
}
|
||||
}
|
18
features/files/client/build.gradle
Normal file
18
features/files/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.files.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
features/files/client/src/commonMain/kotlin/dev/inmo/postssystem/features/files/client/ClientFilesStorage.kt
Normal file
48
features/files/client/src/commonMain/kotlin/dev/inmo/postssystem/features/files/client/ClientFilesStorage.kt
Normal file
@ -0,0 +1,48 @@
|
||||
package dev.inmo.postssystem.features.files.client
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
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 io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.readBytes
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class ClientFilesStorage(
|
||||
baseUrl: String,
|
||||
private val client: HttpClient,
|
||||
private val serialFormat: BinaryFormat
|
||||
) : FilesStorage, ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> by KtorReadStandardCrudRepo(
|
||||
buildStandardUrl(baseUrl, filesRootPathPart),
|
||||
UnifiedRequester(client, serialFormat),
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
) {
|
||||
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)
|
||||
}.readBytes()
|
||||
|
||||
override suspend fun getFullFileInfo(
|
||||
id: FileId
|
||||
): FullFileInfoStorageWrapper? = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFullFileInfoPathPart,
|
||||
filesFileIdParameter to unifiedRequester.encodeUrlQueryValue(FileId.serializer(), id)
|
||||
),
|
||||
FullFileInfoStorageWrapper.serializer().nullable
|
||||
)
|
||||
}
|
1
features/files/client/src/main/AndroidManifest.xml
Normal file
1
features/files/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.files.client"/>
|
19
features/files/common/build.gradle
Normal file
19
features/files/common/build.gradle
Normal file
@ -0,0 +1,19 @@
|
||||
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")
|
||||
api "dev.inmo:micro_utils.mime_types:$microutils_version"
|
||||
api "dev.inmo:micro_utils.repos.common:$microutils_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/Constants.kt
Normal file
6
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/Constants.kt
Normal file
@ -0,0 +1,6 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
const val filesRootPathPart = "files"
|
||||
const val filesGetFilesPathPart = "getFiles"
|
||||
const val filesGetFullFileInfoPathPart = "getFullFileInfo"
|
||||
const val filesFileIdParameter = "fileId"
|
35
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfo.kt
Normal file
35
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfo.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.mime_types.MimeType
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(FileInfoSerializer::class)
|
||||
sealed interface FileInfo {
|
||||
val name: FileName
|
||||
val mimeType: MimeType
|
||||
|
||||
companion object {
|
||||
fun serializer(): KSerializer<FileInfo> = FileInfoSerializer
|
||||
}
|
||||
}
|
||||
|
||||
object FileInfoSerializer : KSerializer<FileInfo> by TypedSerializer(
|
||||
"meta" to MetaFileInfo.serializer(),
|
||||
"full" to FullFileInfo.serializer(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MetaFileInfo(override val name: FileName, override val mimeType: MimeType) : FileInfo
|
||||
|
||||
@Serializable
|
||||
data class FullFileInfo(
|
||||
override val name: FileName,
|
||||
override val mimeType: MimeType,
|
||||
@Serializable(ByteArrayAllocatorSerializer::class)
|
||||
val byteArrayAllocator: ByteArrayAllocator
|
||||
) : FileInfo
|
||||
|
||||
fun FullFileInfo.toMetaFileInfo() = MetaFileInfo(name, mimeType)
|
21
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfoWrapper.kt
Normal file
21
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfoWrapper.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class FileId(val string: String) {
|
||||
override fun toString(): String = string.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class FileInfoStorageWrapper {
|
||||
abstract val id: FileId
|
||||
abstract val fileInfo: FileInfo
|
||||
}
|
||||
@Serializable
|
||||
data class MetaFileInfoStorageWrapper(override val id: FileId, override val fileInfo: MetaFileInfo) : FileInfoStorageWrapper()
|
||||
@Serializable
|
||||
data class FullFileInfoStorageWrapper(override val id: FileId, override val fileInfo: FullFileInfo) : FileInfoStorageWrapper()
|
9
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FilesStorage.kt
Normal file
9
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FilesStorage.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.features.files.common.storage
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
||||
|
||||
interface FilesStorage : ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> {
|
||||
suspend fun getBytes(id: FileId): ByteArray
|
||||
suspend fun getFullFileInfo(id: FileId): FullFileInfoStorageWrapper?
|
||||
}
|
8
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FullFilesStorage.kt
Normal file
8
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FullFilesStorage.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package dev.inmo.postssystem.features.files.common.storage
|
||||
|
||||
interface FullFilesStorage : FilesStorage, WriteFilesStorage
|
||||
|
||||
class DefaultFullFilesStorage(
|
||||
filesStorage: FilesStorage,
|
||||
writeFilesStorage: WriteFilesStorage
|
||||
) : FullFilesStorage, FilesStorage by filesStorage, WriteFilesStorage by writeFilesStorage
|
6
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/WriteFilesStorage.kt
Normal file
6
features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/WriteFilesStorage.kt
Normal file
@ -0,0 +1,6 @@
|
||||
package dev.inmo.postssystem.features.files.common.storage
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.micro_utils.repos.WriteCRUDRepo
|
||||
|
||||
interface WriteFilesStorage : WriteCRUDRepo<FullFileInfoStorageWrapper, FileId, FullFileInfo>
|
63
features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/DiskFilesStorage.kt
Normal file
63
features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/DiskFilesStorage.kt
Normal file
@ -0,0 +1,63 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
||||
import java.io.File
|
||||
|
||||
class DiskFilesStorage(
|
||||
private val filesFolder: File,
|
||||
private val metasKeyValueRepo: ReadKeyValueRepo<FileId, MetaFileInfo>
|
||||
) : FilesStorage {
|
||||
private val FileId.file
|
||||
get() = File(filesFolder, string)
|
||||
|
||||
init {
|
||||
if (!filesFolder.exists()) {
|
||||
filesFolder.mkdirs()
|
||||
} else {
|
||||
require(filesFolder.isDirectory) { "$filesFolder must be a directory" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun FileId.meta(): MetaFileInfoStorageWrapper? {
|
||||
return MetaFileInfoStorageWrapper(
|
||||
this,
|
||||
metasKeyValueRepo.get(this) ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getBytes(id: FileId): ByteArray = id.file.readBytes()
|
||||
|
||||
override suspend fun getFullFileInfo(id: FileId): FullFileInfoStorageWrapper? = getById(
|
||||
id
|
||||
) ?.let {
|
||||
FullFileInfoStorageWrapper(
|
||||
id,
|
||||
FullFileInfo(
|
||||
it.fileInfo.name,
|
||||
it.fileInfo.mimeType
|
||||
) {
|
||||
id.file.readBytes()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun contains(id: FileId): Boolean = metasKeyValueRepo.contains(id)
|
||||
|
||||
override suspend fun count(): Long = metasKeyValueRepo.count()
|
||||
|
||||
override suspend fun getById(id: FileId): MetaFileInfoStorageWrapper? = id.meta()
|
||||
|
||||
override suspend fun getByPagination(pagination: Pagination): PaginationResult<MetaFileInfoStorageWrapper> {
|
||||
val keys = metasKeyValueRepo.keys(pagination)
|
||||
return keys.changeResults(
|
||||
keys.results.mapNotNull {
|
||||
MetaFileInfoStorageWrapper(
|
||||
it,
|
||||
metasKeyValueRepo.get(it) ?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
15
features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/MetasKeyValueRepo.kt
Normal file
15
features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/MetasKeyValueRepo.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import kotlinx.serialization.StringFormat
|
||||
|
||||
fun MetasKeyValueRepo(
|
||||
serialFormat: StringFormat,
|
||||
originalRepo: KeyValueRepo<String, String>
|
||||
) = originalRepo.withMapper<FileId, MetaFileInfo, String, String>(
|
||||
{ string },
|
||||
{ serialFormat.encodeToString(MetaFileInfo.serializer(), this) },
|
||||
{ FileId(this) },
|
||||
{ serialFormat.decodeFromString(MetaFileInfo.serializer(), this) }
|
||||
)
|
69
features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt
Normal file
69
features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt
Normal file
@ -0,0 +1,69 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.File
|
||||
|
||||
class WriteDistFilesStorage(
|
||||
private val filesFolder: File,
|
||||
private val metasKeyValueRepo: WriteKeyValueRepo<FileId, MetaFileInfo>
|
||||
) : WriteFilesStorage {
|
||||
private val FileId.file
|
||||
get() = File(filesFolder, string)
|
||||
|
||||
private val _deletedObjectsIdsFlow = MutableSharedFlow<FileId>()
|
||||
private val _newObjectsFlow = MutableSharedFlow<FullFileInfoStorageWrapper>()
|
||||
private val _updatedObjectsFlow = MutableSharedFlow<FullFileInfoStorageWrapper>()
|
||||
override val deletedObjectsIdsFlow: Flow<FileId> = _deletedObjectsIdsFlow.asSharedFlow()
|
||||
override val newObjectsFlow: Flow<FullFileInfoStorageWrapper> = _newObjectsFlow.asSharedFlow()
|
||||
override val updatedObjectsFlow: Flow<FullFileInfoStorageWrapper> = _updatedObjectsFlow.asSharedFlow()
|
||||
|
||||
init {
|
||||
if (!filesFolder.exists()) {
|
||||
filesFolder.mkdirs()
|
||||
} else {
|
||||
require(filesFolder.isDirectory) { "$filesFolder must be a directory" }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun create(values: List<FullFileInfo>): List<FullFileInfoStorageWrapper> = values.map {
|
||||
var newId: FileId
|
||||
var file: File
|
||||
do {
|
||||
newId = FileId(uuid4().toString())
|
||||
file = newId.file
|
||||
} while (file.exists())
|
||||
metasKeyValueRepo.set(newId, it.toMetaFileInfo())
|
||||
file.writeBytes(it.byteArrayAllocator())
|
||||
FullFileInfoStorageWrapper(newId, it)
|
||||
}
|
||||
|
||||
override suspend fun deleteById(ids: List<FileId>) {
|
||||
ids.forEach {
|
||||
if (it.file.delete()) {
|
||||
metasKeyValueRepo.unset(it)
|
||||
_deletedObjectsIdsFlow.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
id: FileId,
|
||||
value: FullFileInfo
|
||||
): FullFileInfoStorageWrapper? = id.file.takeIf { it.exists() } ?.writeBytes(value.byteArrayAllocator()) ?.let {
|
||||
val result = FullFileInfoStorageWrapper(id, value.copy())
|
||||
|
||||
metasKeyValueRepo.set(id, value.toMetaFileInfo())
|
||||
_updatedObjectsFlow.emit(result)
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
values: List<UpdatedValuePair<FileId, FullFileInfo>>
|
||||
): List<FullFileInfoStorageWrapper> = values.mapNotNull { (id, file) ->
|
||||
update(id, file)
|
||||
}
|
||||
}
|
1
features/files/common/src/main/AndroidManifest.xml
Normal file
1
features/files/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.files.common"/>
|
17
features/files/server/build.gradle
Normal file
17
features/files/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.files.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
features/files/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/server/FilesRoutingConfigurator.kt
Normal file
57
features/files/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/server/FilesRoutingConfigurator.kt
Normal file
@ -0,0 +1,57 @@
|
||||
package dev.inmo.postssystem.features.files.server
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.*
|
||||
import dev.inmo.micro_utils.ktor.server.*
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
|
||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureWriteStandardCrudRepoRoutes
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.routing.*
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class FilesRoutingConfigurator(
|
||||
private val filesStorage: FilesStorage,
|
||||
private val writeFilesStorage: WriteFilesStorage?
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
constructor(fullFilesStorage: FullFilesStorage) : this(fullFilesStorage, fullFilesStorage)
|
||||
|
||||
override fun Route.invoke() {
|
||||
authenticate {
|
||||
route(filesRootPathPart) {
|
||||
configureReadStandardCrudRepoRoutes(
|
||||
filesStorage,
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
)
|
||||
writeFilesStorage ?.let {
|
||||
configureWriteStandardCrudRepoRoutes(
|
||||
writeFilesStorage,
|
||||
FullFileInfoStorageWrapper.serializer(),
|
||||
FullFileInfoStorageWrapper.serializer().nullable,
|
||||
FullFileInfo.serializer(),
|
||||
FileId.serializer()
|
||||
)
|
||||
}
|
||||
post(filesGetFilesPathPart) {
|
||||
call.respondBytes(
|
||||
filesStorage.getBytes(
|
||||
call.uniload(FileId.serializer())
|
||||
)
|
||||
)
|
||||
}
|
||||
get(filesGetFullFileInfoPathPart) {
|
||||
call.unianswer(
|
||||
FullFileInfoStorageWrapper.serializer().nullable,
|
||||
filesStorage.getFullFileInfo(
|
||||
call.decodeUrlQueryValueOrSendError(filesFileIdParameter, FileId.serializer()) ?: return@get
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/roles/client/build.gradle
Normal file
18
features/roles/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.roles.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ClientUsersRolesStorage.kt
Normal file
17
features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ClientUsersRolesStorage.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package dev.inmo.postssystem.features.roles.client
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import kotlinx.serialization.KSerializer
|
||||
|
||||
class ClientUsersRolesStorage<T : UserRole>(
|
||||
private val baseUrl: String,
|
||||
private val unifiedRequester: UnifiedRequester,
|
||||
private val serializer: KSerializer<T>
|
||||
) : UsersRolesStorage<T>,
|
||||
ReadUsersRolesStorage<T> by ReadClientUsersRolesStorage(
|
||||
baseUrl, unifiedRequester, serializer
|
||||
),
|
||||
WriteUsersRolesStorage<T> by WriteClientUsersRolesStorage(
|
||||
baseUrl, unifiedRequester, serializer
|
||||
)
|
70
features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ReadClientUsersRolesStorage.kt
Normal file
70
features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ReadClientUsersRolesStorage.kt
Normal file
@ -0,0 +1,70 @@
|
||||
package dev.inmo.postssystem.features.roles.client
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
class ReadClientUsersRolesStorage<T : UserRole>(
|
||||
private val baseUrl: String,
|
||||
private val unifiedRequester: UnifiedRequester,
|
||||
private val serializer: KSerializer<T>
|
||||
) : ReadUsersRolesStorage<T> {
|
||||
private val userRolesSerializer = ListSerializer(serializer)
|
||||
|
||||
private val userRolesFullUrl = buildStandardUrl(
|
||||
baseUrl,
|
||||
usersRolesRootPathPart
|
||||
)
|
||||
|
||||
override suspend fun getUsers(
|
||||
userRole: T
|
||||
): List<UserId> = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
userRolesFullUrl,
|
||||
usersRolesGetUsersPathPart,
|
||||
usersRolesUserRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, userRole)
|
||||
),
|
||||
UsersIdsSerializer
|
||||
)
|
||||
|
||||
override suspend fun getRoles(
|
||||
userId: UserId
|
||||
): List<T> = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
userRolesFullUrl,
|
||||
usersRolesGetRolesPathPart,
|
||||
usersRolesUserIdQueryParameterName to unifiedRequester.encodeUrlQueryValue(UserId.serializer(), userId)
|
||||
),
|
||||
userRolesSerializer
|
||||
)
|
||||
|
||||
override suspend fun contains(
|
||||
userId: UserId,
|
||||
userRole: T
|
||||
): Boolean = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
userRolesFullUrl,
|
||||
usersRolesContainsPathPart,
|
||||
usersRolesUserIdQueryParameterName to unifiedRequester.encodeUrlQueryValue(UserId.serializer(), userId),
|
||||
usersRolesUserRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, userRole)
|
||||
),
|
||||
Boolean.serializer()
|
||||
)
|
||||
|
||||
override suspend fun containsAny(
|
||||
userId: UserId,
|
||||
userRoles: List<T>
|
||||
): Boolean = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
userRolesFullUrl,
|
||||
usersRolesContainsAnyPathPart,
|
||||
usersRolesUserIdQueryParameterName to unifiedRequester.encodeUrlQueryValue(UserId.serializer(), userId),
|
||||
usersRolesUserRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(userRolesSerializer, userRoles)
|
||||
),
|
||||
Boolean.serializer()
|
||||
)
|
||||
}
|
52
features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/WriteClientUsersRolesStorage.kt
Normal file
52
features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/WriteClientUsersRolesStorage.kt
Normal file
@ -0,0 +1,52 @@
|
||||
package dev.inmo.postssystem.features.roles.client
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
class WriteClientUsersRolesStorage<T : UserRole>(
|
||||
private val baseUrl: String,
|
||||
private val unifiedRequester: UnifiedRequester,
|
||||
private val serializer: KSerializer<T>
|
||||
) : WriteUsersRolesStorage<T> {
|
||||
private val wrapperSerializer = UserRolesStorageIncludeExcludeWrapper.serializer(
|
||||
serializer
|
||||
)
|
||||
private val userRolesFullUrl = buildStandardUrl(
|
||||
baseUrl,
|
||||
usersRolesRootPathPart
|
||||
)
|
||||
private val includeFullUrl = buildStandardUrl(
|
||||
userRolesFullUrl,
|
||||
usersRolesIncludePathPart
|
||||
)
|
||||
private val excludeFullUrl = buildStandardUrl(
|
||||
userRolesFullUrl,
|
||||
usersRolesExcludePathPart
|
||||
)
|
||||
|
||||
override suspend fun include(
|
||||
userId: UserId,
|
||||
userRole: T
|
||||
): Boolean = unifiedRequester.unipost(
|
||||
includeFullUrl,
|
||||
wrapperSerializer to UserRolesStorageIncludeExcludeWrapper(
|
||||
userId, userRole
|
||||
),
|
||||
Boolean.serializer()
|
||||
)
|
||||
|
||||
override suspend fun exclude(
|
||||
userId: UserId,
|
||||
userRole: T
|
||||
): Boolean = unifiedRequester.unipost(
|
||||
excludeFullUrl,
|
||||
wrapperSerializer to UserRolesStorageIncludeExcludeWrapper(
|
||||
userId, userRole
|
||||
),
|
||||
Boolean.serializer()
|
||||
)
|
||||
}
|
1
features/roles/client/src/main/AndroidManifest.xml
Normal file
1
features/roles/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.client"/>
|
18
features/roles/common/build.gradle
Normal file
18
features/roles/common/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.common.common")
|
||||
api project(":postssystem.features.users.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/Constants.kt
Normal file
26
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/Constants.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package dev.inmo.postssystem.features.roles.common
|
||||
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
|
||||
const val usersRolesRootPathPart = "roles"
|
||||
|
||||
val UsersIdsSerializer = ListSerializer(UserId.serializer())
|
||||
|
||||
const val usersRolesUserRoleQueryParameterName = "userRole"
|
||||
const val usersRolesUserIdQueryParameterName = "userId"
|
||||
|
||||
const val usersRolesGetUsersPathPart = "getUsersByRole"
|
||||
const val usersRolesGetRolesPathPart = "getUserRoles"
|
||||
const val usersRolesContainsPathPart = "contains"
|
||||
const val usersRolesContainsAnyPathPart = "containsAny"
|
||||
|
||||
const val usersRolesIncludePathPart = "include"
|
||||
const val usersRolesExcludePathPart = "exclude"
|
||||
|
||||
@Serializable
|
||||
data class UserRolesStorageIncludeExcludeWrapper<T : UserRole>(
|
||||
val userId: UserId,
|
||||
val userRole: T
|
||||
)
|
81
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRole.kt
Normal file
81
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRole.kt
Normal file
@ -0,0 +1,81 @@
|
||||
package dev.inmo.postssystem.features.roles.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.*
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
@Serializable(UserRoleSerializer::class)
|
||||
interface UserRole { // temporarily made as class while interfaces are bugged
|
||||
companion object {
|
||||
fun serializer() = UserRoleSerializer
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UnknownUserRole(val originalJson: JsonElement) : UserRole
|
||||
|
||||
@Serializer(UserRole::class)
|
||||
object UserRoleSerializer : KSerializer<UserRole> {
|
||||
private val userRoleFormat = Json { ignoreUnknownKeys = true }
|
||||
private const val keyField = "key"
|
||||
private const val valueField = "value"
|
||||
private val serializers = mutableMapOf<String, KSerializer<out UserRole>>()
|
||||
override val descriptor: SerialDescriptor = String.serializer().descriptor
|
||||
|
||||
@InternalSerializationApi
|
||||
override fun deserialize(decoder: Decoder): UserRole {
|
||||
return if (decoder is JsonDecoder) {
|
||||
val originalJson = decoder.decodeJsonElement().jsonObject
|
||||
val type = originalJson[keyField]?.jsonPrimitive ?.content
|
||||
return if (type == null || !serializers.containsKey(type)) {
|
||||
UnknownUserRole(originalJson)
|
||||
} else {
|
||||
userRoleFormat.decodeFromJsonElement(
|
||||
serializers.getValue(type),
|
||||
originalJson[valueField] ?: buildJsonObject { }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val encoded = decoder.decodeString()
|
||||
userRoleFormat.decodeFromString(this, encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
private fun <T : UserRole> T.toJson(): JsonElement {
|
||||
return userRoleFormat.encodeToJsonElement(this::class.serializer() as KSerializer<T>, this)
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
override fun serialize(encoder: Encoder, value: UserRole) {
|
||||
if (encoder is JsonEncoder) {
|
||||
if (value is UnknownUserRole) {
|
||||
encoder.encodeJsonElement(value.originalJson)
|
||||
} else {
|
||||
val valueSerializer = value::class.serializer()
|
||||
val type = serializers.keys.first { serializers[it] == valueSerializer }
|
||||
encoder.encodeJsonElement(
|
||||
buildJsonObject {
|
||||
put(keyField, type)
|
||||
put(valueField, value.toJson())
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
encoder.encodeString(
|
||||
userRoleFormat.encodeToString(this, value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : UserRole> includeSerializer(
|
||||
type: String,
|
||||
kSerializer: KSerializer<T>
|
||||
) { serializers[type] = kSerializer }
|
||||
|
||||
fun excludeSerializer(type: String) {
|
||||
serializers.remove(type)
|
||||
}
|
||||
}
|
5
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRolesSerializer.kt
Normal file
5
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRolesSerializer.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package dev.inmo.postssystem.features.roles.common
|
||||
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
|
||||
val UserRolesSerializer = ListSerializer(UserRole.serializer())
|
16
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UsersRolesStorage.kt
Normal file
16
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UsersRolesStorage.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package dev.inmo.postssystem.features.roles.common
|
||||
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
|
||||
interface ReadUsersRolesStorage<T : UserRole> {
|
||||
suspend fun getUsers(userRole: T): List<UserId>
|
||||
suspend fun getRoles(userId: UserId): List<T>
|
||||
suspend fun contains(userId: UserId, userRole: T): Boolean
|
||||
suspend fun containsAny(userId: UserId, userRoles: List<T>): Boolean
|
||||
}
|
||||
interface WriteUsersRolesStorage<T : UserRole> {
|
||||
suspend fun include(userId: UserId, userRole: T): Boolean
|
||||
suspend fun exclude(userId: UserId, userRole: T): Boolean
|
||||
}
|
||||
|
||||
interface UsersRolesStorage<T : UserRole> : ReadUsersRolesStorage<T>, WriteUsersRolesStorage<T>
|
@ -0,0 +1,5 @@
|
||||
package dev.inmo.postssystem.features.roles.common.keyvalue
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValuesRepo
|
||||
|
||||
typealias KeyValuesUsersRolesOriginalRepo = KeyValuesRepo<Long, String>
|
13
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueUsersRolesStorage.kt
Normal file
13
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueUsersRolesStorage.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package dev.inmo.postssystem.features.roles.common.keyvalue
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.StringFormat
|
||||
|
||||
open class KeyValueUsersRolesStorage<T : UserRole>(
|
||||
private val keyValuesRepo: KeyValuesUsersRolesOriginalRepo,
|
||||
private val serializer: KSerializer<T>,
|
||||
private val format: StringFormat = ReadKeyValueUsersRolesStorage.defaultJson
|
||||
) : UsersRolesStorage<T>,
|
||||
ReadUsersRolesStorage<T> by ReadKeyValueUsersRolesStorage(keyValuesRepo, serializer, format),
|
||||
WriteUsersRolesStorage<T> by WriteKeyValueUsersRolesStorage(keyValuesRepo, serializer, format)
|
59
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/ReadKeyValueUsersRolesStorage.kt
Normal file
59
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/ReadKeyValueUsersRolesStorage.kt
Normal file
@ -0,0 +1,59 @@
|
||||
package dev.inmo.postssystem.features.roles.common.keyvalue
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.default
|
||||
import dev.inmo.postssystem.features.roles.common.ReadUsersRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import dev.inmo.micro_utils.pagination.changeResults
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.StringFormat
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
open class ReadKeyValueUsersRolesStorage<T : UserRole>(
|
||||
private val keyValuesRepo: ReadKeyValuesRepo<Long, String>,
|
||||
private val serializer: KSerializer<T>,
|
||||
private val format: StringFormat = defaultJson
|
||||
) : ReadUsersRolesStorage<T> {
|
||||
override suspend fun getUsers(userRole: T): List<UserId> {
|
||||
val serialized = format.encodeToString(serializer, userRole)
|
||||
|
||||
return keyValuesRepo.getAllByWithNextPaging {
|
||||
keys(serialized, it).let { paginationResult ->
|
||||
paginationResult.changeResults(
|
||||
paginationResult.results.map { UserId(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRoles(userId: UserId): List<T> {
|
||||
return keyValuesRepo.getAllByWithNextPaging {
|
||||
get(userId.long, it).let { paginationResult ->
|
||||
paginationResult.changeResults(
|
||||
paginationResult.results.map { serialized ->
|
||||
format.decodeFromString(serializer, serialized)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun contains(userId: UserId, userRole: T): Boolean {
|
||||
val serialized = format.encodeToString(serializer, userRole)
|
||||
|
||||
return keyValuesRepo.contains(userId.long, serialized)
|
||||
}
|
||||
|
||||
override suspend fun containsAny(userId: UserId, userRoles: List<T>): Boolean {
|
||||
return userRoles.any {
|
||||
contains(userId, it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal val defaultJson = Json.default
|
||||
}
|
||||
}
|
||||
|
34
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/WriteKeyValueUsersRolesStorage.kt
Normal file
34
features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/WriteKeyValueUsersRolesStorage.kt
Normal file
@ -0,0 +1,34 @@
|
||||
package dev.inmo.postssystem.features.roles.common.keyvalue
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.common.WriteUsersRolesStorage
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.StringFormat
|
||||
|
||||
open class WriteKeyValueUsersRolesStorage<T : UserRole>(
|
||||
private val keyValuesRepo: WriteKeyValuesRepo<Long, String>,
|
||||
private val serializer: KSerializer<T>,
|
||||
private val format: StringFormat = ReadKeyValueUsersRolesStorage.defaultJson
|
||||
) : WriteUsersRolesStorage<T> {
|
||||
override suspend fun include(userId: UserId, userRole: T): Boolean {
|
||||
return runCatching {
|
||||
keyValuesRepo.add(
|
||||
userId.long,
|
||||
format.encodeToString(serializer, userRole)
|
||||
)
|
||||
true
|
||||
}.getOrElse { false }
|
||||
}
|
||||
|
||||
override suspend fun exclude(userId: UserId, userRole: T): Boolean {
|
||||
return runCatching {
|
||||
keyValuesRepo.remove(
|
||||
userId.long,
|
||||
format.encodeToString(serializer, userRole)
|
||||
)
|
||||
true
|
||||
}.getOrElse { false }
|
||||
}
|
||||
}
|
1
features/roles/common/src/main/AndroidManifest.xml
Normal file
1
features/roles/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.common"/>
|
19
features/roles/manager/client/build.gradle
Normal file
19
features/roles/manager/client/build.gradle
Normal file
@ -0,0 +1,19 @@
|
||||
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.roles.manager.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
api project(":postssystem.features.roles.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.manager.client"/>
|
18
features/roles/manager/common/build.gradle
Normal file
18
features/roles/manager/common/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.common.common")
|
||||
api project(":postssystem.features.roles.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRole.kt
Normal file
32
features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRole.kt
Normal file
@ -0,0 +1,32 @@
|
||||
package dev.inmo.postssystem.features.roles.manager.common
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.common.UserRoleSerializer
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(RolesManagerRoleSerializer::class)
|
||||
interface RolesManagerRole : UserRole {
|
||||
companion object {
|
||||
fun serializer() = RolesManagerRoleSerializer
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object GeneralRolesManagerRole : RolesManagerRole {
|
||||
override fun toString(): String = "GeneralRolesManagerRole"
|
||||
}
|
||||
|
||||
private const val KEY = "roles_manager"
|
||||
|
||||
object RolesManagerRoleSerializer : TypedSerializer<RolesManagerRole>(
|
||||
RolesManagerRole::class,
|
||||
mapOf(
|
||||
"${KEY}_general" to GeneralRolesManagerRole.serializer()
|
||||
)
|
||||
) {
|
||||
init {
|
||||
UserRoleSerializer.includeSerializer(KEY, RolesManagerRoleSerializer)
|
||||
serializers.forEach { (k, v) -> UserRoleSerializer.includeSerializer(k, v) }
|
||||
}
|
||||
}
|
16
features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRoleStorage.kt
Normal file
16
features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRoleStorage.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package dev.inmo.postssystem.features.roles.manager.common
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.default
|
||||
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.common.keyvalue.*
|
||||
import kotlinx.serialization.StringFormat
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class RolesManagerRoleStorage(
|
||||
keyValuesRepo: KeyValuesUsersRolesOriginalRepo,
|
||||
format: StringFormat = Json.default
|
||||
) : UsersRolesStorage<RolesManagerRole>, KeyValueUsersRolesStorage<RolesManagerRole>(
|
||||
keyValuesRepo,
|
||||
RolesManagerRole.serializer(),
|
||||
format
|
||||
)
|
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.manager.common"/>
|
18
features/roles/manager/server/build.gradle
Normal file
18
features/roles/manager/server/build.gradle
Normal file
@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.roles.manager.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
api project(":postssystem.features.roles.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerRolesChecker.kt
Normal file
18
features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerRolesChecker.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.features.roles.manager.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.ReadUsersRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.manager.common.GeneralRolesManagerRole
|
||||
import dev.inmo.postssystem.features.roles.server.RolesChecker
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
object RolesManagerRolesChecker : RolesChecker<UserRole> {
|
||||
override val key: String
|
||||
get() = "RolesManagerRolesChecker"
|
||||
|
||||
override suspend fun ApplicationCall.invoke(
|
||||
usersRolesStorage: ReadUsersRolesStorage<UserRole>,
|
||||
user: User
|
||||
): Boolean = usersRolesStorage.contains(user.id, GeneralRolesManagerRole)
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.roles.manager.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRole
|
||||
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer
|
||||
import dev.inmo.postssystem.features.roles.server.UsersRolesStorageWriteServerRoutesConfigurator
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
|
||||
class RolesManagerUsersRolesStorageServerRoutesConfigurator(
|
||||
storage: UsersRolesStorage<RolesManagerRole>
|
||||
) : ApplicationRoutingConfigurator.Element by UsersRolesStorageWriteServerRoutesConfigurator(
|
||||
storage,
|
||||
RolesManagerRoleSerializer,
|
||||
RolesManagerRolesChecker.key
|
||||
)
|
18
features/roles/server/build.gradle
Normal file
18
features/roles/server/build.gradle
Normal file
@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.roles.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
api project(":postssystem.features.auth.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/RolesChecker.kt
Normal file
29
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/RolesChecker.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package dev.inmo.postssystem.features.roles.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
interface RolesChecker<T : UserRole> {
|
||||
val key: String
|
||||
|
||||
suspend operator fun ApplicationCall.invoke(
|
||||
usersRolesStorage: ReadUsersRolesStorage<T>,
|
||||
user: User
|
||||
): Boolean
|
||||
|
||||
companion object {
|
||||
fun <T : UserRole> default(
|
||||
key: String,
|
||||
role: T
|
||||
): RolesChecker<T> = object : RolesChecker<T> {
|
||||
override val key: String
|
||||
get() = key
|
||||
|
||||
override suspend fun ApplicationCall.invoke(
|
||||
usersRolesStorage: ReadUsersRolesStorage<T>,
|
||||
user: User
|
||||
): Boolean = usersRolesStorage.contains(user.id, role)
|
||||
}
|
||||
}
|
||||
}
|
39
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAggregator.kt
Normal file
39
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAggregator.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package dev.inmo.postssystem.features.roles.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
|
||||
class UsersRolesAggregator(
|
||||
private val otherStorages: List<UsersRolesStorageHolder<*>>
|
||||
) : UsersRolesStorage<UserRole> {
|
||||
private val otherStoragesByClass = otherStorages.associateBy { it.kclass }
|
||||
|
||||
override suspend fun getUsers(userRole: UserRole): List<UserId> {
|
||||
return otherStoragesByClass[userRole::class] ?.getUsers(userRole) ?: emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getRoles(userId: UserId): List<UserRole> = otherStorages.flatMap { it.getRoles(userId) }
|
||||
|
||||
override suspend fun contains(userId: UserId, userRole: UserRole): Boolean {
|
||||
return otherStoragesByClass[userRole::class] ?.contains(userId, userRole) ?: false
|
||||
}
|
||||
|
||||
override suspend fun containsAny(userId: UserId, userRoles: List<UserRole>): Boolean {
|
||||
return userRoles.any {
|
||||
contains(userId, it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun include(
|
||||
userId: UserId,
|
||||
userRole: UserRole
|
||||
): Boolean = otherStoragesByClass[userRole::class] ?.include(userId, userRole) ?: false
|
||||
|
||||
override suspend fun exclude(
|
||||
userId: UserId,
|
||||
userRole: UserRole
|
||||
): Boolean {
|
||||
return otherStoragesByClass[userRole::class] ?.exclude(userId, userRole) ?: false
|
||||
}
|
||||
}
|
40
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAuthenticationConfigurator.kt
Normal file
40
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAuthenticationConfigurator.kt
Normal file
@ -0,0 +1,40 @@
|
||||
package dev.inmo.postssystem.features.roles.server
|
||||
|
||||
import dev.inmo.postssystem.features.auth.common.AuthToken
|
||||
import dev.inmo.postssystem.features.auth.server.principal
|
||||
import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService
|
||||
import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.session
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.response.respond
|
||||
|
||||
class UsersRolesAuthenticationConfigurator<T : UserRole>(
|
||||
private val usersRolesStorage: UsersRolesStorage<T>,
|
||||
private val authTokensService: AuthTokensService,
|
||||
private val rolesCheckers: List<RolesChecker<T>>
|
||||
) : ApplicationAuthenticationConfigurator.Element {
|
||||
override fun Authentication.Configuration.invoke() {
|
||||
rolesCheckers.forEach { checker ->
|
||||
session<AuthToken>(checker.key) {
|
||||
validate {
|
||||
val result = authTokensService.getUserPrincipal(it)
|
||||
if (result.isSuccess) {
|
||||
val user = result.getOrThrow().principal()
|
||||
if (checker.run { invoke(usersRolesStorage, user.user) }) {
|
||||
user
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { call.respond(HttpStatusCode.Unauthorized) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageHolder.kt
Normal file
42
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageHolder.kt
Normal file
@ -0,0 +1,42 @@
|
||||
package dev.inmo.postssystem.features.roles.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class UsersRolesStorageHolder<T : UserRole>(
|
||||
val kclass: KClass<T>,
|
||||
val storage: UsersRolesStorage<T>
|
||||
) {
|
||||
private suspend fun <R> doIfRelevant(
|
||||
userRole: UserRole,
|
||||
block: suspend (T) -> R
|
||||
): Optional<R> = if (kclass.isInstance(userRole)) {
|
||||
block(userRole as T).optional
|
||||
} else {
|
||||
Optional.absent()
|
||||
}
|
||||
|
||||
suspend fun getUsers(userRole: UserRole): List<UserId>? = doIfRelevant(userRole) {
|
||||
storage.getUsers(it)
|
||||
}.dataOrNull()
|
||||
|
||||
suspend fun getRoles(userId: UserId): List<UserRole> = storage.getRoles(userId)
|
||||
|
||||
suspend fun contains(userId: UserId, userRole: UserRole): Boolean? = doIfRelevant(userRole) {
|
||||
storage.contains(userId, it)
|
||||
}.dataOrNull()
|
||||
|
||||
suspend fun include(
|
||||
userId: UserId,
|
||||
userRole: UserRole
|
||||
): Boolean? = doIfRelevant(userRole) {
|
||||
storage.include(userId, it)
|
||||
}.dataOrNull()
|
||||
|
||||
suspend fun exclude(userId: UserId, userRole: UserRole): Boolean? = doIfRelevant(userRole) {
|
||||
storage.exclude(userId, it)
|
||||
}.dataOrNull()
|
||||
}
|
73
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageReadServerRoutesConfigurator.kt
Normal file
73
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageReadServerRoutesConfigurator.kt
Normal file
@ -0,0 +1,73 @@
|
||||
package dev.inmo.postssystem.features.roles.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import dev.inmo.postssystem.features.users.common.UserId
|
||||
import dev.inmo.micro_utils.ktor.server.*
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.routing.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
class UsersRolesStorageReadServerRoutesConfigurator<T : UserRole>(
|
||||
private val storage: ReadUsersRolesStorage<T>,
|
||||
private val serializer: KSerializer<T>
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
private val userRolesSerializer = ListSerializer(serializer)
|
||||
override fun Route.invoke() {
|
||||
authenticate {
|
||||
route(usersRolesRootPathPart) {
|
||||
get(usersRolesGetUsersPathPart) {
|
||||
val userRole = call.decodeUrlQueryValueOrSendError(usersRolesUserRoleQueryParameterName, serializer)
|
||||
?: return@get
|
||||
call.unianswer(
|
||||
UsersIdsSerializer,
|
||||
storage.getUsers(userRole)
|
||||
)
|
||||
}
|
||||
|
||||
get(usersRolesGetRolesPathPart) {
|
||||
val userId =
|
||||
call.decodeUrlQueryValueOrSendError(usersRolesUserIdQueryParameterName, UserId.serializer())
|
||||
?: return@get
|
||||
call.unianswer(
|
||||
userRolesSerializer,
|
||||
storage.getRoles(userId)
|
||||
)
|
||||
}
|
||||
|
||||
get(usersRolesContainsPathPart) {
|
||||
val userId = call.decodeUrlQueryValueOrSendError(
|
||||
usersRolesUserIdQueryParameterName,
|
||||
UserId.serializer()
|
||||
) ?: return@get
|
||||
val userRole = call.decodeUrlQueryValueOrSendError(
|
||||
usersRolesUserRoleQueryParameterName,
|
||||
serializer
|
||||
) ?: return@get
|
||||
call.unianswer(
|
||||
Boolean.serializer(),
|
||||
storage.contains(userId, userRole)
|
||||
)
|
||||
}
|
||||
|
||||
get(usersRolesContainsAnyPathPart) {
|
||||
val userId = call.decodeUrlQueryValueOrSendError(
|
||||
usersRolesUserIdQueryParameterName,
|
||||
UserId.serializer()
|
||||
) ?: return@get
|
||||
val userRoles = call.decodeUrlQueryValueOrSendError(
|
||||
usersRolesUserRoleQueryParameterName,
|
||||
userRolesSerializer
|
||||
) ?: return@get
|
||||
call.unianswer(
|
||||
Boolean.serializer(),
|
||||
storage.containsAny(userId, userRoles)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageWriteServerRoutesConfigurator.kt
Normal file
51
features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageWriteServerRoutesConfigurator.kt
Normal file
@ -0,0 +1,51 @@
|
||||
package dev.inmo.postssystem.features.roles.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.*
|
||||
import dev.inmo.micro_utils.ktor.server.*
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.routing.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
class UsersRolesStorageWriteServerRoutesConfigurator<T : UserRole>(
|
||||
private val storage: WriteUsersRolesStorage<T>,
|
||||
private val serializer: KSerializer<T>,
|
||||
private val includeAuthKey: String,
|
||||
private val excludeAuthKey: String = includeAuthKey
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
override fun Route.invoke() {
|
||||
route(usersRolesRootPathPart) {
|
||||
val wrapperSerializer = UserRolesStorageIncludeExcludeWrapper.serializer(
|
||||
serializer
|
||||
)
|
||||
authenticate(includeAuthKey) {
|
||||
post(usersRolesIncludePathPart) {
|
||||
val wrapper = call.uniload(wrapperSerializer)
|
||||
|
||||
call.unianswer(
|
||||
Boolean.serializer(),
|
||||
storage.include(
|
||||
wrapper.userId,
|
||||
wrapper.userRole
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
authenticate(excludeAuthKey) {
|
||||
post(usersRolesExcludePathPart) {
|
||||
val wrapper = call.uniload(wrapperSerializer)
|
||||
|
||||
call.unianswer(
|
||||
Boolean.serializer(),
|
||||
storage.exclude(
|
||||
wrapper.userId,
|
||||
wrapper.userRole
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/status/client/build.gradle
Normal file
18
features/status/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.status.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
features/status/client/src/commonMain/kotlin/dev/inmo/postssystem/features/status/client/StatusFeatureClient.kt
Normal file
27
features/status/client/src/commonMain/kotlin/dev/inmo/postssystem/features/status/client/StatusFeatureClient.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package dev.inmo.postssystem.features.status.client
|
||||
|
||||
import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart
|
||||
import dev.inmo.postssystem.features.status.common.statusRootPart
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
class StatusFeatureClient(
|
||||
baseUrl: String,
|
||||
private val client: HttpClient
|
||||
) {
|
||||
private val fullStatusUrl = buildStandardUrl(
|
||||
baseUrl,
|
||||
statusRootPart
|
||||
)
|
||||
private val fullAuthorisedStatusUrl = buildStandardUrl(
|
||||
fullStatusUrl,
|
||||
statusAuthorisedPathPart
|
||||
)
|
||||
|
||||
suspend fun checkServerStatus() = client.get<HttpResponse>(fullStatusUrl).status == HttpStatusCode.OK
|
||||
suspend fun checkServerStatusWithAuth() = client.get<HttpResponse>(fullAuthorisedStatusUrl).status == HttpStatusCode.OK
|
||||
}
|
1
features/status/client/src/main/AndroidManifest.xml
Normal file
1
features/status/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.status.client"/>
|
17
features/status/common/build.gradle
Normal file
17
features/status/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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
features/status/common/src/commonMain/kotlin/dev/inmo/postssystem/features/status/common/Constants.kt
Normal file
4
features/status/common/src/commonMain/kotlin/dev/inmo/postssystem/features/status/common/Constants.kt
Normal file
@ -0,0 +1,4 @@
|
||||
package dev.inmo.postssystem.features.status.common
|
||||
|
||||
const val statusRootPart = "status"
|
||||
const val statusAuthorisedPathPart = "auth"
|
1
features/status/common/src/main/AndroidManifest.xml
Normal file
1
features/status/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.status.common"/>
|
17
features/status/server/build.gradle
Normal file
17
features/status/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.status.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
features/status/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/status/server/StatusRoutingConfigurator.kt
Normal file
26
features/status/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/status/server/StatusRoutingConfigurator.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package dev.inmo.postssystem.features.status.server
|
||||
|
||||
import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart
|
||||
import dev.inmo.postssystem.features.status.common.statusRootPart
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.*
|
||||
|
||||
object StatusRoutingConfigurator : ApplicationRoutingConfigurator.Element {
|
||||
override fun Route.invoke() {
|
||||
route(statusRootPart) {
|
||||
get {
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
authenticate {
|
||||
get(statusAuthorisedPathPart) {
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/template/client/build.gradle
Normal file
18
features/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.template.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
features/template/client/src/main/AndroidManifest.xml
Normal file
1
features/template/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.template.client"/>
|
17
features/template/common/build.gradle
Normal file
17
features/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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
features/template/common/src/main/AndroidManifest.xml
Normal file
1
features/template/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.template.common"/>
|
17
features/template/server/build.gradle
Normal file
17
features/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.template.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/users/client/build.gradle
Normal file
18
features/users/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.users.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
features/users/client/src/commonMain/kotlin/dev/inmo/postssystem/features/users/client/UsersStorageKtorClient.kt
Normal file
19
features/users/client/src/commonMain/kotlin/dev/inmo/postssystem/features/users/client/UsersStorageKtorClient.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package dev.inmo.postssystem.features.users.client
|
||||
|
||||
import dev.inmo.postssystem.features.users.common.*
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
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 kotlinx.serialization.builtins.nullable
|
||||
|
||||
class UsersStorageKtorClient(
|
||||
baseUrl: String,
|
||||
unifiedRequester: UnifiedRequester
|
||||
) : ReadUsersStorage, ReadCRUDRepo<User, UserId> by KtorReadStandardCrudRepo(
|
||||
buildStandardUrl(baseUrl, usersServerPathPart),
|
||||
unifiedRequester,
|
||||
User.serializer(),
|
||||
User.serializer().nullable,
|
||||
UserId.serializer()
|
||||
)
|
1
features/users/client/src/main/AndroidManifest.xml
Normal file
1
features/users/client/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.users.client"/>
|
27
features/users/common/build.gradle
Normal file
27
features/users/common/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
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")
|
||||
api "dev.inmo:micro_utils.repos.common:$microutils_version"
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "dev.inmo:micro_utils.repos.exposed:$microutils_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
disableIncludingJvmCodeInAndroidPart()
|
||||
}
|
3
features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/Routes.kt
Normal file
3
features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/Routes.kt
Normal file
@ -0,0 +1,3 @@
|
||||
package dev.inmo.postssystem.features.users.common
|
||||
|
||||
const val usersServerPathPart = "users"
|
46
features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/User.kt
Normal file
46
features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/User.kt
Normal file
@ -0,0 +1,46 @@
|
||||
package dev.inmo.postssystem.features.users.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class UserId(val long: Long) {
|
||||
override fun toString(): String = long.toString()
|
||||
}
|
||||
val Long.userId: UserId
|
||||
get() = UserId(this)
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Username(val string: String) {
|
||||
override fun toString(): String = string
|
||||
}
|
||||
val String.username: Username
|
||||
get() = Username(this)
|
||||
|
||||
sealed interface NewUser {
|
||||
val firstName: String
|
||||
val lastName: String
|
||||
val username: Username
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class User : NewUser {
|
||||
abstract val id: UserId
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DefaultUser(
|
||||
override val id: UserId,
|
||||
override val firstName: String,
|
||||
override val lastName: String,
|
||||
override val username: Username,
|
||||
) : User()
|
||||
|
||||
@Serializable
|
||||
data class DefaultNewUser(
|
||||
override val firstName: String,
|
||||
override val lastName: String,
|
||||
override val username: Username,
|
||||
) : NewUser
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user