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