diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/RedisCacheStorage.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/RedisCacheStorage.kt index 3c05afa..dada5a9 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/RedisCacheStorage.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/RedisCacheStorage.kt @@ -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 write( key: String, - value: T, + value: T?, serializer: KSerializer, duration: Duration ) { + value ?: return val rawValue = json.encodeToString(serializer, value) val args = SetArgs().ex(duration.toJavaDuration()) getCommands().set(key, rawValue, args) diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendAuthTokenRepository.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendAuthTokenRepository.kt index 02d004d..0c114ea 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendAuthTokenRepository.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendAuthTokenRepository.kt @@ -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 { diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendLoginVerifier.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendLoginVerifier.kt index e640302..fbeb297 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendLoginVerifier.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/BackendLoginVerifier.kt @@ -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) { diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/UserInfoCache.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/UserInfoCache.kt new file mode 100644 index 0000000..a46d455 --- /dev/null +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/auth/UserInfoCache.kt @@ -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" + } +}