Convert the rest of the codebase from Kotlin to Java
This commit is contained in:
parent
0b729bb34e
commit
284b4be6bd
59 changed files with 1829 additions and 1102 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<String>) {
|
||||
runApplication<BudgetServerApplication>(*args)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package com.wbrawner.budgetserver
|
||||
|
||||
data class ErrorResponse(val message: String)
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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<BudgetBalanceResponse> 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<BudgetResponse> 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<BudgetResponse> updateBudget(@PathVariable long id, BudgetRequest request) {
|
||||
public ResponseEntity<BudgetResponse> 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<UserPermission>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<List<BudgetResponse>> = 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<BudgetResponse> = 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<BudgetBalanceResponse> =
|
||||
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<BudgetResponse> {
|
||||
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<BudgetResponse> {
|
||||
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<Unit> {
|
||||
val budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, id, null)
|
||||
.firstOrNull()
|
||||
?.budget
|
||||
?: return ResponseEntity.notFound().build()
|
||||
budgetRepository.delete(budget)
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
|
@ -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<UserPermissionRequest> users = new HashSet<>();
|
||||
|
||||
public BudgetRequest() {
|
||||
// Required empty constructor
|
||||
this("", "", Collections.emptySet());
|
||||
}
|
||||
|
||||
public BudgetRequest(String name, String description, Set<UserPermissionRequest> users) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package com.wbrawner.budgetserver.category;
|
||||
|
||||
import com.wbrawner.budgetserver.budget.Budget;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
public class Category implements Comparable<Category> {
|
||||
@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;
|
||||
}
|
||||
}
|
|
@ -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<Transaction> = emptySet(),
|
||||
val expense: Boolean? = true
|
||||
) : Comparable<Category> {
|
||||
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?
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<List<CategoryResponse>> getCategories(
|
||||
@RequestParam(name = "budgetIds", required = false) List<Long> 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<UserPermission> 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<Category> 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<CategoryResponse> 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<CategoryBalanceResponse> 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<Object> 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<CategoryResponse> 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<Void> 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();
|
||||
}
|
||||
}
|
|
@ -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<Long>? = 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<List<CategoryResponse>> {
|
||||
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<CategoryResponse> {
|
||||
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<CategoryBalanceResponse> {
|
||||
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<Any> {
|
||||
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<CategoryResponse> {
|
||||
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<Unit> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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<Category, Long> {
|
||||
List<Category> findAllByBudget(Budget budget, Pageable pageable);
|
||||
|
||||
List<Category> findAllByBudgetIn(List<Budget> budgets, Pageable pageable);
|
||||
|
||||
Optional<Category> findByBudgetInAndId(List<Budget> budgets, Long id);
|
||||
|
||||
List<Category> findAllByBudgetInAndExpense(List<Budget> budgets, Boolean isExpense, Pageable pageable);
|
||||
|
||||
Optional<Category> findByBudgetAndId(Budget budget, Long id);
|
||||
|
||||
List<Category> findAllByBudgetInAndIdIn(List<Budget> budgets, List<Long> ids, Pageable pageable);
|
||||
}
|
|
@ -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<Category, Long> {
|
||||
fun findAllByBudget(budget: Budget, pageable: Pageable): List<Category>
|
||||
fun findAllByBudgetIn(budgets: List<Budget>, pageable: Pageable? = null): List<Category>
|
||||
fun findByBudgetInAndId(budgets: List<Budget>, id: Long): Optional<Category>
|
||||
fun findAllByBudgetInAndExpense(budgets: List<Budget>, isExpense: Boolean, pageable: Pageable? = null): List<Category>
|
||||
fun findByBudgetAndId(budget: Budget, id: Long): Optional<Category>
|
||||
fun findAllByBudgetInAndIdIn(budgets: List<Budget>, ids: List<Long>, pageable: Pageable? = null): List<Category>
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
|
@ -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/");
|
||||
}
|
||||
}
|
|
@ -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/")
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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("-", "")
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package com.wbrawner.budgetserver.passwordresetrequest;
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
public interface PasswordResetRequestRepository extends PagingAndSortingRepository<PasswordResetRequest, Long> {
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package com.wbrawner.budgetserver.passwordresetrequest
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository
|
||||
|
||||
interface PasswordResetRequestRepository: PagingAndSortingRepository<PasswordResetRequest, Long>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<UserPermission, UserPermissionKey> {
|
||||
Optional<UserPermission> findByUserAndBudget_Id(User user, Long budgetId);
|
||||
|
||||
List<UserPermission> findAllByUser(User user, Pageable pageable);
|
||||
|
||||
List<UserPermission> findAllByBudget(Budget budget, Pageable pageable);
|
||||
|
||||
List<UserPermission> findAllByUserAndBudget(User user, Budget budget, Pageable pageable);
|
||||
|
||||
List<UserPermission> findAllByUserAndBudget_IdIn(User user, List<Long> budgetIds, Pageable pageable);
|
||||
}
|
|
@ -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<UserPermission, UserPermissionKey> {
|
||||
fun findAllByUserAndBudget_Id(user: User, budgetId: Long, pageable: Pageable?): List<UserPermission>
|
||||
fun findAllByUser(user: User, pageable: Pageable?): List<UserPermission>
|
||||
fun findAllByBudget(budget: Budget, pageable: Pageable?): List<UserPermission>
|
||||
fun findAllByUserAndBudget(user: User, budget: Budget, pageable: Pageable?): List<UserPermission>
|
||||
fun findAllByUserAndBudget_IdIn(user: User, budgetIds: List<Long>, pageable: Pageable?): List<UserPermission>
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<Transaction> {
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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<Transaction> {
|
||||
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?
|
||||
)
|
|
@ -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<List<TransactionResponse>> getTransactions(
|
||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||
@RequestParam(value = "budgetIds", required = false) List<Long> 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<UserPermission> 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<Category> 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<TransactionResponse> 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<Object> 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<Object> 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<Void> 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();
|
||||
}
|
||||
}
|
|
@ -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<Long>? = null,
|
||||
@RequestParam("budgetId") budgetIds: Array<Long>? = 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<List<TransactionResponse>> {
|
||||
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<TransactionResponse> {
|
||||
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<Any> {
|
||||
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<TransactionResponse> {
|
||||
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<Unit> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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<Transaction, Long> {
|
||||
Optional<Transaction> findByIdAndBudgetIn(Long id, List<Budget> budgets);
|
||||
|
||||
List<Transaction> findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
||||
List<Budget> budgets,
|
||||
List<Category> categories,
|
||||
Instant start,
|
||||
Instant end,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
List<Transaction> 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);
|
||||
}
|
|
@ -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<Transaction, Long> {
|
||||
fun findAllByIdAndBudgetIn(id: Long, budgets: List<Budget>): List<Transaction>
|
||||
fun findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
||||
budgets: List<Budget>,
|
||||
categories: List<Category>,
|
||||
start: Instant,
|
||||
end: Instant,
|
||||
pageable: Pageable? = null
|
||||
): List<Transaction>
|
||||
fun findAllByBudgetAndCategory(budget: Budget, category: Category): List<Transaction>
|
||||
@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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
91
api/src/main/java/com/wbrawner/budgetserver/user/User.java
Normal file
91
api/src/main/java/com/wbrawner/budgetserver/user/User.java
Normal file
|
@ -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<GrantedAuthority> 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<? extends GrantedAuthority> getAuthorities() {
|
||||
return authorities;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<out GrantedAuthority>
|
||||
= mutableListOf<GrantedAuthority>(SimpleGrantedAuthority("USER"))
|
||||
) : UserDetails {
|
||||
override fun getUsername(): String = name
|
||||
|
||||
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = 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)
|
|
@ -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<List<UserPermissionResponse>> 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<UserResponse> 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<UserResponse> 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<List<UserResponse>> 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<UserResponse> 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<Object> 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<Object> 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<Void> deleteUser(@PathVariable Long id) {
|
||||
if (!getCurrentUser().getId().equals(id)) return ResponseEntity.status(403).build();
|
||||
userRepository.deleteById(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
|
@ -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<List<UserPermissionResponse>> {
|
||||
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<UserResponse> {
|
||||
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<UserResponse> {
|
||||
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<List<UserResponse>> {
|
||||
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<UserResponse> = 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<Any> {
|
||||
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<Any> {
|
||||
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<Unit> {
|
||||
if(getCurrentUser()!!.id != id) return ResponseEntity.status(403).build()
|
||||
userRepository.deleteById(id)
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
|
@ -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<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
Optional<User> findByUsernameAndPassword(String username, String password);
|
||||
|
||||
List<User> findByUsernameContains(String username);
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package com.wbrawner.budgetserver.user
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository
|
||||
import java.util.*
|
||||
|
||||
interface UserRepository: PagingAndSortingRepository<User, Long> {
|
||||
fun findByName(username: String): Optional<User>
|
||||
fun findByNameAndPassphrase(username: String, passphrase: String): Optional<User>
|
||||
fun findByNameContains(username: String): List<User>
|
||||
fun findByEmail(email: String): Optional<User>
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue