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
.github/workflows
LICENSEREADME.mdbuild.gradle
business_cases/post_creating
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
business_cases
post_creating
main
common
src
commonMain
kotlin
dev
inmo
postssystem
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
business_cases
post_creating
client
core
defaultAndroidSettingsdefaultAndroidSettings.gradleextensions.gradle
features
auth
common
client
build.gradle
src
commonMain
kotlin
dev
inmo
main
common
build.gradle
src
commonMain
kotlin
dev
inmo
jvmMain
kotlin
dev
inmo
postssystem
features
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
features
files
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
main
common
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
features
roles
status
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
main
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
status
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
features
template
users
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
features
main
common
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
jvmMain
kotlin
dev
inmo
postssystem
features
main
server
build.gradle
src
jvmMain
kotlin
dev
inmo
postssystem
gradle.properties
gradle/wrapper
gradlewgradlew.bat
mimes_generator
mppAndroidProject.gradlemppJavaProject.gradlemppJsProject.gradlemppProjectWithSerialization.gradlepubconf.kpsbpublish.gradlepublish.kpsb
publishing
server
settings.gradle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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"/>

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

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

@ -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"/>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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