Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
eff486898f | |||
e84dedf675 | |||
c97c9b2c67 | |||
95ca0ec9fc | |||
89b1b6ccc7 | |||
09ad68a528 | |||
6f68065b95 | |||
41466a1134 | |||
808945cd78 |
64 changed files with 969 additions and 538 deletions
|
@ -1,31 +1,18 @@
|
||||||
apply plugin: 'java'
|
plugins {
|
||||||
apply plugin: 'application'
|
id 'java'
|
||||||
apply plugin: "io.spring.dependency-management"
|
id 'application'
|
||||||
apply plugin: "org.springframework.boot"
|
id "io.spring.dependency-management"
|
||||||
|
id "org.springframework.boot"
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
|
||||||
url = "http://repo.maven.apache.org/maven2"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':core')
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-security"
|
|
||||||
implementation "org.springframework.session:spring-session-jdbc"
|
|
||||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
runtimeOnly "mysql:mysql-connector-java:8.0.15"
|
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||||
testImplementation "org.springframework.security:spring-security-test:5.1.5.RELEASE"
|
testImplementation "org.springframework.security:spring-security-test"
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
sourceCompatibility = 17
|
||||||
description = "twigs-server"
|
targetCompatibility = 17
|
||||||
}
|
|
||||||
|
|
||||||
mainClassName = "com.wbrawner.budgetserver.TwigsServerApplication"
|
|
||||||
|
|
||||||
sourceCompatibility = 14
|
|
||||||
targetCompatibility = 14
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.wbrawner.budgetserver.budget;
|
|
||||||
|
|
||||||
public class BudgetBalanceResponse {
|
|
||||||
public final String id;
|
|
||||||
public final long balance;
|
|
||||||
|
|
||||||
public BudgetBalanceResponse(String id, long balance) {
|
|
||||||
this.id = id;
|
|
||||||
this.balance = balance;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package com.wbrawner.budgetserver.budget;
|
|
||||||
|
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
|
||||||
|
|
||||||
public interface BudgetRepository extends PagingAndSortingRepository<Budget, String> {
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
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 {
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
package com.wbrawner.budgetserver.config;
|
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.passwordresetrequest.PasswordResetRequestRepository;
|
|
||||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
|
||||||
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.config.http.SessionCreationPolicy;
|
|
||||||
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 UserSessionRepository userSessionRepository;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PasswordResetRequestRepository passwordResetRequestRepository;
|
|
||||||
private final JdbcUserDetailsService userDetailsService;
|
|
||||||
private final Environment environment;
|
|
||||||
|
|
||||||
public SecurityConfig(Environment env,
|
|
||||||
DataSource datasource,
|
|
||||||
UserSessionRepository userSessionRepository,
|
|
||||||
UserRepository userRepository,
|
|
||||||
PasswordResetRequestRepository passwordResetRequestRepository,
|
|
||||||
JdbcUserDetailsService userDetailsService,
|
|
||||||
Environment environment) {
|
|
||||||
this.env = env;
|
|
||||||
this.datasource = datasource;
|
|
||||||
this.userSessionRepository = userSessionRepository;
|
|
||||||
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 TokenAuthenticationProvider(userSessionRepository, userRepository);
|
|
||||||
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()
|
|
||||||
.authenticationEntryPoint(new SilentAuthenticationEntryPoint())
|
|
||||||
.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())
|
|
||||||
);
|
|
||||||
corsConfig.setAllowCredentials(true);
|
|
||||||
return corsConfig;
|
|
||||||
})
|
|
||||||
.and()
|
|
||||||
.csrf()
|
|
||||||
.disable()
|
|
||||||
.addFilter(new TokenAuthenticationFilter(authenticationManager()))
|
|
||||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
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, String> {
|
|
||||||
Optional<Transaction> findByIdAndBudgetIn(String id, List<Budget> budgets);
|
|
||||||
|
|
||||||
List<Transaction> findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
|
||||||
List<Budget> budgets,
|
|
||||||
List<Category> categories,
|
|
||||||
Instant start,
|
|
||||||
Instant end,
|
|
||||||
Pageable pageable
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Transaction> findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
|
||||||
List<Budget> budgets,
|
|
||||||
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 >= :from AND date <= :to), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 1 AND date >= :from AND date <= :to), 0));"
|
|
||||||
)
|
|
||||||
Long sumBalanceByBudgetId(String BudgetId, Instant from, Instant to);
|
|
||||||
|
|
||||||
@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(String categoryId, Date start);
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver;
|
package com.wbrawner.twigs;
|
||||||
|
|
||||||
public class ErrorResponse {
|
public class ErrorResponse {
|
||||||
private final String message;
|
private final String message;
|
|
@ -1,11 +1,13 @@
|
||||||
package com.wbrawner.budgetserver.budget;
|
package com.wbrawner.twigs.budget;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.permission.Permission;
|
import com.wbrawner.twigs.category.CategoryRepository;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermission;
|
import com.wbrawner.twigs.permission.Permission;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
import com.wbrawner.twigs.permission.UserPermission;
|
||||||
import com.wbrawner.budgetserver.transaction.TransactionRepository;
|
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.twigs.transaction.TransactionRepository;
|
||||||
import com.wbrawner.budgetserver.user.UserRepository;
|
import com.wbrawner.twigs.user.User;
|
||||||
|
import com.wbrawner.twigs.user.UserRepository;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
@ -14,20 +16,20 @@ 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.*;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
import static com.wbrawner.twigs.Utils.getCurrentUser;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = "/budgets")
|
@RequestMapping(value = "/api/budgets")
|
||||||
@Transactional
|
@Transactional
|
||||||
public class BudgetController {
|
public class BudgetController {
|
||||||
private final BudgetRepository budgetRepository;
|
private final BudgetRepository budgetRepository;
|
||||||
|
private final CategoryRepository categoryRepository;
|
||||||
private final TransactionRepository transactionRepository;
|
private final TransactionRepository transactionRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final UserPermissionRepository userPermissionsRepository;
|
private final UserPermissionRepository userPermissionsRepository;
|
||||||
|
@ -35,11 +37,13 @@ public class BudgetController {
|
||||||
|
|
||||||
public BudgetController(
|
public BudgetController(
|
||||||
BudgetRepository budgetRepository,
|
BudgetRepository budgetRepository,
|
||||||
|
CategoryRepository categoryRepository,
|
||||||
TransactionRepository transactionRepository,
|
TransactionRepository transactionRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
UserPermissionRepository userPermissionsRepository
|
UserPermissionRepository userPermissionsRepository
|
||||||
) {
|
) {
|
||||||
this.budgetRepository = budgetRepository;
|
this.budgetRepository = budgetRepository;
|
||||||
|
this.categoryRepository = categoryRepository;
|
||||||
this.transactionRepository = transactionRepository;
|
this.transactionRepository = transactionRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.userPermissionsRepository = userPermissionsRepository;
|
this.userPermissionsRepository = userPermissionsRepository;
|
||||||
|
@ -53,12 +57,12 @@ public class BudgetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<BudgetResponse> budgets = userPermissionsRepository.findAllByUser(
|
List<BudgetResponse> budgets = userPermissionsRepository.findAllByUser(
|
||||||
getCurrentUser(),
|
getCurrentUser(),
|
||||||
PageRequest.of(
|
PageRequest.of(
|
||||||
page != null ? page : 0,
|
page != null ? page : 0,
|
||||||
count != null ? count : 1000
|
count != null ? count : 1000
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.stream()
|
.stream()
|
||||||
.map(userPermission -> {
|
.map(userPermission -> {
|
||||||
Budget budget = userPermission.getBudget();
|
Budget budget = userPermission.getBudget();
|
||||||
|
@ -80,35 +84,11 @@ public class BudgetController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/{id}/balance", produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(
|
||||||
public ResponseEntity<BudgetBalanceResponse> getBudgetBalance(
|
value = "",
|
||||||
@PathVariable String id,
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
@RequestParam(value = "from", required = false) String from,
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
@RequestParam(value = "to", required = false) String to
|
)
|
||||||
) {
|
|
||||||
return getBudgetWithPermission(id, Permission.READ, (budget) -> {
|
|
||||||
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 = Instant.ofEpochSecond(0);
|
|
||||||
}
|
|
||||||
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 = Instant.now();
|
|
||||||
}
|
|
||||||
var balance = transactionRepository.sumBalanceByBudgetId(budget.getId(), fromInstant, toInstant);
|
|
||||||
return ResponseEntity.ok(new BudgetBalanceResponse(budget.getId(), balance));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(value = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
|
||||||
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));
|
||||||
var users = request.getUsers()
|
var users = request.getUsers()
|
||||||
|
@ -138,8 +118,13 @@ public class BudgetController {
|
||||||
return ResponseEntity.ok(new BudgetResponse(budget, new ArrayList<>(users)));
|
return ResponseEntity.ok(new BudgetResponse(budget, new ArrayList<>(users)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@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}
|
||||||
|
)
|
||||||
public ResponseEntity<BudgetResponse> updateBudget(@PathVariable String id, @RequestBody BudgetRequest request) {
|
public ResponseEntity<BudgetResponse> updateBudget(@PathVariable String id, @RequestBody BudgetRequest request) {
|
||||||
|
// TODO: Make sure no changes in ownership are being attempted (except by the owner)
|
||||||
return getBudgetWithPermission(id, Permission.MANAGE, (budget) -> {
|
return getBudgetWithPermission(id, Permission.MANAGE, (budget) -> {
|
||||||
if (request.name != null) {
|
if (request.name != null) {
|
||||||
budget.setName(request.name);
|
budget.setName(request.name);
|
||||||
|
@ -161,7 +146,8 @@ public class BudgetController {
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
));
|
));
|
||||||
} else {
|
}
|
||||||
|
if (users.isEmpty()) {
|
||||||
users.addAll(userPermissionsRepository.findAllByBudget(budget, null));
|
users.addAll(userPermissionsRepository.findAllByBudget(budget, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,11 +155,14 @@ public class BudgetController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping(value = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
@DeleteMapping(value = "/{id}")
|
||||||
public ResponseEntity<Void> deleteBudget(@PathVariable String id) {
|
public ResponseEntity<Void> deleteBudget(@PathVariable String id) {
|
||||||
return getBudgetWithPermission(id, Permission.MANAGE, (budget) -> {
|
return getBudgetWithPermission(id, Permission.MANAGE, (budget) -> {
|
||||||
|
categoryRepository.deleteAllByBudget(budget);
|
||||||
|
transactionRepository.deleteAllByBudget(budget);
|
||||||
|
userPermissionsRepository.deleteAllByBudget(budget);
|
||||||
budgetRepository.delete(budget);
|
budgetRepository.delete(budget);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.noContent().build();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package com.wbrawner.budgetserver.budget;
|
package com.wbrawner.twigs.budget;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionRequest;
|
import com.wbrawner.twigs.permission.UserPermissionRequest;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -23,7 +22,6 @@ public class BudgetRequest {
|
||||||
this.users.addAll(users);
|
this.users.addAll(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public Set<UserPermissionRequest> getUsers() {
|
public Set<UserPermissionRequest> getUsers() {
|
||||||
return Set.copyOf(users);
|
return Set.copyOf(users);
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
package com.wbrawner.budgetserver.budget;
|
package com.wbrawner.twigs.budget;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.permission.UserPermission;
|
import com.wbrawner.twigs.permission.UserPermission;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionResponse;
|
import com.wbrawner.twigs.permission.UserPermissionResponse;
|
||||||
import com.wbrawner.budgetserver.user.User;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
|
@ -1,10 +1,11 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.category;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.ErrorResponse;
|
import com.wbrawner.twigs.ErrorResponse;
|
||||||
import com.wbrawner.budgetserver.permission.Permission;
|
import com.wbrawner.twigs.permission.Permission;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermission;
|
import com.wbrawner.twigs.permission.UserPermission;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||||
import com.wbrawner.budgetserver.transaction.TransactionRepository;
|
import com.wbrawner.twigs.transaction.TransactionRepository;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -12,24 +13,24 @@ 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.*;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
|
||||||
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.twigs.Utils.getCurrentUser;
|
||||||
import static com.wbrawner.budgetserver.Utils.getFirstOfMonth;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(path = "/categories")
|
@RequestMapping(path = "/api/categories")
|
||||||
@Transactional
|
@Transactional
|
||||||
class CategoryController {
|
class CategoryController {
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
private final TransactionRepository transactionRepository;
|
private final TransactionRepository transactionRepository;
|
||||||
private final UserPermissionRepository userPermissionsRepository;
|
private final UserPermissionRepository userPermissionsRepository;
|
||||||
|
|
||||||
CategoryController(CategoryRepository categoryRepository,
|
CategoryController(
|
||||||
TransactionRepository transactionRepository,
|
CategoryRepository categoryRepository,
|
||||||
UserPermissionRepository userPermissionsRepository) {
|
TransactionRepository transactionRepository,
|
||||||
|
UserPermissionRepository userPermissionsRepository
|
||||||
|
) {
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
this.transactionRepository = transactionRepository;
|
this.transactionRepository = transactionRepository;
|
||||||
this.userPermissionsRepository = userPermissionsRepository;
|
this.userPermissionsRepository = userPermissionsRepository;
|
||||||
|
@ -76,17 +77,6 @@ class CategoryController {
|
||||||
|
|
||||||
@GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE})
|
@GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
ResponseEntity<CategoryResponse> getCategory(@PathVariable String id) {
|
ResponseEntity<CategoryResponse> getCategory(@PathVariable String 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})
|
|
||||||
ResponseEntity<CategoryBalanceResponse> getCategoryBalance(@PathVariable String id) {
|
|
||||||
var budgets = userPermissionsRepository.findAllByUser(getCurrentUser(), null)
|
var budgets = userPermissionsRepository.findAllByUser(getCurrentUser(), null)
|
||||||
.stream()
|
.stream()
|
||||||
.map(UserPermission::getBudget)
|
.map(UserPermission::getBudget)
|
||||||
|
@ -95,11 +85,14 @@ class CategoryController {
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
var sum = transactionRepository.sumBalanceByCategoryId(category.getId(), getFirstOfMonth());
|
return ResponseEntity.ok(new CategoryResponse(category));
|
||||||
return ResponseEntity.ok(new CategoryBalanceResponse(category.getId(), sum));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(
|
||||||
|
path = "",
|
||||||
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
|
)
|
||||||
ResponseEntity<Object> newCategory(@RequestBody NewCategoryRequest request) {
|
ResponseEntity<Object> newCategory(@RequestBody NewCategoryRequest request) {
|
||||||
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
@ -119,12 +112,25 @@ class CategoryController {
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PutMapping(
|
||||||
ResponseEntity<CategoryResponse> updateCategory(@PathVariable String id, @RequestBody UpdateCategoryRequest request) {
|
path = "/{id}",
|
||||||
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
|
)
|
||||||
|
ResponseEntity<CategoryResponse> updateCategory(
|
||||||
|
@PathVariable String id, @RequestBody UpdateCategoryRequest request
|
||||||
|
) {
|
||||||
var category = categoryRepository.findById(id).orElse(null);
|
var category = categoryRepository.findById(id).orElse(null);
|
||||||
if (category == null) return ResponseEntity.notFound().build();
|
if (category == null) {
|
||||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), category.getBudget().getId()).orElse(null);
|
return ResponseEntity.notFound().build();
|
||||||
if (userPermission == 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)) {
|
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
|
@ -149,9 +155,16 @@ class CategoryController {
|
||||||
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||||
ResponseEntity<Void> deleteCategory(@PathVariable String id) {
|
ResponseEntity<Void> deleteCategory(@PathVariable String id) {
|
||||||
var category = categoryRepository.findById(id).orElse(null);
|
var category = categoryRepository.findById(id).orElse(null);
|
||||||
if (category == null) return ResponseEntity.notFound().build();
|
if (category == null) {
|
||||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), category.getBudget().getId()).orElse(null);
|
return ResponseEntity.notFound().build();
|
||||||
if (userPermission == 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)) {
|
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.category;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -23,7 +23,9 @@ public class CategoryResponse {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CategoryResponse(String id, String title, String description, long amount, String budgetId, boolean expense, boolean archived) {
|
public CategoryResponse(
|
||||||
|
String id, String title, String description, long amount, String budgetId, boolean expense, boolean archived
|
||||||
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.category;
|
||||||
|
|
||||||
public class NewCategoryRequest {
|
public class NewCategoryRequest {
|
||||||
private final String title;
|
private final String title;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.category;
|
||||||
|
|
||||||
public class UpdateCategoryRequest {
|
public class UpdateCategoryRequest {
|
||||||
private final String title;
|
private final String title;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.permission;
|
package com.wbrawner.twigs.permission;
|
||||||
|
|
||||||
public class UserPermissionRequest {
|
public class UserPermissionRequest {
|
||||||
private final String user;
|
private final String user;
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.wbrawner.twigs.permission;
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.user.User;
|
||||||
|
|
||||||
|
public class UserPermissionResponse {
|
||||||
|
private final String user;
|
||||||
|
private final Permission permission;
|
||||||
|
|
||||||
|
public UserPermissionResponse(UserPermission userPermission) {
|
||||||
|
this(userPermission.getUser(), userPermission.getPermission());
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPermissionResponse(User user, Permission permission) {
|
||||||
|
this.user = user.getId();
|
||||||
|
this.permission = permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permission getPermission() {
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,27 @@
|
||||||
package com.wbrawner.budgetserver.session;
|
package com.wbrawner.twigs.session;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class SessionResponse {
|
public class SessionResponse {
|
||||||
|
private final String userId;
|
||||||
|
|
||||||
private final String token;
|
private final String token;
|
||||||
private final String expiration;
|
private final String expiration;
|
||||||
|
|
||||||
public SessionResponse(Session session) {
|
public SessionResponse(Session session) {
|
||||||
this(session.getToken(), session.getExpiration());
|
this(session.getUserId(), session.getToken(), session.getExpiration());
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionResponse(String token, Date expiration) {
|
public SessionResponse(String userId, String token, Date expiration) {
|
||||||
|
this.userId = userId;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.expiration = expiration.toInstant().toString();
|
this.expiration = expiration.toInstant().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
public class CategoryBalanceResponse {
|
public class BalanceResponse {
|
||||||
private final String id;
|
private final String id;
|
||||||
private final long balance;
|
private final long balance;
|
||||||
|
|
||||||
public CategoryBalanceResponse(String id, long balance) {
|
public BalanceResponse(String id, long balance) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.balance = balance;
|
this.balance = balance;
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.transaction;
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
class NewTransactionRequest {
|
class NewTransactionRequest {
|
||||||
private final String title;
|
private final String title;
|
||||||
|
@ -13,7 +13,10 @@ class NewTransactionRequest {
|
||||||
this(null, null, null, null, null, null, null);
|
this(null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
NewTransactionRequest(String title, String description, String date, Long amount, String categoryId, Boolean expense, String budgetId) {
|
NewTransactionRequest(
|
||||||
|
String title, String description, String date, Long amount, String categoryId, Boolean expense,
|
||||||
|
String budgetId
|
||||||
|
) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.date = date;
|
this.date = date;
|
|
@ -1,11 +1,12 @@
|
||||||
package com.wbrawner.budgetserver.transaction;
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.ErrorResponse;
|
import com.wbrawner.twigs.ErrorResponse;
|
||||||
import com.wbrawner.budgetserver.category.Category;
|
import com.wbrawner.twigs.category.Category;
|
||||||
import com.wbrawner.budgetserver.category.CategoryRepository;
|
import com.wbrawner.twigs.category.CategoryRepository;
|
||||||
import com.wbrawner.budgetserver.permission.Permission;
|
import com.wbrawner.twigs.permission.Permission;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermission;
|
import com.wbrawner.twigs.permission.UserPermission;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
@ -15,15 +16,16 @@ 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.*;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.*;
|
import static com.wbrawner.twigs.Utils.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(path = "/transactions")
|
@RequestMapping(path = "/api/transactions")
|
||||||
@Transactional
|
@Transactional
|
||||||
public class TransactionController {
|
public class TransactionController {
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
|
@ -32,9 +34,11 @@ public class TransactionController {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(TransactionController.class);
|
private final Logger logger = LoggerFactory.getLogger(TransactionController.class);
|
||||||
|
|
||||||
public TransactionController(CategoryRepository categoryRepository,
|
public TransactionController(
|
||||||
TransactionRepository transactionRepository,
|
CategoryRepository categoryRepository,
|
||||||
UserPermissionRepository userPermissionsRepository) {
|
TransactionRepository transactionRepository,
|
||||||
|
UserPermissionRepository userPermissionsRepository
|
||||||
|
) {
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
this.transactionRepository = transactionRepository;
|
this.transactionRepository = transactionRepository;
|
||||||
this.userPermissionsRepository = userPermissionsRepository;
|
this.userPermissionsRepository = userPermissionsRepository;
|
||||||
|
@ -79,16 +83,18 @@ public class TransactionController {
|
||||||
try {
|
try {
|
||||||
fromInstant = Instant.parse(from);
|
fromInstant = Instant.parse(from);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!(e instanceof NullPointerException))
|
if (!(e instanceof NullPointerException)) {
|
||||||
logger.error("Failed to parse '" + from + "' to Instant for 'from' parameter", e);
|
logger.error("Failed to parse '" + from + "' to Instant for 'from' parameter", e);
|
||||||
|
}
|
||||||
fromInstant = getFirstOfMonth().toInstant();
|
fromInstant = getFirstOfMonth().toInstant();
|
||||||
}
|
}
|
||||||
Instant toInstant;
|
Instant toInstant;
|
||||||
try {
|
try {
|
||||||
toInstant = Instant.parse(to);
|
toInstant = Instant.parse(to);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!(e instanceof NullPointerException))
|
if (!(e instanceof NullPointerException)) {
|
||||||
logger.error("Failed to parse '" + to + "' to Instant for 'to' parameter", e);
|
logger.error("Failed to parse '" + to + "' to Instant for 'to' parameter", e);
|
||||||
|
}
|
||||||
toInstant = getEndOfMonth().toInstant();
|
toInstant = getEndOfMonth().toInstant();
|
||||||
}
|
}
|
||||||
var query = categories == null ? transactionRepository.findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
var query = categories == null ? transactionRepository.findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
||||||
|
@ -117,11 +123,17 @@ public class TransactionController {
|
||||||
.map(UserPermission::getBudget)
|
.map(UserPermission::getBudget)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
var transaction = transactionRepository.findByIdAndBudgetIn(id, budgets).orElse(null);
|
var transaction = transactionRepository.findByIdAndBudgetIn(id, budgets).orElse(null);
|
||||||
if (transaction == null) return ResponseEntity.notFound().build();
|
if (transaction == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
return ResponseEntity.ok(new TransactionResponse(transaction));
|
return ResponseEntity.ok(new TransactionResponse(transaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(
|
||||||
|
path = "",
|
||||||
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
|
)
|
||||||
public ResponseEntity<Object> newTransaction(@RequestBody NewTransactionRequest request) {
|
public ResponseEntity<Object> newTransaction(@RequestBody NewTransactionRequest request) {
|
||||||
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
@ -148,12 +160,25 @@ public class TransactionController {
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PutMapping(
|
||||||
public ResponseEntity<Object> updateTransaction(@PathVariable String id, @RequestBody UpdateTransactionRequest request) {
|
path = "/{id}",
|
||||||
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
|
)
|
||||||
|
public ResponseEntity<Object> updateTransaction(
|
||||||
|
@PathVariable String id, @RequestBody UpdateTransactionRequest request
|
||||||
|
) {
|
||||||
var transaction = transactionRepository.findById(id).orElse(null);
|
var transaction = transactionRepository.findById(id).orElse(null);
|
||||||
if (transaction == null) return ResponseEntity.notFound().build();
|
if (transaction == null) {
|
||||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null);
|
return ResponseEntity.notFound().build();
|
||||||
if (userPermission == 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)) {
|
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
|
@ -173,7 +198,10 @@ public class TransactionController {
|
||||||
transaction.setExpense(request.getExpense());
|
transaction.setExpense(request.getExpense());
|
||||||
}
|
}
|
||||||
if (request.getBudgetId() != null) {
|
if (request.getBudgetId() != null) {
|
||||||
var newUserPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId()).orElse(null);
|
var newUserPermission = userPermissionsRepository.findByUserAndBudget_Id(
|
||||||
|
getCurrentUser(),
|
||||||
|
request.getBudgetId()
|
||||||
|
).orElse(null);
|
||||||
if (newUserPermission == null || newUserPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
if (newUserPermission == null || newUserPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.badRequest()
|
.badRequest()
|
||||||
|
@ -182,7 +210,8 @@ public class TransactionController {
|
||||||
transaction.setBudget(newUserPermission.getBudget());
|
transaction.setBudget(newUserPermission.getBudget());
|
||||||
}
|
}
|
||||||
if (request.getCategoryId() != null) {
|
if (request.getCategoryId() != null) {
|
||||||
var category = categoryRepository.findByBudgetAndId(transaction.getBudget(), request.getCategoryId()).orElse(null);
|
var category = categoryRepository.findByBudgetAndId(transaction.getBudget(), request.getCategoryId())
|
||||||
|
.orElse(null);
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.badRequest()
|
.badRequest()
|
||||||
|
@ -196,14 +225,111 @@ public class TransactionController {
|
||||||
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||||
public ResponseEntity<Void> deleteTransaction(@PathVariable String id) {
|
public ResponseEntity<Void> deleteTransaction(@PathVariable String id) {
|
||||||
var transaction = transactionRepository.findById(id).orElse(null);
|
var transaction = transactionRepository.findById(id).orElse(null);
|
||||||
if (transaction == null) return ResponseEntity.notFound().build();
|
if (transaction == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
// Check that the transaction belongs to an budget that the user has access to before deleting it
|
// 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);
|
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(
|
||||||
if (userPermission == null) return ResponseEntity.notFound().build();
|
getCurrentUser(),
|
||||||
|
transaction.getBudget().getId()
|
||||||
|
).orElse(null);
|
||||||
|
if (userPermission == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
transactionRepository.delete(transaction);
|
transactionRepository.delete(transaction);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/sum", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
public ResponseEntity<BalanceResponse> getSum(
|
||||||
|
@RequestParam(value = "budgetId", required = false) String budgetId,
|
||||||
|
@RequestParam(value = "categoryId", required = false) String categoryId,
|
||||||
|
@RequestParam(value = "from", required = false) String from,
|
||||||
|
@RequestParam(value = "to", required = false) String to
|
||||||
|
) {
|
||||||
|
var user = getCurrentUser();
|
||||||
|
if (user == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Instant.ofEpochSecond(0);
|
||||||
|
}
|
||||||
|
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 = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((budgetId == null || budgetId.isBlank())
|
||||||
|
&& (categoryId == null || categoryId.isBlank()))
|
||||||
|
|| (budgetId != null && !budgetId.isEmpty() && categoryId != null && !categoryId.isBlank())) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Transaction> transactions;
|
||||||
|
if (categoryId != null) {
|
||||||
|
var budgets = userPermissionsRepository.findAllByUser(user, null)
|
||||||
|
.stream()
|
||||||
|
.map(UserPermission::getBudget)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
var category = categoryRepository.findByBudgetInAndId(budgets, categoryId).orElse(null);
|
||||||
|
if (category == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
transactions = transactionRepository.findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
||||||
|
List.of(category.getBudget()),
|
||||||
|
List.of(category),
|
||||||
|
fromInstant,
|
||||||
|
toInstant,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
AtomicLong balance = new AtomicLong(0L);
|
||||||
|
transactions.forEach(transaction -> {
|
||||||
|
if (transaction.getExpense()) {
|
||||||
|
balance.addAndGet(transaction.getAmount() * -1);
|
||||||
|
} else {
|
||||||
|
balance.addAndGet(transaction.getAmount());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok(new BalanceResponse(categoryId, balance.get()));
|
||||||
|
} else {
|
||||||
|
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(user, budgetId).orElse(null);
|
||||||
|
if (userPermission == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPermission.getPermission().isNotAtLeast(Permission.READ)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
transactions = transactionRepository.findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
||||||
|
List.of(userPermission.getBudget()),
|
||||||
|
fromInstant,
|
||||||
|
toInstant,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
AtomicLong balance = new AtomicLong(0L);
|
||||||
|
transactions.forEach(transaction -> {
|
||||||
|
if (transaction.getExpense()) {
|
||||||
|
balance.addAndGet(transaction.getAmount() * -1);
|
||||||
|
} else {
|
||||||
|
balance.addAndGet(transaction.getAmount());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok(new BalanceResponse(budgetId, balance.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.transaction;
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
class TransactionResponse {
|
class TransactionResponse {
|
||||||
private final String id;
|
private final String id;
|
||||||
|
@ -11,15 +11,17 @@ class TransactionResponse {
|
||||||
private final String categoryId;
|
private final String categoryId;
|
||||||
private final String createdBy;
|
private final String createdBy;
|
||||||
|
|
||||||
TransactionResponse(String id,
|
TransactionResponse(
|
||||||
String title,
|
String id,
|
||||||
String description,
|
String title,
|
||||||
String date,
|
String description,
|
||||||
Long amount,
|
String date,
|
||||||
Boolean expense,
|
Long amount,
|
||||||
String budgetId,
|
Boolean expense,
|
||||||
String categoryId,
|
String budgetId,
|
||||||
String createdBy) {
|
String categoryId,
|
||||||
|
String createdBy
|
||||||
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.transaction;
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
class UpdateTransactionRequest {
|
class UpdateTransactionRequest {
|
||||||
private final String title;
|
private final String title;
|
||||||
|
@ -14,7 +14,10 @@ class UpdateTransactionRequest {
|
||||||
this(null, null, null, null, null, null, null, null);
|
this(null, null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateTransactionRequest(String title, String description, String date, Long amount, String categoryId, Boolean expense, String budgetId, String createdBy) {
|
UpdateTransactionRequest(
|
||||||
|
String title, String description, String date, Long amount, String categoryId, Boolean expense,
|
||||||
|
String budgetId, String createdBy
|
||||||
|
) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.date = date;
|
this.date = date;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
private final String username;
|
private final String username;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
public class NewUserRequest {
|
public class NewUserRequest {
|
||||||
private final String username;
|
private final String username;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
public class UpdateUserRequest {
|
public class UpdateUserRequest {
|
||||||
private final String username;
|
private final String username;
|
|
@ -1,13 +1,15 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.ErrorResponse;
|
import com.wbrawner.twigs.ErrorResponse;
|
||||||
import com.wbrawner.budgetserver.budget.BudgetRepository;
|
import com.wbrawner.twigs.budget.BudgetRepository;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionResponse;
|
import com.wbrawner.twigs.permission.UserPermissionResponse;
|
||||||
import com.wbrawner.budgetserver.session.Session;
|
import com.wbrawner.twigs.session.Session;
|
||||||
import com.wbrawner.budgetserver.session.SessionResponse;
|
import com.wbrawner.twigs.session.SessionResponse;
|
||||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
import com.wbrawner.twigs.session.UserSessionRepository;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
@ -18,15 +20,14 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
import static com.wbrawner.twigs.Utils.getCurrentUser;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/users")
|
@RequestMapping("/api/users")
|
||||||
@Transactional
|
@Transactional
|
||||||
public class UserController {
|
public class UserController {
|
||||||
private final BudgetRepository budgetRepository;
|
private final BudgetRepository budgetRepository;
|
||||||
|
@ -34,20 +35,24 @@ public class UserController {
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final UserPermissionRepository userPermissionsRepository;
|
private final UserPermissionRepository userPermissionsRepository;
|
||||||
private final UserSessionRepository userSessionRepository;
|
private final UserSessionRepository userSessionRepository;
|
||||||
|
private final UserService userService;
|
||||||
private final DaoAuthenticationProvider authenticationProvider;
|
private final DaoAuthenticationProvider authenticationProvider;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public UserController(BudgetRepository budgetRepository,
|
public UserController(
|
||||||
UserRepository userRepository,
|
BudgetRepository budgetRepository,
|
||||||
UserSessionRepository userSessionRepository,
|
UserRepository userRepository,
|
||||||
PasswordEncoder passwordEncoder,
|
UserSessionRepository userSessionRepository,
|
||||||
UserPermissionRepository userPermissionsRepository,
|
PasswordEncoder passwordEncoder,
|
||||||
DaoAuthenticationProvider authenticationProvider) {
|
UserPermissionRepository userPermissionsRepository,
|
||||||
|
UserService userService, DaoAuthenticationProvider authenticationProvider
|
||||||
|
) {
|
||||||
this.budgetRepository = budgetRepository;
|
this.budgetRepository = budgetRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.userSessionRepository = userSessionRepository;
|
this.userSessionRepository = userSessionRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.userPermissionsRepository = userPermissionsRepository;
|
this.userPermissionsRepository = userPermissionsRepository;
|
||||||
|
this.userService = userService;
|
||||||
this.authenticationProvider = authenticationProvider;
|
this.authenticationProvider = authenticationProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,28 +71,28 @@ public class UserController {
|
||||||
if (!userInBudget) {
|
if (!userInBudget) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(userPermissions.stream().map(UserPermissionResponse::new).collect(Collectors.toList()));
|
return ResponseEntity.ok(userPermissions.stream()
|
||||||
|
.map(UserPermissionResponse::new)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/login", produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(path = "/login", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
ResponseEntity<SessionResponse> login(@RequestBody LoginRequest request) {
|
ResponseEntity<SessionResponse> login(@RequestBody LoginRequest request) {
|
||||||
var authReq = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword());
|
|
||||||
Authentication auth;
|
|
||||||
try {
|
try {
|
||||||
auth = authenticationProvider.authenticate(authReq);
|
return ResponseEntity.ok(new SessionResponse(
|
||||||
|
userService.login(request.getUsername(), request.getPassword())
|
||||||
|
));
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
}
|
}
|
||||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
||||||
var user = Objects.requireNonNull(getCurrentUser());
|
|
||||||
var session = userSessionRepository.save(new Session(user.getId()));
|
|
||||||
return ResponseEntity.ok(new SessionResponse(session));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/me", produces = {MediaType.APPLICATION_JSON_VALUE})
|
@GetMapping(path = "/me", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
ResponseEntity<UserResponse> getProfile() {
|
ResponseEntity<UserResponse> getProfile() {
|
||||||
var user = getCurrentUser();
|
var user = getCurrentUser();
|
||||||
if (user == null) return ResponseEntity.status(401).build();
|
if (user == null) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
return ResponseEntity.ok(new UserResponse(user));
|
return ResponseEntity.ok(new UserResponse(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +115,21 @@ public class UserController {
|
||||||
return ResponseEntity.ok(new UserResponse(user));
|
return ResponseEntity.ok(new UserResponse(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(
|
||||||
|
path = "/register",
|
||||||
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
|
)
|
||||||
ResponseEntity<Object> newUser(@RequestBody NewUserRequest request) {
|
ResponseEntity<Object> newUser(@RequestBody NewUserRequest request) {
|
||||||
if (userRepository.findByUsername(request.getUsername()).isPresent())
|
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(new ErrorResponse("Username taken"));
|
return ResponseEntity.badRequest().body(new ErrorResponse("Username taken"));
|
||||||
if (userRepository.findByEmail(request.getEmail()).isPresent())
|
}
|
||||||
|
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(new ErrorResponse("Email taken"));
|
return ResponseEntity.badRequest().body(new ErrorResponse("Email taken"));
|
||||||
if (request.getPassword().isBlank())
|
}
|
||||||
|
if (request.getPassword().isBlank()) {
|
||||||
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password"));
|
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password"));
|
||||||
|
}
|
||||||
return ResponseEntity.ok(new UserResponse(userRepository.save(new User(
|
return ResponseEntity.ok(new UserResponse(userRepository.save(new User(
|
||||||
request.getUsername(),
|
request.getUsername(),
|
||||||
passwordEncoder.encode(request.getPassword()),
|
passwordEncoder.encode(request.getPassword()),
|
||||||
|
@ -125,24 +137,36 @@ public class UserController {
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PutMapping(
|
||||||
ResponseEntity<Object> updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest request) {
|
path = "/{id}",
|
||||||
if (!getCurrentUser().getId().equals(id)) return ResponseEntity.status(403).build();
|
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||||
|
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||||
|
)
|
||||||
|
ResponseEntity<Object> updateUser(@PathVariable String id, @RequestBody UpdateUserRequest request) {
|
||||||
|
var currentUser = getCurrentUser();
|
||||||
|
if (currentUser == null || !currentUser.getId().equals(id)) {
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
var user = userRepository.findById(getCurrentUser().getId()).orElse(null);
|
var user = userRepository.findById(getCurrentUser().getId()).orElse(null);
|
||||||
if (user == null) return ResponseEntity.notFound().build();
|
if (user == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
if (request.getUsername() != null) {
|
if (request.getUsername() != null) {
|
||||||
if (userRepository.findByUsername(request.getUsername()).isPresent())
|
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(new ErrorResponse("Username taken"));
|
return ResponseEntity.badRequest().body(new ErrorResponse("Username taken"));
|
||||||
|
}
|
||||||
user.setUsername(request.getUsername());
|
user.setUsername(request.getUsername());
|
||||||
}
|
}
|
||||||
if (request.getEmail() != null) {
|
if (request.getEmail() != null) {
|
||||||
if (userRepository.findByEmail(request.getEmail()).isPresent())
|
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(new ErrorResponse("Email taken"));
|
return ResponseEntity.badRequest().body(new ErrorResponse("Email taken"));
|
||||||
|
}
|
||||||
user.setEmail(request.getEmail());
|
user.setEmail(request.getEmail());
|
||||||
}
|
}
|
||||||
if (request.getPassword() != null) {
|
if (request.getPassword() != null) {
|
||||||
if (request.getPassword().isBlank())
|
if (request.getPassword().isBlank()) {
|
||||||
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password"));
|
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password"));
|
||||||
|
}
|
||||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(new UserResponse(userRepository.save(user)));
|
return ResponseEntity.ok(new UserResponse(userRepository.save(user)));
|
||||||
|
@ -150,7 +174,10 @@ public class UserController {
|
||||||
|
|
||||||
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||||
ResponseEntity<Void> deleteUser(@PathVariable String id) {
|
ResponseEntity<Void> deleteUser(@PathVariable String id) {
|
||||||
if (!getCurrentUser().getId().equals(id)) return ResponseEntity.status(403).build();
|
var currentUser = getCurrentUser();
|
||||||
|
if (currentUser == null || !currentUser.getId().equals(id)) {
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
userRepository.deleteById(id);
|
userRepository.deleteById(id);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
public class UserResponse {
|
public class UserResponse {
|
||||||
private final String id;
|
private final String id;
|
|
@ -1,10 +1,6 @@
|
||||||
spring.jpa.hibernate.ddl-auto=none
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.datasource.url=jdbc:mysql://localhost:3306/budget
|
spring.datasource.url=jdbc:h2:./twigs.db
|
||||||
spring.datasource.username=budget
|
|
||||||
spring.datasource.password=budget
|
|
||||||
spring.profiles.active=prod
|
spring.profiles.active=prod
|
||||||
spring.session.jdbc.initialize-schema=always
|
spring.session.jdbc.initialize-schema=always
|
||||||
spring.datasource.testWhileIdle=true
|
twigs.cors.domains=http://localhost:4200
|
||||||
spring.datasource.timeBetweenEvictionRunsMillis=60000
|
logging.level.org.springframework.security=DEBUG
|
||||||
spring.datasource.validationQuery=SELECT 1
|
|
||||||
twigs.cors.domains=*
|
|
||||||
|
|
25
app/build.gradle
Normal file
25
app/build.gradle
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'application'
|
||||||
|
id "io.spring.dependency-management"
|
||||||
|
id "org.springframework.boot"
|
||||||
|
id 'org.graalvm.buildtools.native' version '0.9.18'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':core')
|
||||||
|
implementation project(':api')
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
|
implementation "org.springframework.session:spring-session-jdbc"
|
||||||
|
runtimeOnly 'org.postgresql:postgresql:42.2.27'
|
||||||
|
runtimeOnly 'com.h2database:h2:2.2.220'
|
||||||
|
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
description = "twigs"
|
||||||
|
}
|
||||||
|
|
||||||
|
mainClassName = "com.wbrawner.twigs.TwigsServerApplication"
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver;
|
package com.wbrawner.twigs;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
@ -1,6 +1,6 @@
|
||||||
package com.wbrawner.budgetserver.config;
|
package com.wbrawner.twigs.config;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.user.UserRepository;
|
import com.wbrawner.twigs.user.UserRepository;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
127
app/src/main/java/com/wbrawner/twigs/config/SecurityConfig.java
Normal file
127
app/src/main/java/com/wbrawner/twigs/config/SecurityConfig.java
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package com.wbrawner.twigs.config;
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.user.PasswordResetRequestRepository;
|
||||||
|
import com.wbrawner.twigs.session.UserSessionRepository;
|
||||||
|
import com.wbrawner.twigs.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.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
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
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final Environment env;
|
||||||
|
private final DataSource datasource;
|
||||||
|
private final UserSessionRepository userSessionRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordResetRequestRepository passwordResetRequestRepository;
|
||||||
|
private final JdbcUserDetailsService userDetailsService;
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
|
public SecurityConfig(
|
||||||
|
Environment env,
|
||||||
|
DataSource datasource,
|
||||||
|
UserSessionRepository userSessionRepository,
|
||||||
|
UserRepository userRepository,
|
||||||
|
PasswordResetRequestRepository passwordResetRequestRepository,
|
||||||
|
JdbcUserDetailsService userDetailsService,
|
||||||
|
Environment environment
|
||||||
|
) {
|
||||||
|
this.env = env;
|
||||||
|
this.datasource = datasource;
|
||||||
|
this.userSessionRepository = userSessionRepository;
|
||||||
|
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 TokenAuthenticationProvider(userSessionRepository, userRepository);
|
||||||
|
authProvider.setPasswordEncoder(getPasswordEncoder());
|
||||||
|
authProvider.setUserDetailsService(userDetailsService);
|
||||||
|
return authProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder getPasswordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(
|
||||||
|
HttpSecurity httpSecurity, AuthenticationManager authenticationManager
|
||||||
|
) throws Exception {
|
||||||
|
return httpSecurity.authorizeHttpRequests((authz) -> {
|
||||||
|
try {
|
||||||
|
authz
|
||||||
|
.requestMatchers(
|
||||||
|
"/api/^(users/register|users/login)"
|
||||||
|
)
|
||||||
|
.authenticated()
|
||||||
|
.anyRequest()
|
||||||
|
.permitAll()
|
||||||
|
.and()
|
||||||
|
.httpBasic()
|
||||||
|
.authenticationEntryPoint(new SilentAuthenticationEntryPoint())
|
||||||
|
.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(HttpMethod::name)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
corsConfig.setAllowCredentials(true);
|
||||||
|
return corsConfig;
|
||||||
|
})
|
||||||
|
.and()
|
||||||
|
.csrf()
|
||||||
|
.disable()
|
||||||
|
.addFilter(new TokenAuthenticationFilter(authenticationManager));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager getAuthenticationManager(AuthenticationConfiguration auth) throws Exception {
|
||||||
|
return auth.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.config;
|
package com.wbrawner.twigs.config;
|
||||||
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
@ -14,7 +14,9 @@ public class SessionAuthenticationToken extends UsernamePasswordAuthenticationTo
|
||||||
* @param credentials
|
* @param credentials
|
||||||
* @param principal
|
* @param principal
|
||||||
*/
|
*/
|
||||||
public SessionAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
|
public SessionAuthenticationToken(
|
||||||
|
Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities
|
||||||
|
) {
|
||||||
super(principal, credentials, authorities);
|
super(principal, credentials, authorities);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
package com.wbrawner.budgetserver.config;
|
package com.wbrawner.twigs.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +12,9 @@ import java.io.IOException;
|
||||||
*/
|
*/
|
||||||
public class SilentAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
public class SilentAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||||
@Override
|
@Override
|
||||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
public void commence(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, AuthenticationException authException
|
||||||
|
) throws IOException {
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
package com.wbrawner.budgetserver.config;
|
package com.wbrawner.twigs.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@ -18,14 +18,20 @@ public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain chain
|
||||||
|
) throws IOException, ServletException {
|
||||||
var authHeader = request.getHeader("Authorization");
|
var authHeader = request.getHeader("Authorization");
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var token = authHeader.substring(7);
|
var token = authHeader.substring(7);
|
||||||
var authentication = getAuthenticationManager().authenticate(new SessionAuthenticationToken(null, token, Collections.emptyList()));
|
var authentication = getAuthenticationManager().authenticate(new SessionAuthenticationToken(
|
||||||
|
null,
|
||||||
|
token,
|
||||||
|
Collections.emptyList()
|
||||||
|
));
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
package com.wbrawner.budgetserver.config;
|
package com.wbrawner.twigs.config;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
import com.wbrawner.twigs.Utils;
|
||||||
import com.wbrawner.budgetserver.user.UserRepository;
|
import com.wbrawner.twigs.session.UserSessionRepository;
|
||||||
|
import com.wbrawner.twigs.user.UserRepository;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
@ -12,7 +13,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.twoWeeksFromNow;
|
import static com.wbrawner.twigs.Utils.twoWeeksFromNow;
|
||||||
|
|
||||||
public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
private final UserSessionRepository userSessionRepository;
|
private final UserSessionRepository userSessionRepository;
|
||||||
|
@ -24,7 +25,9 @@ public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
protected void additionalAuthenticationChecks(
|
||||||
|
UserDetails userDetails, UsernamePasswordAuthenticationToken authentication
|
||||||
|
) throws AuthenticationException {
|
||||||
if (!(authentication instanceof SessionAuthenticationToken)) {
|
if (!(authentication instanceof SessionAuthenticationToken)) {
|
||||||
// Additional checks aren't needed since they've already been handled
|
// Additional checks aren't needed since they've already been handled
|
||||||
super.additionalAuthenticationChecks(userDetails, authentication);
|
super.additionalAuthenticationChecks(userDetails, authentication);
|
||||||
|
@ -45,10 +48,14 @@ public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
// Update the session on a background thread to avoid holding up the request longer than necessary
|
// Update the session on a background thread to avoid holding up the request longer than necessary
|
||||||
var updatedSession = session.get();
|
var updatedSession = session.get();
|
||||||
updatedSession.setExpiration(twoWeeksFromNow());
|
updatedSession.setExpiration(Utils.twoWeeksFromNow());
|
||||||
userSessionRepository.save(updatedSession);
|
userSessionRepository.save(updatedSession);
|
||||||
}).start();
|
}).start();
|
||||||
return new SessionAuthenticationToken(user.get(), authentication.getCredentials(), authentication.getAuthorities());
|
return new SessionAuthenticationToken(
|
||||||
|
user.get(),
|
||||||
|
authentication.getCredentials(),
|
||||||
|
authentication.getAuthorities()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return super.authenticate(authentication);
|
return super.authenticate(authentication);
|
||||||
}
|
}
|
22
app/src/main/java/com/wbrawner/twigs/config/WebConfig.java
Normal file
22
app/src/main/java/com/wbrawner/twigs/config/WebConfig.java
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
//package com.wbrawner.twigs.config;
|
||||||
|
//
|
||||||
|
//import org.springframework.context.annotation.Configuration;
|
||||||
|
//import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
//import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
|
//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
//
|
||||||
|
//@Configuration
|
||||||
|
//public class WebConfig implements WebMvcConfigurer {
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void addViewControllers(ViewControllerRegistry registry) {
|
||||||
|
// registry.addViewController("/").setViewName("forward:/index.html");
|
||||||
|
// registry.addViewController("/{path:\\w*}").setViewName("forward:/index.html");
|
||||||
|
// registry.addViewController("/(^api)/**").setViewName("forward:/index.html");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
// registry.addResourceHandler("/**").addResourceLocations("classpath:/webapp/");
|
||||||
|
// }
|
||||||
|
//}
|
12
build.gradle
12
build.gradle
|
@ -2,12 +2,12 @@ buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url "http://repo.spring.io/snapshot" }
|
maven { url "https://repo.spring.io/snapshot" }
|
||||||
maven { url "http://repo.spring.io/milestone" }
|
maven { url "https://repo.spring.io/milestone" }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.2.2.RELEASE"
|
classpath "org.springframework.boot:spring-boot-gradle-plugin:3.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,9 +17,9 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url "http://repo.spring.io/snapshot" }
|
maven { url "https://repo.spring.io/snapshot" }
|
||||||
maven { url "http://repo.spring.io/milestone" }
|
maven { url "https://repo.spring.io/milestone" }
|
||||||
maven { url "http://repo.maven.apache.org/maven2" }
|
maven { url "https://repo.maven.apache.org/maven2" }
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
|
|
12
core/build.gradle
Normal file
12
core/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'application'
|
||||||
|
id "io.spring.dependency-management"
|
||||||
|
id "org.springframework.boot"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package com.wbrawner.budgetserver;
|
package com.wbrawner.twigs;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.twigs.user.User;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
|
@ -1,19 +1,22 @@
|
||||||
package com.wbrawner.budgetserver.budget;
|
package com.wbrawner.twigs.budget;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.category.Category;
|
import com.wbrawner.twigs.Utils;
|
||||||
import com.wbrawner.budgetserver.transaction.Transaction;
|
import com.wbrawner.twigs.category.Category;
|
||||||
|
import com.wbrawner.twigs.transaction.Transaction;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
import static com.wbrawner.twigs.Utils.randomId;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class Budget {
|
public class Budget {
|
||||||
@Id
|
@Id
|
||||||
private String id = randomId();
|
private String id = Utils.randomId();
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String currencyCode;
|
private String currencyCode;
|
||||||
|
@ -24,7 +27,8 @@ public class Budget {
|
||||||
@OneToMany(mappedBy = "budget")
|
@OneToMany(mappedBy = "budget")
|
||||||
private final Set<Transaction> users = new HashSet<>();
|
private final Set<Transaction> users = new HashSet<>();
|
||||||
|
|
||||||
public Budget() {}
|
public Budget() {
|
||||||
|
}
|
||||||
|
|
||||||
public Budget(String name, String description) {
|
public Budget(String name, String description) {
|
||||||
this(name, description, "USD");
|
this(name, description, "USD");
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.wbrawner.twigs.budget;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
|
public interface BudgetRepository extends CrudRepository<Budget, String>,
|
||||||
|
PagingAndSortingRepository<Budget, String> {
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.category;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.budget.Budget;
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import static com.wbrawner.twigs.Utils.randomId;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class Category implements Comparable<Category> {
|
public class Category implements Comparable<Category> {
|
|
@ -1,15 +1,16 @@
|
||||||
package com.wbrawner.budgetserver.category;
|
package com.wbrawner.twigs.category;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.budget.Budget;
|
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface CategoryRepository extends PagingAndSortingRepository<Category, String> {
|
public interface CategoryRepository extends CrudRepository<Category, String>,
|
||||||
|
PagingAndSortingRepository<Category, String> {
|
||||||
List<Category> findAllByBudget(Budget budget, Pageable pageable);
|
List<Category> findAllByBudget(Budget budget, Pageable pageable);
|
||||||
|
|
||||||
@Query("SELECT c FROM Category c where c.budget IN (:budgets) AND (:expense IS NULL OR c.expense = :expense) AND (:archived IS NULL OR c.archived = :archived)")
|
@Query("SELECT c FROM Category c where c.budget IN (:budgets) AND (:expense IS NULL OR c.expense = :expense) AND (:archived IS NULL OR c.archived = :archived)")
|
||||||
|
@ -20,4 +21,6 @@ public interface CategoryRepository extends PagingAndSortingRepository<Category,
|
||||||
Optional<Category> findByBudgetAndId(Budget budget, String id);
|
Optional<Category> findByBudgetAndId(Budget budget, String id);
|
||||||
|
|
||||||
List<Category> findAllByBudgetInAndIdIn(List<Budget> budgets, List<String> ids, Pageable pageable);
|
List<Category> findAllByBudgetInAndIdIn(List<Budget> budgets, List<String> ids, Pageable pageable);
|
||||||
|
|
||||||
|
void deleteAllByBudget(Budget budget);
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.permission;
|
package com.wbrawner.twigs.permission;
|
||||||
|
|
||||||
public enum Permission {
|
public enum Permission {
|
||||||
/**
|
/**
|
|
@ -1,9 +1,8 @@
|
||||||
package com.wbrawner.budgetserver.permission;
|
package com.wbrawner.twigs.permission;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.budget.Budget;
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.twigs.user.User;
|
||||||
|
import jakarta.persistence.*;
|
||||||
import javax.persistence.*;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class UserPermission {
|
public class UserPermission {
|
|
@ -1,6 +1,7 @@
|
||||||
package com.wbrawner.budgetserver.permission;
|
package com.wbrawner.twigs.permission;
|
||||||
|
|
||||||
|
import jakarta.persistence.Embeddable;
|
||||||
|
|
||||||
import javax.persistence.Embeddable;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Embeddable
|
@Embeddable
|
|
@ -1,14 +1,16 @@
|
||||||
package com.wbrawner.budgetserver.permission;
|
package com.wbrawner.twigs.permission;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.budget.Budget;
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.twigs.user.User;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserPermissionRepository extends PagingAndSortingRepository<UserPermission, UserPermissionKey> {
|
public interface UserPermissionRepository extends CrudRepository<UserPermission, UserPermissionKey>,
|
||||||
|
PagingAndSortingRepository<UserPermission, UserPermissionKey> {
|
||||||
Optional<UserPermission> findByUserAndBudget_Id(User user, String budgetId);
|
Optional<UserPermission> findByUserAndBudget_Id(User user, String budgetId);
|
||||||
|
|
||||||
List<UserPermission> findAllByUser(User user, Pageable pageable);
|
List<UserPermission> findAllByUser(User user, Pageable pageable);
|
||||||
|
@ -18,4 +20,6 @@ public interface UserPermissionRepository extends PagingAndSortingRepository<Use
|
||||||
List<UserPermission> findAllByUserAndBudget(User user, Budget budget, Pageable pageable);
|
List<UserPermission> findAllByUserAndBudget(User user, Budget budget, Pageable pageable);
|
||||||
|
|
||||||
List<UserPermission> findAllByUserAndBudget_IdIn(User user, List<String> budgetIds, Pageable pageable);
|
List<UserPermission> findAllByUserAndBudget_IdIn(User user, List<String> budgetIds, Pageable pageable);
|
||||||
|
|
||||||
|
void deleteAllByBudget(Budget budget);
|
||||||
}
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package com.wbrawner.twigs.recurringtransaction;
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
|
import com.wbrawner.twigs.category.Category;
|
||||||
|
import com.wbrawner.twigs.user.User;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static com.wbrawner.twigs.Utils.randomId;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class RecurringTransaction implements Comparable<RecurringTransaction> {
|
||||||
|
@Id
|
||||||
|
private final String id = randomId();
|
||||||
|
@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 RecurringTransaction() {
|
||||||
|
this(null, null, null, null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecurringTransaction(
|
||||||
|
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 String 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(RecurringTransaction other) {
|
||||||
|
return this.date.compareTo(other.date);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.wbrawner.twigs.recurringtransaction;
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
|
import com.wbrawner.twigs.category.Category;
|
||||||
|
import com.wbrawner.twigs.transaction.Transaction;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface RecurringTransactionRepository extends CrudRepository<RecurringTransaction, String>,
|
||||||
|
PagingAndSortingRepository<RecurringTransaction, String> {
|
||||||
|
Optional<Transaction> findByIdAndBudgetIn(String id, List<Budget> budgets);
|
||||||
|
|
||||||
|
List<Transaction> findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
||||||
|
List<Budget> budgets,
|
||||||
|
List<Category> categories,
|
||||||
|
Instant start,
|
||||||
|
Instant end,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Transaction> findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
||||||
|
List<Budget> budgets,
|
||||||
|
Instant start,
|
||||||
|
Instant end,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Transaction> findAllByBudgetAndCategory(Budget budget, Category category);
|
||||||
|
|
||||||
|
void deleteAllByBudget(Budget budget);
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
package com.wbrawner.budgetserver.session;
|
package com.wbrawner.twigs.session;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.*;
|
import static com.wbrawner.twigs.Utils.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class Session {
|
public class Session {
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.session;
|
package com.wbrawner.twigs.session;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
@ -1,12 +1,14 @@
|
||||||
package com.wbrawner.budgetserver.session;
|
package com.wbrawner.twigs.session;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserSessionRepository extends PagingAndSortingRepository<Session, String> {
|
public interface UserSessionRepository extends CrudRepository<Session, String>,
|
||||||
|
PagingAndSortingRepository<Session, String> {
|
||||||
List<Session> findByUserId(String userId);
|
List<Session> findByUserId(String userId);
|
||||||
|
|
||||||
Optional<Session> findByToken(String token);
|
Optional<Session> findByToken(String token);
|
|
@ -1,13 +1,16 @@
|
||||||
package com.wbrawner.budgetserver.transaction;
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.budget.Budget;
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
import com.wbrawner.budgetserver.category.Category;
|
import com.wbrawner.twigs.category.Category;
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.twigs.user.User;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
import static com.wbrawner.twigs.Utils.randomId;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class Transaction implements Comparable<Transaction> {
|
public class Transaction implements Comparable<Transaction> {
|
||||||
|
@ -31,14 +34,16 @@ public class Transaction implements Comparable<Transaction> {
|
||||||
this(null, null, null, null, null, null, null, null);
|
this(null, null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Transaction(String title,
|
public Transaction(
|
||||||
String description,
|
String title,
|
||||||
Instant date,
|
String description,
|
||||||
Long amount,
|
Instant date,
|
||||||
Category category,
|
Long amount,
|
||||||
Boolean expense,
|
Category category,
|
||||||
User createdBy,
|
Boolean expense,
|
||||||
Budget budget) {
|
User createdBy,
|
||||||
|
Budget budget
|
||||||
|
) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.date = date;
|
this.date = date;
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.wbrawner.twigs.transaction;
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.budget.Budget;
|
||||||
|
import com.wbrawner.twigs.category.Category;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
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 CrudRepository<Transaction, String>,
|
||||||
|
PagingAndSortingRepository<Transaction, String> {
|
||||||
|
Optional<Transaction> findByIdAndBudgetIn(String id, List<Budget> budgets);
|
||||||
|
|
||||||
|
List<Transaction> findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
||||||
|
List<Budget> budgets,
|
||||||
|
List<Category> categories,
|
||||||
|
Instant start,
|
||||||
|
Instant end,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Transaction> findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
||||||
|
List<Budget> budgets,
|
||||||
|
Instant start,
|
||||||
|
Instant end,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Transaction> findAllByBudgetAndCategory(Budget budget, Category category);
|
||||||
|
|
||||||
|
void deleteAllByBudget(Budget budget);
|
||||||
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
package com.wbrawner.budgetserver.passwordresetrequest;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.twigs.user.User;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
import static com.wbrawner.twigs.Utils.randomId;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class PasswordResetRequest {
|
public class PasswordResetRequest {
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.budgetserver.passwordresetrequest;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Transient;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Transient;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
import static com.wbrawner.twigs.Utils.randomId;
|
||||||
|
|
||||||
@Entity
|
@Entity(name = "users")
|
||||||
public class User implements UserDetails {
|
public class User implements UserDetails {
|
||||||
@Id
|
@Id
|
||||||
private final String id = randomId();
|
private final String id = randomId();
|
|
@ -1,11 +1,13 @@
|
||||||
package com.wbrawner.budgetserver.user;
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserRepository extends PagingAndSortingRepository<User, String> {
|
public interface UserRepository extends CrudRepository<User, String>,
|
||||||
|
PagingAndSortingRepository<User, String> {
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
|
|
||||||
Optional<User> findByUsernameAndPassword(String username, String password);
|
Optional<User> findByUsernameAndPassword(String username, String password);
|
37
core/src/main/java/com/wbrawner/twigs/user/UserService.java
Normal file
37
core/src/main/java/com/wbrawner/twigs/user/UserService.java
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package com.wbrawner.twigs.user;
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.session.Session;
|
||||||
|
import com.wbrawner.twigs.session.UserSessionRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static com.wbrawner.twigs.Utils.getCurrentUser;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserService {
|
||||||
|
private final DaoAuthenticationProvider authenticationProvider;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final UserSessionRepository userSessionRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserService(DaoAuthenticationProvider authenticationProvider, UserRepository userRepository, UserSessionRepository userSessionRepository) {
|
||||||
|
this.authenticationProvider = authenticationProvider;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.userSessionRepository = userSessionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session login(String username, String password) throws AuthenticationException {
|
||||||
|
var authReq = new UsernamePasswordAuthenticationToken(username, password);
|
||||||
|
Authentication auth = authenticationProvider.authenticate(authReq);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||||
|
var user = Objects.requireNonNull(getCurrentUser());
|
||||||
|
return userSessionRepository.save(new Session(user.getId()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
environment:
|
environment:
|
||||||
SPRING_DATASOURCE_URL: "jdbc:mysql://db:3306/budget?useSSL=false"
|
SPRING_DATASOURCE_URL: "jdbc:postgres://db:5432/twigs"
|
||||||
SPRING_JPA_HIBERNATE_DDL-AUTO: update
|
SPRING_JPA_HIBERNATE_DDL-AUTO: update
|
||||||
SERVER_TOMCAT_MAX-THREADS: 5
|
SERVER_TOMCAT_MAX-THREADS: 5
|
||||||
TWIGS_CORS_DOMAINS: "http://localhost:4200"
|
TWIGS_CORS_DOMAINS: "http://localhost:4200"
|
||||||
|
@ -17,14 +17,13 @@ services:
|
||||||
command: sh -c "sleep 5 && /opt/java/openjdk/bin/java $JVM_ARGS -jar /twigs-api.jar"
|
command: sh -c "sleep 5 && /opt/java/openjdk/bin/java $JVM_ARGS -jar /twigs-api.jar"
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mysql:5.7
|
image: postgres:13
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
|
POSTGRES_DB: twigs
|
||||||
MYSQL_DATABASE: budget
|
POSTGRES_USER: twigs
|
||||||
MYSQL_USER: budget
|
POSTGRES_PASSWORD: twigs
|
||||||
MYSQL_PASSWORD: budget
|
|
||||||
networks:
|
networks:
|
||||||
- twigs
|
- twigs
|
||||||
hostname: db
|
hostname: db
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +1,5 @@
|
||||||
#Fri Feb 07 18:11:46 CST 2020
|
#Fri Feb 07 18:11:46 CST 2020
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -4,3 +4,6 @@
|
||||||
|
|
||||||
rootProject.name = 'twigs'
|
rootProject.name = 'twigs'
|
||||||
include ':api'
|
include ':api'
|
||||||
|
include 'app'
|
||||||
|
include 'core'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue