full reborn
This commit is contained in:
18
features/auth/client/build.gradle
Normal file
18
features/auth/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.client")
|
||||
api project(":postssystem.features.auth.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
1
features/auth/client/src/main/AndroidManifest.xml
Normal file
1
features/auth/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.auth.client"/>
|
18
features/auth/common/build.gradle
Normal file
18
features/auth/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api project(":postssystem.features.users.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"
|
1
features/auth/common/src/main/AndroidManifest.xml
Normal file
1
features/auth/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.auth.common"/>
|
17
features/auth/server/build.gradle
Normal file
17
features/auth/server/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.auth.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
20
features/common/client/build.gradle
Normal file
20
features/common/client/build.gradle
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version"
|
||||
api "io.ktor:ktor-client-auth:$ktor_version"
|
||||
api "io.ktor:ktor-client-logging:$ktor_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
1
features/common/client/src/main/AndroidManifest.xml
Normal file
1
features/common/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.common.client"/>
|
30
features/common/common/build.gradle
Normal file
30
features/common/common/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "dev.inmo:micro_utils.common:$microutils_version"
|
||||
api "dev.inmo:micro_utils.serialization.typed_serializer:$microutils_version"
|
||||
api "io.insert-koin:koin-core:$koin_version"
|
||||
api "com.benasher44:uuid:$uuid_version"
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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())
|
||||
}
|
1
features/common/common/src/main/AndroidManifest.xml
Normal file
1
features/common/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.common.common"/>
|
25
features/common/server/build.gradle
Normal file
25
features/common/server/build.gradle
Normal file
@@ -0,0 +1,25 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api "dev.inmo:micro_utils.repos.exposed:$microutils_version"
|
||||
api "dev.inmo:micro_utils.repos.ktor.server:$microutils_version"
|
||||
api "dev.inmo:micro_utils.ktor.server:$microutils_version"
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-auth:$ktor_version"
|
||||
api "ch.qos.logback:logback-classic:$logback_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.features.common.server.sessions
|
||||
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.authentication
|
||||
|
||||
class ApplicationAuthenticationConfigurator(
|
||||
private val elements: List<Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun Authentication.Configuration.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
authentication {
|
||||
elements.forEach { it.apply { invoke() } }
|
||||
}
|
||||
}
|
||||
}
|
18
features/files/client/build.gradle
Normal file
18
features/files/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.files.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package dev.inmo.postssystem.features.files.client
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
||||
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.readBytes
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class ClientFilesStorage(
|
||||
baseUrl: String,
|
||||
private val client: HttpClient,
|
||||
private val serialFormat: BinaryFormat
|
||||
) : FilesStorage, ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> by KtorReadStandardCrudRepo(
|
||||
buildStandardUrl(baseUrl, filesRootPathPart),
|
||||
UnifiedRequester(client, serialFormat),
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
) {
|
||||
private val unifiedRequester = UnifiedRequester(client, serialFormat)
|
||||
private val fullFilesPath = buildStandardUrl(baseUrl, filesRootPathPart)
|
||||
private val fullFilesGetBytesPath = buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFilesPathPart
|
||||
)
|
||||
|
||||
override suspend fun getBytes(id: FileId): ByteArray = client.post<HttpResponse>(fullFilesGetBytesPath) {
|
||||
body = serialFormat.encodeToByteArray(FileId.serializer(), id)
|
||||
}.readBytes()
|
||||
|
||||
override suspend fun getFullFileInfo(
|
||||
id: FileId
|
||||
): FullFileInfoStorageWrapper? = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFullFileInfoPathPart,
|
||||
filesFileIdParameter to unifiedRequester.encodeUrlQueryValue(FileId.serializer(), id)
|
||||
),
|
||||
FullFileInfoStorageWrapper.serializer().nullable
|
||||
)
|
||||
}
|
1
features/files/client/src/main/AndroidManifest.xml
Normal file
1
features/files/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.files.client"/>
|
19
features/files/common/build.gradle
Normal file
19
features/files/common/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api "dev.inmo:micro_utils.mime_types:$microutils_version"
|
||||
api "dev.inmo:micro_utils.repos.common:$microutils_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
1
features/files/common/src/main/AndroidManifest.xml
Normal file
1
features/files/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.files.common"/>
|
17
features/files/server/build.gradle
Normal file
17
features/files/server/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.files.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package dev.inmo.postssystem.features.files.server
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.*
|
||||
import dev.inmo.micro_utils.ktor.server.*
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
|
||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureWriteStandardCrudRepoRoutes
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.routing.*
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class FilesRoutingConfigurator(
|
||||
private val filesStorage: FilesStorage,
|
||||
private val writeFilesStorage: WriteFilesStorage?
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
constructor(fullFilesStorage: FullFilesStorage) : this(fullFilesStorage, fullFilesStorage)
|
||||
|
||||
override fun Route.invoke() {
|
||||
authenticate {
|
||||
route(filesRootPathPart) {
|
||||
configureReadStandardCrudRepoRoutes(
|
||||
filesStorage,
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
)
|
||||
writeFilesStorage ?.let {
|
||||
configureWriteStandardCrudRepoRoutes(
|
||||
writeFilesStorage,
|
||||
FullFileInfoStorageWrapper.serializer(),
|
||||
FullFileInfoStorageWrapper.serializer().nullable,
|
||||
FullFileInfo.serializer(),
|
||||
FileId.serializer()
|
||||
)
|
||||
}
|
||||
post(filesGetFilesPathPart) {
|
||||
call.respondBytes(
|
||||
filesStorage.getBytes(
|
||||
call.uniload(FileId.serializer())
|
||||
)
|
||||
)
|
||||
}
|
||||
get(filesGetFullFileInfoPathPart) {
|
||||
call.unianswer(
|
||||
FullFileInfoStorageWrapper.serializer().nullable,
|
||||
filesStorage.getFullFileInfo(
|
||||
call.decodeUrlQueryValueOrSendError(filesFileIdParameter, FileId.serializer()) ?: return@get
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/roles/client/build.gradle
Normal file
18
features/roles/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.roles.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
)
|
||||
}
|
1
features/roles/client/src/main/AndroidManifest.xml
Normal file
1
features/roles/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.client"/>
|
18
features/roles/common/build.gradle
Normal file
18
features/roles/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api project(":postssystem.features.users.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 }
|
||||
}
|
||||
}
|
1
features/roles/common/src/main/AndroidManifest.xml
Normal file
1
features/roles/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.common"/>
|
19
features/roles/manager/client/build.gradle
Normal file
19
features/roles/manager/client/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.roles.manager.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
api project(":postssystem.features.roles.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.roles.manager.client"/>
|
18
features/roles/manager/common/build.gradle
Normal file
18
features/roles/manager/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api project(":postssystem.features.roles.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"/>
|
18
features/roles/manager/server/build.gradle
Normal file
18
features/roles/manager/server/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.roles.manager.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
api project(":postssystem.features.roles.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package dev.inmo.postssystem.features.roles.manager.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.ReadUsersRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.common.UserRole
|
||||
import dev.inmo.postssystem.features.roles.manager.common.GeneralRolesManagerRole
|
||||
import dev.inmo.postssystem.features.roles.server.RolesChecker
|
||||
import dev.inmo.postssystem.features.users.common.User
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
object RolesManagerRolesChecker : RolesChecker<UserRole> {
|
||||
override val key: String
|
||||
get() = "RolesManagerRolesChecker"
|
||||
|
||||
override suspend fun ApplicationCall.invoke(
|
||||
usersRolesStorage: ReadUsersRolesStorage<UserRole>,
|
||||
user: User
|
||||
): Boolean = usersRolesStorage.contains(user.id, GeneralRolesManagerRole)
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.roles.manager.server
|
||||
|
||||
import dev.inmo.postssystem.features.roles.common.UsersRolesStorage
|
||||
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRole
|
||||
import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer
|
||||
import dev.inmo.postssystem.features.roles.server.UsersRolesStorageWriteServerRoutesConfigurator
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
|
||||
class RolesManagerUsersRolesStorageServerRoutesConfigurator(
|
||||
storage: UsersRolesStorage<RolesManagerRole>
|
||||
) : ApplicationRoutingConfigurator.Element by UsersRolesStorageWriteServerRoutesConfigurator(
|
||||
storage,
|
||||
RolesManagerRoleSerializer,
|
||||
RolesManagerRolesChecker.key
|
||||
)
|
18
features/roles/server/build.gradle
Normal file
18
features/roles/server/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.roles.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
api project(":postssystem.features.auth.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/status/client/build.gradle
Normal file
18
features/status/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.status.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package dev.inmo.postssystem.features.status.client
|
||||
|
||||
import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart
|
||||
import dev.inmo.postssystem.features.status.common.statusRootPart
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
class StatusFeatureClient(
|
||||
baseUrl: String,
|
||||
private val client: HttpClient
|
||||
) {
|
||||
private val fullStatusUrl = buildStandardUrl(
|
||||
baseUrl,
|
||||
statusRootPart
|
||||
)
|
||||
private val fullAuthorisedStatusUrl = buildStandardUrl(
|
||||
fullStatusUrl,
|
||||
statusAuthorisedPathPart
|
||||
)
|
||||
|
||||
suspend fun checkServerStatus() = client.get<HttpResponse>(fullStatusUrl).status == HttpStatusCode.OK
|
||||
suspend fun checkServerStatusWithAuth() = client.get<HttpResponse>(fullAuthorisedStatusUrl).status == HttpStatusCode.OK
|
||||
}
|
1
features/status/client/src/main/AndroidManifest.xml
Normal file
1
features/status/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.status.client"/>
|
17
features/status/common/build.gradle
Normal file
17
features/status/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
package dev.inmo.postssystem.features.status.common
|
||||
|
||||
const val statusRootPart = "status"
|
||||
const val statusAuthorisedPathPart = "auth"
|
1
features/status/common/src/main/AndroidManifest.xml
Normal file
1
features/status/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.status.common"/>
|
17
features/status/server/build.gradle
Normal file
17
features/status/server/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.status.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.postssystem.features.status.server
|
||||
|
||||
import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart
|
||||
import dev.inmo.postssystem.features.status.common.statusRootPart
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.*
|
||||
|
||||
object StatusRoutingConfigurator : ApplicationRoutingConfigurator.Element {
|
||||
override fun Route.invoke() {
|
||||
route(statusRootPart) {
|
||||
get {
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
authenticate {
|
||||
get(statusAuthorisedPathPart) {
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/template/client/build.gradle
Normal file
18
features/template/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.template.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
features/template/client/src/main/AndroidManifest.xml
Normal file
1
features/template/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.template.client"/>
|
17
features/template/common/build.gradle
Normal file
17
features/template/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
features/template/common/src/main/AndroidManifest.xml
Normal file
1
features/template/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.template.common"/>
|
17
features/template/server/build.gradle
Normal file
17
features/template/server/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.template.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
features/users/client/build.gradle
Normal file
18
features/users/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.users.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.postssystem.features.users.client
|
||||
|
||||
import dev.inmo.postssystem.features.users.common.*
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
||||
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class UsersStorageKtorClient(
|
||||
baseUrl: String,
|
||||
unifiedRequester: UnifiedRequester
|
||||
) : ReadUsersStorage, ReadCRUDRepo<User, UserId> by KtorReadStandardCrudRepo(
|
||||
buildStandardUrl(baseUrl, usersServerPathPart),
|
||||
unifiedRequester,
|
||||
User.serializer(),
|
||||
User.serializer().nullable,
|
||||
UserId.serializer()
|
||||
)
|
1
features/users/client/src/main/AndroidManifest.xml
Normal file
1
features/users/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.users.client"/>
|
27
features/users/common/build.gradle
Normal file
27
features/users/common/build.gradle
Normal file
@@ -0,0 +1,27 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api "dev.inmo:micro_utils.repos.common:$microutils_version"
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "dev.inmo:micro_utils.repos.exposed:$microutils_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
disableIncludingJvmCodeInAndroidPart()
|
||||
}
|
@@ -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
Reference in New Issue
Block a user