rename folders

This commit is contained in:
2024-10-06 18:28:31 +02:00
parent 8482770998
commit b6bff9ff7e
42 changed files with 3092 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
bin/
target/
.settings/
.project
.classpath
hs_err*.log
application.properties
usernames.txt
lucene
.vscode
+138
View File
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.champonthis</groupId>
<artifactId>buntspecht</artifactId>
<version>${revision}</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<maven.compiler.release></maven.compiler.release>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<querydsl.version>5.1.0</querydsl.version>
<revision>0.4.0</revision>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath />
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- Query DSL -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
<classifier>jakarta</classifier>
</dependency>
<!-- Utils -->
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>1.6.5</version>
</dependency>
<!-- Datbase -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>de.champonthis.buntspecht.Application</mainClass>
<finalName>buntspecht</finalName>
<executable>true</executable>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,16 @@
package de.champonthis.buntspecht;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,24 @@
package de.champonthis.buntspecht;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
@Configuration
@EnableJpaAuditing
public class JPAConfig {
@Autowired
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
@@ -0,0 +1,74 @@
package de.champonthis.buntspecht.businesslogic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import de.champonthis.buntspecht.model.SystemProperty;
import de.champonthis.buntspecht.repository.SystemPropertyRepository;
@Component
public class SystemPropertyManager {
@Autowired
private SystemPropertyRepository systemPropertyRepository;
public boolean has(String key) {
return systemPropertyRepository.existsById(key);
}
public String get(String key) {
return systemPropertyRepository.findById(key).orElse(new SystemProperty()).getValue();
}
public String get(String key, String defaultValue) {
return systemPropertyRepository.findById(key).orElse(new SystemProperty(key, defaultValue)).getValue();
}
public boolean getBoolean(String key) {
return getBoolean(key, false);
}
public boolean getBoolean(String key, boolean defaultValue) {
return Boolean.valueOf(get(key, String.valueOf(defaultValue)));
}
public int getInteger(String key) {
return getInteger(key, 0);
}
public int getInteger(String key, int defaultValue) {
return Integer.valueOf(get(key, String.valueOf(defaultValue)));
}
public long getLong(String key) {
return getLong(key, 0L);
}
public long getLong(String key, long defaultValue) {
return Long.valueOf(get(key, String.valueOf(defaultValue)));
}
public void add(String key, String value) {
Assert.isTrue(!systemPropertyRepository.existsById(key),
"System Property already exists, use update method to change value!");
systemPropertyRepository.save(new SystemProperty(key, value));
}
public void update(String key, String value) {
Assert.isTrue(systemPropertyRepository.existsById(key),
"System Property does not exists, use add method to add new!");
SystemProperty systemProperty = systemPropertyRepository.findById(key).get();
systemProperty.setValue(value);
systemPropertyRepository.save(systemProperty);
}
public void set(String key, String value) {
if (systemPropertyRepository.existsById(key)) {
update(key, value);
} else {
add(key, value);
}
}
}
@@ -0,0 +1,202 @@
package de.champonthis.buntspecht.businesslogic;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.QueryResults;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import de.champonthis.buntspecht.controller.model.TurnoverFilterModel;
import de.champonthis.buntspecht.model.QTurnover;
import de.champonthis.buntspecht.model.Turnover;
import de.champonthis.buntspecht.repository.TurnoverRepository;
@Service
public class TurnoverManager {
@Autowired
private TurnoverRepository turnoverRepository;
@Autowired
private JPAQueryFactory jpaQueryFactory;
private QTurnover qTurnover = QTurnover.turnover;
public QueryResults<Turnover> fetch(long limit, long offset, String sortBy, boolean descending,
TurnoverFilterModel filter) {
return fetch(null, limit, offset, sortBy, descending, filter);
}
public QueryResults<Turnover> fetch(String username, long limit, long offset, String sortBy, boolean descending,
TurnoverFilterModel filter) {
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(username)) {
builder.and(qTurnover.username.eq(username));
}
builder.and(buildFilter(filter));
JPAQuery<Turnover> query = jpaQueryFactory.from(qTurnover).where(builder.getValue()).select(qTurnover);
Long total = query.clone().select(qTurnover.id.countDistinct()).fetchOne();
if (StringUtils.hasText(sortBy)) {
Path<? extends Comparable<?>> path = null;
switch (sortBy) {
case "created":
path = qTurnover.created;
break;
case "dueDate":
path = qTurnover.dueDate;
break;
case "updated":
path = qTurnover.updated;
break;
case "customer":
path = qTurnover.customer;
break;
case "price":
path = qTurnover.price;
break;
case "timeInvestment":
path = qTurnover.timeInvestment;
break;
}
if (path != null) {
query.orderBy(new OrderSpecifier<>(descending ? Order.DESC : Order.ASC, path));
}
}
List<Turnover> result = query.limit(limit).offset(offset).fetch();
return new QueryResults<Turnover>(result, limit, offset, total == null ? 0L : total);
}
protected Predicate buildFilter(TurnoverFilterModel filter) {
BooleanBuilder builder = new BooleanBuilder();
if (filter != null) {
if (filter.getCreated() != null) {
if (filter.getCreated().getMin() != null) {
builder.and(qTurnover.created.after(filter.getCreated().getMin()));
}
if (filter.getCreated().getMax() != null) {
builder.and(qTurnover.created.before(filter.getCreated().getMax()));
}
}
if (filter.getDueDate() != null) {
if (filter.getDueDate().getMin() != null) {
builder.and(qTurnover.dueDate.after(filter.getDueDate().getMin()));
}
if (filter.getDueDate().getMax() != null) {
builder.and(qTurnover.dueDate.before(filter.getDueDate().getMax()));
}
}
if (filter.getUpdated() != null) {
if (filter.getUpdated().getMin() != null) {
builder.and(qTurnover.updated.after(filter.getUpdated().getMin()));
}
if (filter.getUpdated().getMax() != null) {
builder.and(qTurnover.updated.before(filter.getUpdated().getMax()));
}
}
if (filter.getCustomer() != null) {
builder.and(qTurnover.customer.contains(filter.getCustomer()));
}
if (filter.getMotif() != null) {
builder.and(qTurnover.motif.contains(filter.getMotif()));
}
if (filter.getPrice() != null) {
if (filter.getPrice().getMin() != null) {
builder.and(qTurnover.price.goe(filter.getPrice().getMin()));
}
if (filter.getPrice().getMax() != null) {
builder.and(qTurnover.price.loe(filter.getPrice().getMax()));
}
}
if (filter.getTimeInvestment() != null) {
if (filter.getTimeInvestment().getMin() != null) {
builder.and(qTurnover.timeInvestment.goe(filter.getTimeInvestment().getMin()));
}
if (filter.getTimeInvestment().getMax() != null) {
builder.and(qTurnover.timeInvestment.loe(filter.getTimeInvestment().getMax()));
}
}
}
return builder.getValue();
}
public Turnover get(Long id) {
return turnoverRepository.findById(id).orElse(null);
}
public Turnover save(Turnover turnover) {
return turnoverRepository.save(turnover);
}
public boolean exists(Long id) {
return turnoverRepository.existsById(id);
}
public void delete(Turnover turnover) {
turnoverRepository.delete(turnover);
}
public void deleteById(Long id) {
turnoverRepository.deleteById(id);
}
public void deleteByUsername(String username) {
turnoverRepository.deleteAllInBatch(turnoverRepository.findAll(qTurnover.username.eq(username)));
}
public QueryResults<Tuple> overview(String username, long limit, long offset, String sortBy, boolean descending,
TurnoverFilterModel filter) {
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(username)) {
builder.and(qTurnover.username.eq(username));
}
builder.and(buildFilter(filter));
JPAQuery<Tuple> query = jpaQueryFactory.from(qTurnover).where(builder.getValue()).groupBy(qTurnover.username)
.select(qTurnover.username.as("username"), qTurnover.price.sum().as("price"),
qTurnover.timeInvestment.sum().as("timeInvestment"));
Long total = query.clone().select(qTurnover.username.countDistinct()).fetchOne();
if (StringUtils.hasText(sortBy)) {
Path<? extends Comparable<?>> path = null;
switch (sortBy) {
case "username":
path = qTurnover.username;
break;
case "price":
path = qTurnover.price;
break;
case "timeInvestment":
path = qTurnover.timeInvestment;
break;
}
if (path != null) {
query.orderBy(new OrderSpecifier<>(descending ? Order.DESC : Order.ASC, path));
}
}
List<Tuple> result = query.limit(limit).offset(offset)
.fetch();
return new QueryResults<Tuple>(result, limit, offset, total == null ? 0L : total);
}
}
@@ -0,0 +1,211 @@
package de.champonthis.buntspecht.businesslogic;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import de.champonthis.buntspecht.model.QUser;
import de.champonthis.buntspecht.model.User;
import de.champonthis.buntspecht.repository.UserRepository;
import de.champonthis.buntspecht.security.LocalUserDetails;
import jakarta.transaction.Transactional;
@Service
public class UserManager implements UserDetailsService, SmartInitializingSingleton {
private Logger logger = LoggerFactory.getLogger(UserManager.class);
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JPAQueryFactory jpaQueryFactory;
@Autowired
private TurnoverManager turnoverManager;
private QUser qUser = QUser.user;
@Value("${admin.password:}")
private String adminPassword;
public QueryResults<User> fetch(long limit, long offset, String sortBy, boolean descending, String usernameFilter) {
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(usernameFilter)) {
builder.and(qUser.username.contains(usernameFilter));
}
JPAQuery<User> query = jpaQueryFactory.from(qUser).where(builder.getValue()).select(qUser);
Long total = query.clone().select(qUser.username.countDistinct()).fetchOne();
if (StringUtils.hasText(sortBy)) {
Path<? extends Comparable<?>> path = null;
switch (sortBy) {
case "username":
path = qUser.username;
break;
case "name":
path = qUser.name;
break;
}
if (path != null) {
query.orderBy(new OrderSpecifier<>(descending ? Order.DESC : Order.ASC, path));
}
}
List<User> result = query.limit(limit).offset(offset).fetch();
return new QueryResults<User>(result, limit, offset, total == null ? 0L : total);
}
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = getByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
List<GrantedAuthority> authorities = new ArrayList<>();
if (user.getRoles() != null) {
for (String role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
String passwordHash = user.getPasswordHash();
if (passwordHash == null) {
passwordHash = "";
}
LocalUserDetails userDetails = new LocalUserDetails(username, passwordHash, authorities);
return userDetails;
}
@Override
public void afterSingletonsInstantiated() {
if (!userRepository.exists(qUser.roles.contains("ROLE_ADMIN"))) {
if (!StringUtils.hasText(adminPassword)) {
adminPassword = RandomStringUtils.random(24, true, true);
logger.error("password for 'admin': " + adminPassword);
}
User admin = new User();
admin.setUsername("admin");
admin.setRoles(List.of("ROLE_ADMIN", "ROLE_DEBUG"));
admin.setPasswordHash(passwordEncoder.encode(adminPassword));
admin.setLocale("de");
userRepository.save(admin);
}
}
@Transactional
public User getByUsername(String username) {
return userRepository.findOne(qUser.username.equalsIgnoreCase(username)).orElse(null);
}
public User getByExternalId(String externalId) {
return userRepository.findOne(qUser.externalId.eq(externalId)).orElse(null);
}
public User getByAuth(Authentication authentication) {
if (authentication != null) {
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
return getByUsername(token.getName());
} else if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
String externalId = token.getAuthorizedClientRegistrationId() + "-" +
token.getName();
User user = getByExternalId(externalId);
if (user == null) {
user = new User();
user.setExternalId(externalId);
String tmpUsername = token.getPrincipal().getAttribute("preferred_username");
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = token.getPrincipal().getAttribute("username");
}
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = token.getPrincipal().getAttribute("name");
} else {
user.setName(token.getPrincipal().getAttribute("name"));
}
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = token.getName();
}
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = "user";
}
int count = 1;
String username = tmpUsername;
while (userRepository.exists(qUser.username.equalsIgnoreCase(username))) {
username = tmpUsername + "-" + count;
count++;
}
user.setUsername(username);
user.setEmail(token.getPrincipal().getAttribute("email"));
user = userRepository.save(user);
}
return user;
}
}
return null;
}
public User save(User user) {
if (exists(user.getUsername())) {
user.setPasswordHash(this.getPasswordHash(user.getUsername()));
}
return userRepository.save(user);
}
public boolean exists(String username) {
return userRepository.exists(qUser.username.equalsIgnoreCase(username));
}
public void delete(User user) {
turnoverManager.deleteByUsername(user.getUsername());
userRepository.delete(user);
}
@Transactional
public String getPasswordHash(String username) {
Assert.isTrue(userRepository.existsById(username), "User with username '" + username + "' not exists!");
return userRepository.findById(username).get().getPasswordHash();
}
public User setPassword(String username, String password) {
Assert.isTrue(userRepository.existsById(username), "User with username '" + username + "' not exists!");
User user = userRepository.findById(username).get();
user.setPasswordHash(passwordEncoder.encode(password));
return userRepository.save(user);
}
}
@@ -0,0 +1,85 @@
package de.champonthis.buntspecht.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.champonthis.buntspecht.controller.support.EntityResponseStatusException;
import de.champonthis.buntspecht.security.LocalUserDetails;
@RestController
@RequestMapping("/auth")
public class AuthenticationController extends BaseController {
private static String authorizationRequestBaseUri = "oauth2/authorization";
@Autowired(required = false)
private ClientRegistrationRepository clientRegistrationRepository;
@GetMapping
public Object me() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LocalUserDetails) {
return (LocalUserDetails) auth.getPrincipal();
}
throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED);
}
@SuppressWarnings("unchecked")
@GetMapping("external")
public List<Client> getExternalLoginUrls() {
List<Client> clients = new ArrayList<>();
if (clientRegistrationRepository != null) {
Iterable<ClientRegistration> clientRegistrations = null;
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
clientRegistrations.forEach(registration -> clients.add(new Client(registration.getRegistrationId(),
authorizationRequestBaseUri + "/" + registration.getRegistrationId())));
}
}
return clients;
}
protected static class Client {
private String id;
private String loginUrl;
public Client(String id, String loginUrl) {
super();
this.id = id;
this.loginUrl = loginUrl;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLoginUrl() {
return loginUrl;
}
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
}
}
@@ -0,0 +1,32 @@
package de.champonthis.buntspecht.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import de.champonthis.buntspecht.security.LocalUserDetails;
public class BaseController {
protected boolean authenticated() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null && auth.isAuthenticated();
}
protected String getCurrentUsername() {
LocalUserDetails userDetails = getLocalUserDetails();
return userDetails != null ? userDetails.getUsername() : null;
}
protected boolean hasRole(String role) {
LocalUserDetails userDetails = getLocalUserDetails();
return userDetails != null ? userDetails.getAuthorities().contains(new SimpleGrantedAuthority(role)) : false;
}
protected LocalUserDetails getLocalUserDetails() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return (auth != null && auth.getPrincipal() instanceof LocalUserDetails)
? (LocalUserDetails) auth.getPrincipal()
: null;
}
}
@@ -0,0 +1,174 @@
package de.champonthis.buntspecht.controller;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.querydsl.core.QueryResults;
import com.querydsl.core.Tuple;
import de.champonthis.buntspecht.businesslogic.TurnoverManager;
import de.champonthis.buntspecht.businesslogic.UserManager;
import de.champonthis.buntspecht.controller.model.TurnoverFilterModel;
import de.champonthis.buntspecht.controller.model.TurnoverFilterModel.MinMax;
import de.champonthis.buntspecht.controller.support.EntityResponseStatusException;
import de.champonthis.buntspecht.controller.support.RequestBodyErrors;
import de.champonthis.buntspecht.controller.validation.TurnoverValidator;
import de.champonthis.buntspecht.model.Turnover;
import jakarta.transaction.Transactional;
@RestController
@RequestMapping("/turnovers")
public class TurnoverController extends BaseController {
@Autowired
private TurnoverManager turnoverManager;
@Autowired
private UserManager userManager;
@Autowired
private TurnoverValidator turnoverValidator;
@PreAuthorize("isAuthenticated()")
@GetMapping
@Transactional
public QueryResults<Turnover> fetch(
@RequestParam("limit") Optional<Long> limitParameter,
@RequestParam("offset") Optional<Long> offsetParameter,
@RequestParam("sort") Optional<String> sort,
@RequestParam("descending") Optional<Boolean> descending,
@RequestParam("from") Optional<Instant> from,
@RequestParam("to") Optional<Instant> to,
@RequestParam("created_from") Optional<Instant> fromCreated,
@RequestParam("created_to") Optional<Instant> toCreated,
@RequestParam("customer") Optional<String> customer,
@RequestParam("motif") Optional<String> motif) {
TurnoverFilterModel filter = new TurnoverFilterModel();
filter.setDueDate(new MinMax<Instant>(from.orElse(null), to.orElse(null)));
filter.setCreated(new MinMax<Instant>(fromCreated.orElse(null), toCreated.orElse(null)));
filter.setCustomer(customer.orElse(null));
filter.setMotif(motif.orElse(null));
return turnoverManager.fetch(getCurrentUsername(), limitParameter.orElse(15L), offsetParameter.orElse(0L),
sort.orElse("dueDate"), descending.orElse(false), filter);
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/overview")
@Transactional
public Tuple overview(
@RequestParam("limit") Optional<Long> limitParameter,
@RequestParam("offset") Optional<Long> offsetParameter,
@RequestParam("sort") Optional<String> sort,
@RequestParam("descending") Optional<Boolean> descending,
@RequestParam("from") Optional<Instant> from,
@RequestParam("to") Optional<Instant> to,
@RequestParam("created_from") Optional<Instant> fromCreated,
@RequestParam("created_to") Optional<Instant> toCreated,
@RequestParam("customer") Optional<String> customer,
@RequestParam("motif") Optional<String> motif) {
TurnoverFilterModel filter = new TurnoverFilterModel();
filter.setDueDate(new MinMax<Instant>(from.orElse(null), to.orElse(null)));
filter.setCreated(new MinMax<Instant>(fromCreated.orElse(null), toCreated.orElse(null)));
filter.setCustomer(customer.orElse(null));
filter.setMotif(motif.orElse(null));
List<Tuple> result = turnoverManager.overview(getCurrentUsername(), limitParameter.orElse(15L),
offsetParameter.orElse(0L), sort.orElse("username"),
descending.orElse(false), filter).getResults();
if (result.isEmpty()) {
return null;
}
return result.get(0);
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
@Transactional
public Turnover get(@PathVariable("id") Long id) {
Turnover turnover = turnoverManager.get(id);
if (turnover == null || !getCurrentUsername().equals(turnover.getUsername())) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
return turnover;
}
@PreAuthorize("isAuthenticated()")
@PostMapping
@Transactional
public Turnover create(@RequestBody Turnover turnover) {
Errors errors = new RequestBodyErrors(turnover);
turnoverValidator.validate(turnover, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
if (!hasRole("ROLE_ADMIN") || !StringUtils.hasText(turnover.getUsername())
|| !userManager.exists(turnover.getUsername())) {
turnover.setUsername(getCurrentUsername());
}
turnover.setCreated(Instant.now());
turnover.setUpdated(turnover.getCreated());
if (turnover.getDueDate() == null) {
turnover.setDueDate(turnover.getCreated());
}
return turnoverManager.save(turnover);
}
@PreAuthorize("isAuthenticated()")
@PatchMapping
@Transactional
public Turnover update(@RequestBody Turnover turnover) {
Errors errors = new RequestBodyErrors(turnover);
turnoverValidator.validate(turnover, errors);
if (errors.hasErrors() || turnover.getId() == null || turnover.getId() == 0L
|| !turnoverManager.exists(turnover.getId())) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
if (!hasRole("ROLE_ADMIN")) {
Turnover existing = turnoverManager.get(turnover.getId());
if (!getCurrentUsername().equals(existing.getUsername())) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
turnover.setUsername(getCurrentUsername());
}
Turnover existing = turnoverManager.get(turnover.getId());
if (existing.equals(turnover)) {
throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED);
}
if (turnover.getDueDate() == null) {
turnover.setDueDate(turnover.getCreated());
}
turnover.setUpdated(Instant.now());
return turnoverManager.save(turnover);
}
}
@@ -0,0 +1,84 @@
package de.champonthis.buntspecht.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.champonthis.buntspecht.businesslogic.UserManager;
import de.champonthis.buntspecht.controller.model.UserPasswordModel;
import de.champonthis.buntspecht.controller.support.EntityResponseStatusException;
import de.champonthis.buntspecht.controller.support.RequestBodyErrors;
import de.champonthis.buntspecht.controller.validation.PasswordModelValidator;
import de.champonthis.buntspecht.model.User;
import jakarta.transaction.Transactional;
@RestController
@RequestMapping("/users")
public class UserController extends BaseController {
@Autowired
private UserManager userManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private PasswordModelValidator passwordModelValidator;
@PreAuthorize("isAuthenticated()")
@GetMapping("/user")
@Transactional
public User get() {
return userManager.getByUsername(getCurrentUsername());
}
@PreAuthorize("isAuthenticated()")
@PatchMapping("/user")
@Transactional
public User updateUser(@RequestBody User user) {
if (!getCurrentUsername().equals(user.getUsername())) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
User orgUser = userManager.getByUsername(user.getUsername());
orgUser.setName(user.getName());
orgUser.setAbout(user.getAbout());
orgUser.setDarkTheme(user.isDarkTheme());
orgUser.setEmail(user.getEmail());
orgUser.setLocale(user.getLocale());
user = userManager.save(orgUser);
return user;
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/password")
public void changePassword(@RequestBody UserPasswordModel passwordModel) {
Errors errors = new RequestBodyErrors(passwordModel);
User user = userManager.getByUsername(getCurrentUsername());
if (!StringUtils.hasText(passwordModel.getOld())
|| !passwordEncoder.matches(passwordModel.getOld(), userManager.getPasswordHash(user.getUsername()))) {
errors.rejectValue("old", "UNAUTHORIZED");
}
passwordModelValidator.validate(passwordModel, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
userManager.setPassword(user.getUsername(), passwordModel.getPassword());
}
}
@@ -0,0 +1,118 @@
package de.champonthis.buntspecht.controller.admin;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.SplittableRandom;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.champonthis.buntspecht.model.QUser;
import de.champonthis.buntspecht.model.Turnover;
import de.champonthis.buntspecht.model.User;
import de.champonthis.buntspecht.repository.TurnoverRepository;
import de.champonthis.buntspecht.repository.UserRepository;
@RestController
@RequestMapping("/debug")
public class DebugController {
private Logger logger = LoggerFactory.getLogger(DebugController.class);
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Autowired
private TurnoverRepository turnoverRepository;
SplittableRandom splittableRandom = new SplittableRandom();
@PreAuthorize("hasRole('ROLE_DEBUG')")
@GetMapping("/random")
public void random(
@RequestParam("users") Optional<Integer> usersParameter,
@RequestParam("minEntries") Optional<Integer> minEntriesParameter,
@RequestParam("maxEntries") Optional<Integer> maxEntriesParameter,
@RequestParam("days") Optional<Integer> daysParameter) {
logger.warn("start random generation");
long userCount = userRepository.count(QUser.user.username.startsWith("Tätowier"));
List<String> newUser = new ArrayList<>();
for (long i = userCount + 1; i <= userCount + usersParameter.orElse(5); i++) {
User user = new User();
String username = (splittableRandom.nextBoolean() ? "Tätowiererin " : "Tätowierer ") + i;
String name = "Random " + RandomStringUtils.randomAlphanumeric(splittableRandom.nextInt(4, 8));
user.setUsername(username);
user.setName(name);
user.setPasswordHash(passwordEncoder.encode(username));
user = userRepository.save(user);
logger.trace("Created user: '" + username + "'");
newUser.add(user.getUsername());
}
logger.info("Created " + usersParameter.orElse(5) + " users");
Instant startInclusive = Instant.now().minus(daysParameter.orElse(350), ChronoUnit.DAYS);
Instant endExclusive = Instant.now();
for (String username : newUser) {
long numEntries = splittableRandom.nextLong(minEntriesParameter.orElse(3), maxEntriesParameter.orElse(20));
for (int i = 0; i < numEntries; i++) {
Turnover turnover = new Turnover();
turnover.setUsername(username);
turnover.setCreated(randomDate(startInclusive, endExclusive));
turnover.setUpdated(splittableRandom.nextBoolean() ? turnover.getCreated()
: randomDate(turnover.getCreated(), endExclusive));
turnover.setDueDate(splittableRandom.nextBoolean() ? turnover.getCreated()
: randomDate(startInclusive, turnover.getCreated()));
turnover.setCustomer(RandomStringUtils.randomAlphabetic(splittableRandom.nextInt(3, 10)));
turnover.setMotif(RandomStringUtils.randomAlphabetic(splittableRandom.nextInt(5, 30)));
turnover.setPrice(Float.valueOf(String.format("%.2f", splittableRandom.nextFloat(0.01f, 2000f))));
if (splittableRandom.nextBoolean()) {
turnover.setTimeInvestment(
Float.valueOf(String.format("%.2f", splittableRandom.nextFloat(0.01f, 20f))));
}
if (splittableRandom.nextBoolean()) {
turnover.setRemark(RandomStringUtils.randomAlphabetic(splittableRandom.nextInt(10, 50)));
}
if (splittableRandom.nextInt(5) < 3) {
turnover.setMaterialConsumption(
RandomStringUtils.randomAlphabetic(splittableRandom.nextInt(10, 250)));
}
turnover = turnoverRepository.save(turnover);
logger.trace("Created turnover: '" + turnover.getId() + "'");
}
logger.info("Created " + numEntries + " turnovers of '" + username + "'");
}
logger.warn("finished random generation");
}
protected Instant randomDate(Instant startInclusive, Instant endExclusive) {
long startSeconds = startInclusive.getEpochSecond();
long endSeconds = endExclusive.getEpochSecond();
long random = splittableRandom.nextLong(startSeconds, endSeconds);
return Instant.ofEpochSecond(random);
}
}
@@ -0,0 +1,78 @@
package de.champonthis.buntspecht.controller.admin;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.champonthis.buntspecht.controller.BaseController;
import de.champonthis.buntspecht.controller.support.EntityResponseStatusException;
import de.champonthis.buntspecht.model.SystemProperty;
import de.champonthis.buntspecht.repository.SystemPropertyRepository;
@RestController
@RequestMapping("/system/properties")
public class SystemPropertiesController extends BaseController {
@Autowired
private SystemPropertyRepository systemPropertyRepository;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping()
public List<SystemProperty> getProperties(@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter) {
Sort sort = Sort.by("key").ascending();
return systemPropertyRepository.findAll(PageRequest.of(pageParameter.orElse(0), sizeParameter.orElse(10), sort))
.getContent();
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/{key}")
public SystemProperty getProperty(@PathVariable("key") String key) {
if (!systemPropertyRepository.existsById(key)) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
return systemPropertyRepository.findById(key).get();
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("")
public SystemProperty createOrUpdate(@RequestBody SystemProperty systemProperty) {
return systemPropertyRepository.save(systemProperty);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/list")
public List<SystemProperty> createOrUpdateList(@RequestBody List<SystemProperty> systemProperties) {
List<SystemProperty> result = new ArrayList<>();
for (SystemProperty systemProperty : systemProperties) {
result.add(
systemPropertyRepository.save(systemProperty));
}
return result;
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{key}")
public void deleteProperty(@PathVariable("key") String key) {
if (!systemPropertyRepository.existsById(key)) {
throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED);
}
systemPropertyRepository.deleteById(key);
}
}
@@ -0,0 +1,139 @@
package de.champonthis.buntspecht.controller.admin;
import java.time.Instant;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.querydsl.core.QueryResults;
import com.querydsl.core.Tuple;
import de.champonthis.buntspecht.businesslogic.TurnoverManager;
import de.champonthis.buntspecht.controller.BaseController;
import de.champonthis.buntspecht.controller.model.TurnoverFilterModel;
import de.champonthis.buntspecht.controller.model.TurnoverFilterModel.MinMax;
import de.champonthis.buntspecht.controller.support.EntityResponseStatusException;
import de.champonthis.buntspecht.controller.support.RequestBodyErrors;
import de.champonthis.buntspecht.controller.validation.TurnoverValidator;
import de.champonthis.buntspecht.model.Turnover;
import jakarta.transaction.Transactional;
@RestController
@RequestMapping("/turnovers/manage")
public class TurnoverManagementController extends BaseController {
@Autowired
private TurnoverManager turnoverManager;
@Autowired
private TurnoverValidator turnoverValidator;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping
@Transactional
public QueryResults<Turnover> fetch(
@RequestParam("username") Optional<String> usernameParameter,
@RequestParam("limit") Optional<Long> limitParameter,
@RequestParam("offset") Optional<Long> offsetParameter,
@RequestParam("sort") Optional<String> sort,
@RequestParam("descending") Optional<Boolean> descending,
@RequestParam("from") Optional<Instant> from,
@RequestParam("to") Optional<Instant> to,
@RequestParam("created_from") Optional<Instant> fromCreated,
@RequestParam("created_to") Optional<Instant> toCreated,
@RequestParam("customer") Optional<String> customer,
@RequestParam("motif") Optional<String> motif) {
TurnoverFilterModel filter = new TurnoverFilterModel();
filter.setDueDate(new MinMax<Instant>(from.orElse(null), to.orElse(null)));
filter.setCreated(new MinMax<Instant>(fromCreated.orElse(null), toCreated.orElse(null)));
filter.setCustomer(customer.orElse(null));
filter.setMotif(motif.orElse(null));
return turnoverManager.fetch(usernameParameter.orElse(null), limitParameter.orElse(15L),
offsetParameter.orElse(0L), sort.orElse("dueDate"), descending.orElse(false), filter);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/overview")
@Transactional
public QueryResults<Tuple> overview(
@RequestParam("username") Optional<String> usernameParameter,
@RequestParam("limit") Optional<Long> limitParameter,
@RequestParam("offset") Optional<Long> offsetParameter,
@RequestParam("sort") Optional<String> sort,
@RequestParam("descending") Optional<Boolean> descending,
@RequestParam("from") Optional<Instant> from,
@RequestParam("to") Optional<Instant> to,
@RequestParam("created_from") Optional<Instant> fromCreated,
@RequestParam("created_to") Optional<Instant> toCreated,
@RequestParam("customer") Optional<String> customer,
@RequestParam("motif") Optional<String> motif) {
TurnoverFilterModel filter = new TurnoverFilterModel();
filter.setDueDate(new MinMax<Instant>(from.orElse(null), to.orElse(null)));
filter.setCreated(new MinMax<Instant>(fromCreated.orElse(null), toCreated.orElse(null)));
filter.setCustomer(customer.orElse(null));
filter.setMotif(motif.orElse(null));
return turnoverManager.overview(usernameParameter.orElse(null), limitParameter.orElse(15L),
offsetParameter.orElse(0L), sort.orElse("username"),
descending.orElse(false), filter);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/{id}")
@Transactional
public Turnover getById(@PathVariable("id") Long id) {
if (!turnoverManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
return turnoverManager.get(id);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PatchMapping
@Transactional
public Turnover update(@RequestBody Turnover turnover) {
Errors errors = new RequestBodyErrors(turnover);
turnoverValidator.validate(turnover, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
Turnover existing = turnoverManager.get(turnover.getId());
if (existing.equals(turnover)) {
throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED);
}
if (turnover.getDueDate() == null) {
turnover.setDueDate(turnover.getCreated());
}
turnover.setUpdated(Instant.now());
return turnoverManager.save(turnover);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{id}")
@Transactional
public void deleteById(@PathVariable("id") Long id) {
if (!turnoverManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
turnoverManager.deleteById(id);
}
}
@@ -0,0 +1,148 @@
package de.champonthis.buntspecht.controller.admin;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.querydsl.core.QueryResults;
import de.champonthis.buntspecht.businesslogic.UserManager;
import de.champonthis.buntspecht.controller.BaseController;
import de.champonthis.buntspecht.controller.admin.validation.UserValidator;
import de.champonthis.buntspecht.controller.model.UserPasswordModel;
import de.champonthis.buntspecht.controller.support.EntityResponseStatusException;
import de.champonthis.buntspecht.controller.support.RequestBodyErrors;
import de.champonthis.buntspecht.controller.validation.PasswordModelValidator;
import de.champonthis.buntspecht.model.User;
import jakarta.transaction.Transactional;
@RestController
@RequestMapping("/users/manage")
public class UserManagementController extends BaseController {
@Autowired
private UserManager userManager;
@Autowired
private UserValidator userValidator;
@Autowired
private PasswordModelValidator passwordModelValidator;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping
@Transactional
public QueryResults<User> fetch(
@RequestParam("limit") Optional<Long> limitParameter,
@RequestParam("offset") Optional<Long> offsetParameter,
@RequestParam("sort") Optional<String> sort,
@RequestParam("descending") Optional<Boolean> descending,
@RequestParam("filter") Optional<String> search) {
return userManager.fetch(limitParameter.orElse(15L), offsetParameter.orElse(0L), sort.orElse("username"),
descending.orElse(false), search.orElse(""));
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/pick")
@Transactional
public List<User> pick(
@RequestParam("filter") Optional<String> search) {
return userManager.fetch(5L, 0L, "", false, search.orElse("")).getResults();
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/{username}")
@Transactional
public User get(@PathVariable("username") String username) {
User user = userManager.getByUsername(username);
if (user == null) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
return user;
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping
@Transactional
public User create(@RequestBody User user) {
Errors errors = new RequestBodyErrors(user);
userValidator.validateNew(user, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
if (user.getLocale() == null) {
user.setLocale("de");
}
return userManager.save(user);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PatchMapping
@Transactional
public User update(@RequestBody User user) {
Errors errors = new RequestBodyErrors(user);
userValidator.validate(user, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
if (getCurrentUsername().equals(user.getUsername()) && user.getRoles().indexOf("ROLE_ADMIN") == -1) {
user.getRoles().add("ROLE_ADMIN");
}
return userManager.save(user);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{username}")
@Transactional
public void delete(@PathVariable("username") String username) {
User user = userManager.getByUsername(username);
if (user == null || getCurrentUsername().equals(username)) {
throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED);
}
userManager.delete(user);
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/{username}/password")
public void password(@PathVariable("username") String username,
@RequestBody UserPasswordModel passwordModel) {
Errors errors = new RequestBodyErrors(passwordModel);
User user = userManager.getByUsername(username);
if (user == null) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
passwordModelValidator.validate(passwordModel, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
userManager.setPassword(user.getUsername(), passwordModel.getPassword());
}
}
@@ -0,0 +1,46 @@
package de.champonthis.buntspecht.controller.admin.validation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import de.champonthis.buntspecht.businesslogic.UserManager;
import de.champonthis.buntspecht.model.User;
@Component
public class UserValidator implements Validator {
@Autowired
private UserManager userManager;
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(User.class);
}
@Override
public void validate(Object target, Errors errors) {
User user = (User) target;
if (!StringUtils.hasText(user.getUsername())) {
errors.rejectValue("username", "REQUIRED");
return;
}
}
public void validateNew(Object target, Errors errors) {
validate(target, errors);
if (errors.hasErrors()) {
return;
}
User user = (User) target;
if (userManager.exists(user.getUsername())) {
errors.rejectValue("username", "ALREADY_EXISTS");
return;
}
}
}
@@ -0,0 +1,28 @@
package de.champonthis.buntspecht.controller.model;
import java.io.IOException;
import org.springframework.boot.jackson.JsonComponent;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.querydsl.core.Tuple;
@JsonComponent
public class TupleSerializer extends JsonSerializer<Tuple> {
@Override
public void serialize(Tuple value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value.toArray().length > 1) {
gen.writeStartArray();
}
for (Object object : value.toArray()) {
gen.writeObject(object);
}
if (value.toArray().length > 1) {
gen.writeEndArray();
}
}
}
@@ -0,0 +1,98 @@
package de.champonthis.buntspecht.controller.model;
import java.time.Instant;
public class TurnoverFilterModel {
private MinMax<Instant> created;
private MinMax<Instant> dueDate;
private MinMax<Instant> updated;
private String customer;
private String motif;
private MinMax<Float> price;
private MinMax<Float> timeInvestment;
public MinMax<Instant> getCreated() {
return created;
}
public void setCreated(MinMax<Instant> created) {
this.created = created;
}
public MinMax<Instant> getDueDate() {
return dueDate;
}
public void setDueDate(MinMax<Instant> dueDate) {
this.dueDate = dueDate;
}
public MinMax<Instant> getUpdated() {
return updated;
}
public void setUpdated(MinMax<Instant> updated) {
this.updated = updated;
}
public String getCustomer() {
return customer;
}
public void setCustomer(String customer) {
this.customer = customer;
}
public String getMotif() {
return motif;
}
public void setMotif(String motif) {
this.motif = motif;
}
public MinMax<Float> getPrice() {
return price;
}
public void setPrice(MinMax<Float> price) {
this.price = price;
}
public MinMax<Float> getTimeInvestment() {
return timeInvestment;
}
public void setTimeInvestment(MinMax<Float> timeInvestment) {
this.timeInvestment = timeInvestment;
}
public static class MinMax<T> {
private T min;
private T max;
public MinMax(T min, T max) {
this.min = min;
this.max = max;
}
public T getMin() {
return min;
}
public void setMin(T min) {
this.min = min;
}
public T getMax() {
return max;
}
public void setMax(T max) {
this.max = max;
}
}
}
@@ -0,0 +1,32 @@
package de.champonthis.buntspecht.controller.model;
public class UserPasswordModel {
private String old;
private String password;
private String password2;
public String getOld() {
return old;
}
public void setOld(String old) {
this.old = old;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword2() {
return password2;
}
public void setPassword2(String password2) {
this.password2 = password2;
}
}
@@ -0,0 +1,23 @@
package de.champonthis.buntspecht.controller.support;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = { EntityResponseStatusException.class })
protected ResponseEntity<Object> handleResponseEntityStatusException(RuntimeException exception,
WebRequest request) {
EntityResponseStatusException entityResponseStatusException = (EntityResponseStatusException) exception;
return handleExceptionInternal(exception, entityResponseStatusException.getBody(), new HttpHeaders(),
entityResponseStatusException.getStatus(), request);
}
}
@@ -0,0 +1,56 @@
package de.champonthis.buntspecht.controller.support;
import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import jakarta.annotation.Nullable;
public class EntityResponseStatusException extends NestedRuntimeException {
private static final long serialVersionUID = 1L;
private final HttpStatus status;
@Nullable
private final Object body;
public EntityResponseStatusException(HttpStatus status) {
this(null, status);
}
public EntityResponseStatusException(@Nullable Object body, HttpStatus status) {
this(body, status, null);
}
public EntityResponseStatusException(@Nullable Object body, HttpStatus status, @Nullable Throwable cause) {
super(null, cause);
Assert.notNull(status, "HttpStatus is required");
this.status = status;
this.body = body;
}
public HttpStatus getStatus() {
return this.status;
}
@Nullable
public Object getBody() {
return this.body;
}
/*
* @see org.springframework.core.NestedRuntimeException#getMessage()
*/
@Override
public String getMessage() {
return this.status + (this.body != null ? " \"" + this.body + "\"" : "");
}
}
@@ -0,0 +1,101 @@
package de.champonthis.buntspecht.controller.support;
import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.google.gson.Gson;
import com.google.gson.JsonPrimitive;
@ControllerAdvice
public class JsonStringBodyControllerAdvice implements RequestBodyAdvice, ResponseBodyAdvice<String> {
private Gson gson = new Gson();
/*
* @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* supports(org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return targetType instanceof Class && String.class.equals((Class<?>) targetType);
}
/*
* @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* beforeBodyRead(org.springframework.http.HttpInputMessage,
* org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
/*
* @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* afterBodyRead(java.lang.Object, org.springframework.http.HttpInputMessage,
* org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
body = ((String) body).replaceAll("^\"|\"$", "");
return body;
}
/*
* @see org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* handleEmptyBody(java.lang.Object, org.springframework.http.HttpInputMessage,
* org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
/*
* @see
* org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#
* supports(org.springframework.core.MethodParameter, java.lang.Class)
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return converterType == StringHttpMessageConverter.class;
}
/*
* @see
* org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#
* beforeBodyWrite(java.lang.Object, org.springframework.core.MethodParameter,
* org.springframework.http.MediaType, java.lang.Class,
* org.springframework.http.server.ServerHttpRequest,
* org.springframework.http.server.ServerHttpResponse)
*/
@Override
public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return gson.toJson(new JsonPrimitive(body));
}
}
@@ -0,0 +1,37 @@
package de.champonthis.buntspecht.controller.support;
import org.springframework.lang.Nullable;
import org.springframework.validation.AbstractBindingResult;
public class RequestBodyErrors extends AbstractBindingResult {
@Nullable
private final Object target;
public RequestBodyErrors(@Nullable Object target) {
super("request-body");
this.target = target;
}
/*
* @see org.springframework.validation.AbstractBindingResult#getTarget()
*/
@Override
public Object getTarget() {
return target;
}
/*
* @see
* org.springframework.validation.AbstractBindingResult#getActualFieldValue(java
* .lang.String)
*/
@Override
protected Object getActualFieldValue(String field) {
// Not necessary
return null;
}
}
@@ -0,0 +1,85 @@
package de.champonthis.buntspecht.controller.validation;
import java.util.ArrayList;
import java.util.List;
import org.passay.CharacterRule;
import org.passay.EnglishCharacterData;
import org.passay.LengthRule;
import org.passay.PasswordData;
import org.passay.PasswordValidator;
import org.passay.Rule;
import org.passay.RuleResult;
import org.passay.RuleResultDetail;
import org.passay.WhitespaceRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import de.champonthis.buntspecht.businesslogic.SystemPropertyManager;
import de.champonthis.buntspecht.controller.model.UserPasswordModel;
@Component
public class PasswordModelValidator implements Validator {
@Autowired
private SystemPropertyManager systemPropertyManager;
public static final String SYSTEM_PROPERTY_PASSWORD_RULE_WHITESPACE = "password.rule.whitespace";
public static final String SYSTEM_PROPERTY_PASSWORD_RULE_LENGTH = "password.rule.length";
public static final String SYSTEM_PROPERTY_PASSWORD_RULE_UPPERCASE = "password.rule.uppercase";
public static final String SYSTEM_PROPERTY_PASSWORD_RULE_DIGIT = "password.rule.digit";
public static final String SYSTEM_PROPERTY_PASSWORD_RULE_SPECIAL = "password.rule.special";
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(UserPasswordModel.class);
}
@Override
public void validate(Object target, Errors errors) {
UserPasswordModel passwordModel = (UserPasswordModel) target;
List<Rule> rules = new ArrayList<Rule>();
if (systemPropertyManager.getBoolean(SYSTEM_PROPERTY_PASSWORD_RULE_WHITESPACE, true)) {
rules.add(new WhitespaceRule());
}
int length = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_LENGTH, 8);
if (length > 0) {
rules.add(new LengthRule(length, 4096));
}
int uppercase = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_UPPERCASE, 1);
if (uppercase > 0) {
rules.add(new CharacterRule(EnglishCharacterData.UpperCase, uppercase));
}
int digit = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_DIGIT, 1);
if (digit > 0) {
rules.add(new CharacterRule(EnglishCharacterData.Digit, digit));
}
int special = systemPropertyManager.getInteger(SYSTEM_PROPERTY_PASSWORD_RULE_SPECIAL, 1);
if (special > 0) {
rules.add(new CharacterRule(EnglishCharacterData.Special, special));
}
PasswordValidator validator = new PasswordValidator(rules);
PasswordData password = new PasswordData(passwordModel.getPassword());
RuleResult result = validator.validate(password);
if (!result.isValid()) {
for (RuleResultDetail ruleResultDetail : result.getDetails()) {
errors.rejectValue("password", ruleResultDetail.getErrorCode());
}
}
if (!passwordModel.getPassword().equals(passwordModel.getPassword2())) {
errors.rejectValue("password2", "NOT_MATCH");
}
}
}
@@ -0,0 +1,38 @@
package de.champonthis.buntspecht.controller.validation;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import de.champonthis.buntspecht.model.Turnover;
@Component
public class TurnoverValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Turnover.class);
}
@Override
public void validate(Object target, Errors errors) {
Turnover turnover = (Turnover) target;
if (!StringUtils.hasText(turnover.getCustomer())) {
errors.rejectValue("customer", "REQUIRED");
}
if (!StringUtils.hasText(turnover.getMotif())) {
errors.rejectValue("motif", "REQUIRED");
}
if (turnover.getPrice() == 0) {
errors.rejectValue("price", "REQUIRED");
}
if (turnover.getPrice() < 0) {
errors.rejectValue("price", "POSITIVE_VALUE");
}
}
}
@@ -0,0 +1,143 @@
package de.champonthis.buntspecht.i18n.businesslogic;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.champonthis.buntspecht.i18n.model.I18n;
import de.champonthis.buntspecht.i18n.repository.I18nRepository;
@Component
public class I18nManager implements SmartInitializingSingleton {
private Logger logger = LoggerFactory.getLogger(I18nManager.class);
@Autowired
private I18nRepository i18nRepository;
@Autowired
private ResourceLoader resourceLoader;
private Gson gson = new Gson();
public I18n get(String locale) {
return i18nRepository.findById(locale).orElse(null);
}
public JsonObject getLabel(String locale) {
I18n i18n = get(locale);
if (i18n != null && StringUtils.hasText(i18n.getLabel())) {
JsonElement element = JsonParser.parseString(i18n.getLabel());
if (element != null && element.isJsonObject()) {
return element.getAsJsonObject();
}
}
return null;
}
public List<String> getLocales() {
return i18nRepository.findAll().stream().map(I18n::getLocale).collect(Collectors.toList());
}
protected void extendJsonObject(JsonObject dest, JsonObject src) {
for (Entry<String, JsonElement> srcEntry : src.entrySet()) {
String srcKey = srcEntry.getKey();
JsonElement srcValue = srcEntry.getValue();
if (dest.has(srcKey)) {
JsonElement destValue = dest.get(srcKey);
if (destValue.isJsonObject() && srcValue.isJsonObject()) {
extendJsonObject(destValue.getAsJsonObject(), srcValue.getAsJsonObject());
} else {
dest.add(srcKey, srcValue);
}
} else {
dest.add(srcKey, srcValue);
}
}
}
public I18n addLabel(String locale, JsonObject newLabel) {
JsonObject label = getLabel(locale);
if (label == null || label.size() == 0 || label.entrySet().isEmpty()) {
label = newLabel;
} else {
extendJsonObject(label, newLabel);
}
I18n i18n = new I18n();
i18n.setLocale(locale);
i18n.setLabel(gson.toJson(label));
return i18nRepository.save(i18n);
}
public I18n setLabel(String locale, JsonObject label) {
I18n i18n = new I18n();
i18n.setLocale(locale);
i18n.setLabel(gson.toJson(label));
return i18nRepository.save(i18n);
}
public void deleteLabel(String locale) {
if (i18nRepository.existsById(locale)) {
i18nRepository.deleteById(locale);
}
}
/*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
@Override
public void afterSingletonsInstantiated() {
try {
Resource resource = resourceLoader.getResource("classpath:label");
if (resource.exists()) {
File labelFolder = resource.getFile();
if (labelFolder.exists() && labelFolder.isDirectory()) {
for (File labelFile : labelFolder.listFiles()) {
JsonObject label = JsonParser.parseReader(new FileReader(labelFile, StandardCharsets.UTF_8))
.getAsJsonObject();
String locale = labelFile.getName().replace(".json", "");
addLabel(locale, label);
}
}
}
} catch (IOException e) {
logger.warn("cannot read in label folder", e.getMessage());
}
}
}
@@ -0,0 +1,83 @@
package de.champonthis.buntspecht.i18n.controller;
import java.io.IOException;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import de.champonthis.buntspecht.controller.BaseController;
import de.champonthis.buntspecht.i18n.businesslogic.I18nManager;
@RestController
@RequestMapping("/i18n")
public class I18nController extends BaseController {
@Autowired
private I18nManager i18nManager;
private Gson gson = new Gson();
@GetMapping
public List<String> getLocales() {
return i18nManager.getLocales();
}
@GetMapping("/{locale}")
public void getLabel(@PathVariable("locale") String locale, HttpServletResponse response)
throws JsonIOException, IOException {
JsonObject label = i18nManager.getLabel(locale);
if (label != null) {
response.setCharacterEncoding("utf-8");
gson.toJson(label, response.getWriter());
}
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/{locale}")
public void setLabel(@PathVariable("locale") String locale, @RequestBody Object label) {
JsonElement element = gson.toJsonTree(label);
if (element != null && element.isJsonObject()) {
i18nManager.setLabel(locale, element.getAsJsonObject());
}
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PutMapping("/{locale}")
public void addLabel(@PathVariable("locale") String locale, @RequestBody Object label) {
JsonElement element = gson.toJsonTree(label);
if (element != null && element.isJsonObject()) {
i18nManager.addLabel(locale, element.getAsJsonObject());
}
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{locale}")
public void deleteLocale(@PathVariable("locale") String locale) {
i18nManager.deleteLabel(locale);
}
}
@@ -0,0 +1,43 @@
package de.champonthis.buntspecht.i18n.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
@Entity
@Table(name = "i18n", uniqueConstraints = @UniqueConstraint(columnNames = { "locale" }))
public class I18n {
@Id
@Column(name = "locale", unique = true, nullable = false)
private String locale;
@Lob
@Column(name = "label", length = 100000)
private String label;
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}
@@ -0,0 +1,13 @@
package de.champonthis.buntspecht.i18n.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.champonthis.buntspecht.i18n.model.I18n;
@Repository
public interface I18nRepository extends JpaRepository<I18n, String>, QuerydslPredicateExecutor<I18n> {
}
@@ -0,0 +1,56 @@
package de.champonthis.buntspecht.model;
import java.time.Instant;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "persistent_logins")
public class PersistentLogin {
@Column(name = "username", length = 64, nullable = false)
private String username;
@Id
@Column(name = "series", length = 64)
private String series;
@Column(name = "token", length = 64, nullable = false)
private String token;
@Column(name = "last_used", nullable = false)
private Instant last_used;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getSeries() {
return series;
}
public void setSeries(String series) {
this.series = series;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Instant getLast_used() {
return last_used;
}
public void setLast_used(Instant last_used) {
this.last_used = last_used;
}
}
@@ -0,0 +1,53 @@
package de.champonthis.buntspecht.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
@Entity
@Table(name = "system_properties")
public class SystemProperty {
@Id
@Column(name = "id")
private String key;
@Lob
@Column(name = "value", length = 100000)
private String value;
public SystemProperty() {
super();
}
public SystemProperty(String key, String value) {
super();
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
@@ -0,0 +1,171 @@
package de.champonthis.buntspecht.model;
import java.time.Instant;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
@Entity
@Table(name = "turnovers")
public class Turnover {
@Id
@Column(name = "id", updatable = false, unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "created", nullable = false, updatable = false)
private Instant created;
@Column(name = "due_date", nullable = false)
private Instant dueDate;
@Column(name = "updated", nullable = false)
private Instant updated;
@Column(name = "customer", nullable = false)
private String customer;
@Column(name = "motif", nullable = false)
private String motif;
@Column(name = "price", nullable = false)
private float price;
@Column(name = "time_investment", nullable = true)
private float timeInvestment;
@Lob
@Column(name = "remark", nullable = true, length = 5000)
private String remark;
@Lob
@Column(name = "material_consumption", nullable = true, length = 5000)
private String materialConsumption;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Instant getCreated() {
return created;
}
public void setCreated(Instant created) {
this.created = created;
}
public Instant getDueDate() {
return dueDate;
}
public void setDueDate(Instant dueDate) {
this.dueDate = dueDate;
}
public Instant getUpdated() {
return updated;
}
public void setUpdated(Instant updated) {
this.updated = updated;
}
public String getCustomer() {
return customer;
}
public void setCustomer(String customer) {
this.customer = customer;
}
public String getMotif() {
return motif;
}
public void setMotif(String motif) {
this.motif = motif;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public float getTimeInvestment() {
return timeInvestment;
}
public void setTimeInvestment(float timeInvestment) {
this.timeInvestment = timeInvestment;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getMaterialConsumption() {
return materialConsumption;
}
public void setMaterialConsumption(String materialConsumption) {
this.materialConsumption = materialConsumption;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Turnover)) {
return false;
}
Turnover turnover = (Turnover) obj;
boolean equals = true;
equals &= dueDate.equals(turnover.getDueDate());
equals &= id == null && turnover.getId() == null || id.equals(turnover.getId());
equals &= username == null && turnover.getUsername() == null || username.equals(turnover.getUsername());
equals &= customer == null && turnover.getCustomer() == null || customer.equals(turnover.getCustomer());
equals &= motif == null && turnover.getMotif() == null || motif.equals(turnover.getMotif());
equals &= price == turnover.getPrice();
equals &= timeInvestment == turnover.getTimeInvestment();
equals &= remark == null && turnover.getRemark() == null || remark.equals(turnover.getRemark());
equals &= materialConsumption == null && turnover.getMaterialConsumption() == null
|| materialConsumption.equals(turnover.getMaterialConsumption());
return equals;
}
}
@@ -0,0 +1,133 @@
package de.champonthis.buntspecht.model;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
@Entity
@Table(name = "users")
@JsonInclude(Include.NON_EMPTY)
public class User {
@Id
@Column(name = "username", nullable = false)
private String username;
@Column(name = "external_id", nullable = true)
private String externalId;
@Column(name = "name", nullable = true)
private String name;
@JsonIgnore
@Column(name = "password_hash", nullable = true)
private String passwordHash;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "users_roles")
private List<String> roles;
@Lob
@Column(name = "about", nullable = true, length = 100000)
private String about;
@Column(name = "email", nullable = true)
private String email;
@Column(name = "locale", nullable = true, columnDefinition = "varchar(255) default 'de'")
private String locale;
@Column(name = "dark_theme", nullable = true, columnDefinition = "boolean default false")
private boolean darkTheme;
@Transient
private Map<String, Object> metadata;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public String getAbout() {
return about;
}
public void setAbout(String about) {
this.about = about;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
public boolean isDarkTheme() {
return darkTheme;
}
public void setDarkTheme(boolean darkTheme) {
this.darkTheme = darkTheme;
}
public Map<String, Object> getMetadata() {
if (metadata == null) {
metadata = Map.of();
}
return metadata;
}
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
}
@@ -0,0 +1,14 @@
package de.champonthis.buntspecht.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.champonthis.buntspecht.model.SystemProperty;
@Repository
public interface SystemPropertyRepository
extends JpaRepository<SystemProperty, String>, QuerydslPredicateExecutor<SystemProperty> {
}
@@ -0,0 +1,12 @@
package de.champonthis.buntspecht.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.champonthis.buntspecht.model.Turnover;
@Repository
public interface TurnoverRepository extends JpaRepository<Turnover, Long>, QuerydslPredicateExecutor<Turnover> {
}
@@ -0,0 +1,13 @@
package de.champonthis.buntspecht.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.champonthis.buntspecht.model.User;
@Repository
public interface UserRepository extends JpaRepository<User, String>, QuerydslPredicateExecutor<User> {
}
@@ -0,0 +1,37 @@
package de.champonthis.buntspecht.security;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
public class LocalRememberMeServices extends PersistentTokenBasedRememberMeServices {
public LocalRememberMeServices(String key, UserDetailsService userDetailsService,
PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
/*
* @see org.springframework.security.web.authentication.rememberme.
* AbstractRememberMeServices#rememberMeRequested(javax.servlet.http.
* HttpServletRequest, java.lang.String)
*/
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
Object value = request.getAttribute(parameter);
if (value != null) {
String paramValue = value.toString();
if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
return true;
}
}
return super.rememberMeRequested(request, parameter);
}
}
@@ -0,0 +1,19 @@
package de.champonthis.buntspecht.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
public class LocalUserDetails extends User {
private static final long serialVersionUID = 1L;
public LocalUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
}
@@ -0,0 +1,15 @@
package de.champonthis.buntspecht.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean(name = "passwordEncoder")
public Argon2PasswordEncoder passwordEncoder() {
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}
}
@@ -0,0 +1,113 @@
package de.champonthis.buntspecht.security;
import java.util.Collections;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import de.champonthis.buntspecht.businesslogic.UserManager;
import de.champonthis.buntspecht.security.handler.FormAuthenticationFailureHandler;
import de.champonthis.buntspecht.security.handler.OAuth2AuthenticationSuccessHandler;
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {
@Autowired
private UserManager userManager;
@Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Value("${loginUrl:/login}")
private String loginUrl;
@Value("${loginTargetUrl:/}")
private String loginTargetUrl;
@Value("${spring.security.oauth2.client:false}")
private boolean oauth2Enabled;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
if (oauth2Enabled) {
oAuth2AuthenticationSuccessHandler.setDefaultTargetUrl(loginTargetUrl);
oAuth2AuthenticationSuccessHandler.setRememberMeServices(rememberMeServices());
}
http
// crsf
.csrf((csrf) -> csrf.disable())
// cors
// .cors().configurationSource(corsConfigurationSource()).and()
// anonymous
.anonymous((anonymous) -> anonymous.disable())
// login
.formLogin((formLogin) -> formLogin.loginPage("/login").defaultSuccessUrl(loginTargetUrl)
.failureHandler(new FormAuthenticationFailureHandler(loginUrl)))
// remember me
.rememberMe((rememberMe) -> rememberMe.rememberMeServices(rememberMeServices()))
// logout
.logout((logout) -> logout.logoutUrl("/logout")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)))
// exception
.exceptionHandling((exceptionHandling) -> exceptionHandling
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/api/**")));
if (oauth2Enabled) {
http.oauth2Login((oauth2Login) -> oauth2Login.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(new SimpleUrlAuthenticationFailureHandler(loginUrl + "?externalError"))
.loginPage("/login"));
}
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Bean
public RememberMeServices rememberMeServices() {
PersistentTokenBasedRememberMeServices rememberMeServices = new LocalRememberMeServices("remember-me",
userManager, persistentTokenRepository());
return rememberMeServices;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@@ -0,0 +1,28 @@
package de.champonthis.buntspecht.security.handler;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class FormAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private String failureUrl;
public FormAuthenticationFailureHandler(String failureUrl) {
super(failureUrl);
this.failureUrl = failureUrl;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
setDefaultFailureUrl(failureUrl + "?error&username=" + request.getParameter("username"));
super.onAuthenticationFailure(request, response, exception);
setDefaultFailureUrl(failureUrl);
}
}
@@ -0,0 +1,68 @@
package de.champonthis.buntspecht.security.handler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import de.champonthis.buntspecht.businesslogic.UserManager;
import de.champonthis.buntspecht.model.User;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class OAuth2AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private UserManager userManager;
private RememberMeServices rememberMeServices;
/*
* @see org.springframework.security.web.authentication.
* SavedRequestAwareAuthenticationSuccessHandler#onAuthenticationSuccess(javax.
* servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
User user = userManager.getByAuth(authentication);
UserDetails userDetails = userManager.loadUserByUsername(user.getUsername());
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.addAll(authentication.getAuthorities());
authorities.addAll(userDetails.getAuthorities());
UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(userDetails,
null, authorities);
SecurityContextHolder.getContext().setAuthentication(newAuthentication);
if (rememberMeServices != null) {
request.setAttribute("remember-me", "true");
rememberMeServices.loginSuccess(request, response, newAuthentication);
}
handle(request, response, newAuthentication);
clearAuthenticationAttributes(request);
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
this.rememberMeServices = rememberMeServices;
}
}