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'
|
||||
apply plugin: 'application'
|
||||
apply plugin: "io.spring.dependency-management"
|
||||
apply plugin: "org.springframework.boot"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = "http://repo.maven.apache.org/maven2"
|
||||
}
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id "io.spring.dependency-management"
|
||||
id "org.springframework.boot"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
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"
|
||||
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.security:spring-security-test:5.1.5.RELEASE"
|
||||
testImplementation "org.springframework.security:spring-security-test"
|
||||
}
|
||||
|
||||
jar {
|
||||
description = "twigs-server"
|
||||
}
|
||||
|
||||
mainClassName = "com.wbrawner.budgetserver.TwigsServerApplication"
|
||||
|
||||
sourceCompatibility = 14
|
||||
targetCompatibility = 14
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
|
|
|
@ -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 {
|
||||
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.budgetserver.permission.UserPermission;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
||||
import com.wbrawner.budgetserver.transaction.TransactionRepository;
|
||||
import com.wbrawner.budgetserver.user.User;
|
||||
import com.wbrawner.budgetserver.user.UserRepository;
|
||||
import com.wbrawner.twigs.category.CategoryRepository;
|
||||
import com.wbrawner.twigs.permission.Permission;
|
||||
import com.wbrawner.twigs.permission.UserPermission;
|
||||
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||
import com.wbrawner.twigs.transaction.TransactionRepository;
|
||||
import com.wbrawner.twigs.user.User;
|
||||
import com.wbrawner.twigs.user.UserRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
@ -14,20 +16,20 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
||||
import static com.wbrawner.twigs.Utils.getCurrentUser;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = "/budgets")
|
||||
@RequestMapping(value = "/api/budgets")
|
||||
@Transactional
|
||||
public class BudgetController {
|
||||
private final BudgetRepository budgetRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UserPermissionRepository userPermissionsRepository;
|
||||
|
@ -35,11 +37,13 @@ public class BudgetController {
|
|||
|
||||
public BudgetController(
|
||||
BudgetRepository budgetRepository,
|
||||
CategoryRepository categoryRepository,
|
||||
TransactionRepository transactionRepository,
|
||||
UserRepository userRepository,
|
||||
UserPermissionRepository userPermissionsRepository
|
||||
) {
|
||||
this.budgetRepository = budgetRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.transactionRepository = transactionRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userPermissionsRepository = userPermissionsRepository;
|
||||
|
@ -53,12 +57,12 @@ public class BudgetController {
|
|||
}
|
||||
|
||||
List<BudgetResponse> budgets = userPermissionsRepository.findAllByUser(
|
||||
getCurrentUser(),
|
||||
PageRequest.of(
|
||||
page != null ? page : 0,
|
||||
count != null ? count : 1000
|
||||
getCurrentUser(),
|
||||
PageRequest.of(
|
||||
page != null ? page : 0,
|
||||
count != null ? count : 1000
|
||||
)
|
||||
)
|
||||
)
|
||||
.stream()
|
||||
.map(userPermission -> {
|
||||
Budget budget = userPermission.getBudget();
|
||||
|
@ -80,35 +84,11 @@ public class BudgetController {
|
|||
);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/{id}/balance", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
public ResponseEntity<BudgetBalanceResponse> getBudgetBalance(
|
||||
@PathVariable String id,
|
||||
@RequestParam(value = "from", required = false) String from,
|
||||
@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})
|
||||
@PostMapping(
|
||||
value = "",
|
||||
consumes = {MediaType.APPLICATION_JSON_VALUE},
|
||||
produces = {MediaType.APPLICATION_JSON_VALUE}
|
||||
)
|
||||
public ResponseEntity<BudgetResponse> newBudget(@RequestBody BudgetRequest request) {
|
||||
final var budget = budgetRepository.save(new Budget(request.name, request.description));
|
||||
var users = request.getUsers()
|
||||
|
@ -138,8 +118,13 @@ public class BudgetController {
|
|||
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) {
|
||||
// TODO: Make sure no changes in ownership are being attempted (except by the owner)
|
||||
return getBudgetWithPermission(id, Permission.MANAGE, (budget) -> {
|
||||
if (request.name != null) {
|
||||
budget.setName(request.name);
|
||||
|
@ -161,7 +146,8 @@ public class BudgetController {
|
|||
)
|
||||
))
|
||||
));
|
||||
} else {
|
||||
}
|
||||
if (users.isEmpty()) {
|
||||
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) {
|
||||
return getBudgetWithPermission(id, Permission.MANAGE, (budget) -> {
|
||||
categoryRepository.deleteAllByBudget(budget);
|
||||
transactionRepository.deleteAllByBudget(budget);
|
||||
userPermissionsRepository.deleteAllByBudget(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.HashSet;
|
||||
import java.util.Set;
|
||||
|
@ -23,7 +22,6 @@ public class BudgetRequest {
|
|||
this.users.addAll(users);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Set<UserPermissionRequest> getUsers() {
|
||||
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.budgetserver.permission.UserPermissionResponse;
|
||||
import com.wbrawner.budgetserver.user.User;
|
||||
import com.wbrawner.twigs.permission.UserPermission;
|
||||
import com.wbrawner.twigs.permission.UserPermissionResponse;
|
||||
|
||||
import java.util.List;
|
||||
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.budgetserver.permission.Permission;
|
||||
import com.wbrawner.budgetserver.permission.UserPermission;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
||||
import com.wbrawner.budgetserver.transaction.TransactionRepository;
|
||||
import com.wbrawner.twigs.ErrorResponse;
|
||||
import com.wbrawner.twigs.permission.Permission;
|
||||
import com.wbrawner.twigs.permission.UserPermission;
|
||||
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||
import com.wbrawner.twigs.transaction.TransactionRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
@ -12,24 +13,24 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
||||
import static com.wbrawner.budgetserver.Utils.getFirstOfMonth;
|
||||
import static com.wbrawner.twigs.Utils.getCurrentUser;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/categories")
|
||||
@RequestMapping(path = "/api/categories")
|
||||
@Transactional
|
||||
class CategoryController {
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final UserPermissionRepository userPermissionsRepository;
|
||||
|
||||
CategoryController(CategoryRepository categoryRepository,
|
||||
TransactionRepository transactionRepository,
|
||||
UserPermissionRepository userPermissionsRepository) {
|
||||
CategoryController(
|
||||
CategoryRepository categoryRepository,
|
||||
TransactionRepository transactionRepository,
|
||||
UserPermissionRepository userPermissionsRepository
|
||||
) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.transactionRepository = transactionRepository;
|
||||
this.userPermissionsRepository = userPermissionsRepository;
|
||||
|
@ -76,17 +77,6 @@ class CategoryController {
|
|||
|
||||
@GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
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)
|
||||
.stream()
|
||||
.map(UserPermission::getBudget)
|
||||
|
@ -95,11 +85,14 @@ class CategoryController {
|
|||
if (category == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
var sum = transactionRepository.sumBalanceByCategoryId(category.getId(), getFirstOfMonth());
|
||||
return ResponseEntity.ok(new CategoryBalanceResponse(category.getId(), sum));
|
||||
return ResponseEntity.ok(new CategoryResponse(category));
|
||||
}
|
||||
|
||||
@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) {
|
||||
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
||||
.orElse(null);
|
||||
|
@ -119,12 +112,25 @@ class CategoryController {
|
|||
))));
|
||||
}
|
||||
|
||||
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
ResponseEntity<CategoryResponse> updateCategory(@PathVariable String id, @RequestBody UpdateCategoryRequest request) {
|
||||
@PutMapping(
|
||||
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);
|
||||
if (category == null) return ResponseEntity.notFound().build();
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), category.getBudget().getId()).orElse(null);
|
||||
if (userPermission == null) return ResponseEntity.notFound().build();
|
||||
if (category == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(
|
||||
getCurrentUser(),
|
||||
category.getBudget().getId()
|
||||
).orElse(null);
|
||||
if (userPermission == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
@ -149,9 +155,16 @@ class CategoryController {
|
|||
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||
ResponseEntity<Void> deleteCategory(@PathVariable String id) {
|
||||
var category = categoryRepository.findById(id).orElse(null);
|
||||
if (category == null) return ResponseEntity.notFound().build();
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), category.getBudget().getId()).orElse(null);
|
||||
if (userPermission == null) return ResponseEntity.notFound().build();
|
||||
if (category == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(
|
||||
getCurrentUser(),
|
||||
category.getBudget().getId()
|
||||
).orElse(null);
|
||||
if (userPermission == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.category;
|
||||
package com.wbrawner.twigs.category;
|
||||
|
||||
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.title = title;
|
||||
this.description = description;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.category;
|
||||
package com.wbrawner.twigs.category;
|
||||
|
||||
public class NewCategoryRequest {
|
||||
private final String title;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.category;
|
||||
package com.wbrawner.twigs.category;
|
||||
|
||||
public class UpdateCategoryRequest {
|
||||
private final String title;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.permission;
|
||||
package com.wbrawner.twigs.permission;
|
||||
|
||||
public class UserPermissionRequest {
|
||||
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;
|
||||
|
||||
public class SessionResponse {
|
||||
private final String userId;
|
||||
|
||||
private final String token;
|
||||
private final String expiration;
|
||||
|
||||
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.expiration = expiration.toInstant().toString();
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
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 long balance;
|
||||
|
||||
public CategoryBalanceResponse(String id, long balance) {
|
||||
public BalanceResponse(String id, long balance) {
|
||||
this.id = id;
|
||||
this.balance = balance;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.transaction;
|
||||
package com.wbrawner.twigs.transaction;
|
||||
|
||||
class NewTransactionRequest {
|
||||
private final String title;
|
||||
|
@ -13,7 +13,10 @@ class NewTransactionRequest {
|
|||
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.description = description;
|
||||
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.budgetserver.category.Category;
|
||||
import com.wbrawner.budgetserver.category.CategoryRepository;
|
||||
import com.wbrawner.budgetserver.permission.Permission;
|
||||
import com.wbrawner.budgetserver.permission.UserPermission;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
||||
import com.wbrawner.twigs.ErrorResponse;
|
||||
import com.wbrawner.twigs.category.Category;
|
||||
import com.wbrawner.twigs.category.CategoryRepository;
|
||||
import com.wbrawner.twigs.permission.Permission;
|
||||
import com.wbrawner.twigs.permission.UserPermission;
|
||||
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
@ -15,15 +16,16 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.*;
|
||||
import static com.wbrawner.twigs.Utils.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/transactions")
|
||||
@RequestMapping(path = "/api/transactions")
|
||||
@Transactional
|
||||
public class TransactionController {
|
||||
private final CategoryRepository categoryRepository;
|
||||
|
@ -32,9 +34,11 @@ public class TransactionController {
|
|||
|
||||
private final Logger logger = LoggerFactory.getLogger(TransactionController.class);
|
||||
|
||||
public TransactionController(CategoryRepository categoryRepository,
|
||||
TransactionRepository transactionRepository,
|
||||
UserPermissionRepository userPermissionsRepository) {
|
||||
public TransactionController(
|
||||
CategoryRepository categoryRepository,
|
||||
TransactionRepository transactionRepository,
|
||||
UserPermissionRepository userPermissionsRepository
|
||||
) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.transactionRepository = transactionRepository;
|
||||
this.userPermissionsRepository = userPermissionsRepository;
|
||||
|
@ -79,16 +83,18 @@ public class TransactionController {
|
|||
try {
|
||||
fromInstant = Instant.parse(from);
|
||||
} catch (Exception e) {
|
||||
if (!(e instanceof NullPointerException))
|
||||
if (!(e instanceof NullPointerException)) {
|
||||
logger.error("Failed to parse '" + from + "' to Instant for 'from' parameter", e);
|
||||
}
|
||||
fromInstant = getFirstOfMonth().toInstant();
|
||||
}
|
||||
Instant toInstant;
|
||||
try {
|
||||
toInstant = Instant.parse(to);
|
||||
} catch (Exception e) {
|
||||
if (!(e instanceof NullPointerException))
|
||||
if (!(e instanceof NullPointerException)) {
|
||||
logger.error("Failed to parse '" + to + "' to Instant for 'to' parameter", e);
|
||||
}
|
||||
toInstant = getEndOfMonth().toInstant();
|
||||
}
|
||||
var query = categories == null ? transactionRepository.findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
||||
|
@ -117,11 +123,17 @@ public class TransactionController {
|
|||
.map(UserPermission::getBudget)
|
||||
.collect(Collectors.toList());
|
||||
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));
|
||||
}
|
||||
|
||||
@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) {
|
||||
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
||||
.orElse(null);
|
||||
|
@ -148,12 +160,25 @@ public class TransactionController {
|
|||
))));
|
||||
}
|
||||
|
||||
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
public ResponseEntity<Object> updateTransaction(@PathVariable String id, @RequestBody UpdateTransactionRequest request) {
|
||||
@PutMapping(
|
||||
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);
|
||||
if (transaction == null) return ResponseEntity.notFound().build();
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null);
|
||||
if (userPermission == null) return ResponseEntity.notFound().build();
|
||||
if (transaction == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(
|
||||
getCurrentUser(),
|
||||
transaction.getBudget().getId()
|
||||
).orElse(null);
|
||||
if (userPermission == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
@ -173,7 +198,10 @@ public class TransactionController {
|
|||
transaction.setExpense(request.getExpense());
|
||||
}
|
||||
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)) {
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
|
@ -182,7 +210,8 @@ public class TransactionController {
|
|||
transaction.setBudget(newUserPermission.getBudget());
|
||||
}
|
||||
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) {
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
|
@ -196,14 +225,111 @@ public class TransactionController {
|
|||
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||
public ResponseEntity<Void> deleteTransaction(@PathVariable String id) {
|
||||
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
|
||||
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null);
|
||||
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)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
transactionRepository.delete(transaction);
|
||||
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 {
|
||||
private final String id;
|
||||
|
@ -11,15 +11,17 @@ class TransactionResponse {
|
|||
private final String categoryId;
|
||||
private final String createdBy;
|
||||
|
||||
TransactionResponse(String id,
|
||||
String title,
|
||||
String description,
|
||||
String date,
|
||||
Long amount,
|
||||
Boolean expense,
|
||||
String budgetId,
|
||||
String categoryId,
|
||||
String createdBy) {
|
||||
TransactionResponse(
|
||||
String id,
|
||||
String title,
|
||||
String description,
|
||||
String date,
|
||||
Long amount,
|
||||
Boolean expense,
|
||||
String budgetId,
|
||||
String categoryId,
|
||||
String createdBy
|
||||
) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.description = description;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.transaction;
|
||||
package com.wbrawner.twigs.transaction;
|
||||
|
||||
class UpdateTransactionRequest {
|
||||
private final String title;
|
||||
|
@ -14,7 +14,10 @@ class UpdateTransactionRequest {
|
|||
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.description = description;
|
||||
this.date = date;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.user;
|
||||
package com.wbrawner.twigs.user;
|
||||
|
||||
public class LoginRequest {
|
||||
private final String username;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.user;
|
||||
package com.wbrawner.twigs.user;
|
||||
|
||||
public class NewUserRequest {
|
||||
private final String username;
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.user;
|
||||
package com.wbrawner.twigs.user;
|
||||
|
||||
public class UpdateUserRequest {
|
||||
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.budgetserver.budget.BudgetRepository;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionResponse;
|
||||
import com.wbrawner.budgetserver.session.Session;
|
||||
import com.wbrawner.budgetserver.session.SessionResponse;
|
||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
||||
import com.wbrawner.twigs.ErrorResponse;
|
||||
import com.wbrawner.twigs.budget.BudgetRepository;
|
||||
import com.wbrawner.twigs.permission.UserPermissionRepository;
|
||||
import com.wbrawner.twigs.permission.UserPermissionResponse;
|
||||
import com.wbrawner.twigs.session.Session;
|
||||
import com.wbrawner.twigs.session.SessionResponse;
|
||||
import com.wbrawner.twigs.session.UserSessionRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.web.bind.annotation.*;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
||||
import static com.wbrawner.twigs.Utils.getCurrentUser;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
@RequestMapping("/api/users")
|
||||
@Transactional
|
||||
public class UserController {
|
||||
private final BudgetRepository budgetRepository;
|
||||
|
@ -34,20 +35,24 @@ public class UserController {
|
|||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserPermissionRepository userPermissionsRepository;
|
||||
private final UserSessionRepository userSessionRepository;
|
||||
private final UserService userService;
|
||||
private final DaoAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Autowired
|
||||
public UserController(BudgetRepository budgetRepository,
|
||||
UserRepository userRepository,
|
||||
UserSessionRepository userSessionRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
UserPermissionRepository userPermissionsRepository,
|
||||
DaoAuthenticationProvider authenticationProvider) {
|
||||
public UserController(
|
||||
BudgetRepository budgetRepository,
|
||||
UserRepository userRepository,
|
||||
UserSessionRepository userSessionRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
UserPermissionRepository userPermissionsRepository,
|
||||
UserService userService, DaoAuthenticationProvider authenticationProvider
|
||||
) {
|
||||
this.budgetRepository = budgetRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userSessionRepository = userSessionRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.userPermissionsRepository = userPermissionsRepository;
|
||||
this.userService = userService;
|
||||
this.authenticationProvider = authenticationProvider;
|
||||
}
|
||||
|
||||
|
@ -66,28 +71,28 @@ public class UserController {
|
|||
if (!userInBudget) {
|
||||
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})
|
||||
ResponseEntity<SessionResponse> login(@RequestBody LoginRequest request) {
|
||||
var authReq = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword());
|
||||
Authentication auth;
|
||||
try {
|
||||
auth = authenticationProvider.authenticate(authReq);
|
||||
return ResponseEntity.ok(new SessionResponse(
|
||||
userService.login(request.getUsername(), request.getPassword())
|
||||
));
|
||||
} 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})
|
||||
ResponseEntity<UserResponse> getProfile() {
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -110,14 +115,21 @@ public class UserController {
|
|||
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) {
|
||||
if (userRepository.findByUsername(request.getUsername()).isPresent())
|
||||
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
|
||||
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"));
|
||||
if (request.getPassword().isBlank())
|
||||
}
|
||||
if (request.getPassword().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password"));
|
||||
}
|
||||
return ResponseEntity.ok(new UserResponse(userRepository.save(new User(
|
||||
request.getUsername(),
|
||||
passwordEncoder.encode(request.getPassword()),
|
||||
|
@ -125,24 +137,36 @@ public class UserController {
|
|||
))));
|
||||
}
|
||||
|
||||
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
ResponseEntity<Object> updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest request) {
|
||||
if (!getCurrentUser().getId().equals(id)) return ResponseEntity.status(403).build();
|
||||
@PutMapping(
|
||||
path = "/{id}",
|
||||
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);
|
||||
if (user == null) return ResponseEntity.notFound().build();
|
||||
if (user == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (request.getUsername() != null) {
|
||||
if (userRepository.findByUsername(request.getUsername()).isPresent())
|
||||
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse("Username taken"));
|
||||
}
|
||||
user.setUsername(request.getUsername());
|
||||
}
|
||||
if (request.getEmail() != null) {
|
||||
if (userRepository.findByEmail(request.getEmail()).isPresent())
|
||||
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse("Email taken"));
|
||||
}
|
||||
user.setEmail(request.getEmail());
|
||||
}
|
||||
if (request.getPassword() != null) {
|
||||
if (request.getPassword().isBlank())
|
||||
if (request.getPassword().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid password"));
|
||||
}
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
}
|
||||
return ResponseEntity.ok(new UserResponse(userRepository.save(user)));
|
||||
|
@ -150,7 +174,10 @@ public class UserController {
|
|||
|
||||
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||
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);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.user;
|
||||
package com.wbrawner.twigs.user;
|
||||
|
||||
public class UserResponse {
|
||||
private final String id;
|
|
@ -1,10 +1,6 @@
|
|||
spring.jpa.hibernate.ddl-auto=none
|
||||
spring.datasource.url=jdbc:mysql://localhost:3306/budget
|
||||
spring.datasource.username=budget
|
||||
spring.datasource.password=budget
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.datasource.url=jdbc:h2:./twigs.db
|
||||
spring.profiles.active=prod
|
||||
spring.session.jdbc.initialize-schema=always
|
||||
spring.datasource.testWhileIdle=true
|
||||
spring.datasource.timeBetweenEvictionRunsMillis=60000
|
||||
spring.datasource.validationQuery=SELECT 1
|
||||
twigs.cors.domains=*
|
||||
twigs.cors.domains=http://localhost:4200
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
|
|
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.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.security.core.userdetails.UserDetails;
|
||||
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.core.GrantedAuthority;
|
||||
|
@ -14,7 +14,9 @@ public class SessionAuthenticationToken extends UsernamePasswordAuthenticationTo
|
|||
* @param credentials
|
||||
* @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);
|
||||
}
|
||||
}
|
|
@ -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.web.AuthenticationEntryPoint;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
@ -13,7 +12,9 @@ import java.io.IOException;
|
|||
*/
|
||||
public class SilentAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@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());
|
||||
}
|
||||
}
|
|
@ -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.core.context.SecurityContextHolder;
|
||||
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.util.Collections;
|
||||
|
||||
|
@ -18,14 +18,20 @@ public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
|
|||
}
|
||||
|
||||
@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");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
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.budgetserver.user.UserRepository;
|
||||
import com.wbrawner.twigs.Utils;
|
||||
import com.wbrawner.twigs.session.UserSessionRepository;
|
||||
import com.wbrawner.twigs.user.UserRepository;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
|
@ -12,7 +13,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
|||
|
||||
import java.util.Date;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.twoWeeksFromNow;
|
||||
import static com.wbrawner.twigs.Utils.twoWeeksFromNow;
|
||||
|
||||
public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
||||
private final UserSessionRepository userSessionRepository;
|
||||
|
@ -24,7 +25,9 @@ public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
||||
protected void additionalAuthenticationChecks(
|
||||
UserDetails userDetails, UsernamePasswordAuthenticationToken authentication
|
||||
) throws AuthenticationException {
|
||||
if (!(authentication instanceof SessionAuthenticationToken)) {
|
||||
// Additional checks aren't needed since they've already been handled
|
||||
super.additionalAuthenticationChecks(userDetails, authentication);
|
||||
|
@ -45,10 +48,14 @@ public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
|||
new Thread(() -> {
|
||||
// Update the session on a background thread to avoid holding up the request longer than necessary
|
||||
var updatedSession = session.get();
|
||||
updatedSession.setExpiration(twoWeeksFromNow());
|
||||
updatedSession.setExpiration(Utils.twoWeeksFromNow());
|
||||
userSessionRepository.save(updatedSession);
|
||||
}).start();
|
||||
return new SessionAuthenticationToken(user.get(), authentication.getCredentials(), authentication.getAuthorities());
|
||||
return new SessionAuthenticationToken(
|
||||
user.get(),
|
||||
authentication.getCredentials(),
|
||||
authentication.getAuthorities()
|
||||
);
|
||||
} else {
|
||||
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 {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url "http://repo.spring.io/snapshot" }
|
||||
maven { url "http://repo.spring.io/milestone" }
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
}
|
||||
|
||||
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 {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url "http://repo.spring.io/snapshot" }
|
||||
maven { url "http://repo.spring.io/milestone" }
|
||||
maven { url "http://repo.maven.apache.org/maven2" }
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
maven { url "https://repo.maven.apache.org/maven2" }
|
||||
}
|
||||
|
||||
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 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.budgetserver.transaction.Transaction;
|
||||
import com.wbrawner.twigs.Utils;
|
||||
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.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
||||
import static com.wbrawner.twigs.Utils.randomId;
|
||||
|
||||
@Entity
|
||||
public class Budget {
|
||||
@Id
|
||||
private String id = randomId();
|
||||
private String id = Utils.randomId();
|
||||
private String name;
|
||||
private String description;
|
||||
private String currencyCode;
|
||||
|
@ -24,7 +27,8 @@ public class Budget {
|
|||
@OneToMany(mappedBy = "budget")
|
||||
private final Set<Transaction> users = new HashSet<>();
|
||||
|
||||
public Budget() {}
|
||||
public Budget() {
|
||||
}
|
||||
|
||||
public Budget(String name, String description) {
|
||||
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 java.util.UUID;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
||||
import static com.wbrawner.twigs.Utils.randomId;
|
||||
|
||||
@Entity
|
||||
public class Category implements Comparable<Category> {
|
|
@ -1,15 +1,16 @@
|
|||
package com.wbrawner.budgetserver.category;
|
||||
|
||||
import com.wbrawner.budgetserver.budget.Budget;
|
||||
package com.wbrawner.twigs.category;
|
||||
|
||||
import com.wbrawner.twigs.budget.Budget;
|
||||
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.util.List;
|
||||
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);
|
||||
|
||||
@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);
|
||||
|
||||
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 {
|
||||
/**
|
|
@ -1,9 +1,8 @@
|
|||
package com.wbrawner.budgetserver.permission;
|
||||
package com.wbrawner.twigs.permission;
|
||||
|
||||
import com.wbrawner.budgetserver.budget.Budget;
|
||||
import com.wbrawner.budgetserver.user.User;
|
||||
|
||||
import javax.persistence.*;
|
||||
import com.wbrawner.twigs.budget.Budget;
|
||||
import com.wbrawner.twigs.user.User;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
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;
|
||||
|
||||
@Embeddable
|
|
@ -1,14 +1,16 @@
|
|||
package com.wbrawner.budgetserver.permission;
|
||||
package com.wbrawner.twigs.permission;
|
||||
|
||||
import com.wbrawner.budgetserver.budget.Budget;
|
||||
import com.wbrawner.budgetserver.user.User;
|
||||
import com.wbrawner.twigs.budget.Budget;
|
||||
import com.wbrawner.twigs.user.User;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
import java.util.List;
|
||||
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);
|
||||
|
||||
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_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 static com.wbrawner.budgetserver.Utils.*;
|
||||
import static com.wbrawner.twigs.Utils.*;
|
||||
|
||||
@Entity
|
||||
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.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 java.util.Date;
|
||||
import java.util.List;
|
||||
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);
|
||||
|
||||
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.budgetserver.category.Category;
|
||||
import com.wbrawner.budgetserver.user.User;
|
||||
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 javax.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
||||
import static com.wbrawner.twigs.Utils.randomId;
|
||||
|
||||
@Entity
|
||||
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);
|
||||
}
|
||||
|
||||
public Transaction(String title,
|
||||
String description,
|
||||
Instant date,
|
||||
Long amount,
|
||||
Category category,
|
||||
Boolean expense,
|
||||
User createdBy,
|
||||
Budget budget) {
|
||||
public Transaction(
|
||||
String title,
|
||||
String description,
|
||||
Instant date,
|
||||
Long amount,
|
||||
Category category,
|
||||
Boolean expense,
|
||||
User createdBy,
|
||||
Budget budget
|
||||
) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.date = date;
|
|
@ -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.GregorianCalendar;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
||||
import static com.wbrawner.twigs.Utils.randomId;
|
||||
|
||||
@Entity
|
||||
public class PasswordResetRequest {
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.budgetserver.passwordresetrequest;
|
||||
package com.wbrawner.twigs.user;
|
||||
|
||||
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.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
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.Collections;
|
||||
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 {
|
||||
@Id
|
||||
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 java.util.List;
|
||||
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> 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:
|
||||
- db
|
||||
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
|
||||
SERVER_TOMCAT_MAX-THREADS: 5
|
||||
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"
|
||||
|
||||
db:
|
||||
image: mysql:5.7
|
||||
image: postgres:13
|
||||
ports:
|
||||
- "3306:3306"
|
||||
- "5432:5432"
|
||||
environment:
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
|
||||
MYSQL_DATABASE: budget
|
||||
MYSQL_USER: budget
|
||||
MYSQL_PASSWORD: budget
|
||||
POSTGRES_DB: twigs
|
||||
POSTGRES_USER: twigs
|
||||
POSTGRES_PASSWORD: twigs
|
||||
networks:
|
||||
- twigs
|
||||
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
|
||||
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
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -4,3 +4,6 @@
|
|||
|
||||
rootProject.name = 'twigs'
|
||||
include ':api'
|
||||
include 'app'
|
||||
include 'core'
|
||||
|
||||
|
|
Loading…
Reference in a new issue