From 284b4be6bd802ff78bf525d01b8b169d5677594b Mon Sep 17 00:00:00 2001 From: William Brawner Date: Sun, 31 May 2020 22:06:21 -0700 Subject: [PATCH] Convert the rest of the codebase from Kotlin to Java --- api/build.gradle | 17 +- .../budgetserver/BudgetServerApplication.java | 11 + .../budgetserver/BudgetServerApplication.kt | 14 -- .../wbrawner/budgetserver/ErrorResponse.java | 13 ++ .../wbrawner/budgetserver/ErrorResponse.kt | 3 - .../java/com/wbrawner/budgetserver/Utils.java | 22 +- .../java/com/wbrawner/budgetserver/Utils.kt | 22 -- .../budgetserver/budget/BudgetController.java | 42 ++-- .../budgetserver/budget/BudgetController.kt | 115 ---------- .../budgetserver/budget/BudgetRequest.java | 6 + .../budgetserver/category/Category.java | 86 +++++++ .../budgetserver/category/Category.kt | 56 ----- .../category/CategoryBalanceResponse.java | 19 ++ .../category/CategoryController.java | 177 +++++++++++++++ .../category/CategoryController.kt | 135 ----------- .../category/CategoryRepository.java | 22 ++ .../category/CategoryRepository.kt | 15 -- .../category/CategoryResponse.java | 56 +++++ .../category/NewCategoryRequest.java | 41 ++++ .../category/UpdateCategoryRequest.java | 35 +++ .../config/JdbcUserDetailsService.java | 32 +++ .../config/JdbcUserDetailsService.kt | 24 -- .../budgetserver/config/MethodSecurity.java | 10 + .../budgetserver/config/SecurityConfig.java | 109 +++++++++ .../budgetserver/config/SecurityConfig.kt | 92 -------- .../budgetserver/config/SwaggerConfig.java | 35 +++ .../budgetserver/config/SwaggerConfig.kt | 30 --- .../PasswordResetRequest.java | 39 ++++ .../PasswordResetRequest.kt | 16 -- .../PasswordResetRequestRepository.java | 6 + .../PasswordResetRequestRepository.kt | 5 - .../permission/UserPermission.java | 54 +++++ .../budgetserver/permission/UserPermission.kt | 44 ---- .../permission/UserPermissionKey.java | 19 ++ .../permission/UserPermissionRepository.java | 21 ++ .../permission/UserPermissionRepository.kt | 14 -- .../permission/UserPermissionRequest.java | 23 ++ .../permission/UserPermissionResponse.java | 25 ++ .../transaction/NewTransactionRequest.java | 53 +++++ .../budgetserver/transaction/Transaction.java | 121 ++++++++++ .../budgetserver/transaction/Transaction.kt | 71 ------ .../transaction/TransactionController.java | 214 ++++++++++++++++++ .../transaction/TransactionController.kt | 172 -------------- .../transaction/TransactionRepository.java | 38 ++++ .../transaction/TransactionRepository.kt | 33 --- .../transaction/TransactionResponse.java | 83 +++++++ .../transaction/UpdateTransactionRequest.java | 59 +++++ .../budgetserver/user/LoginRequest.java | 23 ++ .../budgetserver/user/NewUserRequest.java | 29 +++ .../budgetserver/user/UpdateUserRequest.java | 29 +++ .../com/wbrawner/budgetserver/user/User.java | 91 ++++++++ .../com/wbrawner/budgetserver/user/User.kt | 48 ---- .../budgetserver/user/UserController.java | 160 +++++++++++++ .../budgetserver/user/UserController.kt | 129 ----------- .../budgetserver/user/UserRepository.java | 16 ++ .../budgetserver/user/UserRepository.kt | 11 - .../budgetserver/user/UserResponse.java | 29 +++ .../BudgetServerApplicationTests.kt | 16 -- build.gradle | 1 - 59 files changed, 1829 insertions(+), 1102 deletions(-) create mode 100644 api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.kt delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/Utils.kt delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/Category.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/Category.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/CategoryBalanceResponse.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/CategoryResponse.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/NewCategoryRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/category/UpdateCategoryRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/MethodSecurity.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionKey.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionResponse.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/NewTransactionRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionResponse.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/transaction/UpdateTransactionRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/LoginRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/NewUserRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/UpdateUserRequest.java create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/User.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/User.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/UserController.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/UserController.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.java delete mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.kt create mode 100644 api/src/main/java/com/wbrawner/budgetserver/user/UserResponse.java delete mode 100644 api/src/test/java/com/wbrawner/budgetserver/BudgetServerApplicationTests.kt diff --git a/api/build.gradle b/api/build.gradle index 00cff2b..ca8b55c 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'java' -apply plugin: 'kotlin' apply plugin: 'application' apply plugin: "io.spring.dependency-management" apply plugin: "org.springframework.boot" @@ -17,9 +16,6 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-security" implementation "org.springframework.session:spring-session-jdbc" implementation "org.springframework.boot:spring-boot-starter-web" - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8" - implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61" implementation "io.springfox:springfox-swagger2:2.8.0" implementation "io.springfox:springfox-swagger-ui:2.8.0" runtimeOnly "mysql:mysql-connector-java:8.0.15" @@ -31,14 +27,7 @@ jar { description = "twigs-server" } -mainClassName = "com.wbrawner.budgetserver.BudgetServerApplicationKt" - -sourceCompatibility = 11 -targetCompatibility = 11 - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { - kotlinOptions { - jvmTarget = '11' - } -} +mainClassName = "com.wbrawner.budgetserver.BudgetServerApplication" +sourceCompatibility = 14 +targetCompatibility = 14 diff --git a/api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.java b/api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.java new file mode 100644 index 0000000..08b4d66 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.java @@ -0,0 +1,11 @@ +package com.wbrawner.budgetserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BudgetServerApplication { + public static void main(String[] args) { + SpringApplication.run(BudgetServerApplication.class, args); + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.kt b/api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.kt deleted file mode 100644 index 18875b1..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/BudgetServerApplication.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.wbrawner.budgetserver - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -open class BudgetServerApplication { - companion object { - @JvmStatic - fun main(args: Array) { - runApplication(*args) - } - } -} diff --git a/api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.java b/api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.java new file mode 100644 index 0000000..3c544d8 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.wbrawner.budgetserver; + +public class ErrorResponse { + private final String message; + + public ErrorResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.kt b/api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.kt deleted file mode 100644 index f5b0dc0..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/ErrorResponse.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.wbrawner.budgetserver - -data class ErrorResponse(val message: String) \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/Utils.java b/api/src/main/java/com/wbrawner/budgetserver/Utils.java index 2bca0bd..5876cec 100644 --- a/api/src/main/java/com/wbrawner/budgetserver/Utils.java +++ b/api/src/main/java/com/wbrawner/budgetserver/Utils.java @@ -1,11 +1,14 @@ package com.wbrawner.budgetserver; +import com.wbrawner.budgetserver.user.User; +import org.springframework.security.core.context.SecurityContextHolder; + import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; public final class Utils { - private static int[] CALENDAR_FIELDS = new int[]{ + private static final int[] CALENDAR_FIELDS = new int[]{ Calendar.MILLISECOND, Calendar.SECOND, Calendar.MINUTE, @@ -20,4 +23,21 @@ public final class Utils { } return calendar.getTime(); } + + public static Date getEndOfMonth() { + GregorianCalendar calendar = new GregorianCalendar(); + for (int field : CALENDAR_FIELDS) { + calendar.set(field, calendar.getActualMaximum(field)); + } + return calendar.getTime(); + } + + public static User getCurrentUser() { + Object user = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (user instanceof User) { + return (User) user; + } + + return null; + } } diff --git a/api/src/main/java/com/wbrawner/budgetserver/Utils.kt b/api/src/main/java/com/wbrawner/budgetserver/Utils.kt deleted file mode 100644 index c391cd4..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/Utils.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.wbrawner.budgetserver - -import com.wbrawner.budgetserver.user.User -import org.springframework.security.core.context.SecurityContextHolder -import java.util.* - -fun getCurrentUser(): User? { - val user = SecurityContextHolder.getContext().authentication.principal - return if (user is User) user else null -} - -fun GregorianCalendar.setToFirstOfMonth(): GregorianCalendar = this.apply { - for (field in arrayOf(Calendar.MILLISECOND, Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DATE)) { - set(field, getActualMinimum(field)) - } -} - -fun GregorianCalendar.setToEndOfMonth(): GregorianCalendar = this.apply { - for (field in arrayOf(Calendar.MILLISECOND, Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DATE)) { - set(field, getActualMaximum(field)) - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java index c80480d..71ab161 100644 --- a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java @@ -10,6 +10,7 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.Authorization; import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -19,8 +20,8 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import static com.wbrawner.budgetserver.Utils.getCurrentUser; import static com.wbrawner.budgetserver.Utils.getFirstOfMonth; -import static com.wbrawner.budgetserver.UtilsKt.getCurrentUser; @RestController @RequestMapping(value = "/budgets") @@ -81,7 +82,7 @@ public class BudgetController { return ResponseEntity.status(401).build(); } - var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(user, id).orElse(null); if (userPermission == null) { return ResponseEntity.notFound().build(); } @@ -99,10 +100,10 @@ public class BudgetController { public ResponseEntity getBudgetBalance(@PathVariable long id) { var user = getCurrentUser(); if (user == null) { - return ResponseEntity.status(401).build(); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(user, id).orElse(null); if (userPermission == null) { return ResponseEntity.notFound().build(); } @@ -115,7 +116,7 @@ public class BudgetController { return ResponseEntity.ok(new BudgetBalanceResponse(budget.getId(), balance)); } - @PostMapping(value = "/new", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @PostMapping(value = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) @ApiOperation(value = "newBudget", nickname = "newBudget", tags = {"Budgets"}) public ResponseEntity newBudget(@RequestBody BudgetRequest request) { final var budget = budgetRepository.save(new Budget(request.name, request.description)); @@ -148,19 +149,19 @@ public class BudgetController { @PutMapping(value = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) @ApiOperation(value = "updateBudget", nickname = "updateBudget", tags = {"Budgets"}) - public ResponseEntity updateBudget(@PathVariable long id, BudgetRequest request) { + public ResponseEntity updateBudget(@PathVariable long id, @RequestBody BudgetRequest request) { var user = getCurrentUser(); if (user == null) { return ResponseEntity.status(401).build(); } - var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(user, id).orElse(null); if (userPermission == null) { return ResponseEntity.notFound().build(); } if (userPermission.getPermission().isNotAtLeast(Permission.MANAGE)) { - return ResponseEntity.status(403).build(); + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } var budget = userPermission.getBudget(); @@ -178,17 +179,16 @@ public class BudgetController { var users = new ArrayList(); if (!request.getUsers().isEmpty()) { - request.getUsers().forEach(userPermissionRequest -> { - userRepository.findById(userPermissionRequest.getUser()).ifPresent(requestedUser -> - users.add(userPermissionsRepository.save( - new UserPermission( - budget, - requestedUser, - userPermissionRequest.getPermission() - ) - )) - ); - }); + request.getUsers().forEach(userPermissionRequest -> + userRepository.findById(userPermissionRequest.getUser()).ifPresent(requestedUser -> + users.add(userPermissionsRepository.save( + new UserPermission( + budget, + requestedUser, + userPermissionRequest.getPermission() + ) + )) + )); } else { users.addAll(userPermissionsRepository.findAllByBudget(budget, null)); } @@ -204,7 +204,7 @@ public class BudgetController { return ResponseEntity.status(401).build(); } - var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(user, id).orElse(null); if (userPermission == null) { return ResponseEntity.notFound().build(); } @@ -218,6 +218,6 @@ public class BudgetController { return ResponseEntity.notFound().build(); } budgetRepository.delete(budget); - return ResponseEntity.noContent().build(); + return ResponseEntity.ok().build(); } } diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt deleted file mode 100644 index fd5dd8b..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.wbrawner.budgetserver.budget - -import com.wbrawner.budgetserver.getCurrentUser -import com.wbrawner.budgetserver.permission.Permission -import com.wbrawner.budgetserver.permission.UserPermission -import com.wbrawner.budgetserver.permission.UserPermissionRepository -import com.wbrawner.budgetserver.transaction.TransactionRepository -import com.wbrawner.budgetserver.user.UserRepository -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.Authorization -import org.hibernate.Hibernate -import org.springframework.data.domain.PageRequest -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import javax.transaction.Transactional - -@RestController -@RequestMapping("/budgets") -@Api(value = "Budgets", tags = ["Budgets"], authorizations = [Authorization("basic")]) -@Transactional -open class BudgetControllerKt( - private val budgetRepository: BudgetRepository, - private val transactionRepository: TransactionRepository, - private val userRepository: UserRepository, - private val userPermissionsRepository: UserPermissionRepository -) { - @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getBudgets", nickname = "getBudgets", tags = ["Budgets"]) - open fun getBudgets(page: Int?, count: Int?): ResponseEntity> = ResponseEntity.ok( - userPermissionsRepository.findAllByUser( - user = getCurrentUser()!!, - pageable = PageRequest.of(page ?: 0, count ?: 1000)) - .map { - Hibernate.initialize(it.budget) - BudgetResponse(it.budget!!, userPermissionsRepository.findAllByUserAndBudget(getCurrentUser()!!, it.budget, null)) - } - ) - - @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getBudget", nickname = "getBudget", tags = ["Budgets"]) - open fun getBudget(@PathVariable id: Long): ResponseEntity = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, id, null) - .firstOrNull() - ?.budget - ?.let { - ResponseEntity.ok(BudgetResponse(it, userPermissionsRepository.findAllByBudget(it, null))) - } - ?: ResponseEntity.notFound().build() - - @GetMapping("/{id}/balance", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getBudgetBalance", nickname = "getBudgetBalance", tags = ["Budgets"]) - open fun getBudgetBalance(@PathVariable id: Long): ResponseEntity = - userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, id, null) - .firstOrNull() - ?.budget - ?.let { - ResponseEntity.ok(BudgetBalanceResponse(it.id!!, transactionRepository.sumBalanceByBudgetId(it.id))) - } ?: ResponseEntity.notFound().build() - - @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "newBudget", nickname = "newBudget", tags = ["Budgets"]) - open fun newBudget(@RequestBody request: BudgetRequest): ResponseEntity { - val budget = budgetRepository.save(Budget(request.name, request.description)) - val users = request.users - .mapNotNull { - userRepository.findById(it.user).orElse(null)?.let { user -> - userPermissionsRepository.save( - UserPermission(budget = budget, user = user, permission = it.permission) - ) - } - } - .toMutableSet() - if (users.firstOrNull { it.user?.id == getCurrentUser()!!.id } == null) { - users.add( - userPermissionsRepository.save( - UserPermission(budget = budget, user = getCurrentUser(), permission = Permission.OWNER) - ) - ) - } - return ResponseEntity.ok(BudgetResponse(budget, users.toMutableList())) - } - - @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "updateBudget", nickname = "updateBudget", tags = ["Budgets"]) - open fun updateBudget(@PathVariable id: Long, request: BudgetRequest): ResponseEntity { - var budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, id, null) - .firstOrNull() - ?.budget - ?: return ResponseEntity.notFound().build() - request.name?.let { - budget.name = it - } - request.description?.let { - budget.description = it - } - val users = request.users?.mapNotNull { req -> - userRepository.findById(req.user).orElse(null)?.let { - userPermissionsRepository.save(UserPermission(budget = budget, user = it, permission = req.permission)) - } - } ?: userPermissionsRepository.findAllByUserAndBudget(getCurrentUser()!!, budget, null) - return ResponseEntity.ok(BudgetResponse(budgetRepository.save(budget), users)) - } - - @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) - @ApiOperation(value = "deleteBudget", nickname = "deleteBudget", tags = ["Budgets"]) - open fun deleteBudget(@PathVariable id: Long): ResponseEntity { - val budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, id, null) - .firstOrNull() - ?.budget - ?: return ResponseEntity.notFound().build() - budgetRepository.delete(budget) - return ResponseEntity.ok().build() - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java index 5c95a9d..e7e3b67 100644 --- a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java @@ -3,6 +3,7 @@ package com.wbrawner.budgetserver.budget; import com.wbrawner.budgetserver.permission.UserPermissionRequest; import javax.validation.constraints.NotNull; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -11,6 +12,11 @@ public class BudgetRequest { public final String description; private final Set users = new HashSet<>(); + public BudgetRequest() { + // Required empty constructor + this("", "", Collections.emptySet()); + } + public BudgetRequest(String name, String description, Set users) { this.name = name; this.description = description; diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/Category.java b/api/src/main/java/com/wbrawner/budgetserver/category/Category.java new file mode 100644 index 0000000..c41a624 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/Category.java @@ -0,0 +1,86 @@ +package com.wbrawner.budgetserver.category; + +import com.wbrawner.budgetserver.budget.Budget; + +import javax.persistence.*; + +@Entity +public class Category implements Comparable { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private final Long id = null; + private String title; + private String description; + private long amount; + @JoinColumn(nullable = false) + @ManyToOne + private Budget budget; + private boolean expense; + + public Category() { + this(null, null, 0L, null, true); + } + + public Category( + String title, + String description, + Long amount, + Budget budget, + boolean expense + ) { + this.title = title; + this.description = description; + this.amount = amount; + this.budget = budget; + this.expense = expense; + } + + @Override + public int compareTo(Category other) { + return title.compareTo(other.title); + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public Budget getBudget() { + return budget; + } + + public void setBudget(Budget budget) { + this.budget = budget; + } + + public boolean isExpense() { + return expense; + } + + public void setExpense(boolean expense) { + this.expense = expense; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/Category.kt b/api/src/main/java/com/wbrawner/budgetserver/category/Category.kt deleted file mode 100644 index d927f15..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/category/Category.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.wbrawner.budgetserver.category - -import com.wbrawner.budgetserver.budget.Budget -import com.wbrawner.budgetserver.transaction.Transaction -import javax.persistence.* - -@Entity -data class Category( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, - val title: String = "", - val description: String? = null, - val amount: Long = 0, - @JoinColumn(nullable = false) - @ManyToOne - val budget: Budget? = null, - @OneToMany(mappedBy = "category") val transactions: Set = emptySet(), - val expense: Boolean? = true -) : Comparable { - override fun compareTo(other: Category): Int = title.compareTo(other.title) -} - -data class CategoryResponse( - val id: Long, - val title: String, - val description: String?, - val amount: Long, - val budgetId: Long, - val expense: Boolean? = true -) { - constructor(category: Category) : this( - category.id!!, - category.title, - category.description, - category.amount, - category.budget!!.id!!, - category.expense - ) -} - -data class CategoryBalanceResponse(val id: Long, val balance: Long) - -data class NewCategoryRequest( - val title: String, - val description: String?, - val amount: Long, - val budgetId: Long, - val expense: Boolean? = true -) - -data class UpdateCategoryRequest( - val title: String?, - val description: String?, - val amount: Long?, - val expense: Boolean? -) \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryBalanceResponse.java b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryBalanceResponse.java new file mode 100644 index 0000000..b3bf0e1 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryBalanceResponse.java @@ -0,0 +1,19 @@ +package com.wbrawner.budgetserver.category; + +public class CategoryBalanceResponse { + private final long id; + private final long balance; + + public CategoryBalanceResponse(long id, long balance) { + this.id = id; + this.balance = balance; + } + + public long getId() { + return id; + } + + public long getBalance() { + return balance; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.java b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.java new file mode 100644 index 0000000..9acc73c --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.java @@ -0,0 +1,177 @@ +package com.wbrawner.budgetserver.category; + +import com.wbrawner.budgetserver.ErrorResponse; +import com.wbrawner.budgetserver.permission.Permission; +import com.wbrawner.budgetserver.permission.UserPermission; +import com.wbrawner.budgetserver.permission.UserPermissionRepository; +import com.wbrawner.budgetserver.transaction.TransactionRepository; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.Authorization; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.stream.Collectors; + +import static com.wbrawner.budgetserver.Utils.getCurrentUser; +import static com.wbrawner.budgetserver.Utils.getFirstOfMonth; + +@RestController +@RequestMapping(path = "/categories") +@Api(value = "Categories", tags = {"Categories"}, authorizations = {@Authorization("basic")}) +@Transactional +class CategoryController { + private final CategoryRepository categoryRepository; + private final TransactionRepository transactionRepository; + private final UserPermissionRepository userPermissionsRepository; + + CategoryController(CategoryRepository categoryRepository, + TransactionRepository transactionRepository, + UserPermissionRepository userPermissionsRepository) { + this.categoryRepository = categoryRepository; + this.transactionRepository = transactionRepository; + this.userPermissionsRepository = userPermissionsRepository; + } + + @GetMapping(path = "", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getCategories", nickname = "getCategories", tags = {"Categories"}) + ResponseEntity> getCategories( + @RequestParam(name = "budgetIds", required = false) List budgetIds, + @RequestParam(name = "isExpense", required = false) Boolean isExpense, + @RequestParam(name = "count", required = false) Integer count, + @RequestParam(name = "page", required = false) Integer page, + @RequestParam(name = "false", required = false) String sortBy, + @RequestParam(name = "sortOrder", required = false) Sort.Direction sortOrder + ) { + List userPermissions; + if (budgetIds != null && !budgetIds.isEmpty()) { + userPermissions = userPermissionsRepository.findAllByUserAndBudget_IdIn( + getCurrentUser(), + budgetIds, + PageRequest.of(page != null ? page : 0, count != null ? count : 1000) + ); + } else { + userPermissions = userPermissionsRepository.findAllByUser(getCurrentUser(), null); + } + var budgets = userPermissions.stream() + .map(UserPermission::getBudget) + .collect(Collectors.toList()); + + var pageRequest = PageRequest.of( + Math.min(0, page != null ? page - 1 : 0), + count != null ? count : 1000, + sortOrder != null ? sortOrder : Sort.Direction.ASC, + sortBy != null ? sortBy : "title" + ); + List categories; + if (isExpense == null) { + categories = categoryRepository.findAllByBudgetIn(budgets, pageRequest); + } else { + categories = categoryRepository.findAllByBudgetInAndExpense(budgets, isExpense, pageRequest); + } + + return ResponseEntity.ok( + categories.stream() + .map(CategoryResponse::new) + .collect(Collectors.toList()) + ); + } + + @GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getCategory", nickname = "getCategory", tags = {"Categories"}) + ResponseEntity getCategory(@PathVariable Long id) { + var budgets = userPermissionsRepository.findAllByUser(getCurrentUser(), null) + .stream() + .map(UserPermission::getBudget) + .collect(Collectors.toList()); + var category = categoryRepository.findByBudgetInAndId(budgets, id).orElse(null); + if (category == null) return ResponseEntity.notFound().build(); + return ResponseEntity.ok(new CategoryResponse(category)); + } + + @GetMapping(path = "/{id}/balance", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getCategoryBalance", nickname = "getCategoryBalance", tags = {"Categories"}) + ResponseEntity getCategoryBalance(@PathVariable Long id) { + var budgets = userPermissionsRepository.findAllByUser(getCurrentUser(), null) + .stream() + .map(UserPermission::getBudget) + .collect(Collectors.toList()); + var category = categoryRepository.findByBudgetInAndId(budgets, id).orElse(null); + if (category == null) { + return ResponseEntity.notFound().build(); + } + var sum = transactionRepository.sumBalanceByCategoryId(category.getId(), getFirstOfMonth()); + return ResponseEntity.ok(new CategoryBalanceResponse(category.getId(), sum)); + } + + @PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "newCategory", nickname = "newCategory", tags = {"Categories"}) + ResponseEntity newCategory(@RequestBody NewCategoryRequest request) { + var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId()) + .orElse(null); + if (userResponse == null) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid budget ID")); + } + if (userResponse.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + var budget = userResponse.getBudget(); + return ResponseEntity.ok(new CategoryResponse(categoryRepository.save(new Category( + request.getTitle(), + request.getDescription(), + request.getAmount(), + budget, + request.getExpense() + )))); + } + + @PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "updateCategory", nickname = "updateCategory", tags = {"Categories"}) + ResponseEntity updateCategory(@PathVariable Long id, @RequestBody UpdateCategoryRequest request) { + var category = categoryRepository.findById(id).orElse(null); + if (category == null) return ResponseEntity.notFound().build(); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), category.getBudget().getId()).orElse(null); + if (userPermission == null) return ResponseEntity.notFound().build(); + if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + if (request.getTitle() != null) { + category.setTitle(request.getTitle()); + } + if (request.getDescription() != null) { + category.setDescription(request.getDescription()); + } + if (request.getAmount() != null) { + category.setAmount(request.getAmount()); + } + if (request.getExpense() != null) { + category.setExpense(request.getExpense()); + } + return ResponseEntity.ok(new CategoryResponse(categoryRepository.save(category))); + } + + @DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE}) + @ApiOperation(value = "deleteCategory", nickname = "deleteCategory", tags = {"Categories"}) + ResponseEntity deleteCategory(@PathVariable Long id) { + var category = categoryRepository.findById(id).orElse(null); + if (category == null) return ResponseEntity.notFound().build(); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), category.getBudget().getId()).orElse(null); + if (userPermission == null) return ResponseEntity.notFound().build(); + if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + transactionRepository.findAllByBudgetAndCategory(userPermission.getBudget(), category) + .forEach(transaction -> { + transaction.setCategory(null); + transactionRepository.save(transaction); + }); + categoryRepository.delete(category); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.kt b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.kt deleted file mode 100644 index 39e943c..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryController.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.wbrawner.budgetserver.category - -import com.wbrawner.budgetserver.ErrorResponse -import com.wbrawner.budgetserver.budget.BudgetRepository -import com.wbrawner.budgetserver.getCurrentUser -import com.wbrawner.budgetserver.permission.UserPermissionRepository -import com.wbrawner.budgetserver.transaction.TransactionRepository -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.Authorization -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import java.lang.Integer.min -import javax.transaction.Transactional - -@RestController -@RequestMapping("/categories") -@Api(value = "Categories", tags = ["Categories"], authorizations = [Authorization("basic")]) -@Transactional -open class CategoryController( - private val budgetRepository: BudgetRepository, - private val categoryRepository: CategoryRepository, - private val transactionRepository: TransactionRepository, - private val userPermissionsRepository: UserPermissionRepository -) { - @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getCategories", nickname = "getCategories", tags = ["Categories"]) - open fun getCategories( - @RequestParam("budgetIds", required = false) budgetIds: List? = null, - @RequestParam("isExpense", required = false) isExpense: Boolean? = null, - @RequestParam("count", required = false) count: Int?, - @RequestParam("page", required = false) page: Int?, - @RequestParam("false", required = false) sortBy: String?, - @RequestParam("sortOrder", required = false) sortOrder: Sort.Direction? - ): ResponseEntity> { - val budgets = ( - budgetIds - ?.let { - userPermissionsRepository.findAllByUserAndBudget_IdIn(getCurrentUser()!!, it, null) - } - ?: userPermissionsRepository.findAllByUser( - user = getCurrentUser()!!, - pageable = PageRequest.of(page ?: 0, count ?: 1000) - ) - ) - .mapNotNull { - it.budget - } - val pageRequest = PageRequest.of( - min(0, page?.minus(1) ?: 0), - count ?: 1000, - Sort.by(sortOrder ?: Sort.Direction.ASC, sortBy ?: "title") - ) - val categories = if (isExpense == null) { - categoryRepository.findAllByBudgetIn(budgets, pageRequest) - } else { - categoryRepository.findAllByBudgetInAndExpense(budgets, isExpense, pageRequest) - } - return ResponseEntity.ok(categories.map { CategoryResponse(it) }) - } - - @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getCategory", nickname = "getCategory", tags = ["Categories"]) - open fun getCategory(@PathVariable id: Long): ResponseEntity { - val budgets = userPermissionsRepository.findAllByUser(getCurrentUser()!!, null) - .mapNotNull { it.budget } - - val category = categoryRepository.findByBudgetInAndId(budgets, id) - .orElse(null) - ?: return ResponseEntity.notFound().build() - return ResponseEntity.ok(CategoryResponse(category)) - } - - @GetMapping("/{id}/balance", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getCategoryBalance", nickname = "getCategoryBalance", tags = ["Categories"]) - open fun getCategoryBalance(@PathVariable id: Long): ResponseEntity { - val budgets = userPermissionsRepository.findAllByUser(getCurrentUser()!!, null) - .mapNotNull { it.budget } - val category = categoryRepository.findByBudgetInAndId(budgets, id) - .orElse(null) - ?: return ResponseEntity.notFound().build() - val transactions = transactionRepository.sumBalanceByCategoryId(category.id!!) - return ResponseEntity.ok(CategoryBalanceResponse(category.id, transactions)) - } - - @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "newCategory", nickname = "newCategory", tags = ["Categories"]) - open fun newCategory(@RequestBody request: NewCategoryRequest): ResponseEntity { - val budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, request.budgetId, null) - .firstOrNull() - ?.budget - ?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid budget ID")) - return ResponseEntity.ok(CategoryResponse(categoryRepository.save(Category( - title = request.title, - description = request.description, - amount = request.amount, - budget = budget - )))) - } - - @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "updateCategory", nickname = "updateCategory", tags = ["Categories"]) - open fun updateCategory(@PathVariable id: Long, @RequestBody request: UpdateCategoryRequest): ResponseEntity { - val budgets = userPermissionsRepository.findAllByUser(getCurrentUser()!!, null) - .mapNotNull { it.budget } - var category = categoryRepository.findByBudgetInAndId(budgets, id) - .orElse(null) - ?: return ResponseEntity.notFound().build() - request.title?.let { category = category.copy(title = it) } - request.description?.let { category = category.copy(description = it) } - request.amount?.let { category = category.copy(amount = it) } - request.expense?.let { category = category.copy(expense = it) } - return ResponseEntity.ok(CategoryResponse(categoryRepository.save(category))) - } - - @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) - @ApiOperation(value = "deleteCategory", nickname = "deleteCategory", tags = ["Categories"]) - open fun deleteCategory(@PathVariable id: Long): ResponseEntity { - val budgets = userPermissionsRepository.findAllByUser(getCurrentUser()!!, null) - .mapNotNull { it.budget } - val category = categoryRepository.findByBudgetInAndId(budgets, id) - .orElse(null) - ?: return ResponseEntity.notFound().build() - val budget = budgets.first { it.id == category.budget!!.id } - transactionRepository.findAllByBudgetAndCategory(budget, category) - .forEach { transaction -> - transactionRepository.save(transaction.copy(category = null)) - } - categoryRepository.delete(category) - return ResponseEntity.ok().build() - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.java b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.java new file mode 100644 index 0000000..bf93f56 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.java @@ -0,0 +1,22 @@ +package com.wbrawner.budgetserver.category; + +import com.wbrawner.budgetserver.budget.Budget; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.util.List; +import java.util.Optional; + +public interface CategoryRepository extends PagingAndSortingRepository { + List findAllByBudget(Budget budget, Pageable pageable); + + List findAllByBudgetIn(List budgets, Pageable pageable); + + Optional findByBudgetInAndId(List budgets, Long id); + + List findAllByBudgetInAndExpense(List budgets, Boolean isExpense, Pageable pageable); + + Optional findByBudgetAndId(Budget budget, Long id); + + List findAllByBudgetInAndIdIn(List budgets, List ids, Pageable pageable); +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.kt b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.kt deleted file mode 100644 index 88af301..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.wbrawner.budgetserver.category - -import com.wbrawner.budgetserver.budget.Budget -import org.springframework.data.domain.Pageable -import org.springframework.data.repository.PagingAndSortingRepository -import java.util.* - -interface CategoryRepository: PagingAndSortingRepository { - fun findAllByBudget(budget: Budget, pageable: Pageable): List - fun findAllByBudgetIn(budgets: List, pageable: Pageable? = null): List - fun findByBudgetInAndId(budgets: List, id: Long): Optional - fun findAllByBudgetInAndExpense(budgets: List, isExpense: Boolean, pageable: Pageable? = null): List - fun findByBudgetAndId(budget: Budget, id: Long): Optional - fun findAllByBudgetInAndIdIn(budgets: List, ids: List, pageable: Pageable? = null): List -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/CategoryResponse.java b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryResponse.java new file mode 100644 index 0000000..15839cd --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/CategoryResponse.java @@ -0,0 +1,56 @@ +package com.wbrawner.budgetserver.category; + +import java.util.Objects; + +public class CategoryResponse { + private final long id; + private final String title; + private final String description; + private final long amount; + private final long budgetId; + private final boolean expense; + + public CategoryResponse(Category category) { + this( + Objects.requireNonNull(category.getId()), + category.getTitle(), + category.getDescription(), + category.getAmount(), + Objects.requireNonNull(category.getBudget()).getId(), + category.isExpense() + ); + } + + public CategoryResponse(long id, String title, String description, long amount, long budgetId, boolean expense) { + this.id = id; + this.title = title; + this.description = description; + this.amount = amount; + this.budgetId = budgetId; + this.expense = expense; + } + + public long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public long getAmount() { + return amount; + } + + public long getBudgetId() { + return budgetId; + } + + public boolean isExpense() { + return expense; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/NewCategoryRequest.java b/api/src/main/java/com/wbrawner/budgetserver/category/NewCategoryRequest.java new file mode 100644 index 0000000..8c9a344 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/NewCategoryRequest.java @@ -0,0 +1,41 @@ +package com.wbrawner.budgetserver.category; + +public class NewCategoryRequest { + private final String title; + private final String description; + private final Long amount; + private final Long budgetId; + private final Boolean expense; + + public NewCategoryRequest() { + this(null, null, null, null, null); + } + + public NewCategoryRequest(String title, String description, Long amount, Long budgetId, Boolean expense) { + this.title = title; + this.description = description; + this.amount = amount; + this.budgetId = budgetId; + this.expense = expense; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public Long getAmount() { + return amount; + } + + public Long getBudgetId() { + return budgetId; + } + + public Boolean getExpense() { + return expense; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/category/UpdateCategoryRequest.java b/api/src/main/java/com/wbrawner/budgetserver/category/UpdateCategoryRequest.java new file mode 100644 index 0000000..2fc9530 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/category/UpdateCategoryRequest.java @@ -0,0 +1,35 @@ +package com.wbrawner.budgetserver.category; + +public class UpdateCategoryRequest { + private final String title; + private final String description; + private final Long amount; + private final Boolean expense; + + public UpdateCategoryRequest() { + this(null, null, null, null); + } + + public UpdateCategoryRequest(String title, String description, Long amount, Boolean expense) { + this.title = title; + this.description = description; + this.amount = amount; + this.expense = expense; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public Long getAmount() { + return amount; + } + + public Boolean getExpense() { + return expense; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.java b/api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.java new file mode 100644 index 0000000..0e18b4f --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.java @@ -0,0 +1,32 @@ +package com.wbrawner.budgetserver.config; + +import com.wbrawner.budgetserver.user.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class JdbcUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Autowired + public JdbcUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserDetails userDetails; + userDetails = userRepository.findByUsername(username).orElse(null); + if (userDetails != null) { + return userDetails; + } + userDetails = userRepository.findByEmail(username).orElse(null); + if (userDetails != null) { + return userDetails; + } + throw new UsernameNotFoundException("Unable to find user with username $username"); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt b/api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt deleted file mode 100644 index d522eee..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.wbrawner.budgetserver.config - -import com.wbrawner.budgetserver.user.UserRepository -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.security.core.userdetails.UserDetails -import org.springframework.security.core.userdetails.UserDetailsService -import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.stereotype.Component - -@Component -open class JdbcUserDetailsService @Autowired -constructor(private val userRepository: UserRepository) : UserDetailsService { - - @Throws(UsernameNotFoundException::class) - override fun loadUserByUsername(username: String): UserDetails { - userRepository.findByName(username).orElse(null)?.let { - return it - } - userRepository.findByEmail(username).orElse(null)?.let { - return it - } - throw UsernameNotFoundException("Unable to find user with username $username") - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/MethodSecurity.java b/api/src/main/java/com/wbrawner/budgetserver/config/MethodSecurity.java new file mode 100644 index 0000000..c1b784a --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/config/MethodSecurity.java @@ -0,0 +1,10 @@ +package com.wbrawner.budgetserver.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; + +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class MethodSecurity extends GlobalMethodSecurityConfiguration { +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.java b/api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.java new file mode 100644 index 0000000..50a3b59 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.java @@ -0,0 +1,109 @@ +package com.wbrawner.budgetserver.config; + +import com.wbrawner.budgetserver.passwordresetrequest.PasswordResetRequestRepository; +import com.wbrawner.budgetserver.user.UserRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.JdbcUserDetailsManager; +import org.springframework.web.cors.CorsConfiguration; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final Environment env; + private final DataSource datasource; + private final UserRepository userRepository; + private final PasswordResetRequestRepository passwordResetRequestRepository; + private final JdbcUserDetailsService userDetailsService; + private final Environment environment; + + public SecurityConfig(Environment env, + DataSource datasource, + UserRepository userRepository, + PasswordResetRequestRepository passwordResetRequestRepository, + JdbcUserDetailsService userDetailsService, + Environment environment) { + this.env = env; + this.datasource = datasource; + this.userRepository = userRepository; + this.passwordResetRequestRepository = passwordResetRequestRepository; + this.userDetailsService = userDetailsService; + this.environment = environment; + } + + @Bean + public JdbcUserDetailsManager getUserDetailsManager() { + var userDetailsManager = new JdbcUserDetailsManager(); + userDetailsManager.setDataSource(datasource); + return userDetailsManager; + } + + @Bean + public DaoAuthenticationProvider getAuthenticationProvider() { + var authProvider = new DaoAuthenticationProvider(); + authProvider.setPasswordEncoder(getPasswordEncoder()); + authProvider.setUserDetailsService(userDetailsService); + return authProvider; + } + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + public void configure(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(getAuthenticationProvider()); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers("/users/new", "/users/login") + .permitAll() + .anyRequest() + .authenticated() + .and() + .httpBasic() + .and() + .cors() + .configurationSource(request -> { + var corsConfig = new CorsConfiguration(); + corsConfig.applyPermitDefaultValues(); + var corsDomains = environment.getProperty("twigs.cors.domains", "*"); + corsConfig.setAllowedOrigins(Arrays.asList(corsDomains.split(","))); + corsConfig.setAllowedMethods( + Stream.of( + HttpMethod.GET, + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.DELETE, + HttpMethod.OPTIONS + ) + .map(Enum::name) + .collect(Collectors.toList()) + ); + return corsConfig; + }) + .and() + .csrf() + .disable(); + } +} + diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.kt b/api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.kt deleted file mode 100644 index aa1467e..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/config/SecurityConfig.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.wbrawner.budgetserver.config - -import com.wbrawner.budgetserver.passwordresetrequest.PasswordResetRequestRepository -import com.wbrawner.budgetserver.user.UserRepository -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.core.env.Environment -import org.springframework.core.env.get -import org.springframework.http.HttpMethod -import org.springframework.security.authentication.dao.DaoAuthenticationProvider -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity -import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.security.provisioning.JdbcUserDetailsManager -import org.springframework.web.cors.CorsConfiguration -import javax.sql.DataSource - - -@Configuration -@EnableWebSecurity -open class SecurityConfig( - private val env: Environment, - private val datasource: DataSource, - private val userRepository: UserRepository, - private val passwordResetRequestRepository: PasswordResetRequestRepository, - private val userDetailsService: JdbcUserDetailsService, - private val environment: Environment -) : WebSecurityConfigurerAdapter() { - - open val userDetailsManager: JdbcUserDetailsManager - @Bean - get() { - val userDetailsManager = JdbcUserDetailsManager() - userDetailsManager.setDataSource(datasource) - return userDetailsManager - } - - open val authenticationProvider: DaoAuthenticationProvider - @Bean - get() = DaoAuthenticationProvider().apply { - this.setPasswordEncoder(passwordEncoder) - this.setUserDetailsService(userDetailsService) - } - - open val passwordEncoder: PasswordEncoder - @Bean - get() = BCryptPasswordEncoder() - - public override fun configure(auth: AuthenticationManagerBuilder?) { - auth!!.authenticationProvider(authenticationProvider) - } - - @Throws(Exception::class) - public override fun configure(http: HttpSecurity) { - http.authorizeRequests() - .antMatchers("/users/new", "/users/login") - .permitAll() - .anyRequest() - .authenticated() - .and() - .httpBasic() - .and() - .cors() - .configurationSource { - with(CorsConfiguration()) { - applyPermitDefaultValues() - allowedOrigins = environment["twigs.cors.domains"]?.split(",") ?: listOf("*") - allowedMethods = listOf( - HttpMethod.GET, - HttpMethod.POST, - HttpMethod.PUT, - HttpMethod.DELETE, - HttpMethod.OPTIONS - ).map { it.name } - allowCredentials = true - this - } - } - .and() - .csrf() - .disable() - } -} - -@Configuration -@EnableGlobalMethodSecurity(prePostEnabled = true) -open class MethodSecurity : GlobalMethodSecurityConfiguration() diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.java b/api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.java new file mode 100644 index 0000000..40fa6e7 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.java @@ -0,0 +1,35 @@ +package com.wbrawner.budgetserver.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.BasicAuth; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Collections; + +@Configuration +@EnableSwagger2 +class SwaggerConfig extends WebMvcConfigurationSupport { + @Bean + Docket budgetApi() { + return new Docket(DocumentationType.SWAGGER_2) + .securitySchemes(Collections.singletonList(new BasicAuth("basic"))) + .select() + .apis(RequestHandlerSelectors.basePackage("com.wbrawner.budgetserver")) + .build(); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("swagger-ui.html") + .addResourceLocations("classpath:/META-INF/resources/"); + + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.kt b/api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.kt deleted file mode 100644 index 33eea5e..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/config/SwaggerConfig.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.wbrawner.budgetserver.config - -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry -import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport -import springfox.documentation.builders.RequestHandlerSelectors -import springfox.documentation.service.BasicAuth -import springfox.documentation.spi.DocumentationType -import springfox.documentation.spring.web.plugins.Docket -import springfox.documentation.swagger2.annotations.EnableSwagger2 - -@Configuration -@EnableSwagger2 -open class SwaggerConfig : WebMvcConfigurationSupport() { - @Bean - open fun budgetApi(): Docket = Docket(DocumentationType.SWAGGER_2) - .securitySchemes(mutableListOf(BasicAuth("basic"))) - .select() - .apis(RequestHandlerSelectors.basePackage("com.wbrawner.budgetserver")) - .build() - - override fun addResourceHandlers(registry: ResourceHandlerRegistry) { - registry.addResourceHandler("swagger-ui.html") - .addResourceLocations("classpath:/META-INF/resources/") - - registry.addResourceHandler("/webjars/**") - .addResourceLocations("classpath:/META-INF/resources/webjars/") - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.java b/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.java new file mode 100644 index 0000000..d5f8ca5 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.java @@ -0,0 +1,39 @@ +package com.wbrawner.budgetserver.passwordresetrequest; + +import com.wbrawner.budgetserver.user.User; + +import javax.persistence.*; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.UUID; + +@Entity +public class PasswordResetRequest { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private final Long id; + @ManyToOne + private final User user; + private final Calendar date; + private final String token; + + public PasswordResetRequest() { + this(null, null); + } + + public PasswordResetRequest(Long id, User user) { + this(id, user, new GregorianCalendar(), UUID.randomUUID().toString().replace("-", "")); + } + + public PasswordResetRequest( + Long id, + User user, + Calendar date, + String token + ) { + this.id = id; + this.user = user; + this.date = date; + this.token = token; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.kt b/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.kt deleted file mode 100644 index 1542469..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.wbrawner.budgetserver.passwordresetrequest - -import com.wbrawner.budgetserver.user.User -import java.util.* -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id - -@Entity -data class PasswordResetRequest( - @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, - val user: User? = null, - val date: Calendar = GregorianCalendar(), - val token: String = UUID.randomUUID().toString().replace("-", "") -) \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.java b/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.java new file mode 100644 index 0000000..836b30f --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.java @@ -0,0 +1,6 @@ +package com.wbrawner.budgetserver.passwordresetrequest; + +import org.springframework.data.repository.PagingAndSortingRepository; + +public interface PasswordResetRequestRepository extends PagingAndSortingRepository { +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.kt b/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.kt deleted file mode 100644 index ae1e30e..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/passwordresetrequest/PasswordResetRequestRepository.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wbrawner.budgetserver.passwordresetrequest - -import org.springframework.data.repository.PagingAndSortingRepository - -interface PasswordResetRequestRepository: PagingAndSortingRepository \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.java b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.java new file mode 100644 index 0000000..2f51b46 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.java @@ -0,0 +1,54 @@ +package com.wbrawner.budgetserver.permission; + +import com.wbrawner.budgetserver.budget.Budget; +import com.wbrawner.budgetserver.user.User; + +import javax.persistence.*; + +@Entity +public class UserPermission { + @EmbeddedId + private UserPermissionKey id; + @ManyToOne + @MapsId("budgetId") + @JoinColumn(nullable = false, name = "budget_id") + private Budget budget; + @ManyToOne + @MapsId("userId") + @JoinColumn(nullable = false, name = "user_id") + private User user; + @JoinColumn(nullable = false) + @Enumerated(EnumType.STRING) + private Permission permission; + + public UserPermission() { + this(null, null, null, null); + } + + public UserPermission(Budget budget, User user, Permission permission) { + this(new UserPermissionKey(budget.getId(), user.getId()), budget, user, permission); + } + + public UserPermission(UserPermissionKey userPermissionKey, Budget budget, User user, Permission permission) { + this.id = userPermissionKey; + this.budget = budget; + this.user = user; + this.permission = permission; + } + + public UserPermissionKey getId() { + return id; + } + + public Budget getBudget() { + return budget; + } + + public User getUser() { + return user; + } + + public Permission getPermission() { + return permission; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.kt b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.kt deleted file mode 100644 index 5e8d435..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermission.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.wbrawner.budgetserver.permission - -import com.wbrawner.budgetserver.budget.Budget -import com.wbrawner.budgetserver.user.User -import com.wbrawner.budgetserver.user.UserResponse -import java.io.Serializable -import javax.persistence.* - -@Entity -data class UserPermission( - @EmbeddedId - val id: UserPermissionKey? = null, - @ManyToOne - @MapsId("budgetId") - @JoinColumn(nullable = false, name = "budget_id") - val budget: Budget? = null, - @ManyToOne - @MapsId("userId") - @JoinColumn(nullable = false, name = "user_id") - val user: User? = null, - @JoinColumn(nullable = false) - @Enumerated(EnumType.STRING) - val permission: Permission? = null -) { - constructor(budget: Budget, user: User, permission: Permission) : this(UserPermissionKey(budget.id, user.id), budget, user, permission) -} - -@Embeddable -data class UserPermissionKey( - var budgetId: Long? = null, - var userId: Long? = null -) : Serializable - -data class UserPermissionResponse( - val user: UserResponse, - val permission: Permission -) { - constructor(userPermission: UserPermission) : this(UserResponse(userPermission.user!!), userPermission.permission!!) -} - -data class UserPermissionRequest( - val user: Long, - val permission: Permission -) diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionKey.java b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionKey.java new file mode 100644 index 0000000..20e39cd --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionKey.java @@ -0,0 +1,19 @@ +package com.wbrawner.budgetserver.permission; + +import javax.persistence.Embeddable; +import java.io.Serializable; + +@Embeddable +public class UserPermissionKey implements Serializable { + private final Long budgetId; + private final Long userId; + + public UserPermissionKey() { + this(0, 0); + } + + public UserPermissionKey(long budgetId, long userId) { + this.budgetId = budgetId; + this.userId = userId; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.java b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.java new file mode 100644 index 0000000..9384c58 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.java @@ -0,0 +1,21 @@ +package com.wbrawner.budgetserver.permission; + +import com.wbrawner.budgetserver.budget.Budget; +import com.wbrawner.budgetserver.user.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.util.List; +import java.util.Optional; + +public interface UserPermissionRepository extends PagingAndSortingRepository { + Optional findByUserAndBudget_Id(User user, Long budgetId); + + List findAllByUser(User user, Pageable pageable); + + List findAllByBudget(Budget budget, Pageable pageable); + + List findAllByUserAndBudget(User user, Budget budget, Pageable pageable); + + List findAllByUserAndBudget_IdIn(User user, List budgetIds, Pageable pageable); +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.kt b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.kt deleted file mode 100644 index 7e93b92..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.wbrawner.budgetserver.permission - -import com.wbrawner.budgetserver.budget.Budget -import com.wbrawner.budgetserver.user.User -import org.springframework.data.domain.Pageable -import org.springframework.data.repository.PagingAndSortingRepository - -interface UserPermissionRepository : PagingAndSortingRepository { - fun findAllByUserAndBudget_Id(user: User, budgetId: Long, pageable: Pageable?): List - fun findAllByUser(user: User, pageable: Pageable?): List - fun findAllByBudget(budget: Budget, pageable: Pageable?): List - fun findAllByUserAndBudget(user: User, budget: Budget, pageable: Pageable?): List - fun findAllByUserAndBudget_IdIn(user: User, budgetIds: List, pageable: Pageable?): List -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRequest.java b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRequest.java new file mode 100644 index 0000000..15e1053 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionRequest.java @@ -0,0 +1,23 @@ +package com.wbrawner.budgetserver.permission; + +public class UserPermissionRequest { + private final Long user; + private final Permission permission; + + public UserPermissionRequest() { + this(0L, Permission.READ); + } + + public UserPermissionRequest(Long user, Permission permission) { + this.user = user; + this.permission = permission; + } + + public Long getUser() { + return user; + } + + public Permission getPermission() { + return permission; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionResponse.java b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionResponse.java new file mode 100644 index 0000000..3d85001 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/permission/UserPermissionResponse.java @@ -0,0 +1,25 @@ +package com.wbrawner.budgetserver.permission; + +import com.wbrawner.budgetserver.user.UserResponse; + +public class UserPermissionResponse { + private final UserResponse user; + private final Permission permission; + + public UserPermissionResponse(UserPermission userPermission) { + this(new UserResponse(userPermission.getUser()), userPermission.getPermission()); + } + + public UserPermissionResponse(UserResponse userResponse, Permission permission) { + this.user = userResponse; + this.permission = permission; + } + + public UserResponse getUser() { + return user; + } + + public Permission getPermission() { + return permission; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/NewTransactionRequest.java b/api/src/main/java/com/wbrawner/budgetserver/transaction/NewTransactionRequest.java new file mode 100644 index 0000000..aed67cb --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/transaction/NewTransactionRequest.java @@ -0,0 +1,53 @@ +package com.wbrawner.budgetserver.transaction; + +class NewTransactionRequest { + private final String title; + private final String description; + private final String date; + private final Long amount; + private final Long categoryId; + private final Boolean expense; + private final Long budgetId; + + NewTransactionRequest() { + this(null, null, null, null, null, null, null); + } + + NewTransactionRequest(String title, String description, String date, Long amount, Long categoryId, Boolean expense, Long budgetId) { + this.title = title; + this.description = description; + this.date = date; + this.amount = amount; + this.categoryId = categoryId; + this.expense = expense; + this.budgetId = budgetId; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getDate() { + return date; + } + + public Long getAmount() { + return amount; + } + + public Long getCategoryId() { + return categoryId; + } + + public Boolean getExpense() { + return expense; + } + + public Long getBudgetId() { + return budgetId; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.java b/api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.java new file mode 100644 index 0000000..9839e01 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.java @@ -0,0 +1,121 @@ +package com.wbrawner.budgetserver.transaction; + +import com.wbrawner.budgetserver.budget.Budget; +import com.wbrawner.budgetserver.category.Category; +import com.wbrawner.budgetserver.user.User; + +import javax.persistence.*; +import java.time.Instant; + +@Entity +public class Transaction implements Comparable { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private final Long id = null; + @ManyToOne + @JoinColumn(nullable = false) + private final User createdBy; + private String title; + private String description; + private Instant date; + private Long amount; + @ManyToOne + private Category category; + private Boolean expense; + @ManyToOne + @JoinColumn(nullable = false) + private Budget budget; + + public Transaction() { + this(null, null, null, null, null, null, null, null); + } + + public Transaction(String title, + String description, + Instant date, + Long amount, + Category category, + Boolean expense, + User createdBy, + Budget budget) { + this.title = title; + this.description = description; + this.date = date; + this.amount = amount; + this.category = category; + this.expense = expense; + this.createdBy = createdBy; + this.budget = budget; + } + + public Long getId() { + // This should only be set from Hibernate so it shouldn't actually be null ever + //noinspection ConstantConditions + return id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Instant getDate() { + return date; + } + + public void setDate(Instant date) { + this.date = date; + } + + public Long getAmount() { + return amount; + } + + public void setAmount(Long amount) { + this.amount = amount; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public Boolean getExpense() { + return expense; + } + + public void setExpense(Boolean expense) { + this.expense = expense; + } + + public User getCreatedBy() { + return createdBy; + } + + public Budget getBudget() { + return budget; + } + + public void setBudget(Budget budget) { + this.budget = budget; + } + + @Override + public int compareTo(Transaction other) { + return this.date.compareTo(other.date); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.kt b/api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.kt deleted file mode 100644 index 3ecbeea..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/transaction/Transaction.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.wbrawner.budgetserver.transaction - -import com.wbrawner.budgetserver.budget.Budget -import com.wbrawner.budgetserver.category.Category -import com.wbrawner.budgetserver.user.User -import java.time.Instant -import javax.persistence.* - -@Entity -data class Transaction( - @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, - val title: String = "", - val description: String? = null, - val date: Instant = Instant.now(), - val amount: Long = 0, - @ManyToOne val category: Category? = null, - val expense: Boolean = true, - @ManyToOne - @JoinColumn(nullable = false) - val createdBy: User? = null, - @ManyToOne - @JoinColumn(nullable = false) - val budget: Budget? = null -) : Comparable { - override fun compareTo(other: Transaction): Int = this.date.compareTo(other.date) -} - -data class TransactionResponse( - val id: Long, - val title: String, - val description: String?, - val date: String, - val amount: Long, - val expense: Boolean, - val budgetId: Long, - val categoryId: Long?, - val createdBy: Long -) { - constructor(transaction: Transaction) : this( - transaction.id!!, - transaction.title, - transaction.description, - transaction.date.toString(), - transaction.amount, - transaction.expense, - transaction.budget!!.id!!, - if (transaction.category != null) transaction.category.id!! else null, - transaction.createdBy!!.id!! - ) -} - -data class NewTransactionRequest( - val title: String, - val description: String?, - val date: String, - val amount: Long, - val categoryId: Long?, - val expense: Boolean, - val budgetId: Long -) - -data class UpdateTransactionRequest( - val title: String?, - val description: String?, - val date: String?, - val amount: Long?, - val categoryId: Long?, - val expense: Boolean?, - val budgetId: Long?, - val createdBy: Long? -) \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.java b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.java new file mode 100644 index 0000000..40110f3 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.java @@ -0,0 +1,214 @@ +package com.wbrawner.budgetserver.transaction; + +import com.wbrawner.budgetserver.ErrorResponse; +import com.wbrawner.budgetserver.category.Category; +import com.wbrawner.budgetserver.category.CategoryRepository; +import com.wbrawner.budgetserver.permission.Permission; +import com.wbrawner.budgetserver.permission.UserPermission; +import com.wbrawner.budgetserver.permission.UserPermissionRepository; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.Authorization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.transaction.Transactional; +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +import static com.wbrawner.budgetserver.Utils.*; + +@RestController +@RequestMapping(path = "/transactions") +@Api(value = "Transactions", tags = {"Transactions"}, authorizations = {@Authorization("basic")}) +@Transactional +public class TransactionController { + private final CategoryRepository categoryRepository; + private final TransactionRepository transactionRepository; + private final UserPermissionRepository userPermissionsRepository; + + private final Logger logger = LoggerFactory.getLogger(TransactionController.class); + + public TransactionController(CategoryRepository categoryRepository, + TransactionRepository transactionRepository, + UserPermissionRepository userPermissionsRepository) { + this.categoryRepository = categoryRepository; + this.transactionRepository = transactionRepository; + this.userPermissionsRepository = userPermissionsRepository; + } + + @GetMapping(path = "", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getTransactions", nickname = "getTransactions", tags = {"Transactions"}) + public ResponseEntity> getTransactions( + @RequestParam(value = "categoryIds", required = false) List categoryIds, + @RequestParam(value = "budgetIds", required = false) List budgetIds, + @RequestParam(value = "from", required = false) String from, + @RequestParam(value = "to", required = false) String to, + @RequestParam(value = "count", required = false) Integer count, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "sortBy", required = false) String sortBy, + @RequestParam(value = "sortOrder", required = false) Sort.Direction sortOrder + ) { + List userPermissions; + if (budgetIds != null && !budgetIds.isEmpty()) { + userPermissions = userPermissionsRepository.findAllByUserAndBudget_IdIn( + getCurrentUser(), + budgetIds, + PageRequest.of(page != null ? page : 0, count != null ? count : 1000) + ); + } else { + userPermissions = userPermissionsRepository.findAllByUser(getCurrentUser(), null); + } + var budgets = userPermissions.stream() + .map(UserPermission::getBudget) + .collect(Collectors.toList()); + + List categories; + if (categoryIds != null && !categoryIds.isEmpty()) { + categories = categoryRepository.findAllByBudgetInAndIdIn(budgets, categoryIds, null); + } else { + categories = categoryRepository.findAllByBudgetIn(budgets, null); + } + var pageRequest = PageRequest.of( + Math.min(0, page != null ? page - 1 : 0), + count != null ? count : 1000, + sortOrder != null ? sortOrder : Sort.Direction.DESC, + sortBy != null ? sortBy : "date" + ); + Instant fromInstant; + try { + fromInstant = Instant.parse(from); + } catch (Exception e) { + if (!(e instanceof NullPointerException)) + logger.error("Failed to parse '" + from + "' to Instant for 'from' parameter", e); + fromInstant = getFirstOfMonth().toInstant(); + } + Instant toInstant; + try { + toInstant = Instant.parse(to); + } catch (Exception e) { + if (!(e instanceof NullPointerException)) + logger.error("Failed to parse '" + to + "' to Instant for 'to' parameter", e); + toInstant = getEndOfMonth().toInstant(); + } + var transactions = transactionRepository.findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan( + budgets, + categories, + fromInstant, + toInstant, + pageRequest + ) + .stream() + .map(TransactionResponse::new) + .collect(Collectors.toList()); + return ResponseEntity.ok(transactions); + } + + @GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getTransaction", nickname = "getTransaction", tags = {"Transactions"}) + public ResponseEntity getTransaction(@PathVariable Long id) { + var budgets = userPermissionsRepository.findAllByUser(getCurrentUser(), null) + .stream() + .map(UserPermission::getBudget) + .collect(Collectors.toList()); + var transaction = transactionRepository.findByIdAndBudgetIn(id, budgets).orElse(null); + if (transaction == null) return ResponseEntity.notFound().build(); + return ResponseEntity.ok(new TransactionResponse(transaction)); + } + + @PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "newTransaction", nickname = "newTransaction", tags = {"Transactions"}) + public ResponseEntity newTransaction(@RequestBody NewTransactionRequest request) { + var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId()) + .orElse(null); + if (userResponse == null) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid budget ID")); + } + if (userResponse.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + var budget = userResponse.getBudget(); + Category category = null; + if (request.getCategoryId() != null) { + category = categoryRepository.findByBudgetAndId(budget, request.getCategoryId()).orElse(null); + } + return ResponseEntity.ok(new TransactionResponse(transactionRepository.save(new Transaction( + request.getTitle(), + request.getDescription(), + Instant.parse(request.getDate()), + request.getAmount(), + category, + request.getExpense(), + getCurrentUser(), + budget + )))); + } + + @PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "updateTransaction", nickname = "updateTransaction", tags = {"Transactions"}) + public ResponseEntity updateTransaction(@PathVariable Long id, @RequestBody UpdateTransactionRequest request) { + var transaction = transactionRepository.findById(id).orElse(null); + if (transaction == null) return ResponseEntity.notFound().build(); + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null); + if (userPermission == null) return ResponseEntity.notFound().build(); + if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + if (request.getTitle() != null) { + transaction.setTitle(request.getTitle()); + } + if (request.getDescription() != null) { + transaction.setDescription(request.getDescription()); + } + if (request.getDate() != null) { + transaction.setDate(Instant.parse(request.getDate())); + } + if (request.getAmount() != null) { + transaction.setAmount(request.getAmount()); + } + if (request.getExpense() != null) { + transaction.setExpense(request.getExpense()); + } + if (request.getBudgetId() != null) { + var newUserPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId()).orElse(null); + if (newUserPermission == null || newUserPermission.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity + .badRequest() + .body(new ErrorResponse("Invalid budget")); + } + transaction.setBudget(newUserPermission.getBudget()); + } + if (request.getCategoryId() != null) { + var category = categoryRepository.findByBudgetAndId(transaction.getBudget(), request.getCategoryId()).orElse(null); + if (category == null) { + return ResponseEntity + .badRequest() + .body(new ErrorResponse("Invalid category")); + } + transaction.setCategory(category); + } + return ResponseEntity.ok(new TransactionResponse(transactionRepository.save(transaction))); + } + + @DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE}) + @ApiOperation(value = "deleteTransaction", nickname = "deleteTransaction", tags = {"Transactions"}) + public ResponseEntity deleteTransaction(@PathVariable Long id) { + var transaction = transactionRepository.findById(id).orElse(null); + if (transaction == null) return ResponseEntity.notFound().build(); + // Check that the transaction belongs to an budget that the user has access to before deleting it + var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null); + if (userPermission == null) return ResponseEntity.notFound().build(); + if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + transactionRepository.delete(transaction); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.kt b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.kt deleted file mode 100644 index 1756645..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionController.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.wbrawner.budgetserver.transaction - -import com.wbrawner.budgetserver.ErrorResponse -import com.wbrawner.budgetserver.budget.BudgetRepository -import com.wbrawner.budgetserver.category.Category -import com.wbrawner.budgetserver.category.CategoryRepository -import com.wbrawner.budgetserver.getCurrentUser -import com.wbrawner.budgetserver.permission.UserPermissionRepository -import com.wbrawner.budgetserver.setToEndOfMonth -import com.wbrawner.budgetserver.setToFirstOfMonth -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.Authorization -import org.slf4j.LoggerFactory -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import java.lang.Integer.min -import java.time.Instant -import java.util.* -import javax.transaction.Transactional - -@RestController -@RequestMapping("/transactions") -@Api(value = "Transactions", tags = ["Transactions"], authorizations = [Authorization("basic")]) -@Transactional -open class TransactionController( - private val budgetRepository: BudgetRepository, - private val categoryRepository: CategoryRepository, - private val transactionRepository: TransactionRepository, - private val userPermissionsRepository: UserPermissionRepository -) { - private val logger = LoggerFactory.getLogger(TransactionController::class.java) - - @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getTransactions", nickname = "getTransactions", tags = ["Transactions"]) - open fun getTransactions( - @RequestParam("categoryId") categoryIds: Array? = null, - @RequestParam("budgetId") budgetIds: Array? = null, - @RequestParam("from") from: String? = null, - @RequestParam("to") to: String? = null, - @RequestParam count: Int?, - @RequestParam page: Int?, - @RequestParam sortBy: String?, - @RequestParam sortOrder: Sort.Direction? - ): ResponseEntity> { - val budgets = if (budgetIds != null) { - userPermissionsRepository.findAllByUserAndBudget_IdIn( - user = getCurrentUser()!!, - budgetIds = budgetIds.toList(), - pageable = PageRequest.of(page ?: 0, count ?: 1000)) - } else { - userPermissionsRepository.findAllByUser(getCurrentUser()!!, null) - }.mapNotNull { - it.budget - } - val categories = if (categoryIds?.isNotEmpty() == true) { - categoryRepository.findAllByBudgetInAndIdIn(budgets, categoryIds.toList()) - } else { - categoryRepository.findAllByBudgetIn(budgets) - } - val pageRequest = PageRequest.of( - min(0, page?.minus(1) ?: 0), - count ?: 1000, - sortOrder ?: Sort.Direction.DESC, - sortBy ?: "date" - ) - val fromInstant = try { - Instant.parse(from!!) - } catch (ignored: NullPointerException) { - null - } catch (e: Exception) { - logger.error("Failed to parse $to to Instant for 'from' parameter", e) - null - } - val toInstant = try { - Instant.parse(to!!) - } catch (ignored: NullPointerException) { - null - } catch (e: Exception) { - logger.error("Failed to parse $to to Instant for 'to' parameter", e) - null - } - val transactions = transactionRepository.findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan( - budgets, - categories, - fromInstant ?: GregorianCalendar().setToFirstOfMonth().toInstant(), - toInstant ?: GregorianCalendar().setToEndOfMonth().toInstant(), - pageRequest - ).map { TransactionResponse(it) } - return ResponseEntity.ok(transactions) - } - - @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getTransaction", nickname = "getTransaction", tags = ["Transactions"]) - open fun getTransaction(@PathVariable id: Long): ResponseEntity { - val budgets = userPermissionsRepository.findAllByUser(getCurrentUser()!!, null) - .mapNotNull { - it.budget - } - val transaction = transactionRepository.findAllByIdAndBudgetIn(id, budgets).firstOrNull() - ?: return ResponseEntity.notFound().build() - return ResponseEntity.ok(TransactionResponse(transaction)) - } - - @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "newTransaction", nickname = "newTransaction", tags = ["Transactions"]) - open fun newTransaction(@RequestBody request: NewTransactionRequest): ResponseEntity { - val budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, request.budgetId, null) - .firstOrNull() - ?.budget - ?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid budget ID")) - val category: Category? = request.categoryId?.let { - categoryRepository.findByBudgetAndId(budget, request.categoryId).orElse(null) - } - return ResponseEntity.ok(TransactionResponse(transactionRepository.save(Transaction( - title = request.title, - description = request.description, - date = Instant.parse(request.date), - amount = request.amount, - category = category, - expense = request.expense, - budget = budget, - createdBy = getCurrentUser()!! - )))) - } - - @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "updateTransaction", nickname = "updateTransaction", tags = ["Transactions"]) - open fun updateTransaction(@PathVariable id: Long, @RequestBody request: UpdateTransactionRequest): ResponseEntity { - var transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() - var budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, transaction.budget!!.id!!, null) - .firstOrNull() - ?.budget - ?: return ResponseEntity.notFound().build() - request.title?.let { transaction = transaction.copy(title = it) } - request.description?.let { transaction = transaction.copy(description = it) } - request.date?.let { transaction = transaction.copy(date = Instant.parse(it)) } - request.amount?.let { transaction = transaction.copy(amount = it) } - request.expense?.let { transaction = transaction.copy(expense = it) } - request.budgetId?.let { budgetId -> - userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, budgetId, null) - .firstOrNull() - ?.budget - ?.let { - budget = it - transaction = transaction.copy(budget = it, category = null) - } - } - request.categoryId?.let { - categoryRepository.findByBudgetAndId(budget, it).orElse(null)?.let { category -> - transaction = transaction.copy(category = category) - } - } - return ResponseEntity.ok(TransactionResponse(transactionRepository.save(transaction))) - } - - @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) - @ApiOperation(value = "deleteTransaction", nickname = "deleteTransaction", tags = ["Transactions"]) - open fun deleteTransaction(@PathVariable id: Long): ResponseEntity { - val transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() - // Check that the transaction belongs to an budget that the user has access to before deleting it - userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, transaction.budget!!.id!!, null) - .firstOrNull() - ?.budget - ?: return ResponseEntity.notFound().build() - transactionRepository.delete(transaction) - return ResponseEntity.ok().build() - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.java b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.java new file mode 100644 index 0000000..d2ad227 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.java @@ -0,0 +1,38 @@ +package com.wbrawner.budgetserver.transaction; + +import com.wbrawner.budgetserver.budget.Budget; +import com.wbrawner.budgetserver.category.Category; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +public interface TransactionRepository extends PagingAndSortingRepository { + Optional findByIdAndBudgetIn(Long id, List budgets); + + List findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan( + List budgets, + List categories, + Instant start, + Instant end, + Pageable pageable + ); + + List findAllByBudgetAndCategory(Budget budget, Category category); + + @Query( + nativeQuery = true, + value = "SELECT (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 0 AND date > :start), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 1 AND date > :date), 0));" + ) + Long sumBalanceByBudgetId(Long BudgetId, Date start); + + @Query( + nativeQuery = true, + value = "SELECT (COALESCE((SELECT SUM(amount) from transaction WHERE category_id = :categoryId AND expense = 0 AND date > :start), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE category_id = :categoryId AND expense = 1 AND date > :start), 0));" + ) + Long sumBalanceByCategoryId(Long categoryId, Date start); +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.kt b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.kt deleted file mode 100644 index 4561b6a..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionRepository.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.wbrawner.budgetserver.transaction - -import com.wbrawner.budgetserver.budget.Budget -import com.wbrawner.budgetserver.category.Category -import com.wbrawner.budgetserver.setToFirstOfMonth -import org.springframework.data.domain.Pageable -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.PagingAndSortingRepository -import java.time.Instant -import java.util.* - -interface TransactionRepository: PagingAndSortingRepository { - fun findAllByIdAndBudgetIn(id: Long, budgets: List): List - fun findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan( - budgets: List, - categories: List, - start: Instant, - end: Instant, - pageable: Pageable? = null - ): List - fun findAllByBudgetAndCategory(budget: Budget, category: Category): List - @Query( - nativeQuery = true, - value = "SELECT (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 0 AND date > :start), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 1 AND date > :date), 0));" - ) - fun sumBalanceByBudgetId(BudgetId: Long, start: Date = GregorianCalendar().setToFirstOfMonth().time): Long - - @Query( - nativeQuery = true, - value = "SELECT (COALESCE((SELECT SUM(amount) from transaction WHERE category_id = :categoryId AND expense = 0 AND date > :start), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE category_id = :categoryId AND expense = 1 AND date > :start), 0));" - ) - fun sumBalanceByCategoryId(categoryId: Long, start: Date = GregorianCalendar().setToFirstOfMonth().time): Long -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionResponse.java b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionResponse.java new file mode 100644 index 0000000..09c6d57 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/transaction/TransactionResponse.java @@ -0,0 +1,83 @@ +package com.wbrawner.budgetserver.transaction; + +class TransactionResponse { + private final Long id; + private final String title; + private final String description; + private final String date; + private final Long amount; + private final Boolean expense; + private final Long budgetId; + private final Long categoryId; + private final Long createdBy; + + TransactionResponse(Long id, + String title, + String description, + String date, + Long amount, + Boolean expense, + Long budgetId, + Long categoryId, + Long createdBy) { + this.id = id; + this.title = title; + this.description = description; + this.date = date; + this.amount = amount; + this.expense = expense; + this.budgetId = budgetId; + this.categoryId = categoryId; + this.createdBy = createdBy; + } + + TransactionResponse(Transaction transaction) { + this( + transaction.getId(), + transaction.getTitle(), + transaction.getDescription(), + transaction.getDate().toString(), + transaction.getAmount(), + transaction.getExpense(), + transaction.getBudget().getId(), + transaction.getCategory() != null ? transaction.getCategory().getId() : null, + transaction.getCreatedBy().getId() + ); + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getDate() { + return date; + } + + public Long getAmount() { + return amount; + } + + public Boolean getExpense() { + return expense; + } + + public Long getBudgetId() { + return budgetId; + } + + public Long getCategoryId() { + return categoryId; + } + + public Long getCreatedBy() { + return createdBy; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/transaction/UpdateTransactionRequest.java b/api/src/main/java/com/wbrawner/budgetserver/transaction/UpdateTransactionRequest.java new file mode 100644 index 0000000..4763dd1 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/transaction/UpdateTransactionRequest.java @@ -0,0 +1,59 @@ +package com.wbrawner.budgetserver.transaction; + +class UpdateTransactionRequest { + private final String title; + private final String description; + private final String date; + private final Long amount; + private final Long categoryId; + private final Boolean expense; + private final Long budgetId; + private final Long createdBy; + + UpdateTransactionRequest() { + this(null, null, null, null, null, null, null, null); + } + + UpdateTransactionRequest(String title, String description, String date, Long amount, Long categoryId, Boolean expense, Long budgetId, Long createdBy) { + this.title = title; + this.description = description; + this.date = date; + this.amount = amount; + this.categoryId = categoryId; + this.expense = expense; + this.budgetId = budgetId; + this.createdBy = createdBy; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getDate() { + return date; + } + + public Long getAmount() { + return amount; + } + + public Long getCategoryId() { + return categoryId; + } + + public Boolean getExpense() { + return expense; + } + + public Long getBudgetId() { + return budgetId; + } + + public Long getCreatedBy() { + return createdBy; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/LoginRequest.java b/api/src/main/java/com/wbrawner/budgetserver/user/LoginRequest.java new file mode 100644 index 0000000..aae3955 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/LoginRequest.java @@ -0,0 +1,23 @@ +package com.wbrawner.budgetserver.user; + +public class LoginRequest { + private final String username; + private final String password; + + public LoginRequest() { + this(null, null); + } + + public LoginRequest(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/NewUserRequest.java b/api/src/main/java/com/wbrawner/budgetserver/user/NewUserRequest.java new file mode 100644 index 0000000..c8b3934 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/NewUserRequest.java @@ -0,0 +1,29 @@ +package com.wbrawner.budgetserver.user; + +public class NewUserRequest { + private final String username; + private final String password; + private final String email; + + public NewUserRequest() { + this(null, null, null); + } + + public NewUserRequest(String username, String password, String email) { + this.username = username; + this.password = password; + this.email = email; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/UpdateUserRequest.java b/api/src/main/java/com/wbrawner/budgetserver/user/UpdateUserRequest.java new file mode 100644 index 0000000..571f3b2 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/UpdateUserRequest.java @@ -0,0 +1,29 @@ +package com.wbrawner.budgetserver.user; + +public class UpdateUserRequest { + private final String username; + private final String password; + private final String email; + + public UpdateUserRequest() { + this(null, null, null); + } + + public UpdateUserRequest(String username, String password, String email) { + this.username = username; + this.password = password; + this.email = email; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/User.java b/api/src/main/java/com/wbrawner/budgetserver/user/User.java new file mode 100644 index 0000000..aea2a55 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/User.java @@ -0,0 +1,91 @@ +package com.wbrawner.budgetserver.user; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.persistence.*; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@Entity +public class User implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private final Long id = null; + @Transient + private final List authorities = Collections.singletonList(new SimpleGrantedAuthority("USER")); + private String username; + private String password; + private String email; + + public User() { + this(null, null, null); + } + + public User(String username, String password, String email) { + this.username = username; + this.password = password; + this.email = email; + } + + public Long getId() { + // This shouldn't ever need to be set manually, only through Hibernate + //noinspection ConstantConditions + return id; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public Collection getAuthorities() { + return authorities; + } +} + + diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/User.kt b/api/src/main/java/com/wbrawner/budgetserver/user/User.kt deleted file mode 100644 index aea8645..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/user/User.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.wbrawner.budgetserver.user - -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.core.userdetails.UserDetails -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id - -@Entity -data class User( - @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, - val name: String = "", - val passphrase: String = "", - val email: String = "", - val enabled: Boolean = true, - val credentialsExpired: Boolean = false, - val isExpired: Boolean = false, - val isLocked: Boolean = false, - @Transient val grantedAuthorities: MutableCollection - = mutableListOf(SimpleGrantedAuthority("USER")) -) : UserDetails { - override fun getUsername(): String = name - - override fun getAuthorities(): MutableCollection = grantedAuthorities - - override fun isEnabled(): Boolean = enabled - - override fun isCredentialsNonExpired(): Boolean = !credentialsExpired - - override fun getPassword(): String = passphrase - - override fun isAccountNonExpired(): Boolean = !isExpired - - override fun isAccountNonLocked(): Boolean = !isLocked -} - - -data class UserResponse(val id: Long, val username: String, val email: String) { - constructor(user: User) : this(user.id!!, user.name, user.email) -} - -data class NewUserRequest(val username: String, val password: String, val email: String) - -data class UpdateUserRequest(val username: String?, val password: String?, val email: String?) - -data class LoginRequest(val username: String, val password: String) \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/UserController.java b/api/src/main/java/com/wbrawner/budgetserver/user/UserController.java new file mode 100644 index 0000000..a03b5e8 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/UserController.java @@ -0,0 +1,160 @@ +package com.wbrawner.budgetserver.user; + +import com.wbrawner.budgetserver.ErrorResponse; +import com.wbrawner.budgetserver.budget.BudgetRepository; +import com.wbrawner.budgetserver.permission.UserPermissionRepository; +import com.wbrawner.budgetserver.permission.UserPermissionResponse; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.Authorization; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.stream.Collectors; + +import static com.wbrawner.budgetserver.Utils.getCurrentUser; + +@RestController +@RequestMapping("/users") +@Api(value = "Users", tags = {"Users"}, authorizations = {@Authorization("basic")}) +@Transactional +public class UserController { + private final BudgetRepository budgetRepository; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserPermissionRepository userPermissionsRepository; + private final DaoAuthenticationProvider authenticationProvider; + + @Autowired + public UserController(BudgetRepository budgetRepository, + UserRepository userRepository, + PasswordEncoder passwordEncoder, + UserPermissionRepository userPermissionsRepository, + DaoAuthenticationProvider authenticationProvider) { + this.budgetRepository = budgetRepository; + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userPermissionsRepository = userPermissionsRepository; + this.authenticationProvider = authenticationProvider; + } + + + @GetMapping(path = "", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getUsers", nickname = "getUsers", tags = {"Users"}) + ResponseEntity> getUsers(Long budgetId) { + var budget = budgetRepository.findById(budgetId).orElse(null); + if (budget == null) { + return ResponseEntity.notFound().build(); + } + var userPermissions = userPermissionsRepository.findAllByBudget(budget, null); + + var userInBudget = userPermissions.stream() + .anyMatch(userPermission -> + userPermission.getUser().getId().equals(getCurrentUser().getId())); + if (!userInBudget) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(userPermissions.stream().map(UserPermissionResponse::new).collect(Collectors.toList())); + } + + @PostMapping(path = "/login", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "login", nickname = "login", tags = {"Users"}) + ResponseEntity login(@RequestBody LoginRequest request) { + var authReq = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()); + Authentication auth; + try { + auth = authenticationProvider.authenticate(authReq); + } catch (AuthenticationException e) { + return ResponseEntity.notFound().build(); + } + SecurityContextHolder.getContext().setAuthentication(auth); + return ResponseEntity.ok(new UserResponse(getCurrentUser())); + } + + @GetMapping(path = "/me", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getProfile", nickname = "getProfile", tags = {"Users"}) + ResponseEntity getProfile() { + var user = getCurrentUser(); + if (user == null) return ResponseEntity.status(401).build(); + return ResponseEntity.ok(new UserResponse(user)); + } + + @GetMapping(path = "/search", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "searchUsers", nickname = "searchUsers", tags = {"Users"}) + ResponseEntity> searchUsers(String query) { + return ResponseEntity.ok( + userRepository.findByUsernameContains(query) + .stream() + .map(UserResponse::new) + .collect(Collectors.toList()) + ); + } + + @GetMapping(path = "/{id}") + @ApiOperation(value = "getUser", nickname = "getUser", tags = {"Users"}) + ResponseEntity getUser(@PathVariable Long id) { + var user = userRepository.findById(id).orElse(null); + if (user == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(new UserResponse(user)); + } + + @PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "newUser", nickname = "newUser", tags = {"Users"}) + ResponseEntity newUser(@RequestBody NewUserRequest request) { + if (userRepository.findByUsername(request.getUsername()).isPresent()) + return ResponseEntity.badRequest().body(new ErrorResponse("Username taken")); + if (userRepository.findByEmail(request.getEmail()).isPresent()) + return ResponseEntity.badRequest().body(new ErrorResponse("Email taken")); + if (request.getPassword().isBlank()) + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password")); + return ResponseEntity.ok(new UserResponse(userRepository.save(new User( + request.getUsername(), + passwordEncoder.encode(request.getPassword()), + request.getEmail() + )))); + } + + @PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "updateUser", nickname = "updateUser", tags = {"Users"}) + ResponseEntity updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest request) { + if (!getCurrentUser().getId().equals(id)) return ResponseEntity.status(403).build(); + var user = userRepository.findById(getCurrentUser().getId()).orElse(null); + if (user == null) return ResponseEntity.notFound().build(); + if (request.getUsername() != null) { + if (userRepository.findByUsername(request.getUsername()).isPresent()) + return ResponseEntity.badRequest().body(new ErrorResponse("Username taken")); + user.setUsername(request.getUsername()); + } + if (request.getEmail() != null) { + if (userRepository.findByEmail(request.getEmail()).isPresent()) + return ResponseEntity.badRequest().body(new ErrorResponse("Email taken")); + user.setEmail(request.getEmail()); + } + if (request.getPassword() != null) { + if (request.getPassword().isBlank()) + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password")); + user.setPassword(passwordEncoder.encode(request.getPassword())); + } + return ResponseEntity.ok(new UserResponse(userRepository.save(user))); + } + + @DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE}) + @ApiOperation(value = "deleteUser", nickname = "deleteUser", tags = {"Users"}) + ResponseEntity deleteUser(@PathVariable Long id) { + if (!getCurrentUser().getId().equals(id)) return ResponseEntity.status(403).build(); + userRepository.deleteById(id); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/UserController.kt b/api/src/main/java/com/wbrawner/budgetserver/user/UserController.kt deleted file mode 100644 index 6b11d85..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/user/UserController.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.wbrawner.budgetserver.user - -import com.wbrawner.budgetserver.ErrorResponse -import com.wbrawner.budgetserver.budget.BudgetRepository -import com.wbrawner.budgetserver.getCurrentUser -import com.wbrawner.budgetserver.permission.UserPermissionRepository -import com.wbrawner.budgetserver.permission.UserPermissionResponse -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.Authorization -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.authentication.dao.DaoAuthenticationProvider -import org.springframework.security.core.AuthenticationException -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.web.bind.annotation.* -import javax.transaction.Transactional - -@RestController -@RequestMapping("/users") -@Api(value = "Users", tags = ["Users"], authorizations = [Authorization("basic")]) -@Transactional -open class UserController( - private val budgetRepository: BudgetRepository, - private val userRepository: UserRepository, - private val passwordEncoder: PasswordEncoder, - private val userPermissionsRepository: UserPermissionRepository, - private val authenticationProvider: DaoAuthenticationProvider -) { - - @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getUsers", nickname = "getUsers", tags = ["Users"]) - open fun getUsers(budgetId: Long): ResponseEntity> { - val userPermissions = budgetRepository.findById(budgetId) - .orElse(null) - ?.run { - userPermissionsRepository.findAllByBudget(this, null) - } - ?: return ResponseEntity.notFound().build() - if (userPermissions.none { it.user!!.id == getCurrentUser()!!.id }) { - return ResponseEntity.notFound().build() - } - return ResponseEntity.ok(userPermissions.map { UserPermissionResponse(it) }) - } - - @PostMapping("/login", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "login", nickname = "login", tags = ["Users"]) - open fun login(@RequestBody request: LoginRequest): ResponseEntity { - val authReq = UsernamePasswordAuthenticationToken(request.username, request.password) - val auth = try { - authenticationProvider.authenticate(authReq) - } catch (e: AuthenticationException) { - return ResponseEntity.notFound().build() - } - SecurityContextHolder.getContext().authentication = auth - return ResponseEntity.ok(UserResponse(getCurrentUser()!!)) - } - - @GetMapping("/me", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "getProfile", nickname = "getProfile", tags = ["Users"]) - open fun getProfile(): ResponseEntity { - val user = getCurrentUser()?: return ResponseEntity.status(401).build() - return ResponseEntity.ok(UserResponse(user)) - } - - @GetMapping("/search", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "searchUsers", nickname = "searchUsers", tags = ["Users"]) - open fun searchUsers(query: String): ResponseEntity> { - return ResponseEntity.ok(userRepository.findByNameContains(query).map { UserResponse(it) }) - } - - @GetMapping("/{id}") - @ApiOperation(value = "getUser", nickname = "getUser", tags = ["Users"]) - open fun getUser(@PathVariable id: Long): ResponseEntity = userRepository.findById(id).orElse(null) - ?.let { - ResponseEntity.ok(UserResponse(it)) - } - ?: ResponseEntity.notFound().build() - - @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "newUser", nickname = "newUser", tags = ["Users"]) - open fun newUser(@RequestBody request: NewUserRequest): ResponseEntity { - if (userRepository.findByName(request.username).isPresent) - return ResponseEntity.badRequest() - .body(ErrorResponse("Username taken")) - if (userRepository.findByEmail(request.email).isPresent) - return ResponseEntity.badRequest() - .body(ErrorResponse("Email taken")) - if (request.password.isBlank()) - return ResponseEntity.badRequest() - .body(ErrorResponse("Invalid password")) - return ResponseEntity.ok(UserResponse(userRepository.save(User( - name = request.username, - passphrase = passwordEncoder.encode(request.password), - email = request.email - )))) - } - - @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "updateUser", nickname = "updateUser", tags = ["Users"]) - open fun updateUser(@PathVariable id: Long, @RequestBody request: UpdateUserRequest): ResponseEntity { - if (getCurrentUser()!!.id != id) return ResponseEntity.status(403) - .body(ErrorResponse("Attempting to modify another user's budget")) - var user = userRepository.findById(getCurrentUser()!!.id!!).orElse(null)?: return ResponseEntity.notFound().build() - if (request.username != null) { - if (userRepository.findByName(request.username).isPresent) throw RuntimeException("Username taken") - user = user.copy(name = request.username) - } - if (request.email != null) { - if (userRepository.findByEmail(request.email).isPresent) throw RuntimeException("Email taken") - user = user.copy(email = request.email) - } - if (request.password != null) { - if (request.password.isBlank()) throw RuntimeException("Invalid password") - user = user.copy(passphrase = passwordEncoder.encode(request.password)) - } - return ResponseEntity.ok(UserResponse(userRepository.save(user))) - } - - @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) - @ApiOperation(value = "deleteUser", nickname = "deleteUser", tags = ["Users"]) - open fun deleteUser(@PathVariable id: Long): ResponseEntity { - if(getCurrentUser()!!.id != id) return ResponseEntity.status(403).build() - userRepository.deleteById(id) - return ResponseEntity.ok().build() - } -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.java b/api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.java new file mode 100644 index 0000000..0303954 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.java @@ -0,0 +1,16 @@ +package com.wbrawner.budgetserver.user; + +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends PagingAndSortingRepository { + Optional findByUsername(String username); + + Optional findByUsernameAndPassword(String username, String password); + + List findByUsernameContains(String username); + + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.kt b/api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.kt deleted file mode 100644 index 8eac169..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/user/UserRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.wbrawner.budgetserver.user - -import org.springframework.data.repository.PagingAndSortingRepository -import java.util.* - -interface UserRepository: PagingAndSortingRepository { - fun findByName(username: String): Optional - fun findByNameAndPassphrase(username: String, passphrase: String): Optional - fun findByNameContains(username: String): List - fun findByEmail(email: String): Optional -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/user/UserResponse.java b/api/src/main/java/com/wbrawner/budgetserver/user/UserResponse.java new file mode 100644 index 0000000..d4fff3c --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/user/UserResponse.java @@ -0,0 +1,29 @@ +package com.wbrawner.budgetserver.user; + +public class UserResponse { + private final long id; + private final String username; + private final String email; + + public UserResponse(User user) { + this(user.getId(), user.getUsername(), user.getEmail()); + } + + public UserResponse(long id, String username, String email) { + this.id = id; + this.username = username; + this.email = email; + } + + public long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getEmail() { + return email; + } +} diff --git a/api/src/test/java/com/wbrawner/budgetserver/BudgetServerApplicationTests.kt b/api/src/test/java/com/wbrawner/budgetserver/BudgetServerApplicationTests.kt deleted file mode 100644 index 70f61b1..0000000 --- a/api/src/test/java/com/wbrawner/budgetserver/BudgetServerApplicationTests.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.wbrawner.budgetserver - -import org.junit.Test -import org.junit.runner.RunWith -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.context.junit4.SpringRunner - -@RunWith(SpringRunner::class) -@SpringBootTest -class BudgetServerApplicationTests { - - @Test - fun contextLoads() { - } - -} diff --git a/build.gradle b/build.gradle index 9b23724..fd68fce 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,6 @@ buildscript { } dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72" classpath "org.springframework.boot:spring-boot-gradle-plugin:2.2.2.RELEASE" } }