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,16 +1,11 @@
package space.luminic.budgerapp.configs package space.luminic.budgerapp.configs
import kotlinx.coroutines.reactor.mono
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -23,37 +18,36 @@ import space.luminic.budgerapp.services.AuthService
class BearerTokenFilter(private val authService: AuthService) : SecurityContextServerWebExchangeWebFilter() { class BearerTokenFilter(private val authService: AuthService) : SecurityContextServerWebExchangeWebFilter() {
private val logger = LoggerFactory.getLogger(BearerTokenFilter::class.java) private val logger = LoggerFactory.getLogger(BearerTokenFilter::class.java)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ") val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ")
if (exchange.request.path.value() in listOf("/api/auth/login","/api/auth/register", "/api/auth/tgLogin") || exchange.request.path.value() if (exchange.request.path.value() in listOf(
.startsWith("/api/actuator") "/api/auth/login",
"/api/auth/register",
"/api/auth/tgLogin"
) || exchange.request.path.value().startsWith("/api/actuator")
) { ) {
return chain.filter(exchange) return chain.filter(exchange)
} }
return if (token != null) { return if (token != null) {
authService.isTokenValid(token) mono {
.flatMap { userDetails -> val userDetails = authService.isTokenValid(token) // suspend вызов
val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) } val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) }
val securityContext = SecurityContextImpl( val securityContext = SecurityContextImpl(
UsernamePasswordAuthenticationToken( UsernamePasswordAuthenticationToken(userDetails.username, null, authorities)
userDetails.username, null, authorities
)
) )
securityContext
}.flatMap { securityContext ->
chain.filter(exchange) chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))) .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
} }
.onErrorMap(AuthException::class.java) { ex ->
BadCredentialsException(ex.message ?: "Unauthorized")
}
} else { } else {
Mono.error(AuthException("Authorization token is missing")) Mono.error(AuthException("Authorization token is missing"))
} }
} }
} }

View File

@@ -1,6 +1,11 @@
package space.luminic.budgerapp.controllers package space.luminic.budgerapp.controllers
import kotlinx.coroutines.reactive.awaitFirst
import kotlinx.coroutines.reactive.awaitSingle
import org.slf4j.LoggerFactory
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.User import space.luminic.budgerapp.models.User
@@ -10,16 +15,23 @@ import space.luminic.budgerapp.services.UserService
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
class AuthController( class AuthController(
private val userService: UserService, private val userService: UserService,
private val authService: AuthService private val authService: AuthService
) { ) {
private val logger = LoggerFactory.getLogger(javaClass)
@GetMapping("/test")
fun test(): String {
val authentication = SecurityContextHolder.getContext().authentication
logger.info("SecurityContext in controller: $authentication")
return "Hello, ${authentication.name}"
}
@PostMapping("/login") @PostMapping("/login")
fun login(@RequestBody request: AuthRequest): Mono<Map<String, String>> { suspend fun login(@RequestBody request: AuthRequest): Map<String, String> {
return authService.login(request.username, request.password) return authService.login(request.username, request.password)
.map { token -> mapOf("token" to token) } .map { token -> mapOf("token" to token) }.awaitFirst()
} }
@PostMapping("/register") @PostMapping("/register")
@@ -34,11 +46,9 @@ class AuthController(
@GetMapping("/me") @GetMapping("/me")
fun getMe(@RequestHeader("Authorization") token: String): Mono<User> { suspend fun getMe(): User {
return authService.isTokenValid(token.removePrefix("Bearer ")) val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle()
.flatMap { username -> return userService.getByUserNameWoPass(securityContext.authentication.name)
userService.getByUserNameWoPass(username.username!!)
}
} }
} }

View File

@@ -1,12 +1,11 @@
package space.luminic.budgerapp.controllers package space.luminic.budgerapp.controllers
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.bson.Document import org.bson.Document
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.client.HttpClientErrorException
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import space.luminic.budgerapp.controllers.BudgetController.LimitValue import space.luminic.budgerapp.controllers.BudgetController.LimitValue
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
import space.luminic.budgerapp.models.* import space.luminic.budgerapp.models.*
@@ -33,13 +32,13 @@ class SpaceController(
) )
@GetMapping @GetMapping
fun getSpaces(): Mono<List<Space>> { suspend fun getSpaces(): List<Space> {
return spaceService.getSpaces() return spaceService.getSpaces()
} }
@PostMapping @PostMapping
fun createSpace(@RequestBody space: SpaceCreateDTO): Mono<Space> { suspend fun createSpace(@RequestBody space: SpaceCreateDTO): Space {
return spaceService.createSpace( return spaceService.createSpace(
Space(name = space.name, description = space.description), Space(name = space.name, description = space.description),
space.createCategories space.createCategories
@@ -48,36 +47,37 @@ class SpaceController(
@GetMapping("{spaceId}") @GetMapping("{spaceId}")
fun getSpace(@PathVariable spaceId: String): Mono<Space> { suspend fun getSpace(@PathVariable spaceId: String): Space {
return spaceService.getSpace(spaceId) return spaceService.getSpace(spaceId)
} }
@DeleteMapping("/{spaceId}") @DeleteMapping("/{spaceId}")
fun deleteSpace(@PathVariable spaceId: String): Mono<Void> { suspend fun deleteSpace(@PathVariable spaceId: String) {
return spaceService.isValidRequest(spaceId).flatMap { return spaceService.deleteSpace(spaceService.isValidRequest(spaceId))
spaceService.deleteSpace(it)
} }
}
@PostMapping("/{spaceId}/invite") @PostMapping("/{spaceId}/invite")
fun inviteSpace(@PathVariable spaceId: String): Mono<SpaceInvite> { suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite {
spaceService.isValidRequest(spaceId)
return spaceService.createInviteSpace(spaceId) return spaceService.createInviteSpace(spaceId)
} }
@PostMapping("/invite/{code}") @PostMapping("/invite/{code}")
fun acceptInvite(@PathVariable code: String): Mono<Space> { suspend fun acceptInvite(@PathVariable code: String): Space {
return spaceService.acceptInvite(code) return spaceService.acceptInvite(code)
} }
@DeleteMapping("/{spaceId}/leave") @DeleteMapping("/{spaceId}/leave")
fun leaveSpace(@PathVariable spaceId: String): Mono<Void> { suspend fun leaveSpace(@PathVariable spaceId: String) {
spaceService.isValidRequest(spaceId)
return spaceService.leaveSpace(spaceId) return spaceService.leaveSpace(spaceId)
} }
@DeleteMapping("/{spaceId}/members/kick/{username}") @DeleteMapping("/{spaceId}/members/kick/{username}")
fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String): Mono<Void> { suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) {
spaceService.isValidRequest(spaceId)
return spaceService.kickMember(spaceId, username) return spaceService.kickMember(spaceId, username)
} }
@@ -86,49 +86,46 @@ class SpaceController(
//Budgets API //Budgets API
// //
@GetMapping("/{spaceId}/budgets") @GetMapping("/{spaceId}/budgets")
fun getBudgets(@PathVariable spaceId: String): Mono<List<Budget>> { suspend fun getBudgets(@PathVariable spaceId: String): List<Budget> {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
financialService.getBudgets(spaceId) return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty()
}
} }
@GetMapping("/{spaceId}/budgets/{id}") @GetMapping("/{spaceId}/budgets/{id}")
fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): Mono<BudgetDTO> { suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
financialService.getBudget(spaceId, id) return financialService.getBudget(spaceId, id)
}
} }
@PostMapping("/{spaceId}/budgets") @PostMapping("/{spaceId}/budgets")
fun createBudget( suspend fun createBudget(
@PathVariable spaceId: String, @PathVariable spaceId: String,
@RequestBody budgetCreationDTO: BudgetCreationDTO, @RequestBody budgetCreationDTO: BudgetCreationDTO,
): Mono<Budget> { ): Budget? {
return spaceService.isValidRequest(spaceId).flatMap { return financialService.createBudget(
financialService.createBudget(it, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent) spaceService.isValidRequest(spaceId),
} budgetCreationDTO.budget,
budgetCreationDTO.createRecurrent
)
} }
@DeleteMapping("/{spaceId}/budgets/{id}") @DeleteMapping("/{spaceId}/budgets/{id}")
fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String): Mono<Void> { suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
financialService.deleteBudget(spaceId, id) financialService.deleteBudget(spaceId, id)
}
} }
@PostMapping("/{spaceId}/budgets/{budgetId}/categories/{catId}/limit") @PostMapping("/{spaceId}/budgets/{budgetId}/categories/{catId}/limit")
fun setCategoryLimit( suspend fun setCategoryLimit(
@PathVariable spaceId: String, @PathVariable spaceId: String,
@PathVariable budgetId: String, @PathVariable budgetId: String,
@PathVariable catId: String, @PathVariable catId: String,
@RequestBody limit: LimitValue, @RequestBody limit: LimitValue,
): Mono<BudgetCategory> { ): BudgetCategory {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
financialService.setCategoryLimit(it.id!!, budgetId, catId, limit.limit) return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit)
}
} }
// //
@@ -164,123 +161,102 @@ class SpaceController(
@GetMapping("/{spaceId}/transactions/{id}") @GetMapping("/{spaceId}/transactions/{id}")
fun getTransaction( suspend fun getTransaction(
@PathVariable spaceId: String, @PathVariable spaceId: String,
@PathVariable id: String @PathVariable id: String
): ResponseEntity<Any> { ): Transaction {
try { return financialService.getTransactionById(id)
return ResponseEntity.ok(financialService.getTransactionById(id))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
} }
@PostMapping("/{spaceId}/transactions") @PostMapping("/{spaceId}/transactions")
fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Mono<Transaction> { suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
financialService.createTransaction(it, transaction) return financialService.createTransaction(space, transaction)
}
} }
@PutMapping("/{spaceId}/transactions/{id}") @PutMapping("/{spaceId}/transactions/{id}")
fun editTransaction( suspend fun editTransaction(
@PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction @PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction
): Mono<Transaction> { ): Transaction {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
transaction.space = it transaction.space = space
financialService.editTransaction(transaction) return financialService.editTransaction(transaction)
}
} }
@DeleteMapping("/{spaceId}/transactions/{id}") @DeleteMapping("/{spaceId}/transactions/{id}")
fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String): Mono<Void> { suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
financialService.getTransactionById(id).flatMap { financialService.deleteTransaction(it) } val transaction = financialService.getTransactionById(id)
} financialService.deleteTransaction(transaction)
} }
// //
// Categories API // Categories API
// //
@GetMapping("/{spaceId}/categories") @GetMapping("/{spaceId}/categories")
fun getCategories( suspend fun getCategories(
@PathVariable spaceId: String, @PathVariable spaceId: String,
@RequestParam("type") type: String? = null, @RequestParam("type") type: String? = null,
@RequestParam("sort") sortBy: String = "name", @RequestParam("sort") sortBy: String = "name",
@RequestParam("direction") direction: String = "ASC" @RequestParam("direction") direction: String = "ASC"
): Mono<List<Category>> { ): List<Category> {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
categoryService.getCategories(spaceId, type, sortBy, direction) return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty()
}
} }
@GetMapping("/{spaceId}/categories/types") @GetMapping("/{spaceId}/categories/types")
fun getCategoriesTypes(@PathVariable spaceId: String): ResponseEntity<Any> { fun getCategoriesTypes(@PathVariable spaceId: String): List<CategoryType> {
return try { return categoryService.getCategoryTypes()
ResponseEntity.ok(categoryService.getCategoryTypes())
} catch (e: Exception) {
ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR)
}
} }
@PostMapping("/{spaceId}/categories") @PostMapping("/{spaceId}/categories")
fun createCategory( suspend fun createCategory(
@PathVariable spaceId: String, @RequestBody category: Category @PathVariable spaceId: String, @RequestBody category: Category
): Mono<Category> { ): Category {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
financialService.createCategory(it, category) return financialService.createCategory(space, category).awaitSingle()
}
} }
@PutMapping("/{spaceId}/categories/{categoryId}") @PutMapping("/{spaceId}/categories/{categoryId}")
fun editCategory( suspend fun editCategory(
@PathVariable categoryId: String, @PathVariable categoryId: String,
@RequestBody category: Category, @RequestBody category: Category,
@PathVariable spaceId: String @PathVariable spaceId: String
): Mono<Category> { ): Category {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
categoryService.editCategory(it, category) return categoryService.editCategory(space, category)
}
} }
@DeleteMapping("/{spaceId}/categories/{categoryId}") @DeleteMapping("/{spaceId}/categories/{categoryId}")
fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String): Mono<String> { suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
categoryService.deleteCategory(it, categoryId) categoryService.deleteCategory(space, categoryId)
}
} }
@GetMapping("/{spaceId}/categories/tags") @GetMapping("/{spaceId}/categories/tags")
fun getTags(@PathVariable spaceId: String): Mono<List<Tag>> { suspend fun getTags(@PathVariable spaceId: String): List<Tag> {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
spaceService.getTags(it) return spaceService.getTags(space)
}
} }
@PostMapping("/{spaceId}/categories/tags") @PostMapping("/{spaceId}/categories/tags")
fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Mono<Tag> { suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
spaceService.createTag(it, tag) return spaceService.createTag(space, tag)
}
} }
@DeleteMapping("/{spaceId}/categories/tags/{tagId}") @DeleteMapping("/{spaceId}/categories/tags/{tagId}")
fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String): Mono<Void> { suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
spaceService.deleteTag(it, tagId) return spaceService.deleteTag(space, tagId)
}
} }
@GetMapping("/{spaceId}/analytics/by-month") @GetMapping("/{spaceId}/analytics/by-month")
fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): Mono<List<Document>> { suspend fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): List<Document> {
return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6)) return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6))
} }
@@ -290,46 +266,40 @@ class SpaceController(
// //
@GetMapping("/{spaceId}/recurrents") @GetMapping("/{spaceId}/recurrents")
fun getRecurrents(@PathVariable spaceId: String): Mono<List<Recurrent>> { suspend fun getRecurrents(@PathVariable spaceId: String): List<Recurrent> {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
recurrentService.getRecurrents(it.id!!) return recurrentService.getRecurrents(spaceId).awaitSingleOrNull().orEmpty()
}
} }
@GetMapping("/{spaceId}/recurrents/{id}") @GetMapping("/{spaceId}/recurrents/{id}")
fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono<Recurrent> { suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
recurrentService.getRecurrentById(it, id) return recurrentService.getRecurrentById(space, id).awaitSingle()
}
} }
@PostMapping("/{spaceId}/recurrent") @PostMapping("/{spaceId}/recurrent")
fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Mono<Recurrent> { suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent {
return spaceService.isValidRequest(spaceId).flatMap { val space = spaceService.isValidRequest(spaceId)
recurrentService.createRecurrent(it, recurrent) return recurrentService.createRecurrent(space, recurrent).awaitSingle()
}
} }
@PutMapping("/{spaceId}/recurrent/{id}") @PutMapping("/{spaceId}/recurrent/{id}")
fun editRecurrent( suspend fun editRecurrent(
@PathVariable spaceId: String, @PathVariable spaceId: String,
@PathVariable id: String, @PathVariable id: String,
@RequestBody recurrent: Recurrent @RequestBody recurrent: Recurrent
): Mono<Recurrent> { ): Recurrent {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
recurrentService.editRecurrent(recurrent) return recurrentService.editRecurrent(recurrent).awaitSingle()
}
} }
@DeleteMapping("/{spaceId}/recurrent/{id}") @DeleteMapping("/{spaceId}/recurrent/{id}")
fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono<Void> { suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) {
return spaceService.isValidRequest(spaceId).flatMap { spaceService.isValidRequest(spaceId)
recurrentService.deleteRecurrent(id) recurrentService.deleteRecurrent(id).awaitSingle()
}
} }
// @GetMapping("/regen") // @GetMapping("/regen")

View File

@@ -1,13 +1,10 @@
package space.luminic.budgerapp.controllers package space.luminic.budgerapp.controllers
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.GetMapping import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.PushMessage import space.luminic.budgerapp.models.PushMessage
import space.luminic.budgerapp.models.SubscriptionDTO import space.luminic.budgerapp.models.SubscriptionDTO
import space.luminic.budgerapp.services.SubscriptionService import space.luminic.budgerapp.services.SubscriptionService
@@ -28,17 +25,13 @@ class SubscriptionController(
} }
@PostMapping("/subscribe") @PostMapping("/subscribe")
fun subscribe( suspend fun subscribe(
@RequestBody subscription: SubscriptionDTO, @RequestBody subscription: SubscriptionDTO,
authentication: Authentication authentication: Authentication
): Mono<String> { ): String {
return userService.getByUserNameWoPass(authentication.name) val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle()
.flatMap { user -> val user = userService.getByUserNameWoPass(securityContext.authentication.name)
subscriptionService.subscribe(subscription, user) return subscriptionService.subscribe(subscription, user)
.thenReturn("Subscription successful")
}
.switchIfEmpty(Mono.just("User not found"))
} }
@PostMapping("/notifyAll") @PostMapping("/notifyAll")

View File

@@ -11,13 +11,16 @@ class BudgetMapper(private val categoryMapper: CategoryMapper) : FromDocumentMap
override fun fromDocument(document: Document): Budget { override fun fromDocument(document: Document): Budget {
val spaceId = document.get("spaceDetails", Document::class.java)?.getObjectId("_id")?.toString()
val categoriesList = document.getList("categories", Document::class.java).orEmpty()
val incomeCategoriesList = document.getList("incomeCategories", Document::class.java).orEmpty()
return Budget( return Budget(
id = document.getObjectId("_id").toString(), id = document.getObjectId("_id").toString(),
space = Space(id = document.get("spaceDetails", Document::class.java).getObjectId("_id").toString()), space = Space(id=spaceId),
name = document.getString("name"), name = document.getString("name"),
dateFrom = document.getDate("dateFrom").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), dateFrom = document.getDate("dateFrom").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(),
dateTo = document.getDate("dateTo").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), dateTo = document.getDate("dateTo").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(),
categories = document.getList("categories", Document::class.java).map { cat -> categories = categoriesList.map { cat ->
val categoryDetailed = document.getList("categoriesDetails", Document::class.java).first { val categoryDetailed = document.getList("categoriesDetails", Document::class.java).first {
it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString() it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString()
} }
@@ -26,7 +29,7 @@ class BudgetMapper(private val categoryMapper: CategoryMapper) : FromDocumentMap
currentLimit = cat.getDouble("currentLimit") currentLimit = cat.getDouble("currentLimit")
) )
}.toMutableList(), }.toMutableList(),
incomeCategories = document.getList("incomeCategories", Document::class.java).map { cat -> incomeCategories = incomeCategoriesList.map { cat ->
val categoryDetailed = val categoryDetailed =
document.getList("incomeCategoriesDetails", Document::class.java).first { it -> document.getList("incomeCategoriesDetails", Document::class.java).first { it ->
it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString() it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString()

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
package space.luminic.budgerapp.services 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.Document
import org.bson.types.ObjectId import org.bson.types.ObjectId
import org.slf4j.LoggerFactory 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.data.mongodb.core.query.Criteria
import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.mappers.RecurrentMapper import space.luminic.budgerapp.mappers.RecurrentMapper
import space.luminic.budgerapp.models.* import space.luminic.budgerapp.models.*
@@ -73,26 +75,15 @@ 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 currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue)
val daysInCurrentMonth = currentYearMonth.lengthOfMonth() val daysInCurrentMonth = currentYearMonth.lengthOfMonth()
val context = ReactiveSecurityContextHolder.getContext() val context = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
.doOnNext { println("Security context: $it") } ?: throw IllegalStateException("SecurityContext is empty!")
.switchIfEmpty(Mono.error(IllegalStateException("SecurityContext is empty!"))) val user = userService.getByUserNameWoPass(context.authentication.name)
val recurrents = getRecurrents(space.id!!).awaitSingle()
return context val transactions = recurrents.map { recurrent ->
.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 { val transactionDate = when {
recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> { recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> {
currentYearMonth.atDay(recurrent.atDay) currentYearMonth.atDay(recurrent.atDay)
@@ -107,7 +98,6 @@ class RecurrentService(
currentYearMonth.plusMonths(1).atDay(extraDays) currentYearMonth.plusMonths(1).atDay(extraDays)
} }
} }
// Создаем транзакцию // Создаем транзакцию
Transaction( Transaction(
space = space, space = space,
@@ -120,11 +110,7 @@ class RecurrentService(
type = TransactionType("PLANNED", "Запланированные") type = TransactionType("PLANNED", "Запланированные")
) )
} }
} transactionRepo.saveAll(transactions).awaitLast()
.collectList() // Собираем все транзакции в список
.flatMap { transactions ->
transactionRepo.saveAll(transactions).then() // Сохраняем все транзакции разом и возвращаем Mono<Void>
}
} }
@@ -139,6 +125,4 @@ class RecurrentService(
} }
} }

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
package space.luminic.budgerapp.services package space.luminic.budgerapp.services
import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.CacheEvict
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -25,15 +26,12 @@ class TokenService(private val tokenRepository: TokenRepo) {
return tokenRepository.findByToken(token) 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 package space.luminic.budgerapp.services
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -14,13 +15,10 @@ class UserService(val userRepo: UserRepo) {
val logger = LoggerFactory.getLogger(javaClass) val logger = LoggerFactory.getLogger(javaClass)
@Cacheable("users", key = "#username") @Cacheable("users", key = "#username")
fun getByUsername(username: String): Mono<User> { suspend fun getByUsername(username: String): User {
return userRepo.findByUsernameWOPassword(username).switchIfEmpty( return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
Mono.error(NotFoundException("User with username: $username not found")) ?: throw NotFoundException("User with username: $username not found")
)
} }
fun getById(id: String): Mono<User> { fun getById(id: String): Mono<User> {
@@ -33,8 +31,9 @@ class UserService(val userRepo: UserRepo) {
@Cacheable("users", key = "#username") @Cacheable("users", key = "#username")
fun getByUserNameWoPass(username: String): Mono<User> { suspend fun getByUserNameWoPass(username: String): User {
return userRepo.findByUsernameWOPassword(username) return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
?: throw NotFoundException("User with username: $username not found")
} }
@Cacheable("usersList") @Cacheable("usersList")