add UserInfoCache

This commit is contained in:
2026-08-22 17:27:34 +03:00
parent 7775579b70
commit a0d5843054
4 changed files with 84 additions and 23 deletions
@@ -44,8 +44,8 @@ class RedisCacheStorage(
return mutex.withLock {
try {
read(key, serializer)?.let { return@withLock it }
factory().also {
write(key, it, serializer, ttl)
withContext(dispatcherProvider.io) {
factory().also { write(key, it, serializer, ttl) }
}
} finally {
locks.remove(key, mutex)
@@ -55,10 +55,11 @@ class RedisCacheStorage(
private suspend fun <T> write(
key: String,
value: T,
value: T?,
serializer: KSerializer<T>,
duration: Duration
) {
value ?: return
val rawValue = json.encodeToString(serializer, value)
val args = SetArgs().ex(duration.toJavaDuration())
getCommands().set(key, rawValue, args)
@@ -30,7 +30,8 @@ class AuthTokenRepositoryFactory(
private val loginVerifier: LoginVerifier,
private val log: Log,
private val stringFetcher: StringFetcher,
private val envFetcher: EnvFetcher
private val envFetcher: EnvFetcher,
private val userInfoCache: UserInfoCache
) {
fun create(userId: Long = -1): AuthTokenRepository {
@@ -42,7 +43,8 @@ class AuthTokenRepositoryFactory(
userId,
log,
stringFetcher,
envFetcher
envFetcher,
userInfoCache
)
}
}
@@ -55,7 +57,8 @@ class BackendAuthTokenRepository(
private val userId: Long,
private val log: Log,
private val stringFetcher: StringFetcher,
private val envFetcher: EnvFetcher
private val envFetcher: EnvFetcher,
private val userInfoCache: UserInfoCache
) : AuthTokenRepository {
private val random = SecureRandom()
@@ -69,10 +72,13 @@ class BackendAuthTokenRepository(
private suspend fun registerInternal(loginInfo: LoginInfo): TokenInfo {
return withContext(dispatcherProvider.io) {
val user = db.usersQueries.selectUserByLogin(loginInfo.login).executeAsOneOrNull()
val user = userInfoCache.query(loginInfo.login)
if (user != null) throw BadRequestException(stringFetcher.get(StringResource.ALREADY_REGISTERED))
db.usersQueries.addUser(
loginInfo.login, loginInfo.passwordHash, System.currentTimeMillis(), null
loginInfo.login,
loginInfo.passwordHash,
System.currentTimeMillis(),
null
)
TokenInfo("")
}
@@ -80,9 +86,9 @@ class BackendAuthTokenRepository(
override suspend fun login(loginInfo: LoginInfo): TokenInfo =
withContext(dispatcherProvider.io) {
val userInfo = db.usersQueries.selectUserByLogin(loginInfo.login).executeAsOneOrNull()
val userInfo = userInfoCache.query(loginInfo.login)
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
if (loginInfo.passwordHash != userInfo.password_hash) {
if (loginInfo.passwordHash != userInfo.passwordHash) {
throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
}
create(loginInfo.login)
@@ -92,7 +98,7 @@ class BackendAuthTokenRepository(
val info = db.refresh_tokensQueries.selectByHash(refresh.token.toHash())
.executeAsOneOrNull()
?: throw BadRequestException("Token not found")
val userInfo = db.usersQueries.selectUserByUserId(info.user_id).executeAsOneOrNull()
val userInfo = userInfoCache.query(info.user_id)
?: throw BadRequestException("User not found")
val newTokens = create(userInfo.login)
revokeInternal(refresh)
@@ -126,34 +132,35 @@ class BackendAuthTokenRepository(
private suspend fun create(login: String): TokenInfo = withContext(dispatcherProvider.io) {
loginVerifier.verify(login)
val userInfo = db.usersQueries.selectUserByLogin(login).executeAsOneOrNull()
val userInfo = userInfoCache.query(login)
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
val rsp = jwtPreparer.prepare(login, userInfo.user_id)
val rsp = jwtPreparer.prepare(login, userInfo.userId)
val refresh = ByteArray(32)
random.nextBytes(refresh)
val refreshStr = refresh.toHexString()
db.refresh_tokensQueries.insertToken(
userInfo.user_id,
userInfo.userId,
refreshStr.toHash(),
System.currentTimeMillis() + TimeUnit.DAYS.toMillis(30)
).executeAsOneOrNull()
log.d(
"BackendAuthTokenRepository",
"${refreshStr.toHash()} created for user ${userInfo.user_id}"
"${refreshStr.toHash()} created for user ${userInfo.userId}"
)
TokenInfo(rsp, refreshStr)
}
override suspend fun changePassword(request: ChangePasswordRequest): Unit =
withContext(dispatcherProvider.io) {
val user = db.usersQueries.selectUserByUserId(userId).executeAsOneOrNull()
val user = userInfoCache.query(userId)
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
if (user.password_hash != request.oldPasswordHash) {
if (user.passwordHash != request.oldPasswordHash) {
throw BadRequestException(stringFetcher.get(StringResource.INVALID_PASSWORD))
} else if (user.password_hash == request.newPasswordHash) {
} else if (user.passwordHash == request.newPasswordHash) {
throw BadRequestException(stringFetcher.get(StringResource.NO_CHANGES_PASS))
}
db.usersQueries.updatePassword(request.newPasswordHash, userId).await()
userInfoCache.delete(userId)
}
private fun String.toHash(): String {
@@ -5,17 +5,15 @@ import ru.shadowsparky.backend.data.StringFetcher
import ru.shadowsparky.backend.data.StringResource
import ru.shadowsparky.backend.domain.LoginVerifier
import ru.shadowsparky.backend.domain.VerifyTokenException
import ru.shadowsparky.vbox.backend.AppDatabase
import ru.shadowsparky.vbox.shared.domain.FLAG_USER_BLOCKED
@Factory
class BackendLoginVerifier(
private val db: AppDatabase,
private val stringFetcher: StringFetcher
private val stringFetcher: StringFetcher,
private val userInfoCache: UserInfoCache
) : LoginVerifier {
override suspend fun verify(login: String) {
val user = db.usersQueries.selectUserByLogin(login)
.executeAsOneOrNull()
val user = userInfoCache.query(login)
?: throw VerifyTokenException(stringFetcher.get(StringResource.UNKNOWN_USER))
user.flags?.let {
if ((it and FLAG_USER_BLOCKED) != 0) {
@@ -0,0 +1,55 @@
package ru.shadowsparky.vbox.backend.data.auth
import kotlinx.serialization.Serializable
import org.koin.core.annotation.Factory
import ru.shadowsparky.vbox.backend.AppDatabase
import ru.shadowsparky.vbox.backend.domain.CacheStorage
import ru.shadowsparky.vbox.backend.domain.getOrCreate
import ru.shadowsparky.vbox.server.User
import kotlin.time.Duration.Companion.days
@Factory
class UserInfoCache(
private val db: AppDatabase,
private val cacheStorage: CacheStorage
) {
private val ttl = 3.days
suspend fun query(login: String): UserEntry? {
return cacheStorage.getOrCreate("$PREFIX:login:$login", ttl) {
db.usersQueries.selectUserByLogin(login)
.executeAsOneOrNull()
?.toUserEntry()
}
}
suspend fun query(userId: Long): UserEntry? {
return cacheStorage.getOrCreate("$PREFIX:id:$userId", ttl) {
db.usersQueries.selectUserByUserId(userId)
.executeAsOneOrNull()
?.toUserEntry()
}
}
private fun User.toUserEntry(): UserEntry {
return UserEntry(login, user_id, password_hash, flags)
}
@Serializable
data class UserEntry(
val login: String,
val userId: Long,
val passwordHash: String,
val flags: Int?
)
suspend fun delete(userId: Long) {
val login = query(userId)?.login
cacheStorage.delete("$PREFIX:id:$userId")
login?.let { cacheStorage.delete("$PREFIX:login:$login") }
}
private companion object {
const val PREFIX = "login_storage"
}
}