Initial commit.

This commit is contained in:
Steve Schafer 2025-07-13 09:59:20 -06:00
parent c70b66057a
commit 373708f991
57 changed files with 2408 additions and 2 deletions

50
.classpath Normal file
View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="test" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

74
.factorypath Normal file
View file

@ -0,0 +1,74 @@
<factorypath>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-web/2.1.12.RELEASE/spring-boot-starter-web-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter/2.1.12.RELEASE/spring-boot-starter-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot/2.1.12.RELEASE/spring-boot-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-autoconfigure/2.1.12.RELEASE/spring-boot-autoconfigure-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-logging/2.1.12.RELEASE/spring-boot-starter-logging-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-to-slf4j/2.11.2/log4j-to-slf4j-2.11.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-api/2.11.2/log4j-api-2.11.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-core/5.1.13.RELEASE/spring-core-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jcl/5.1.13.RELEASE/spring-jcl-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/yaml/snakeyaml/1.23/snakeyaml-1.23.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-json/2.1.12.RELEASE/spring-boot-starter-json-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.9.10/jackson-datatype-jdk8-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.9.10/jackson-datatype-jsr310-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/module/jackson-module-parameter-names/2.9.10/jackson-module-parameter-names-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-tomcat/2.1.12.RELEASE/spring-boot-starter-tomcat-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-core/9.0.30/tomcat-embed-core-9.0.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-el/9.0.30/tomcat-embed-el-9.0.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.30/tomcat-embed-websocket-9.0.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/validator/hibernate-validator/6.0.18.Final/hibernate-validator-6.0.18.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/validation/validation-api/2.0.1.Final/validation-api-2.0.1.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/logging/jboss-logging/3.3.3.Final/jboss-logging-3.3.3.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/classmate/1.4.0/classmate-1.4.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-web/5.1.13.RELEASE/spring-web-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-beans/5.1.13.RELEASE/spring-beans-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-webmvc/5.1.13.RELEASE/spring-webmvc-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context/5.1.13.RELEASE/spring-context-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-expression/5.1.13.RELEASE/spring-expression-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-data-jpa/2.1.12.RELEASE/spring-boot-starter-data-jpa-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-aop/2.1.12.RELEASE/spring-boot-starter-aop-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/aspectj/aspectjweaver/1.9.5/aspectjweaver-1.9.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/transaction/javax.transaction-api/1.3/javax.transaction-api-1.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/activation/javax.activation-api/1.2.0/javax.activation-api-1.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/hibernate-core/5.3.15.Final/hibernate-core-5.3.15.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/persistence/javax.persistence-api/2.2/javax.persistence-api-2.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/javassist/javassist/3.23.2-GA/javassist-3.23.2-GA.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/net/bytebuddy/byte-buddy/1.9.16/byte-buddy-1.9.16.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/antlr/antlr/2.7.7/antlr-2.7.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/jandex/2.0.5.Final/jandex-2.0.5.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/dom4j/dom4j/2.1.1/dom4j-2.1.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/common/hibernate-commons-annotations/5.0.4.Final/hibernate-commons-annotations-5.0.4.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-runtime/2.3.1/jaxb-runtime-2.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/txw2/2.3.1/txw2-2.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/istack/istack-commons-runtime/3.0.7/istack-commons-runtime-3.0.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jvnet/staxex/stax-ex/1.8/stax-ex-1.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/xml/fastinfoset/FastInfoset/1.2.15/FastInfoset-1.2.15.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-jpa/2.1.15.RELEASE/spring-data-jpa-2.1.15.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-commons/2.1.15.RELEASE/spring-data-commons-2.1.15.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-orm/5.1.13.RELEASE/spring-orm-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-tx/5.1.13.RELEASE/spring-tx-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aspects/5.1.13.RELEASE/spring-aspects-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-jdbc/2.1.12.RELEASE/spring-boot-starter-jdbc-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/zaxxer/HikariCP/3.2.0/HikariCP-3.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jdbc/5.1.13.RELEASE/spring-jdbc-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-security/2.1.12.RELEASE/spring-boot-starter-security-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aop/5.1.13.RELEASE/spring-aop-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-config/5.1.7.RELEASE/spring-security-config-5.1.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-core/5.1.7.RELEASE/spring-security-core-5.1.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-web/5.1.7.RELEASE/spring-security-web-5.1.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.16/lombok-1.18.16.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/jsonwebtoken/jjwt/0.9.0/jjwt-0.9.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-databind/2.9.10.2/jackson-databind-2.9.10.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-annotations/2.9.10/jackson-annotations-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-core/2.9.10/jackson-core-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/mysql/mysql-connector-java/8.0.19/mysql-connector-java-8.0.19.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/joda-time/joda-time/2.10.5/joda-time-2.10.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/commons/commons-csv/1.5/commons-csv-1.5.jar" enabled="true" runInBatchMode="false"/>
</factorypath>

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/logs/
/run-local
*.log
/target/

39
.project Normal file
View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>budget-api</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
<filteredResources>
<filter>
<id>1626184189329</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -0,0 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding/<project>=UTF-8

View file

@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations
org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations

View file

@ -0,0 +1,14 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.processAnnotations=enabled
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=17

View file

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View file

@ -0,0 +1,2 @@
boot.validation.initialized=true
eclipse.preferences.version=1

View file

@ -1,3 +1,6 @@
# com.stephenschafer.budget.api
# Budget REST API
Provides a REST API to support my React Budget Application.
Built from this example as a starting point: [Spring Boot Security JWT Authentication](http://www.devglan.com/spring-security/spring-boot-jwt-auth)
Back-end to budget react app

11
build Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh
cd "$(dirname "${BASH_SOURCE[0]}")"
ROOT=$(pwd)
./stop
# export JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
mkdir -p logs
if ! mvn clean package > logs/build.log 2> logs/build.err.log; then
echo "build failed"
exit 1
fi
echo "success"

5
deploy Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
cd "$(dirname "${BASH_SOURCE[0]}")"
ssh pi@raspi "./budget/backup"
scp $(find target -name "*.jar") pi@raspi:~/budget
ssh pi@raspi "./budget/start"

6
eclipse Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
cd "$(dirname "${BASH_SOURCE[0]}")"
LOG=./logs
mkdir -p $LOG
/home/eclipse/sts-4.30.0.RELEASE/SpringToolSuite4 -data .. \
>$LOG/eclipse-sts.log 2>$LOG/eclipse-sts.err.log &

98
pom.xml Normal file
View file

@ -0,0 +1,98 @@
<?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>com.stephenschafer</groupId>
<artifactId>budget</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.7</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.9</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<!-- Temporary explicit version to fix Thymeleaf bug -->
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<properties>
<java.version>17</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

33
run Executable file
View file

@ -0,0 +1,33 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
ROOT=$(pwd)
./stop
mkdir -p logs
rm -f $ROOT/logs/run-*.log
SUSPEND="n"
ARGS=""
while (( "$#" )); do
case $1 in
suspend)
SUSPEND="y"
;;
init)
ARGS="init"
;;
*)
echo "Unrecognized argument"
exit 1
;;
esac
shift
done
JVM_ARGS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=$SUSPEND,address=8004"
# JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
$JAVA_HOME/bin/java $JVM_ARGS -jar $(find target -name "*.jar") $ARGS\
--server.port=$PORT\
--spring.datasource.url=$DB_URL\
--spring.datasource.username=$DB_USERNAME\
--spring.datasource.password=$DB_PASSWORD\
> $ROOT/logs/run-budget.log 2> $ROOT/logs/run-budget.err.log &
echo "$!" > $ROOT/logs/run-budget.pid
echo "running"

87
spring-boot-jwt.iml Normal file
View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="web" name="Web">
<configuration>
<webroots>
<root url="file://$MODULE_DIR$/src/main/webapp" relative="/" />
</webroots>
</configuration>
</facet>
<facet type="Spring" name="Spring">
<configuration />
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-web:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-logging:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.3" level="project" />
<orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.3" level="project" />
<orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.10.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-api:2.10.0" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:jul-to-slf4j:1.7.25" level="project" />
<orderEntry type="library" name="Maven: javax.annotation:javax.annotation-api:1.3.2" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-jcl:5.0.5.RELEASE" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.yaml:snakeyaml:1.19" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-json:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.5" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.5" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.9.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-tomcat:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-core:8.5.29" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-el:8.5.29" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-websocket:8.5.29" level="project" />
<orderEntry type="library" name="Maven: org.hibernate.validator:hibernate-validator:6.0.9.Final" level="project" />
<orderEntry type="library" name="Maven: javax.validation:validation-api:2.0.1.Final" level="project" />
<orderEntry type="library" name="Maven: org.jboss.logging:jboss-logging:3.3.2.Final" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml:classmate:1.3.4" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-web:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-beans:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-webmvc:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-expression:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-data-jpa:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-aop:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.aspectj:aspectjweaver:1.8.13" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-jdbc:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: com.zaxxer:HikariCP:2.7.8" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-jdbc:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.hibernate:hibernate-core:5.2.16.Final" level="project" />
<orderEntry type="library" name="Maven: org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" level="project" />
<orderEntry type="library" name="Maven: org.javassist:javassist:3.22.0-GA" level="project" />
<orderEntry type="library" name="Maven: antlr:antlr:2.7.7" level="project" />
<orderEntry type="library" name="Maven: org.jboss:jandex:2.0.3.Final" level="project" />
<orderEntry type="library" name="Maven: dom4j:dom4j:1.6.1" level="project" />
<orderEntry type="library" name="Maven: org.hibernate.common:hibernate-commons-annotations:5.0.1.Final" level="project" />
<orderEntry type="library" name="Maven: javax.transaction:javax.transaction-api:1.2" level="project" />
<orderEntry type="library" name="Maven: org.springframework.data:spring-data-jpa:2.0.6.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.data:spring-data-commons:2.0.6.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-orm:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-tx:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.25" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-aspects:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-security:2.0.1.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-aop:5.0.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.security:spring-security-config:5.0.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.security:spring-security-core:5.0.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.security:spring-security-web:5.0.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: io.jsonwebtoken:jjwt:0.9.0" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.9.5" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.9.0" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.9.5" level="project" />
<orderEntry type="library" name="Maven: mysql:mysql-connector-java:5.1.46" level="project" />
</component>
</module>

View file

@ -0,0 +1,14 @@
package com.stephenschafer.budget;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class ApiResponse<T> {
private int status;
private String message;
private T result;
}

View file

@ -0,0 +1,55 @@
package com.stephenschafer.budget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootApplication
public class Application {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
CommandLineRunner init(final UserDao userDao) {
return args -> {
if (args.length >= 1 && args[0].equals("init")) {
final UserEntity user1 = new UserEntity();
user1.setFirstName("Devglan");
user1.setLastName("Devglan");
user1.setUsername("devglan");
user1.setPassword(passwordEncoder.encode("devglan"));
userDao.save(user1);
final UserEntity user2 = new UserEntity();
user2.setFirstName("John");
user2.setLastName("Doe");
user2.setUsername("john");
user2.setPassword(passwordEncoder.encode("john"));
userDao.save(user2);
}
else {
}
};
}
@Bean
RegexDao getRegexDao() {
return new RegexDaoImpl();
}
@Bean
CategoryDao getCategoryDao() {
return new CategoryDaoImpl();
}
@Bean
TransactionDao getTransactionDao() {
return new TransactionDaoImpl();
}
}

View file

@ -0,0 +1,15 @@
package com.stephenschafer.budget;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class AuthToken {
private String token;
private String username;
}

View file

@ -0,0 +1,49 @@
package com.stephenschafer.budget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.CrossOrigin;
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;
@CrossOrigin
@RestController
@RequestMapping("/token")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
@CrossOrigin
@PostMapping("/generate-token")
public ApiResponse<AuthToken> register(@RequestBody final LoginUser loginUser)
throws AuthenticationException {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginUser.getUsername(), loginUser.getPassword()));
final UserEntity user = userService.findByUsername(loginUser.getUsername());
final String token = jwtTokenUtil.generateToken(user);
return new ApiResponse<>(200, "success", new AuthToken(token, user.getUsername()));
}
@CrossOrigin
@PostMapping("/register")
public ApiResponse<AuthToken> saveUser(@RequestBody final UserDto loginUser) {
final UserEntity userEntity = userService.findByUsername(loginUser.getUsername());
if (userEntity != null) {
return new ApiResponse<>(400, "username already exists", null);
}
userService.save(loginUser);
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginUser.getUsername(), loginUser.getPassword()));
final UserEntity user = userService.findByUsername(loginUser.getUsername());
final String token = jwtTokenUtil.generateToken(user);
return new ApiResponse<>(200, "success", new AuthToken(token, user.getUsername()));
}
}

View file

@ -0,0 +1,23 @@
package com.stephenschafer.budget;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Category {
public Category() {
}
public Category(final int intValue, final Category category) {
this(intValue, category.getParentCategoryId(), category.getName());
}
Integer id;
Integer parentCategoryId;
String name;
}

View file

@ -0,0 +1,124 @@
package com.stephenschafer.budget;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class CategoryController {
@Autowired
private CategoryDao categoryDao;
@Autowired
private UserService userService;
@PostMapping("/categories")
@ResponseBody
public ApiResponse<Category> postCategory(@RequestBody final Category category,
final HttpServletRequest req) {
if (!userService.isAuthorized(req)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
return new ApiResponse<>(HttpStatus.OK.value(), "Category inserted successfully",
categoryDao.add(category));
}
@PutMapping("/categories")
@ResponseBody
public ApiResponse<Category> putCategory(@RequestBody final Category category,
final HttpServletRequest req) {
if (!userService.isAuthorized(req)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
categoryDao.update(category);
return new ApiResponse<>(HttpStatus.OK.value(), "Category updated successfully", category);
}
@GetMapping("/categories")
@ResponseBody
public ApiResponse<List<Category>> getCategories(final HttpServletRequest request) {
log.info("GET /categories");
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final List<Category> categories = new ArrayList<>();
categoryDao.getAll(category -> {
categories.add(category);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Category list fetched successfully",
categories);
}
@GetMapping("/categories/parent/{parentCategoryId}")
@ResponseBody
public ApiResponse<List<Category>> getCategoriesByCategory(
@PathVariable(required = true) final Integer parentCategoryId,
final HttpServletRequest request) {
log.info("GET /categories/parent/" + parentCategoryId);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final Optional<Category> parentCategory = categoryDao.getById(parentCategoryId);
if (!parentCategory.isPresent()) {
return new ApiResponse<>(HttpStatus.NOT_FOUND.value(), "Parent category not found",
null);
}
final int categoryId = parentCategory.get().getId();
final List<Category> categories = new ArrayList<>();
categoryDao.getByParent(categoryId, category -> {
categories.add(category);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Category list fetched successfully",
categories);
}
@GetMapping("/categories/root")
@ResponseBody
public ApiResponse<List<Category>> getRootCategories(final HttpServletRequest request) {
log.info("GET /categories/root");
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final List<Category> categories = new ArrayList<>();
categoryDao.getRoot(category -> {
categories.add(category);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Category list fetched successfully",
categories);
}
@GetMapping("/categories/ancestry/{categoryId}")
@ResponseBody
public ApiResponse<List<Category>> getCategoryAncestry(
@PathVariable(required = true) final Integer categoryId,
final HttpServletRequest request) {
log.info("GET /categories/ancestry/" + categoryId);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final List<Category> ancestry = categoryDao.getCategoryAncestry(categoryId);
return new ApiResponse<>(HttpStatus.OK.value(), "Category ancestry fetched successfully",
ancestry);
}
}

View file

@ -0,0 +1,25 @@
package com.stephenschafer.budget;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public interface CategoryDao {
int deleteById(int id);
Optional<Category> getById(int id);
Optional<Category> getByName(String name);
List<Category> getCategoryAncestry(int categoryId);
Category add(Category category);
void update(Category category);
void getAll(Consumer<Category> consumer);
void getByParent(int parentCategoryId, Consumer<Category> consumer);
void getRoot(Consumer<Category> consumer);
}

View file

@ -0,0 +1,230 @@
package com.stephenschafer.budget;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CategoryDaoImpl implements CategoryDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int deleteById(final int id) {
return jdbcTemplate.update("delete from budget.category where id = ?", id);
}
@Override
public Optional<Category> getById(final int id) {
final String sql = "select coalesce(parent_category_id, 0), name"
+ " from budget.category where id = ?";
try {
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
final int parentCategoryId = rs.getInt(1);
final boolean parentCategoryIdWasNull = rs.wasNull();
final String name = rs.getString(2);
return Optional.of(new Category(id,
parentCategoryIdWasNull ? null : Integer.valueOf(parentCategoryId), name));
}, id);
}
catch (final DataAccessException e) {
return Optional.empty();
}
}
@Override
public Optional<Category> getByName(final String name) {
final String[] parts = name.split("\\.");
Category category = null;
for (final String part : parts) {
Optional<Category> optional;
if (category == null) {
final String sql = "select id"
+ " from budget.category where coalesce(parent_category_id, 0) = 0 and name = ?";
optional = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
final int id = rs.getInt(1);
return Optional.of(new Category(id, null, part));
}, part);
}
else {
final Integer parentId = category.getId();
final String sql = "select id"
+ " from budget.category where coalesce(parent_category_id, 0) = ? and name = ?";
optional = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
final int id = rs.getInt(1);
return Optional.of(new Category(id, parentId, part));
}, parentId, part);
}
if (!optional.isPresent()) {
return optional;
}
category = optional.get();
}
return Optional.of(category);
}
@Override
public List<Category> getCategoryAncestry(final int categoryId) {
return getCategoryAncestry(null, categoryId);
}
private List<Category> getCategoryAncestry(final List<Category> ancestry,
final int categoryId) {
final Optional<Category> optionalCategory = getById(categoryId);
if (!optionalCategory.isPresent()) {
return ancestry;
}
final Category category = optionalCategory.get();
final List<Category> result = new ArrayList<>();
result.add(category);
if (ancestry != null) {
result.addAll(ancestry);
}
if (category.parentCategoryId == null) {
return result;
}
return getCategoryAncestry(result, category.parentCategoryId);
}
@Override
public Category add(final Category category) {
log.info("CategoryDaoImpl.add " + category);
final String sql = "insert into budget.category" + " (parent_category_id, name)"
+ " values (?, ?)";
final PreparedStatementCreatorFactory factory = new PreparedStatementCreatorFactory(sql,
Types.INTEGER, Types.VARCHAR);
factory.setReturnGeneratedKeys(true);
factory.setGeneratedKeysColumnNames("id");
final PreparedStatementCreator creator = factory.newPreparedStatementCreator(
new Object[] { category.getParentCategoryId(), category.getName() });
final KeyHolder keyHolder = new GeneratedKeyHolder();
final int rowCount = jdbcTemplate.update(creator, keyHolder);
if (rowCount == 0) {
return null;
}
final Number generatedId = keyHolder.getKey();
return new Category(generatedId.intValue(), category);
}
@Override
public void update(final Category category) {
log.info("CategoryDaoImpl.update " + category);
final String sql = "update budget.category" + " set parent_category_id = ?, name = ?"
+ " where id = ?";
final PreparedStatementCreatorFactory factory = new PreparedStatementCreatorFactory(sql,
Types.INTEGER, Types.VARCHAR, Types.INTEGER);
PreparedStatementCreator creator;
try {
creator = factory.newPreparedStatementCreator(new Object[] {
category.getParentCategoryId(), category.getName(), category.getId() });
}
catch (final Exception e) {
log.error("failed to get creator");
e.printStackTrace();
return;
}
log.info("update creator = " + creator);
jdbcTemplate.update(creator);
}
private static class PreparedStatementHolder {
PreparedStatement statement;
}
@Override
public void getAll(final Consumer<Category> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select" + " id, coalesce(parent_category_id, 0), name"
+ " from budget.category" + " order by id";
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final int parentCategoryId = rs.getInt(++i);
final String name = rs.getString(++i);
consumer.accept(new Category(id, parentCategoryId, name));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getByCategory failed", e1);
}
}
}
@Override
public void getByParent(final int parentCategoryId, final Consumer<Category> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select" + " id, name" + " from budget.category"
+ " where coalesce(parent_category_id, 0) = ?" + " order by id";
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
holder.statement.setInt(1, parentCategoryId);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final String name = rs.getString(++i);
consumer.accept(new Category(id, parentCategoryId, name));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getByCategory failed", e1);
}
}
}
@Override
public void getRoot(final Consumer<Category> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select id, name from budget.category"
+ " where coalesce(parent_category_id, 0) = 0 order by id";
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final String name = rs.getString(++i);
consumer.accept(new Category(id, null, name));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getRoot failed", e1);
}
}
}
}

View file

@ -0,0 +1,8 @@
package com.stephenschafer.budget;
public class Constants {
public static final long ACCESS_TOKEN_VALIDITY_SECONDS = 24L * 60L * 60L;
public static final String SIGNING_KEY = "sschafer123r";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
}

View file

@ -0,0 +1,21 @@
package com.stephenschafer.budget;
import java.util.List;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class CustomCorsConfiguration implements CorsConfigurationSource {
@Override
public CorsConfiguration getCorsConfiguration(final HttpServletRequest request) {
final CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
return config;
}
}

View file

@ -0,0 +1,13 @@
package com.stephenschafer.budget;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
public ApiResponse<Void> handleNotFoundException(final RuntimeException ex) {
final ApiResponse<Void> apiResponse = new ApiResponse<>(400, "Bad request", null);
return apiResponse;
}
}

View file

@ -0,0 +1,9 @@
package com.stephenschafer.budget;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
public interface FileDao extends CrudRepository<FileEntity, Integer> {
Optional<FileEntity> findByName(String filename);
}

View file

@ -0,0 +1,29 @@
package com.stephenschafer.budget;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "file")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class FileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column
private String name;
}

View file

@ -0,0 +1,11 @@
package com.stephenschafer.budget;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FindProjectResult {
private int id;
private String code;
}

View file

@ -0,0 +1,26 @@
package com.stephenschafer.budget;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Override
public void commence(final HttpServletRequest request, final HttpServletResponse response,
final AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}

View file

@ -0,0 +1,69 @@
package com.stephenschafer.budget;
import static com.stephenschafer.budget.Constants.HEADER_STRING;
import static com.stephenschafer.budget.Constants.TOKEN_PREFIX;
import java.io.IOException;
import java.util.Arrays;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain) throws IOException, ServletException {
final String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX, "");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
}
catch (final IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
}
catch (final ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
}
catch (final SignatureException e) {
logger.error("Authentication Failed. Username or Password not valid.");
}
}
else {
logger.warn("couldn't find bearer string, will ignore the header");
}
req.setAttribute("username", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}

View file

@ -0,0 +1,65 @@
package com.stephenschafer.budget;
import static com.stephenschafer.budget.Constants.ACCESS_TOKEN_VALIDITY_SECONDS;
import static com.stephenschafer.budget.Constants.SIGNING_KEY;
import java.io.Serial;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Date;
import java.util.function.Function;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JwtTokenUtil implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
public String getUsernameFromToken(final String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(final String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(final String token, final Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(final String token) {
return Jwts.parser().setSigningKey(SIGNING_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(final String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(final UserEntity user) {
return doGenerateToken(user.getUsername());
}
private String doGenerateToken(final String subject) {
final Claims claims = Jwts.claims().setSubject(subject);
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
return Jwts.builder().setClaims(claims).setIssuer("http://stephenschafer.com").setIssuedAt(
new Date(System.currentTimeMillis())).setExpiration(
new Date(System.currentTimeMillis()
+ ACCESS_TOKEN_VALIDITY_SECONDS * 1000L)).signWith(SignatureAlgorithm.HS256,
SIGNING_KEY).compact();
}
public Boolean validateToken(final String token, final UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

View file

@ -0,0 +1,11 @@
package com.stephenschafer.budget;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoginUser {
private String username;
private String password;
}

View file

@ -0,0 +1,33 @@
package com.stephenschafer.budget;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class MyUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private final UserEntity user;
public MyUserDetails(final UserEntity user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
final SimpleGrantedAuthority authority = new SimpleGrantedAuthority(user.getRole());
return Arrays.asList(authority);
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
}

View file

@ -0,0 +1,7 @@
package com.stephenschafer.budget;
import java.sql.PreparedStatement;
public class PreparedStatementHolder {
PreparedStatement statement;
}

View file

@ -0,0 +1,29 @@
package com.stephenschafer.budget;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Regex {
public Regex() {
}
public Regex(final int id, final Regex regex) {
this(id, regex.categoryId, regex.regex, regex.flags, regex.source, regex.priority,
regex.description, regex.year);
}
Integer id;
Integer categoryId;
String regex;
int flags;
String source;
int priority;
String description;
Integer year;
}

View file

@ -0,0 +1,130 @@
package com.stephenschafer.budget;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class RegexController {
@Autowired
private RegexDao regexDao;
@Autowired
private CategoryDao categoryDao;
@Autowired
private UserService userService;
@PostMapping("/regexes")
@ResponseBody
public ApiResponse<Regex> postRegex(@RequestBody final Regex regex,
final HttpServletRequest req) {
if (!userService.isAuthorized(req)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
return new ApiResponse<>(HttpStatus.OK.value(), "Regex inserted successfully",
regexDao.add(regex));
}
@PutMapping("/regexes")
@ResponseBody
public ApiResponse<Regex> putRegex(@RequestBody final Regex regex,
final HttpServletRequest req) {
if (!userService.isAuthorized(req)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
regexDao.update(regex);
return new ApiResponse<>(HttpStatus.OK.value(), "Regex updated successfully", regex);
}
@GetMapping("/regexes")
@ResponseBody
public ApiResponse<List<RegexDisplay>> getRegexes(final HttpServletRequest request) {
log.info("GET /regexes");
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final List<RegexDisplay> regexDisplays = new ArrayList<>();
regexDao.getAllDisplay(regexDisplay -> {
regexDisplays.add(regexDisplay);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Regex list fetched successfully",
regexDisplays);
}
@GetMapping("/regex/{regexId}")
@ResponseBody
public ApiResponse<Regex> getRegex(@PathVariable(required = true) final Integer regexId,
final HttpServletRequest request) {
log.info("GET /regex/" + regexId);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
if (regexId == null) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Regex ID not specified",
null);
}
final Optional<Regex> optionalRegex = regexDao.getById(regexId.intValue());
if (!optionalRegex.isPresent()) {
return new ApiResponse<>(HttpStatus.NOT_FOUND.value(), "Regex ID not found", null);
}
final Regex regex = optionalRegex.get();
return new ApiResponse<>(HttpStatus.OK.value(), "Regex retrieved successfully", regex);
}
@GetMapping("/regexes/category/{categoryName}")
@ResponseBody
public ApiResponse<List<Regex>> getRegexesByCategory(
@PathVariable(required = true) final String categoryName,
final HttpServletRequest request) {
log.info("GET /regexes/" + categoryName);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final Optional<Category> category = categoryDao.getByName(categoryName);
if (!category.isPresent()) {
return new ApiResponse<>(HttpStatus.NOT_FOUND.value(), "Category not found", null);
}
final int categoryId = category.get().getId();
final List<Regex> regexes = new ArrayList<>();
regexDao.getByCategory(categoryId, regex -> {
regexes.add(regex);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Regex list fetched successfully", regexes);
}
@GetMapping("/regexes/source/{source}")
@ResponseBody
public ApiResponse<List<Regex>> getRegexesBySource(
@PathVariable(required = true) final String source, final HttpServletRequest request) {
log.info("GET /regexes/source/" + source);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final List<Regex> regexes = new ArrayList<>();
regexDao.getBySource(source, regex -> {
regexes.add(regex);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Regex list fetched successfully", regexes);
}
}

View file

@ -0,0 +1,22 @@
package com.stephenschafer.budget;
import java.util.Optional;
import java.util.function.Consumer;
public interface RegexDao {
int deleteById(int id);
Optional<Regex> getById(int id);
Regex add(Regex regex);
void update(Regex regex);
void getAll(Consumer<Regex> consumer);
void getAllDisplay(Consumer<RegexDisplay> consumer);
void getByCategory(int categoryId, Consumer<Regex> consumer);
void getBySource(String source, Consumer<Regex> consumer);
}

View file

@ -0,0 +1,228 @@
package com.stephenschafer.budget;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RegexDaoImpl implements RegexDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private CategoryDao categoryDao;
@Override
public int deleteById(final int id) {
return jdbcTemplate.update("delete from budget.regex where id = ?", id);
}
@Override
public Optional<Regex> getById(final int id) {
final String sql = "select"
+ " category_id, regex, flags, source, priority, description, year"
+ " from budget.regex where id = ?";
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
int i = 0;
final int categoryId = rs.getInt(++i);
final String regex = rs.getString(++i);
final int flags = rs.getInt(++i);
final String source = rs.getString(++i);
final int priority = rs.getInt(++i);
final String description = rs.getString(++i);
final int yearInt = rs.getInt(++i);
final Integer year = rs.wasNull() ? null : Integer.valueOf(yearInt);
return Optional.of(
new Regex(id, categoryId, regex, flags, source, priority, description, year));
}, id);
}
@Override
public Regex add(final Regex regex) {
log.info("RegexDaoImpl.add " + regex);
final String sql = "insert into budget.regex"
+ " (category_id, regex, flags, source, priority, description, year)"
+ " values (?, ?, ?, ?, ?, ?, ?)";
final PreparedStatementCreatorFactory factory = new PreparedStatementCreatorFactory(sql,
Types.INTEGER, Types.VARCHAR, Types.INTEGER, Types.VARCHAR, Types.INTEGER,
Types.VARCHAR, Types.INTEGER);
factory.setReturnGeneratedKeys(true);
factory.setGeneratedKeysColumnNames("id");
final PreparedStatementCreator creator = factory.newPreparedStatementCreator(
new Object[] { regex.getCategoryId(), regex.getRegex(), regex.getFlags(),
regex.getSource(), regex.getPriority(), regex.getDescription(), regex.getYear() });
final KeyHolder keyHolder = new GeneratedKeyHolder();
final int rowCount = jdbcTemplate.update(creator, keyHolder);
if (rowCount == 0) {
return null;
}
final Number generatedId = keyHolder.getKey();
return new Regex(generatedId.intValue(), regex);
}
@Override
public void update(final Regex regex) {
log.info("RegexDaoImpl.update " + regex);
final String sql = "update budget.regex"
+ " set category_id = ?, regex = ?, flags = ?, source = ?, priority = ?, description = ?, year = ?"
+ " where id = ?";
final PreparedStatementCreatorFactory factory = new PreparedStatementCreatorFactory(sql,
Types.INTEGER, Types.VARCHAR, Types.INTEGER, Types.VARCHAR, Types.INTEGER,
Types.VARCHAR, Types.INTEGER, Types.INTEGER);
PreparedStatementCreator creator;
try {
creator = factory.newPreparedStatementCreator(new Object[] { regex.getCategoryId(),
regex.getRegex(), regex.getFlags(), regex.getSource(), regex.getPriority(),
regex.getDescription(), regex.getYear(), regex.getId() });
}
catch (final Exception e) {
log.error("failed to get creator");
e.printStackTrace();
return;
}
log.info("update creator = " + creator);
jdbcTemplate.update(creator);
}
@Override
public void getAll(final Consumer<Regex> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select"
+ " id, category_id, regex, flags, source, priority, description, year"
+ " from budget.regex" + " order by id";
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final int categoryId = rs.getInt(++i);
final String regex = rs.getString(++i);
final int flags = rs.getInt(++i);
final String source = rs.getString(++i);
final int priority = rs.getInt(++i);
final String description = rs.getString(++i);
final int yearInt = rs.getInt(++i);
final Integer year = rs.wasNull() ? null : Integer.valueOf(yearInt);
consumer.accept(
new Regex(id, categoryId, regex, flags, source, priority, description, year));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getAll failed", e1);
}
}
}
@Override
public void getAllDisplay(final Consumer<RegexDisplay> consumer) {
final List<Regex> regexes = new ArrayList<>();
getAll(regex -> {
regexes.add(regex);
});
for (final Regex regex : regexes) {
final List<Category> categoryAncestry = categoryDao.getCategoryAncestry(
regex.categoryId);
final String fqCategoryName = getFQCategoryName(categoryAncestry);
consumer.accept(new RegexDisplay(regex, fqCategoryName));
}
}
private String getFQCategoryName(final List<Category> categoryAncestry) {
final StringBuilder sb = new StringBuilder();
String sep = "";
for (final Category category : categoryAncestry) {
sb.append(sep);
sep = ".";
sb.append(category.name);
}
return sb.toString();
}
@Override
public void getByCategory(final int categoryId, final Consumer<Regex> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select" + " id, regex, flags, source, priority, description, year"
+ " from budget.regex" + " where category_id = ?" + " order by id";
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
holder.statement.setInt(1, categoryId);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final String regex = rs.getString(++i);
final int flags = rs.getInt(++i);
final String source = rs.getString(++i);
final int priority = rs.getInt(++i);
final String description = rs.getString(++i);
final int yearInt = rs.getInt(++i);
final Integer year = rs.wasNull() ? null : Integer.valueOf(yearInt);
consumer.accept(
new Regex(id, categoryId, regex, flags, source, priority, description, year));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getByCategory failed", e1);
}
}
}
@Override
public void getBySource(final String source, final Consumer<Regex> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select" + " id, category_id, regex, flags, priority, description, year"
+ " from budget.regex" + " where source = ?" + " order by id";
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
holder.statement.setString(1, source);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final int categoryId = rs.getInt(++i);
final String regex = rs.getString(++i);
final int flags = rs.getInt(++i);
final int priority = rs.getInt(++i);
final String description = rs.getString(++i);
final int yearInt = rs.getInt(++i);
final Integer year = rs.wasNull() ? null : Integer.valueOf(yearInt);
consumer.accept(
new Regex(id, categoryId, regex, flags, source, priority, description, year));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getBySource failed", e1);
}
}
}
}

View file

@ -0,0 +1,27 @@
package com.stephenschafer.budget;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
public class RegexDisplay {
public RegexDisplay(final Regex regex, final String fqCategoryName) {
this(regex.id, regex.categoryId, fqCategoryName, regex.regex, regex.flags, regex.source,
regex.priority, regex.description, regex.year);
}
Integer id;
Integer categoryId;
String fqCategoryName;
String regex;
int flags;
String source;
int priority;
String description;
Integer year;
}

View file

@ -0,0 +1,42 @@
package com.stephenschafer.budget;
import java.text.ParseException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Unknown error")
public ApiResponse<Void> handleNotFoundException(final RuntimeException ex) {
log.info("Exception: " + ex);
final ApiResponse<Void> apiResponse = new ApiResponse<>(400, "Unknown error", null);
return apiResponse;
}
@ExceptionHandler(MultipartException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Multipart exception")
public ApiResponse<Void> handleError1(final MultipartException e,
final RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("message", e.getCause().getMessage());
log.info("Exception: " + e);
final ApiResponse<Void> apiResponse = new ApiResponse<>(400, "Multipart Exception", null);
return apiResponse;
}
@ExceptionHandler(ParseException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Bad date format")
public ApiResponse<Void> handleParseException(final ParseException ex) {
log.info("Exception: " + ex);
final ApiResponse<Void> apiResponse = new ApiResponse<>(400, "Bad date format", null);
return apiResponse;
}
}

View file

@ -0,0 +1,8 @@
package com.stephenschafer.budget;
import java.io.Serial;
public class StopException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
}

View file

@ -0,0 +1,26 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.sql.Date;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Transaction {
Integer id;
String source;
String uniqueIdentifier;
String type;
String description;
String extraDescription;
Date date;
BigDecimal amount;
Integer optional;
Integer regexId;
}

View file

@ -0,0 +1,86 @@
package com.stephenschafer.budget;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class TransactionController {
@Autowired
private TransactionDao transactionDao;
@Autowired
private UserService userService;
@GetMapping("/sources/{year}")
@ResponseBody
public ApiResponse<List<String>> geSources(@PathVariable(required = true) final String year,
final HttpServletRequest request) {
log.info("GET /sources/" + year);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
if (year == null) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Year not specified", null);
}
final List<String> sources = new ArrayList<>();
transactionDao.getSources(year, source -> {
sources.add(source);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Sources retrieved successfully", sources);
}
@GetMapping("/transactions/{year}")
@ResponseBody
public ApiResponse<List<Transaction>> getTransactions(
@PathVariable(required = true) final String year, final HttpServletRequest request) {
log.info("GET /transactions/" + year);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
if (year == null) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Year not specified", null);
}
final List<Transaction> transactions = new ArrayList<>();
transactionDao.getAll(year, transaction -> {
transactions.add(transaction);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Transactions retrieved successfully",
transactions);
}
@GetMapping("/transactionsByRegexId/{year}/{regexId}")
@ResponseBody
public ApiResponse<List<Transaction>> getTransactionsByRegexId(
@PathVariable(required = true) final String year,
@PathVariable(required = true) final Integer regexId,
final HttpServletRequest request) {
log.info("GET /transactions/" + year);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
if (year == null) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Year not specified", null);
}
final List<Transaction> transactions = new ArrayList<>();
transactionDao.getByRegexId(year, regexId, transaction -> {
transactions.add(transaction);
});
return new ApiResponse<>(HttpStatus.OK.value(), "Transactions retrieved successfully",
transactions);
}
}

View file

@ -0,0 +1,20 @@
package com.stephenschafer.budget;
import java.util.Optional;
import java.util.function.Consumer;
public interface TransactionDao {
Optional<Transaction> getById(String year, int id);
void getAll(String year, Consumer<Transaction> consumer);
void getByCategory(String year, int categoryId, Consumer<Transaction> consumer);
void getBySource(String year, String source, Consumer<Transaction> consumer);
void getByRegexId(String year, Integer regexId, Consumer<Transaction> consumer);
void getSources(String year, Consumer<String> consumer);
void update(String year, Transaction transaction);
}

View file

@ -0,0 +1,159 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.SQLException;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TransactionDaoImpl implements TransactionDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Optional<Transaction> getById(final String year, final int id) {
final String sql = ("select"
+ " source, unique_identifier, type, description, extra_description, date, amount, optional, regex_id"
+ " from budget_${year}.transaction where id = ?").replace("${year}", year);
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
int i = 0;
final String source = rs.getString(++i);
final String uniqueIdentifier = rs.getString(++i);
final String type = rs.getString(++i);
final String description = rs.getString(++i);
final String extraDescription = rs.getString(++i);
final Date date = rs.getDate(++i);
final BigDecimal amount = rs.getBigDecimal(++i);
final int optional = rs.getInt(++i);
final int regexId = rs.getInt(++i);
return Optional.of(new Transaction(id, source, uniqueIdentifier, type, description,
extraDescription, date, amount, optional, regexId));
}, id);
}
@Override
public void update(final String year, final Transaction transaction) {
// TODO Auto-generated method stub
}
@Override
public void getAll(final String year, final Consumer<Transaction> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = ("select"
+ " id, source, unique_identifier, type, description, extra_description, date, amount, optional, regex_id"
+ " from budget_${year}.transaction" + " order by id").replace("${year}", year);
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final String source = rs.getString(++i);
final String uniqueIdentifier = rs.getString(++i);
final String type = rs.getString(++i);
final String description = rs.getString(++i);
final String extraDescription = rs.getString(++i);
final Date date = rs.getDate(++i);
final BigDecimal amount = rs.getBigDecimal(++i);
final int optional = rs.getInt(++i);
final int regexId = rs.getInt(++i);
consumer.accept(new Transaction(id, source, uniqueIdentifier, type, description,
extraDescription, date, amount, optional, regexId));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getByCategory failed", e1);
}
}
}
@Override
public void getByCategory(final String year, final int categoryId,
final Consumer<Transaction> consumer) {
// TODO Auto-generated method stub
}
@Override
public void getBySource(final String year, final String source,
final Consumer<Transaction> consumer) {
// TODO Auto-generated method stub
}
@Override
public void getByRegexId(final String year, final Integer regexId,
final Consumer<Transaction> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = ("select"
+ " id, source, unique_identifier, type, description, extra_description, date, amount, optional"
+ " from budget_${year}.transaction where regex_id = ?" + " order by id").replace(
"${year}", year);
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
holder.statement.setInt(1, regexId.intValue());
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final int id = rs.getInt(++i);
final String source = rs.getString(++i);
final String uniqueIdentifier = rs.getString(++i);
final String type = rs.getString(++i);
final String description = rs.getString(++i);
final String extraDescription = rs.getString(++i);
final Date date = rs.getDate(++i);
final BigDecimal amount = rs.getBigDecimal(++i);
final int optional = rs.getInt(++i);
consumer.accept(new Transaction(id, source, uniqueIdentifier, type, description,
extraDescription, date, amount, optional, regexId));
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getByCategory failed", e1);
}
}
}
@Override
public void getSources(final String year, final Consumer<String> consumer) {
final PreparedStatementHolder holder = new PreparedStatementHolder();
final String sql = "select source from budget_${year}.transaction group by source order by source".replace(
"${year}", year);
final PreparedStatementCreator creator = connection -> {
holder.statement = connection.prepareStatement(sql);
return holder.statement;
};
try {
jdbcTemplate.query(creator, rs -> {
int i = 0;
final String source = rs.getString(++i);
consumer.accept(source);
});
}
catch (final StopException e) {
try {
holder.statement.cancel();
}
catch (final SQLException e1) {
log.error("getSources failed", e1);
}
}
}
}

View file

@ -0,0 +1,53 @@
package com.stephenschafer.budget;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
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;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ApiResponse<UserEntity> saveUser(@RequestBody final UserDto user) {
return new ApiResponse<>(HttpStatus.OK.value(), "User saved successfully.",
userService.save(user));
}
@GetMapping
public ApiResponse<List<UserEntity>> listUser() {
return new ApiResponse<>(HttpStatus.OK.value(), "User list fetched successfully.",
userService.findAll());
}
@GetMapping("/{id}")
public ApiResponse<UserEntity> getOne(@PathVariable final int id) {
return new ApiResponse<>(HttpStatus.OK.value(), "User fetched successfully.",
userService.findById(id));
}
@PutMapping("/{id}")
public ApiResponse<UserDto> update(@RequestBody final UserDto userDto) {
return new ApiResponse<>(HttpStatus.OK.value(), "User updated successfully.",
userService.update(userDto));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable final int id) {
userService.delete(id);
return new ApiResponse<>(HttpStatus.OK.value(), "User deleted successfully.", null);
}
}

View file

@ -0,0 +1,9 @@
package com.stephenschafer.budget;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDao extends CrudRepository<UserEntity, Integer> {
UserEntity findByUsername(String username);
}

View file

@ -0,0 +1,15 @@
package com.stephenschafer.budget;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserDto {
private int id;
private String firstName;
private String lastName;
private String username;
private String password;
private String role;
}

View file

@ -0,0 +1,33 @@
package com.stephenschafer.budget;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "user_1")
@Getter
@Setter
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column
private String firstName;
@Column
private String lastName;
@Column
private String username;
@Column
private String role;
@Column
@JsonIgnore
private String password;
}

View file

@ -0,0 +1,23 @@
package com.stephenschafer.budget;
import java.util.List;
import org.springframework.security.core.userdetails.UserDetailsService;
import jakarta.servlet.http.HttpServletRequest;
public interface UserService extends UserDetailsService {
UserEntity save(UserDto user);
List<UserEntity> findAll();
void delete(int id);
UserEntity findByUsername(String username);
UserEntity findById(int id);
UserDto update(UserDto userDto);
boolean isAuthorized(final HttpServletRequest request);
}

View file

@ -0,0 +1,98 @@
package com.stephenschafer.budget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Transactional
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
@Autowired
private UserDao userDao;
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
@Override
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
final UserEntity user = userDao.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(),
user.getPassword(), getAuthority());
}
private List<SimpleGrantedAuthority> getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
@Override
public List<UserEntity> findAll() {
final List<UserEntity> list = new ArrayList<>();
userDao.findAll().iterator().forEachRemaining(list::add);
return list;
}
@Override
public void delete(final int id) {
userDao.deleteById(id);
}
@Override
public UserEntity findByUsername(final String username) {
return userDao.findByUsername(username);
}
@Override
public UserEntity findById(final int id) {
final Optional<UserEntity> optionalUser = userDao.findById(id);
return optionalUser.isPresent() ? optionalUser.get() : null;
}
@Override
public UserDto update(final UserDto userDto) {
final UserEntity user = findById(userDto.getId());
if (user != null) {
BeanUtils.copyProperties(userDto, user, "password", "username");
userDao.save(user);
}
return userDto;
}
@Override
public UserEntity save(final UserDto user) {
final UserEntity newUser = new UserEntity();
newUser.setUsername(user.getUsername());
newUser.setFirstName(user.getFirstName());
newUser.setLastName(user.getLastName());
newUser.setPassword(bcryptEncoder.encode(user.getPassword()));
newUser.setRole(user.getRole());
return userDao.save(newUser);
}
@Override
public boolean isAuthorized(final HttpServletRequest request) {
final String username = (String) request.getAttribute("username");
log.info("username = " + username);
final UserEntity userEntity = findByUsername(username);
if (userEntity == null || !"administrator".equals(userEntity.getRole())) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,4 @@
package com.stephenschafer.budget;
public class Util {
}

View file

@ -0,0 +1,75 @@
package com.stephenschafer.budget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
CustomCorsConfiguration customCorsConfiguration;
@Bean
AuthenticationManager authenticationManager(final UserDetailsService userDetailsService,
final PasswordEncoder passwordEncoder) {
final var provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
@Bean
BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
@Bean
SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
http.cors(c -> c.configurationSource(customCorsConfiguration)) //
.csrf(AbstractHttpConfigurer::disable) //
.authorizeHttpRequests(requests -> {
requests.requestMatchers("/token/*",
"/signup").permitAll().anyRequest().authenticated();
}) //
.exceptionHandling(
configurer -> configurer.authenticationEntryPoint(unauthorizedHandler)) //
.sessionManagement(configurer -> configurer.sessionCreationPolicy(
SessionCreationPolicy.STATELESS));
http.addFilterBefore(authenticationTokenFilterBean(),
UsernamePasswordAuthenticationFilter.class);
/*
requests.requestMatchers("/token/*",
"/signup").permitAll().anyRequest().authenticated();
*/
/*
http.cors().and().csrf().disable().authorizeRequests().antMatchers("/token/*",
"/signup").permitAll().anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(
unauthorizedHandler).and().sessionManagement().sessionCreationPolicy(
SessionCreationPolicy.STATELESS);
// @formatter:on
http.addFilterBefore(authenticationTokenFilterBean(),
UsernamePasswordAuthenticationFilter.class);
*/
return http.build();
}
}

View file

@ -0,0 +1,20 @@
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306?serverTimezone=UTC&useSSL=false
spring.datasource.username=elephant
# spring.datasource.password=CHANGEME
spring.jpa.show-sql=true
# this unconditionally re-creates the table even if it's already populated
#spring.jpa.hibernate.ddl-auto=create-drop
# this will add columns to the table if they are missing but doesn't remove any
spring.jpa.hibernate.ddl-auto=update
# spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
#server.port=8443
#server.ssl.key-store=classpath:keystore.p12
#server.ssl.key-store-password=foobar
#server.ssl.key-store-type=PKCS12
#server.ssl.key-alias=timesheet
#server.ssl.key-password=foobar
#server.ssl.enabled=true

24
stop Executable file
View file

@ -0,0 +1,24 @@
#!/bin/sh
cd "$(dirname "${BASH_SOURCE[0]}")"
ROOT=$(pwd)
echo "$ROOT"
function k() {
if ! test -f "$1"; then
echo "nothing to stop"
return 1
fi
PID=$(cat "$1")
if kill -9 $PID; then
echo "process $PID stopped"
else
echo "no such process"
fi
rm "$1"
}
if [ -z "$1" ]; then
for file in $ROOT/logs/run-*.pid; do
k $file
done
else
k "$ROOT/logs/run-$1.pid"
fi