Convert the rest of the codebase from Kotlin to Java

This commit is contained in:
William Brawner 2020-05-31 22:06:21 -07:00
parent 0b729bb34e
commit 284b4be6bd
59 changed files with 1829 additions and 1102 deletions

View file

@ -1,5 +1,4 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'application' apply plugin: 'application'
apply plugin: "io.spring.dependency-management" apply plugin: "io.spring.dependency-management"
apply plugin: "org.springframework.boot" apply plugin: "org.springframework.boot"
@ -17,9 +16,6 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-security" implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.session:spring-session-jdbc" implementation "org.springframework.session:spring-session-jdbc"
implementation "org.springframework.boot:spring-boot-starter-web" 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-swagger2:2.8.0"
implementation "io.springfox:springfox-swagger-ui:2.8.0" implementation "io.springfox:springfox-swagger-ui:2.8.0"
runtimeOnly "mysql:mysql-connector-java:8.0.15" runtimeOnly "mysql:mysql-connector-java:8.0.15"
@ -31,14 +27,7 @@ jar {
description = "twigs-server" description = "twigs-server"
} }
mainClassName = "com.wbrawner.budgetserver.BudgetServerApplicationKt" mainClassName = "com.wbrawner.budgetserver.BudgetServerApplication"
sourceCompatibility = 11
targetCompatibility = 11
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = '11'
}
}
sourceCompatibility = 14
targetCompatibility = 14

View file

@ -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);
}
}

View file

@ -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)
}
}
}

View file

@ -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;
}
}

View file

@ -1,3 +0,0 @@
package com.wbrawner.budgetserver
data class ErrorResponse(val message: String)

View file

@ -1,11 +1,14 @@
package com.wbrawner.budgetserver; package com.wbrawner.budgetserver;
import com.wbrawner.budgetserver.user.User;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
public final class Utils { public final class Utils {
private static int[] CALENDAR_FIELDS = new int[]{ private static final int[] CALENDAR_FIELDS = new int[]{
Calendar.MILLISECOND, Calendar.MILLISECOND,
Calendar.SECOND, Calendar.SECOND,
Calendar.MINUTE, Calendar.MINUTE,
@ -20,4 +23,21 @@ public final class Utils {
} }
return calendar.getTime(); 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;
}
} }

View file

@ -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))
}
}

View file

@ -10,6 +10,7 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Authorization; import io.swagger.annotations.Authorization;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -19,8 +20,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
import static com.wbrawner.budgetserver.Utils.getFirstOfMonth; import static com.wbrawner.budgetserver.Utils.getFirstOfMonth;
import static com.wbrawner.budgetserver.UtilsKt.getCurrentUser;
@RestController @RestController
@RequestMapping(value = "/budgets") @RequestMapping(value = "/budgets")
@ -81,7 +82,7 @@ public class BudgetController {
return ResponseEntity.status(401).build(); 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) { if (userPermission == null) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
@ -99,10 +100,10 @@ public class BudgetController {
public ResponseEntity<BudgetBalanceResponse> getBudgetBalance(@PathVariable long id) { public ResponseEntity<BudgetBalanceResponse> getBudgetBalance(@PathVariable long id) {
var user = getCurrentUser(); var user = getCurrentUser();
if (user == null) { 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) { if (userPermission == null) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
@ -115,7 +116,7 @@ public class BudgetController {
return ResponseEntity.ok(new BudgetBalanceResponse(budget.getId(), balance)); 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"}) @ApiOperation(value = "newBudget", nickname = "newBudget", tags = {"Budgets"})
public ResponseEntity<BudgetResponse> newBudget(@RequestBody BudgetRequest request) { public ResponseEntity<BudgetResponse> newBudget(@RequestBody BudgetRequest request) {
final var budget = budgetRepository.save(new Budget(request.name, request.description)); 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}) @PutMapping(value = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
@ApiOperation(value = "updateBudget", nickname = "updateBudget", tags = {"Budgets"}) @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(); var user = getCurrentUser();
if (user == null) { if (user == null) {
return ResponseEntity.status(401).build(); 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) { if (userPermission == null) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
if (userPermission.getPermission().isNotAtLeast(Permission.MANAGE)) { if (userPermission.getPermission().isNotAtLeast(Permission.MANAGE)) {
return ResponseEntity.status(403).build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
} }
var budget = userPermission.getBudget(); var budget = userPermission.getBudget();
@ -178,17 +179,16 @@ public class BudgetController {
var users = new ArrayList<UserPermission>(); var users = new ArrayList<UserPermission>();
if (!request.getUsers().isEmpty()) { if (!request.getUsers().isEmpty()) {
request.getUsers().forEach(userPermissionRequest -> { request.getUsers().forEach(userPermissionRequest ->
userRepository.findById(userPermissionRequest.getUser()).ifPresent(requestedUser -> userRepository.findById(userPermissionRequest.getUser()).ifPresent(requestedUser ->
users.add(userPermissionsRepository.save( users.add(userPermissionsRepository.save(
new UserPermission( new UserPermission(
budget, budget,
requestedUser, requestedUser,
userPermissionRequest.getPermission() userPermissionRequest.getPermission()
) )
)) ))
); ));
});
} else { } else {
users.addAll(userPermissionsRepository.findAllByBudget(budget, null)); users.addAll(userPermissionsRepository.findAllByBudget(budget, null));
} }
@ -204,7 +204,7 @@ public class BudgetController {
return ResponseEntity.status(401).build(); 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) { if (userPermission == null) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
@ -218,6 +218,6 @@ public class BudgetController {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
budgetRepository.delete(budget); budgetRepository.delete(budget);
return ResponseEntity.noContent().build(); return ResponseEntity.ok().build();
} }
} }

View file

@ -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()
}
}

View file

@ -3,6 +3,7 @@ package com.wbrawner.budgetserver.budget;
import com.wbrawner.budgetserver.permission.UserPermissionRequest; import com.wbrawner.budgetserver.permission.UserPermissionRequest;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -11,6 +12,11 @@ public class BudgetRequest {
public final String description; public final String description;
private final Set<UserPermissionRequest> users = new HashSet<>(); private final Set<UserPermissionRequest> users = new HashSet<>();
public BudgetRequest() {
// Required empty constructor
this("", "", Collections.emptySet());
}
public BudgetRequest(String name, String description, Set<UserPermissionRequest> users) { public BudgetRequest(String name, String description, Set<UserPermissionRequest> users) {
this.name = name; this.name = name;
this.description = description; this.description = description;

View file

@ -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;
}
}

View file

@ -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?
)

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -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")
}
}

View file

@ -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 {
}

View file

@ -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();
}
}

View file

@ -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()

View file

@ -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/");
}
}

View file

@ -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/")
}
}

View file

@ -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;
}
}

View file

@ -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("-", "")
)

View file

@ -0,0 +1,6 @@
package com.wbrawner.budgetserver.passwordresetrequest;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface PasswordResetRequestRepository extends PagingAndSortingRepository<PasswordResetRequest, Long> {
}

View file

@ -1,5 +0,0 @@
package com.wbrawner.budgetserver.passwordresetrequest
import org.springframework.data.repository.PagingAndSortingRepository
interface PasswordResetRequestRepository: PagingAndSortingRepository<PasswordResetRequest, Long>

View file

@ -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;
}
}

View file

@ -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
)

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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?
)

View file

@ -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();
}
}

View file

@ -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()
}
}

View file

@ -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);
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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;
}
}

View file

@ -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)

View file

@ -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();
}
}

View file

@ -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()
}
}

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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;
}
}

View file

@ -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() {
}
}

View file

@ -7,7 +7,6 @@ buildscript {
} }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.2.2.RELEASE" classpath "org.springframework.boot:spring-boot-gradle-plugin:2.2.2.RELEASE"
} }
} }