full reborn

This commit is contained in:
2021-11-24 13:52:27 +06:00
parent 0ac6b0a4df
commit 6a6a197041
246 changed files with 4327 additions and 6952 deletions

View File

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

View 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
}
}

View 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)
}
}
}
}

View 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)
}

View 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()

View 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)
)
}
}
}

View File

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

View File

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

View 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?
}

View 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
)

View 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"

View File

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

View File

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

View 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) }
}
}
}

View 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
}
}
}
}

View 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())
}

View 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>
}

View 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
)
}
}

View 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?
}

View 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)
}
}

View 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"
}
}
}
}

View 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)

View 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()
}

View File

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

View 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
}

View File

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

View 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"
}
}
}
}

View 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

View 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()

View File

@@ -0,0 +1,3 @@
package dev.inmo.postssystem.features.common.common
typealias Milliseconds = Long

View 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())
}

View File

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

View 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"
}
}
}
}

View 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() } }
}
}
}

View File

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

View 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
)
}

View File

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

View 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"
}
}
}
}

View 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"

View 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)

View 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()

View 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?
}

View 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

View 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>

View 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
)
}
)
}
}

View 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) }
)

View 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)
}
}

View File

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

View File

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

View 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
)
)
}
}
}
}
}

View File

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

View 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
)

View 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()
)
}

View 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()
)
}

View File

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

View File

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

View 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
)

View 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)
}
}

View File

@@ -0,0 +1,5 @@
package dev.inmo.postssystem.features.roles.common
import kotlinx.serialization.builtins.ListSerializer
val UserRolesSerializer = ListSerializer(UserRole.serializer())

View 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>

View File

@@ -0,0 +1,5 @@
package dev.inmo.postssystem.features.roles.common.keyvalue
import dev.inmo.micro_utils.repos.KeyValuesRepo
typealias KeyValuesUsersRolesOriginalRepo = KeyValuesRepo<Long, String>

View 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)

View 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
}
}

View 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 }
}
}

View File

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

View 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")
}
}
}
}

View File

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

View File

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

View 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) }
}
}

View 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
)

View File

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

View 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")
}
}
}
}

View 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)
}

View File

@@ -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
)

View 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")
}
}
}
}

View 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)
}
}
}

View 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
}
}

View 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) }
}
}
}
}

View 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()
}

View 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)
)
}
}
}
}
}

View 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
)
)
}
}
}
}
}

View File

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

View 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
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
package dev.inmo.postssystem.features.status.common
const val statusRootPart = "status"
const val statusAuthorisedPathPart = "auth"

View File

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

View File

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

View 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)
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()
)

View File

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

View 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()
}

View File

@@ -0,0 +1,3 @@
package dev.inmo.postssystem.features.users.common
const val usersServerPathPart = "users"

View 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