More ktor parity work

This commit is contained in:
William Brawner 2023-08-05 20:59:27 -06:00
parent e84dedf675
commit eff486898f
35 changed files with 295 additions and 43 deletions

View file

@ -1,23 +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"
}
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 "org.postgresql:postgresql:42.2.23" 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 {
description = "twigs"
}
mainClassName = "com.wbrawner.twigs.TwigsServerApplication"
sourceCompatibility = 17 sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17

View file

@ -9,6 +9,7 @@ import com.wbrawner.twigs.session.SessionResponse;
import com.wbrawner.twigs.session.UserSessionRepository; import com.wbrawner.twigs.session.UserSessionRepository;
import jakarta.transaction.Transactional; 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;
@ -34,6 +35,7 @@ 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
@ -43,13 +45,14 @@ public class UserController {
UserSessionRepository userSessionRepository, UserSessionRepository userSessionRepository,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
UserPermissionRepository userPermissionsRepository, UserPermissionRepository userPermissionsRepository,
DaoAuthenticationProvider authenticationProvider 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;
} }
@ -75,17 +78,13 @@ public class UserController {
@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})

View file

@ -1,11 +1,6 @@
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:postgresql://localhost:5432/twigs spring.datasource.url=jdbc:h2:./twigs.db
spring.datasource.username=twigs
spring.datasource.password=twigs
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
spring.datasource.validationQuery=SELECT 1
twigs.cors.domains=*
logging.level.org.springframework.security=DEBUG logging.level.org.springframework.security=DEBUG

25
app/build.gradle Normal file
View 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"

View file

@ -1,6 +1,6 @@
package com.wbrawner.twigs.config; package com.wbrawner.twigs.config;
import com.wbrawner.twigs.passwordresetrequest.PasswordResetRequestRepository; import com.wbrawner.twigs.user.PasswordResetRequestRepository;
import com.wbrawner.twigs.session.UserSessionRepository; import com.wbrawner.twigs.session.UserSessionRepository;
import com.wbrawner.twigs.user.UserRepository; import com.wbrawner.twigs.user.UserRepository;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -11,7 +11,6 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.security.provisioning.JdbcUserDetailsManager;
@ -80,10 +79,12 @@ public class SecurityConfig {
return httpSecurity.authorizeHttpRequests((authz) -> { return httpSecurity.authorizeHttpRequests((authz) -> {
try { try {
authz authz
.requestMatchers("/api/users/register", "/api/users/login") .requestMatchers(
.permitAll() "/api/^(users/register|users/login)"
.anyRequest() )
.authenticated() .authenticated()
.anyRequest()
.permitAll()
.and() .and()
.httpBasic() .httpBasic()
.authenticationEntryPoint(new SilentAuthenticationEntryPoint()) .authenticationEntryPoint(new SilentAuthenticationEntryPoint())
@ -111,9 +112,7 @@ public class SecurityConfig {
.and() .and()
.csrf() .csrf()
.disable() .disable()
.addFilter(new TokenAuthenticationFilter(authenticationManager)) .addFilter(new TokenAuthenticationFilter(authenticationManager));
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -1,5 +1,6 @@
package com.wbrawner.twigs.config; package com.wbrawner.twigs.config;
import com.wbrawner.twigs.Utils;
import com.wbrawner.twigs.session.UserSessionRepository; import com.wbrawner.twigs.session.UserSessionRepository;
import com.wbrawner.twigs.user.UserRepository; import com.wbrawner.twigs.user.UserRepository;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
@ -47,7 +48,7 @@ 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( return new SessionAuthenticationToken(

View 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
core/build.gradle Normal file
View 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"
}

View file

@ -1,5 +1,6 @@
package com.wbrawner.twigs.budget; package com.wbrawner.twigs.budget;
import com.wbrawner.twigs.Utils;
import com.wbrawner.twigs.category.Category; import com.wbrawner.twigs.category.Category;
import com.wbrawner.twigs.transaction.Transaction; import com.wbrawner.twigs.transaction.Transaction;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@ -15,7 +16,7 @@ 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;

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.wbrawner.twigs.passwordresetrequest; package com.wbrawner.twigs.user;
import com.wbrawner.twigs.user.User; import com.wbrawner.twigs.user.User;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;

View file

@ -1,4 +1,4 @@
package com.wbrawner.twigs.passwordresetrequest; package com.wbrawner.twigs.user;
import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.PagingAndSortingRepository;

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

View file

@ -3,4 +3,7 @@
*/ */
rootProject.name = 'twigs' rootProject.name = 'twigs'
include ':api' include ':api'
include 'app'
include 'core'