suspend coroutines

This commit is contained in:
xds
2025-02-28 01:17:52 +03:00
parent 35090b946d
commit db0ada5ee8
13 changed files with 1099 additions and 1184 deletions

View File

@@ -1,5 +1,6 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.reactive.awaitFirstOrNull
import org.springframework.cache.annotation.Cacheable
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service
@@ -11,16 +12,17 @@ import space.luminic.budgerapp.repos.UserRepo
import space.luminic.budgerapp.utils.JWTUtil
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.Date
import java.util.*
@Service
class AuthService(
private val userRepository: UserRepo,
private val tokenService: TokenService,
private val jwtUtil: JWTUtil
private val jwtUtil: JWTUtil,
private val userService: UserService
) {
) {
private val passwordEncoder = BCryptPasswordEncoder()
fun login(username: String, password: String): Mono<String> {
@@ -82,23 +84,20 @@ class AuthService(
)
}
@Cacheable("tokens")
fun isTokenValid(token: String): Mono<User> {
return tokenService.getToken(token)
.flatMap { tokenDetails ->
when {
tokenDetails.status == TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
userRepository.findByUsername(tokenDetails.username)
.switchIfEmpty(Mono.error(AuthException("User not found for token")))
}
else -> {
tokenService.revokeToken(token)
.then(Mono.error(AuthException("Token expired or inactive")))
}
}
@Cacheable(cacheNames = ["tokens"], key = "#token")
suspend fun isTokenValid(token: String): User {
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Invalid token")
when {
tokenDetails.status == TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
return userService.getByUserNameWoPass(tokenDetails.username)
}
.switchIfEmpty(Mono.error(AuthException("Token not found")))
else -> {
tokenService.revokeToken(tokenDetails.token)
throw AuthException("Token expired or inactive")
}
}
}
}

View File

@@ -1,22 +1,25 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import org.bson.Document
import org.bson.types.ObjectId
import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.isEqualTo
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import space.luminic.budgerapp.mappers.CategoryMapper
import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.models.Category
import space.luminic.budgerapp.models.CategoryType
import space.luminic.budgerapp.models.NotFoundException
import space.luminic.budgerapp.models.Space
import space.luminic.budgerapp.repos.BudgetRepo
import space.luminic.budgerapp.repos.CategoryRepo
@@ -33,13 +36,12 @@ class CategoryService(
private val logger = LoggerFactory.getLogger(javaClass)
fun findCategory(
suspend fun findCategory(
space: Space? = null,
id: String? = null,
name: String? = null,
tagCode: String? = null
): Mono<Category> {
): Category {
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val unwindSpace = unwind("spaceDetails")
val matchCriteria = mutableListOf<Criteria>()
@@ -51,7 +53,6 @@ class CategoryService(
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
// val project = project("_id", "type", "name", "description", "icon")
val aggregationBuilder = mutableListOf(
@@ -63,10 +64,9 @@ class CategoryService(
val aggregation = newAggregation(aggregationBuilder)
return mongoTemplate.aggregate(
aggregation, "categories", Document::class.java
).next()
.map { doc ->
categoryMapper.fromDocument(doc)
}
).map { doc ->
categoryMapper.fromDocument(doc)
}.awaitFirstOrNull() ?: throw NotFoundException("Category not found")
}
@@ -122,55 +122,56 @@ class CategoryService(
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
fun editCategory(space: Space, category: Category): Mono<Category> {
return findCategory(space, id = category.id) // Возвращаем Mono<Category>
.flatMap { oldCategory ->
if (oldCategory.type.code != category.type.code) {
return@flatMap Mono.error<Category>(IllegalArgumentException("You cannot change category type"))
}
category.space = space
categoryRepo.save(category) // Сохраняем категорию, если тип не изменился
}
}
suspend fun editCategory(space: Space, category: Category): Category {
val oldCategory = findCategory(space, id = category.id)
fun deleteCategory(space: Space, categoryId: String): Mono<String> {
return findCategory(space, categoryId).switchIfEmpty(
Mono.error(IllegalArgumentException("Category with id: $categoryId not found"))
).flatMap { categoryToDelete ->
financialService.getTransactions(space.id!!, categoryId = categoryId)
.flatMapMany { transactions ->
findCategory(space, name = "Другое")
.switchIfEmpty(
categoryRepo.save(
Category(
space = space,
type = CategoryType("EXPENSE", "Траты"),
name = "Другое",
description = "Категория для других трат",
icon = "🚮"
)
)
)
.flatMapMany { otherCategory ->
Flux.fromIterable(transactions).flatMap { transaction ->
transaction.category = otherCategory
financialService.editTransaction(transaction)
}
}
}
.then(
financialService.findProjectedBudgets(ObjectId(space.id))
.flatMapMany { budgets ->
Flux.fromIterable(budgets).flatMap { budget ->
budget.categories.removeIf { it.category.id == categoryId }
budgetRepo.save(budget)
}
}.collectList()
)
.then(categoryRepo.deleteById(categoryId)) // Удаление категории
.thenReturn(categoryId)
if (oldCategory.type.code != category.type.code) {
throw IllegalArgumentException("You cannot change category type")
}
category.space = space
return categoryRepo.save(category).awaitSingle() // Сохраняем категорию, если тип не изменился
}
suspend fun deleteCategory(space: Space, categoryId: String) {
findCategory(space, categoryId)
val transactions = financialService.getTransactions(space.id!!, categoryId = categoryId).awaitSingle()
val otherCategory = try {
findCategory(space, name = "Другое")
} catch (nfe: NotFoundException) {
categoryRepo.save(
Category(
space = space,
type = CategoryType("EXPENSE", "Траты"),
name = "Другое",
description = "Категория для других трат",
icon = "🚮"
)
).awaitSingle()
}
transactions.map { transaction ->
transaction.category = otherCategory
financialService.editTransaction(transaction)
}
val budgets = financialService.findProjectedBudgets(
ObjectId(space.id),
projectKeys = arrayOf(
"_id",
"name",
"dateFrom",
"dateTo",
"space",
"spaceDetails",
"categories",
"categoriesDetails",
"incomeCategories",
"incomeCategoriesDetails"
)
).awaitSingle()
budgets.map { budget ->
budget.categories.removeIf { it.category.id == categoryId }
budgetRepo.save(budget)
}
categoryRepo.deleteById(categoryId).awaitSingle()
}
}

View File

@@ -1,6 +1,9 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.reactive.awaitLast
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.bson.Document
import org.bson.types.ObjectId
import org.slf4j.LoggerFactory
@@ -13,7 +16,6 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import space.luminic.budgerapp.mappers.RecurrentMapper
import space.luminic.budgerapp.models.*
@@ -73,58 +75,42 @@ class RecurrentService(
)
}
fun createRecurrentsForBudget(space: Space, budget: Budget): Mono<Void> {
suspend fun createRecurrentsForBudget(space: Space, budget: Budget) {
val currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue)
val daysInCurrentMonth = currentYearMonth.lengthOfMonth()
val context = ReactiveSecurityContextHolder.getContext()
.doOnNext { println("Security context: $it") }
.switchIfEmpty(Mono.error(IllegalStateException("SecurityContext is empty!")))
val context = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw IllegalStateException("SecurityContext is empty!")
val user = userService.getByUserNameWoPass(context.authentication.name)
val recurrents = getRecurrents(space.id!!).awaitSingle()
val transactions = recurrents.map { recurrent ->
val transactionDate = when {
recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> {
currentYearMonth.atDay(recurrent.atDay)
}
return context
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
}
.flatMapMany { user ->
getRecurrents(space.id!!) // Теперь это Mono<List<Recurrent>>
.flatMapMany { Flux.fromIterable(it) } // Преобразуем List<Recurrent> в Flux<Recurrent>
.map { recurrent ->
// Определяем дату транзакции
val transactionDate = when {
recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> {
currentYearMonth.atDay(recurrent.atDay)
}
recurrent.atDay < budget.dateFrom.dayOfMonth -> {
currentYearMonth.atDay(recurrent.atDay).plusMonths(1)
}
else -> {
val extraDays = recurrent.atDay - daysInCurrentMonth
currentYearMonth.plusMonths(1).atDay(extraDays)
}
}
// Создаем транзакцию
Transaction(
space = space,
date = transactionDate,
amount = recurrent.amount.toDouble(),
category = recurrent.category,
isDone = false,
comment = recurrent.name,
user = user,
type = TransactionType("PLANNED", "Запланированные")
)
}
}
.collectList() // Собираем все транзакции в список
.flatMap { transactions ->
transactionRepo.saveAll(transactions).then() // Сохраняем все транзакции разом и возвращаем Mono<Void>
recurrent.atDay < budget.dateFrom.dayOfMonth -> {
currentYearMonth.atDay(recurrent.atDay).plusMonths(1)
}
else -> {
val extraDays = recurrent.atDay - daysInCurrentMonth
currentYearMonth.plusMonths(1).atDay(extraDays)
}
}
// Создаем транзакцию
Transaction(
space = space,
date = transactionDate,
amount = recurrent.amount.toDouble(),
category = recurrent.category,
isDone = false,
comment = recurrent.name,
user = user,
type = TransactionType("PLANNED", "Запланированные")
)
}
transactionRepo.saveAll(transactions).awaitLast()
}
@@ -139,6 +125,4 @@ class RecurrentService(
}
}

View File

@@ -1,5 +1,11 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.awaitFirst
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.bson.Document
import org.bson.types.ObjectId
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
@@ -8,8 +14,7 @@ import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.Query
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import space.luminic.budgerapp.configs.AuthException
import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.repos.*
import java.time.LocalDateTime
@@ -30,262 +35,214 @@ class SpaceService(
private val tagRepo: TagRepo
) {
fun isValidRequest(spaceId: String): Mono<Space> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
// Получаем пользователя по имени
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
// Получаем пространство по ID
getSpace(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId")))
.flatMap { space ->
// Проверяем доступ пользователя к пространству
if (space.users.none { it.id.toString() == user.id }) {
return@flatMap Mono.error<Space>(IllegalArgumentException("User does not have access to this Space"))
}
// Если проверка прошла успешно, возвращаем пространство
Mono.just(space)
}
}
}
suspend fun isValidRequest(spaceId: String): Space {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
// Получаем пользователя по имени
val user = userService.getByUsername(username)
val space = getSpace(spaceId)
// Проверяем доступ пользователя к пространству
return if (space.users.none { it.id.toString() == user.id }) {
throw IllegalArgumentException("User does not have access to this Space")
} else space
}
fun getSpaces(): Mono<List<Space>> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
val userId = ObjectId(user.id!!)
suspend fun getSpaces(): List<Space> {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle()
val authentication = securityContext.authentication
val username = authentication.name
val user = userService.getByUsername(username)
// Поиск пространств пользователя
val userId = ObjectId(user.id!!)
// Агрегация для загрузки владельца и пользователей
val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails")
val unwindOwner = unwind("ownerDetails")
// Агрегация для загрузки владельца и пользователей
val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails")
val unwindOwner = unwind("ownerDetails")
val lookupUsers = lookup("users", "users.\$id", "_id", "usersDetails")
val lookupUsers = lookup("users", "users.\$id", "_id", "usersDetails")
// val unwindUsers = unwind("usersDetails")
val matchStage = match(Criteria.where("usersDetails._id").`is`(userId))
val aggregation = newAggregation(lookupOwner, unwindOwner, lookupUsers, matchStage)
val matchStage = match(Criteria.where("usersDetails._id").`is`(userId))
val aggregation = newAggregation(lookupOwner, unwindOwner, lookupUsers, matchStage)
reactiveMongoTemplate.aggregate(aggregation, "spaces", Document::class.java)
.collectList()
.map { docs ->
docs.map { doc ->
val ownerDoc = doc.get("ownerDetails", Document::class.java)
val usersDocList = doc.getList("usersDetails", Document::class.java)
return reactiveMongoTemplate.aggregate(aggregation, "spaces", Document::class.java)
.collectList()
.map { docs ->
docs.map { doc ->
val ownerDoc = doc.get("ownerDetails", Document::class.java)
val usersDocList = doc.getList("usersDetails", Document::class.java)
Space(
id = doc.getObjectId("_id").toString(),
name = doc.getString("name"),
description = doc.getString("description"),
owner = User(
id = ownerDoc.getObjectId("_id").toString(),
username = ownerDoc.getString("username"),
firstName = ownerDoc.getString("firstName")
),
users = usersDocList.map { userDoc ->
User(
id = userDoc.getObjectId("_id").toString(),
username = userDoc.getString("username"),
firstName = userDoc.getString("firstName")
)
}.toMutableList()
)
}
}
}
}
Space(
id = doc.getObjectId("_id").toString(),
name = doc.getString("name"),
description = doc.getString("description"),
owner = User(
id = ownerDoc.getObjectId("_id").toString(),
username = ownerDoc.getString("username"),
firstName = ownerDoc.getString("firstName")
),
users = usersDocList.map { userDoc ->
User(
id = userDoc.getObjectId("_id").toString(),
username = userDoc.getString("username"),
firstName = userDoc.getString("firstName")
)
}.toMutableList()
)
}
}.awaitFirst()
}
fun getSpace(spaceId: String): Mono<Space> {
return spaceRepo.findById(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("SpaceId not found for spaceId: $spaceId")))
suspend fun getSpace(spaceId: String): Space {
return spaceRepo.findById(spaceId).awaitSingleOrNull()
?: throw IllegalArgumentException("SpaceId not found for spaceId: $spaceId")
}
fun createSpace(space: Space, createCategories: Boolean): Mono<Space> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
space.owner = user
space.users.add(user)
spaceRepo.save(space).flatMap { savedSpace ->
if (!createCategories) {
return@flatMap Mono.just(savedSpace) // Если не нужно создавать категории, просто возвращаем пространство
}
suspend fun createSpace(space: Space, createCategories: Boolean): Space {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
val user = userService.getByUsername(username)
space.owner = user
space.users.add(user)
val savedSpace = spaceRepo.save(space).awaitSingle()
return if (!createCategories) {
savedSpace // Если не нужно создавать категории, просто возвращаем пространство
} else {
val categories = reactiveMongoTemplate.find(Query(), Category::class.java, "categories-etalon")
.map { category ->
category.copy(id = null, space = savedSpace) // Создаем новую копию
}
categoryRepo.saveAll(categories).awaitSingle()
savedSpace
}
reactiveMongoTemplate.find(Query(), Category::class.java, "categories-etalon")
.map { category ->
category.copy(id = null, space = savedSpace) // Создаем новую копию
}
.collectList() // Собираем в список перед сохранением
.flatMap { categoryRepo.saveAll(it).collectList() } // Сохраняем и возвращаем список
.then(Mono.just(savedSpace)) // После сохранения всех категорий, возвращаем пространство
}
}
}
}
fun deleteSpace(space: Space): Mono<Void> {
suspend fun deleteSpace(space: Space) {
val objectId = ObjectId(space.id)
return Mono.`when`(
financialService.findProjectedBudgets(objectId)
.flatMap { budgetRepo.deleteAll(it) },
financialService.getTransactions(objectId.toString())
.flatMap { transactionRepo.deleteAll(it) },
categoryService.getCategories(objectId.toString(), null, "name", "ASC")
.flatMap { categoryRepo.deleteAll(it) },
recurrentService.getRecurrents(objectId.toString())
.flatMap { recurrentRepo.deleteAll(it) }
).then(spaceRepo.deleteById(space.id!!)) // Удаление Space после завершения всех операций
}
fun createInviteSpace(spaceId: String): Mono<SpaceInvite> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
spaceRepo.findById(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId")))
.flatMap { space ->
if (space.users.none { it.id.toString() == user.id }) {
return@flatMap Mono.error<SpaceInvite>(IllegalArgumentException("User does not have access to this Space"))
}
val invite = SpaceInvite(
UUID.randomUUID().toString().split("-")[0],
user,
LocalDateTime.now().plusHours(1),
)
space.invites.add(invite)
// Сохраняем изменения и возвращаем созданное приглашение
spaceRepo.save(space).thenReturn(invite)
}
}
coroutineScope {
launch {
val budgets = financialService.findProjectedBudgets(objectId).awaitFirstOrNull().orEmpty()
budgetRepo.deleteAll(budgets).awaitFirstOrNull()
}
}
fun acceptInvite(code: String): Mono<Space> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
spaceRepo.findSpaceByInvites(code)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space with invite code: $code not found")))
.flatMap { space ->
val invite = space.invites.find { it.code == code }
// Проверяем, есть ли инвайт и не истек ли он
if (invite == null || invite.activeTill.isBefore(LocalDateTime.now())) {
return@flatMap Mono.error<Space>(IllegalArgumentException("Invite is invalid or expired"))
}
// Проверяем, не является ли пользователь уже участником
if (space.users.any { it.id == user.id }) {
return@flatMap Mono.error<Space>(IllegalArgumentException("User is already a member of this Space"))
}
// Добавляем пользователя и удаляем использованный инвайт
space.users.add(user)
space.invites.remove(invite)
spaceRepo.save(space)
}
}
launch {
val transactions = financialService.getTransactions(objectId.toString()).awaitFirstOrNull().orEmpty()
transactionRepo.deleteAll(transactions).awaitFirstOrNull()
}
}
fun leaveSpace(spaceId: String): Mono<Void> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
spaceRepo.findById(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId")))
.flatMap { space ->
if (space.users.none { it.id.toString() == user.id }) {
return@flatMap Mono.error<Void>(IllegalArgumentException("User does not have access to this Space"))
}
// Удаляем пользователя из массива
space.users.removeIf { it.id == user.id }
// Сохраняем изменения
spaceRepo.save(space).then() // .then() для Mono<Void>
}
}
launch {
val categories =
categoryService.getCategories(objectId.toString(), null, "name", "ASC").awaitFirstOrNull().orEmpty()
categoryRepo.deleteAll(categories).awaitFirstOrNull()
}
}
fun kickMember(spaceId: String, kickedUsername: String): Mono<Void> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
// Получаем текущего пользователя
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
// Получаем пользователя, которого нужно исключить
userService.getByUsername(kickedUsername)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $kickedUsername")))
.flatMap { kickedUser ->
// Получаем пространство
spaceRepo.findById(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId")))
.flatMap { space ->
// Проверяем, является ли текущий пользователь владельцем
if (space.owner?.id != user.id) {
return@flatMap Mono.error<Void>(IllegalArgumentException("Only owners allowed for this action"))
}
// Проверяем, что пользователь, которого нужно исключить, присутствует в списке пользователей
val userToKick = space.users.find { it.username == kickedUsername }
if (userToKick != null) {
// Удаляем пользователя из пространства
space.users.removeIf { it.username == kickedUsername }
// Сохраняем изменения
return@flatMap spaceRepo.save(space).then()
} else {
return@flatMap Mono.error<Void>(IllegalArgumentException("User not found in this space"))
}
}
}
}
launch {
val recurrents = recurrentService.getRecurrents(objectId.toString()).awaitFirstOrNull().orEmpty()
recurrentRepo.deleteAll(recurrents).awaitFirstOrNull()
}
}
spaceRepo.deleteById(space.id!!).awaitFirstOrNull() // Удаляем Space после всех операций
}
fun findTag(space: Space, tagCode: String): Mono<Tag> {
suspend fun createInviteSpace(spaceId: String): SpaceInvite {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContext.authentication
val user = userService.getByUsername(authentication.name)
val space = getSpace(spaceId)
if (space.owner?.id != user.id) {
throw AuthException("Only owner could create invite into space")
}
val invite = SpaceInvite(
UUID.randomUUID().toString().split("-")[0],
user,
LocalDateTime.now().plusHours(1),
)
space.invites.add(invite)
spaceRepo.save(space).awaitFirstOrNull()
// Сохраняем изменения и возвращаем созданное приглашение
return invite
}
suspend fun acceptInvite(code: String): Space {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val user = userService.getByUsername(securityContextHolder.authentication.name)
val space = spaceRepo.findSpaceByInvites(code).awaitFirstOrNull()
?: throw IllegalArgumentException("Space with invite code: $code not found")
val invite = space.invites.find { it.code == code }
// Проверяем, есть ли инвайт и не истек ли он
if (invite == null || invite.activeTill.isBefore(LocalDateTime.now())) {
throw IllegalArgumentException("Invite is invalid or expired")
}
// Проверяем, не является ли пользователь уже участником
if (space.users.any { it.id == user.id }) {
throw IllegalArgumentException("User is already a member of this Space")
}
// Добавляем пользователя и удаляем использованный инвайт
space.users.add(user)
space.invites.remove(invite)
return spaceRepo.save(space).awaitFirst()
}
suspend fun leaveSpace(spaceId: String) {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val user = userService.getByUsername(securityContext.authentication.name)
val space = getSpace(spaceId)
// Удаляем пользователя из массива
space.users.removeIf { it.id == user.id }
// Сохраняем изменения
spaceRepo.save(space).awaitFirst()
}
suspend fun kickMember(spaceId: String, kickedUsername: String) {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val currentUser = userService.getByUsername(securityContext.authentication.name)
//проверяем что кикнутый пользователь сушествует
userService.getByUsername(kickedUsername)
val space = getSpace(spaceId)
if (space.owner?.id != currentUser.id) {
throw IllegalArgumentException("Only owners allowed for this action")
}
// Проверяем, что пользователь, которого нужно исключить, присутствует в списке пользователей
val userToKick = space.users.find { it.username == kickedUsername }
if (userToKick != null) {
// Удаляем пользователя из пространства
space.users.removeIf { it.username == kickedUsername }
// Сохраняем изменения
spaceRepo.save(space).awaitSingle()
} else {
throw IllegalArgumentException("User not found in this space")
}
}
suspend fun findTag(space: Space, tagCode: String): Tag? {
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val unwindSpace = unwind("spaceDetails")
val matchCriteria = mutableListOf<Criteria>()
@@ -310,37 +267,32 @@ class SpaceService(
code = doc.getString("code"),
name = doc.getString("name")
)
}
}.awaitSingleOrNull()
}
fun createTag(space: Space, tag: Tag): Mono<Tag> {
suspend fun createTag(space: Space, tag: Tag): Tag {
tag.space = space
return findTag(space, tag.code)
.flatMap { existingTag ->
Mono.error<Tag>(IllegalArgumentException("Tag with code ${existingTag.code} already exists"))
}
.switchIfEmpty(tagRepo.save(tag))
val existedTag = findTag(space, tag.code)
return existedTag?.let {
throw IllegalArgumentException("Tag with code ${tag.code} already exists")
} ?: tagRepo.save(tag).awaitFirst()
}
fun deleteTag(space: Space, tagCode: String): Mono<Void> {
return findTag(space, tagCode)
.switchIfEmpty(Mono.error(IllegalArgumentException("Tag with code $tagCode not found")))
.flatMap { tag ->
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = tag.code)
.flatMapMany { cats ->
Flux.fromIterable(cats)
.map { cat ->
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
cat
}
.flatMap { categoryRepo.save(it) } // Сохраняем обновлённые категории
}
.then(tagRepo.deleteById(tag.id!!)) // Удаляем тег только после обновления категорий
}
suspend fun deleteTag(space: Space, tagCode: String) {
val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found")
val categoriesWithTag =
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code)
.awaitSingleOrNull().orEmpty()
categoriesWithTag.map { cat ->
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
cat
}
categoryRepo.saveAll(categoriesWithTag).awaitFirst() // Сохраняем обновлённые категории
tagRepo.deleteById(existedTag.id!!).awaitFirst()
}
fun getTags(space: Space): Mono<List<Tag>> {
suspend fun getTags(space: Space): List<Tag> {
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val unwindSpace = unwind("spaceDetails")
val matchCriteria = mutableListOf<Criteria>()
@@ -363,27 +315,30 @@ class SpaceService(
docs.map { doc ->
Tag(
id = doc.getObjectId("_id").toString(),
space = Space(id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()),
space = Space(
id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()
),
code = doc.getString("code"),
name = doc.getString("name")
)
}
}
.awaitSingleOrNull().orEmpty()
}
fun regenSpaceCategory(): Mono<Category> {
return getSpace("67af3c0f652da946a7dd9931")
.flatMap { space ->
categoryService.findCategory(id = "677bc767c7857460a491bd4f")
.flatMap { category -> // заменил map на flatMap
category.space = space
category.name = "Сбережения"
category.description = "Отчисления в накопления или инвестиционные счета"
category.icon = "💰"
categoryRepo.save(category) // теперь возвращаем Mono<Category>
}
}
}
// fun regenSpaceCategory(): Mono<Category> {
// return getSpace("67af3c0f652da946a7dd9931")
// .flatMap { space ->
// categoryService.findCategory(id = "677bc767c7857460a491bd4f")
// .flatMap { category -> // заменил map на flatMap
// category.space = space
// category.name = "Сбережения"
// category.description = "Отчисления в накопления или инвестиционные счета"
// category.icon = "💰"
// categoryRepo.save(category) // теперь возвращаем Mono<Category>
// }
// }
// }
// fun regenSpaces(): Mono<List<Space>> {
// return spaceRepo.findAll()

View File

@@ -3,6 +3,7 @@ package space.luminic.budgerapp.services
import com.interaso.webpush.VapidKeys
import com.interaso.webpush.WebPushService
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
@@ -70,7 +71,7 @@ class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
}
fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): Mono<String> {
suspend fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): String {
val subscription = Subscription(
id = null,
user = user,
@@ -80,18 +81,15 @@ class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
isActive = true
)
return subscriptionRepo.save(subscription)
.flatMap { savedSubscription ->
Mono.just("Subscription created with ID: ${savedSubscription.id}")
}
.onErrorResume(DuplicateKeyException::class.java) {
logger.info("Subscription already exists. Skipping.")
Mono.just("Subscription already exists. Skipping.")
}
.onErrorResume { e ->
logger.error("Error while saving subscription: ${e.message}")
Mono.error(RuntimeException("Error while saving subscription"))
}
return try {
val savedSubscription = subscriptionRepo.save(subscription).awaitSingle()
"Subscription created with ID: ${savedSubscription.id}"
} catch (e: DuplicateKeyException) {
logger.info("Subscription already exists. Skipping.")
"Subscription already exists. Skipping."
} catch (e: Exception) {
logger.error("Error while saving subscription: ${e.message}")
throw RuntimeException("Error while saving subscription")
}
}
}

View File

@@ -1,4 +1,5 @@
package space.luminic.budgerapp.services
import org.springframework.cache.annotation.CacheEvict
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@@ -25,15 +26,12 @@ class TokenService(private val tokenRepository: TokenRepo) {
return tokenRepository.findByToken(token)
}
@CacheEvict("tokens", allEntries = true)
fun revokeToken(token: String): Mono<Void> {
return tokenRepository.findByToken(token)
.switchIfEmpty(Mono.error(Exception("Token not found")))
.flatMap { existingToken ->
val updatedToken = existingToken.copy(status = TokenStatus.REVOKED)
tokenRepository.save(updatedToken).then()
}
fun revokeToken(token: String) {
val tokenDetail =
tokenRepository.findByToken(token).block()!!
val updatedToken = tokenDetail.copy(status = TokenStatus.REVOKED)
tokenRepository.save(updatedToken).block()
}

View File

@@ -1,6 +1,7 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
@@ -14,13 +15,10 @@ class UserService(val userRepo: UserRepo) {
val logger = LoggerFactory.getLogger(javaClass)
@Cacheable("users", key = "#username")
fun getByUsername(username: String): Mono<User> {
return userRepo.findByUsernameWOPassword(username).switchIfEmpty(
Mono.error(NotFoundException("User with username: $username not found"))
)
suspend fun getByUsername(username: String): User {
return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
?: throw NotFoundException("User with username: $username not found")
}
fun getById(id: String): Mono<User> {
@@ -32,9 +30,10 @@ class UserService(val userRepo: UserRepo) {
}
@Cacheable("users", key = "#username")
fun getByUserNameWoPass(username: String): Mono<User> {
return userRepo.findByUsernameWOPassword(username)
@Cacheable("users", key = "#username")
suspend fun getByUserNameWoPass(username: String): User {
return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
?: throw NotFoundException("User with username: $username not found")
}
@Cacheable("usersList")