40 Commits

Author SHA1 Message Date
726be3f613 bu commit
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m16s
2025-07-11 14:16:05 +02:00
936140e76f dockerCompose backup
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m4s
2025-05-11 10:15:15 +02:00
15792bad28 Add CategoryService and integrate category handling in ExpenseListController
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m8s
- Introduced CategoryService to manage standard categories.
- Updated ExpenseListController to set default categories when creating an expense list.
- Modified ExpenseChangeRequest and ExpenseInput to include category field.
- Enhanced DataInitializer to ensure standard categories are initialized.
2025-05-11 00:59:54 +02:00
814b2221c8 #7
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m8s
2025-05-10 19:07:50 +02:00
011bb03d3f Merge pull request 'sync' (#3) from main into dev
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 9m53s
Reviewed-on: #3
2025-01-12 05:19:50 -08:00
5e0311971d Merge pull request 'rc' (#2) from dev into main
All checks were successful
Build and Deploy Versioned Spring Boot Server / build (push) Successful in 9m54s
Reviewed-on: #2
2025-01-12 05:18:03 -08:00
31566d1bd8 adjusted compose
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-12 14:14:13 +01:00
b669855a56 Dateien nach "src/main/resources/static" hochladen 2025-01-12 04:51:41 -08:00
3830449377 major minor version tagging
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 9m55s
Build and Deploy Versioned Spring Boot Server / build (push) Successful in 9m54s
2025-01-12 12:36:43 +01:00
3db2806a04 fix gitea tag
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 9m53s
Build and Deploy Versioned Spring Boot Server / build (push) Successful in 9m58s
2025-01-12 11:46:29 +01:00
d26a9bffc5 tag
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
Build and Deploy Versioned Spring Boot Server / build (push) Failing after 9m46s
2025-01-12 11:23:17 +01:00
f49530653b Merge pull request 'initial Release request' (#1) from dev into main
Some checks failed
Build and Deploy Versioned Spring Boot Server / build (push) Failing after 9m45s
Reviewed-on: #1
2025-01-12 01:46:30 -08:00
25e70ddf68 tag releases
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 9m53s
2025-01-12 10:35:50 +01:00
4fca98dc72 add logos
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 9m55s
2025-01-12 10:11:57 +01:00
c453411444 fixes
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m0s
2025-01-11 21:37:11 +01:00
85e4a2b125 remove dotenv
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m11s
2025-01-11 21:20:58 +01:00
ece3e1d697 fix app_props
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m0s
2025-01-11 20:48:53 +01:00
d39b5e875c docker compose sample
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m1s
2025-01-11 20:24:33 +01:00
12f6733b48 fix user
All checks were successful
Build and Deploy Spring Boot Server / build (push) Successful in 10m7s
2025-01-11 14:16:49 +01:00
Cedric Hornberger
e12e8067ce ich dreh am login
Some checks failed
Build and Deploy Spring Boot Server / build (push) Failing after 9m57s
2025-01-11 11:58:07 +01:00
Cedric Hornberger
0b624f1562 docker login 2025-01-11 11:46:19 +01:00
76cfaecdda push docker
Some checks failed
Build and Deploy Spring Boot Server / build (push) Failing after 9m49s
2025-01-11 01:12:46 +01:00
82cdca6f0a aaaaahhhh
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-11 01:04:20 +01:00
4fbee3852a docker fix
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-11 00:58:27 +01:00
bed8a2e0f5 docker fix
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-11 00:50:18 +01:00
197e40dfd5 fix
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-11 00:41:19 +01:00
01aa12e8a2 temurin 2025-01-11 00:37:32 +01:00
6d806fbc20 fix adoptium
Some checks failed
Build and Deploy Spring Boot Server / build (push) Failing after 3s
2025-01-11 00:35:56 +01:00
77073ddba6 openjdk
Some checks failed
Build and Deploy Spring Boot Server / build (push) Failing after 4s
2025-01-11 00:34:43 +01:00
96b9989a2a fix maven
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-11 00:27:25 +01:00
38bb0f131c test
Some checks failed
Build and Deploy Spring Boot Server / build (push) Failing after 23s
2025-01-11 00:24:49 +01:00
2bcc2ec23f Merge branch 'dev' of ssh://tea.zendric.de:2223/Cedric/XpenselyServer into dev
Some checks failed
Build and Deploy Spring Boot Server / build (push) Failing after 34s
2025-01-11 00:13:30 +01:00
1fd1e8ae75 test workflow 2025-01-11 00:13:25 +01:00
ac804385c9 Merge branch 'dev' of ssh://tea.zendric.de:2223/Cedric/XpenselyServer into dev
Some checks failed
Build and Deploy Spring Boot Server / build (push) Has been cancelled
2025-01-11 00:05:52 +01:00
823b1182be test workflow 2025-01-10 23:53:01 +01:00
49401a8d09 test workflow
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m21s
2025-01-10 23:49:34 +01:00
5546b0ba3b better secret handling docker api upgrade 2025-01-07 23:40:00 +01:00
53a262ddb9 Never had a stupid bug dont look 2025-01-06 00:28:36 +01:00
f26f365b3c +Data Structure Changes
+Api Functionality
2025-01-05 01:30:28 +01:00
b3d5b5ad11 Bugfix, support for Expense without AppUser id 2024-12-31 01:14:28 +01:00
37 changed files with 904 additions and 50 deletions

View File

@@ -0,0 +1,54 @@
name: Build and Deploy Spring Boot Server
on:
push:
branches:
- dev
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. Checkout the code
- name: Checkout code
uses: actions/checkout@v2
# 2. Set up Java and Maven
- name: Set up JDK (Eclipse Temurin)
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: "17"
cache: maven
# 3. Verify Maven installation
- name: Install Maven
run: |
sudo apt-get update
sudo apt-get install -y maven
mvn -version
# 4. Build the Spring Boot application
- name: Build Spring Boot Application
run: |
mvn clean package -DskipTests
# 5. Set up Docker
- name: Set up Docker
run: |
docker --version
# 6. Build the Docker image
- name: Build and Package Docker Image
run: |
docker build -t tea.zendric.de/cedric/xpensely-server:latest .
# 7. Docker login
- name: Login to Docker Registry
run: |
echo "${{ secrets.TEAPASSWORD }}" | docker login tea.zendric.de -u ${{ secrets.TEAUSER }} --password-stdin
# 8. Push Docker image
- name: Push the Docker Image to registry
run: |
docker push tea.zendric.de/cedric/xpensely-server:latest

View File

@@ -0,0 +1,84 @@
name: Build and Deploy Versioned Spring Boot Server
on:
push:
tags:
- "*" # Match all tags
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. Checkout the code
- name: Checkout code
uses: actions/checkout@v2
# 2. Set up Java and Maven
- name: Set up JDK (Eclipse Temurin)
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: "17"
cache: maven
# 3. Verify Maven installation
- name: Install Maven
run: |
sudo apt-get update
sudo apt-get install -y maven
mvn -version
# 4. Build the Spring Boot application
- name: Build Spring Boot Application
run: |
mvn clean package -DskipTests
# 5. Set up Docker
- name: Set up Docker
run: |
docker --version
# 6. Extract the tag name
- name: Extract Tag Version
id: extract_version
run: |
TAG_VERSION=$(echo "${GITHUB_REF}" | sed 's#refs/tags/##')
if [ -z "$TAG_VERSION" ]; then
echo "Error: TAG_VERSION is empty."
exit 1
fi
echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV
# Extract major and minor versions
MAJOR_VERSION=$(echo "${TAG_VERSION}" | cut -d. -f1)
MINOR_VERSION=$(echo "${TAG_VERSION}" | cut -d. -f1,2)
echo "MAJOR_VERSION=$MAJOR_VERSION" >> $GITHUB_ENV
echo "MINOR_VERSION=$MINOR_VERSION" >> $GITHUB_ENV
# 7. Build the Docker image with the tag
- name: Build and Package Docker Image
run: |
docker build -t tea.zendric.de/cedric/xpensely-server:${{ env.TAG_VERSION }} .
# 8. Tag the image with Major Version (e.g., 0)
- name: Tag with Major Version
run: |
docker tag tea.zendric.de/cedric/xpensely-server:${{ env.TAG_VERSION }} tea.zendric.de/cedric/xpensely-server:${{ env.MAJOR_VERSION }}
# 9. Tag the image with Minor Version (e.g., 0.1)
- name: Tag with Minor Version
run: |
docker tag tea.zendric.de/cedric/xpensely-server:${{ env.TAG_VERSION }} tea.zendric.de/cedric/xpensely-server:${{ env.MINOR_VERSION }}
# 10. Docker login
- name: Login to Docker Registry
run: |
echo "${{ secrets.TEAPASSWORD }}" | docker login tea.zendric.de -u ${{ secrets.TEAUSER }} --password-stdin
# 11. Push the Docker images with the tags
- name: Push the Docker Image to registry
run: |
docker push tea.zendric.de/cedric/xpensely-server:${{ env.TAG_VERSION }}
docker push tea.zendric.de/cedric/xpensely-server:${{ env.MAJOR_VERSION }}
docker push tea.zendric.de/cedric/xpensely-server:${{ env.MINOR_VERSION }}

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.env
### STS ###
.apt_generated

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
Run it locally:
1. build the current state:
mvn clean install or mvn clean install -DskipTests
2. docker it up and run it
docker-compose -f dev-docker-compose.yml up --build

View File

@@ -1,6 +1,6 @@
<mxfile host="65bd71144e">
<diagram id="TZX9Tq6sZIlTxQ58HocZ" name="Page-1">
<mxGraphModel dx="826" dy="472" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<mxGraphModel dx="989" dy="570" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
@@ -193,6 +193,90 @@
<mxCell id="73" value="&lt;span style=&quot;font-size: 8px;&quot;&gt;24.12.24 ; Expense ; 24,12 €&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="1399" y="226" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="86" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="74" target="79">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="74" value="List" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;" vertex="1" parent="1">
<mxGeometry x="1880" y="160" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="75" value="DB-Structure for Categories" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1930" y="35" width="100" height="30" as="geometry"/>
</mxCell>
<mxCell id="78" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="76" target="77">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="76" value="List Entry" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1920" y="220" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="79" value="Available Categories" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2160" y="150" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="83" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="81" target="79">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="81" value="Standard Categories" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2060" y="35" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="84" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="82" target="79">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="82" value="Custom Categories" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2270" y="35" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="91" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="87" target="90">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="87" value="List Entry" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1920" y="280" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="92" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="88" target="89">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="88" value="List Entry" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1920" y="340" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="89" value="Category" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2150" y="370" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="90" value="Category" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2150" y="315" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="94" value="&lt;h1 style=&quot;font-size: 18px;&quot;&gt;- list id&lt;/h1&gt;&lt;div&gt;- List &amp;lt;String&amp;gt;&lt;/div&gt;" style="text;html=1;strokeColor=none;fillColor=none;spacing=5;spacingTop=-20;whiteSpace=wrap;overflow=hidden;rounded=0;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="2410" y="50" width="130" height="70" as="geometry"/>
</mxCell>
<mxCell id="77" value="Category" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2150" y="260" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="97" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1730" y="510" width="200" height="320" as="geometry"/>
</mxCell>
<mxCell id="98" value="ExpenseList" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1770" y="520" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="100" value="+" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1880" y="790" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="101" value="Categorie" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1770" y="480" width="120" height="30" as="geometry"/>
</mxCell>
<mxCell id="102" value="&lt;span style=&quot;font-size: 8px;&quot;&gt;Amount : 24,12 €&lt;br&gt;Title: Expense&lt;br&gt;Date: 24.12.24&lt;br&gt;From: Jessi&lt;br&gt;Deviation: 0 €&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="1745" y="550" width="170" height="170" as="geometry"/>
</mxCell>
<mxCell id="105" value="&lt;span style=&quot;font-size: 8px;&quot;&gt;Essen&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="1760" y="680" width="40" height="10" as="geometry"/>
</mxCell>
<mxCell id="106" value="&lt;span style=&quot;font-size: 8px;&quot;&gt;Trinken&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#a20025;fontColor=#ffffff;strokeColor=#6F0000;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="1800" y="680" width="40" height="10" as="geometry"/>
</mxCell>
<mxCell id="107" value="&lt;span style=&quot;font-size: 8px;&quot;&gt;Auto&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d80073;fontColor=#ffffff;strokeColor=#A50040;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="1840" y="680" width="40" height="10" as="geometry"/>
</mxCell>
<mxCell id="111" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=18;" edge="1" parent="1" source="108" target="74">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="108" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="1715" y="150" width="30" height="60" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>

52
dev-docker-compose.yml Normal file
View File

@@ -0,0 +1,52 @@
version: "3.8"
services:
xpensely-server:
build:
context: .
dockerfile: Dockerfile
image: xpensely-server:local
labels:
net.unraid.docker.icon: https://tea.zendric.de/Cedric/XpenselyServer/raw/branch/main/src/main/resources/static/xpensely_icon_white.png
container_name: xpensely-server
ports:
- 3636:8080
environment:
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
DB_PORT: 5432
DB_P_NAME: xpensely
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
SPRING_PROFILES_ACTIVE: test
depends_on:
postgresdb:
condition: service_healthy
networks:
- xpensely-network
postgresdb:
labels:
net.unraid.docker.icon: https://raw.githubusercontent.com/docker-library/docs/01c12653951b2fe592c1f93a13b4e289ada0e3a1/postgres/logo.png
image: postgres:14
container_name: postgresdb
ports:
- 5435:5432
environment:
POSTGRES_DB: xpensely
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
networks:
- xpensely-network
volumes:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test:
- CMD-SHELL
- pg_isready -U ${DB_USERNAME} -d xpensely
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data: null
networks:
xpensely-network: null

49
docker-compose.yml Normal file
View File

@@ -0,0 +1,49 @@
version: "3.8"
services:
xpensely-server:
image: tea.zendric.de/cedric/xpensely-server:0
labels:
net.unraid.docker.icon: https://tea.zendric.de/Cedric/XpenselyServer/raw/branch/main/src/main/resources/static/xpensely_icon_white.png
container_name: xpensely-server
ports:
- 3636:8080
environment:
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
DB_PORT: 5432
DB_P_NAME: xpensely
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
postgresdb:
condition: service_healthy
networks:
- xpensely-network
postgresdb:
labels:
net.unraid.docker.icon: https://raw.githubusercontent.com/docker-library/docs/01c12653951b2fe592c1f93a13b4e289ada0e3a1/postgres/logo.png
image: postgres:14
container_name: postgresdb
ports:
- 5435:5432
environment:
POSTGRES_DB: xpensely
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
networks:
- xpensely-network
volumes:
- db_data:/var/lib/postgresql/data
- /mnt/user/appdata/xpensely_backups:/backups
restart: unless-stopped
healthcheck:
test:
- CMD-SHELL
- pg_isready -U ${DB_USERNAME} -d xpensely
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data: null
networks:
xpensely-network: null

7
dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM openjdk:17-jdk-slim
COPY ./target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar", "app.jar"]

View File

@@ -10,9 +10,9 @@
</parent>
<groupId>de.zendric.app</groupId>
<artifactId>XpenselyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>1.0.0</version>
<name>XpenselyServer</name>
<description>Demo project for Spring Boot</description>
<description>XpenselyServer used to handle the Xpensely App</description>
<url/>
<licenses>
<license/>
@@ -37,6 +37,10 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -46,7 +50,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>

View File

@@ -3,7 +3,6 @@ package de.zendric.app.xpensely_server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -13,6 +12,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.AppUserCreateRequest;
import de.zendric.app.xpensely_server.model.Exception.UsernameAlreadyExistsException;
import de.zendric.app.xpensely_server.services.UserService;
@RestController
@@ -36,18 +37,32 @@ public class AppUserController {
return userService.getUserByName(username);
}
@PostMapping
public ResponseEntity<AppUser> createUser(@RequestBody AppUser user) {
AppUser appUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(appUser);
@GetMapping("/byGoogleId")
public ResponseEntity<AppUser> getUserByGoogleId(@RequestParam String id) {
try {
AppUser userByGoogleId = userService.getUserByGoogleId(id);
return new ResponseEntity<>(userByGoogleId, HttpStatus.OK);
} catch (IllegalArgumentException e) {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/createUser")
public ResponseEntity<?> createUsername(@RequestBody AppUser user, Authentication authentication) {
String googleUserId = authentication.getName();
// Validate and store the username with googleUserId
public ResponseEntity<AppUser> createUser(@RequestBody AppUserCreateRequest userRequest) {
try {
AppUser convertedUser = userRequest.convertToAppUser();
AppUser nUser = userService.createUser(convertedUser);
return new ResponseEntity<>(nUser, HttpStatus.CREATED);
} catch (UsernameAlreadyExistsException e) {
return new ResponseEntity<>(null, HttpStatus.CONFLICT);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
return ResponseEntity.ok("Username created successfully");
}
@DeleteMapping

View File

@@ -12,6 +12,7 @@ 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.RequestParam;
@@ -19,8 +20,12 @@ import org.springframework.web.bind.annotation.RestController;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.Expense;
import de.zendric.app.xpensely_server.model.ExpenseChangeRequest;
import de.zendric.app.xpensely_server.model.ExpenseInput;
import de.zendric.app.xpensely_server.model.ExpenseList;
import de.zendric.app.xpensely_server.model.InviteRequest;
import de.zendric.app.xpensely_server.model.XpenselyStandardCategories;
import de.zendric.app.xpensely_server.services.CategoryService;
import de.zendric.app.xpensely_server.services.ExpenseListService;
import de.zendric.app.xpensely_server.services.UserService;
@@ -30,11 +35,14 @@ class ExpenseListController {
private ExpenseListService expenseListService;
private UserService userService;
private CategoryService categoryService;
@Autowired
public ExpenseListController(ExpenseListService expenseListService, UserService userService) {
public ExpenseListController(ExpenseListService expenseListService, UserService userService,
CategoryService categoryService) {
this.expenseListService = expenseListService;
this.userService = userService;
this.categoryService = categoryService;
}
@GetMapping("/all")
@@ -92,12 +100,28 @@ class ExpenseListController {
}
}
@PostMapping
@PostMapping("/create")
// TODO add handling of categories by using DTO
public ResponseEntity<ExpenseList> create(@RequestBody ExpenseList expenseList) {
try {
ExpenseList savedItem = (ExpenseList) expenseListService.save(expenseList);
if (expenseList.getOwner() != null) {
AppUser existingOwner = userService.getUser(expenseList.getOwner().getId());
if (existingOwner == null) {
throw new IllegalArgumentException("Owner does not exist.");
}
expenseList.setOwner(existingOwner);
XpenselyStandardCategories standardCategories = categoryService.getDefaultCategories();
expenseList.setXpenselyStandardCategories(standardCategories);
} else {
throw new IllegalArgumentException("Owner is required.");
}
expenseList.setSharedWith(null);
ExpenseList savedItem = expenseListService.createList(expenseList);
return new ResponseEntity<>(savedItem, HttpStatus.CREATED);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>(null, HttpStatus.EXPECTATION_FAILED);
}
}
@@ -115,8 +139,11 @@ class ExpenseListController {
@PostMapping("/{id}/add")
public ResponseEntity<Expense> addExpenseToList(
@PathVariable("id") Long expenseListId,
@RequestBody Expense expense) {
@RequestBody ExpenseInput expenseInput) {
try {
AppUser expenseOwner = userService.getUserByName(expenseInput.getOwner());
Expense expense = expenseInput.convertToExpense(expenseOwner.getId());
Expense addedExpense = expenseListService.addExpenseToList(expenseListId, expense);
return new ResponseEntity<>(addedExpense, HttpStatus.CREATED);
} catch (Exception e) {
@@ -124,6 +151,26 @@ class ExpenseListController {
}
}
@PutMapping("/{id}/update")
public ResponseEntity<Expense> updateExpenseInList(
@PathVariable("id") Long expenseListId,
@RequestBody ExpenseChangeRequest expenseChangeRequest) {
try {
AppUser expenseOwner = userService.getUserByName(expenseChangeRequest.getOwnerName());
Optional<ExpenseList> expenseList = expenseListService.findById(expenseListId);
if (expenseList.isPresent()) {
Expense expense = expenseChangeRequest.convertToExpense(expenseOwner.getId(), expenseList.get());
Expense addedExpense = expenseListService.updateExpense(expenseListId, expense);
return new ResponseEntity<>(addedExpense, HttpStatus.CREATED);
}
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
@DeleteMapping("/{id}/delete")
public ResponseEntity<Expense> deleteExpenseFromList(
@PathVariable("id") Long expenseListId,
@@ -153,6 +200,9 @@ class ExpenseListController {
if (list.getSharedWith() != null) {
return ResponseEntity.status(HttpStatus.IM_USED).body("List has already been shared");
}
if (list.getOwner().getId() == inviteRequest.getUserId()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("You cant join your own List");
}
AppUser user = null;
try {
user = userService.getUser(inviteRequest.getUserId());

View File

@@ -1,10 +1,17 @@
package de.zendric.app.xpensely_server.model;
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
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 lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -13,6 +20,7 @@ import lombok.Setter;
@Setter
@NoArgsConstructor
@Entity
@EqualsAndHashCode
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -21,4 +29,12 @@ public class AppUser {
@Column(name = "username", nullable = false, unique = true)
private String username;
@JsonIgnore
private String googleId;
@Column(updatable = false)
@CreationTimestamp
@JsonIgnore
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,21 @@
package de.zendric.app.xpensely_server.model;
import jakarta.persistence.Column;
import lombok.Data;
@Data
public class AppUserCreateRequest {
@Column(name = "username", nullable = false, unique = true)
private String username;
private String googleId;
public AppUser convertToAppUser() {
AppUser appUser = new AppUser();
appUser.setGoogleId(googleId);
appUser.setUsername(username);
return appUser;
}
}

View File

@@ -0,0 +1,8 @@
package de.zendric.app.xpensely_server.model.DTO;
public class ExpenseListDTO {
// TODO should combine the two categories to one;
// private List<CategoryDTO> availableCategories;
}

View File

@@ -0,0 +1,11 @@
package de.zendric.app.xpensely_server.model.Exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.CONFLICT)
public class UsernameAlreadyExistsException extends RuntimeException {
public UsernameAlreadyExistsException(String message) {
super(message);
}
}

View File

@@ -32,8 +32,9 @@ public class Expense {
private AppUser owner;
private Double amount;
private Double deviation;
private Double personalUseAmount;
private Double otherPersonAmount;
private String category;
private LocalDate date;
@ManyToOne

View File

@@ -0,0 +1,41 @@
package de.zendric.app.xpensely_server.model;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExpenseChangeRequest {
private Long id;
private String title;
private String ownerName;
private Double amount;
private Double personalUseAmount;
private Double otherPersonAmount;
private LocalDate date;
private String category;
public Expense convertToExpense(Long userId, ExpenseList expenseList) {
AppUser appUser = new AppUser();
appUser.setId(userId);
appUser.setUsername(ownerName);
Expense expense = new Expense();
expense.setAmount(amount);
expense.setDate(date);
expense.setPersonalUseAmount(personalUseAmount);
expense.setOtherPersonAmount(otherPersonAmount);
expense.setExpenseList(expenseList);
expense.setId(id);
expense.setOwner(appUser);
expense.setTitle(title);
expense.setCategory(category);
return expense;
}
}

View File

@@ -0,0 +1,54 @@
package de.zendric.app.xpensely_server.model;
import java.time.LocalDate;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ExpenseInput {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String owner;
private Double amount;
private Double personalUseAmount;
private Double otherPersonAmount;
private LocalDate date;
private String category;
private ExpenseList expenseList;
public Expense convertToExpense(Long userId) {
AppUser appUser = new AppUser();
appUser.setId(userId);
appUser.setUsername(owner);
Expense expense = new Expense();
expense.setAmount(amount);
expense.setDate(date);
expense.setPersonalUseAmount(personalUseAmount);
expense.setOtherPersonAmount(otherPersonAmount);
expense.setExpenseList(expenseList);
expense.setId(id);
expense.setOwner(appUser);
expense.setTitle(title);
expense.setCategory(category);
return expense;
}
}

View File

@@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@@ -40,8 +41,17 @@ public class ExpenseList {
@ManyToOne
private AppUser sharedWith;
@ManyToOne(fetch = FetchType.EAGER)
private XpenselyStandardCategories xpenselyStandardCategories;
@OneToMany(mappedBy = "expenseList", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
@jakarta.persistence.OrderBy("name ASC")
private List<XpenselyCustomCategory> customCategories;
@OneToMany(mappedBy = "expenseList", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
@jakarta.persistence.OrderBy("date ASC, id ASC")
private List<Expense> expenses;
public void addExpense(Expense expense) {

View File

@@ -0,0 +1,37 @@
package de.zendric.app.xpensely_server.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class XpenselyCustomCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "color_code", length = 7, nullable = false)
private String colorCode;
@ManyToOne
@JoinColumn(name = "expense_list_id", nullable = false)
@JsonBackReference
private ExpenseList expenseList;
}

View File

@@ -0,0 +1,29 @@
package de.zendric.app.xpensely_server.model;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class XpenselyStandardCategories {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "global_categories_id")
private List<XpenselyStandardCategory> categories;
}

View File

@@ -0,0 +1,27 @@
package de.zendric.app.xpensely_server.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class XpenselyStandardCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "color_code", length = 7, nullable = false)
private String colorCode;
}

View File

@@ -0,0 +1,40 @@
package de.zendric.app.xpensely_server.preparation;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import de.zendric.app.xpensely_server.model.XpenselyStandardCategories;
import de.zendric.app.xpensely_server.model.XpenselyStandardCategory;
import de.zendric.app.xpensely_server.repo.XpenselyStandardCategoriesRepository;
@Component
public class DataInitializer implements CommandLineRunner {
@Autowired
private XpenselyStandardCategoriesRepository globalRepo;
@Override
public void run(String... args) throws Exception {
Optional<XpenselyStandardCategories> optional = globalRepo.findById(1L);
XpenselyStandardCategories global = optional.orElseGet(() -> {
XpenselyStandardCategories g = new XpenselyStandardCategories();
g.setId(1L);
return g;
});
List<XpenselyStandardCategory> categories = List.of(
new XpenselyStandardCategory(null, "Food", "#FF5733"),
new XpenselyStandardCategory(null, "Transportation", "#33C3FF"),
new XpenselyStandardCategory(null, "Entertainment", "#33FF57"),
new XpenselyStandardCategory(null, "Shopping", "#FF33A8"),
new XpenselyStandardCategory(null, "Household", "#FFC733"),
new XpenselyStandardCategory(null, "Other", "#9D33FF"));
global.setCategories(categories);
globalRepo.save(global);
}
}

View File

@@ -1,5 +1,7 @@
package de.zendric.app.xpensely_server.repo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -7,4 +9,5 @@ import de.zendric.app.xpensely_server.model.Expense;
@Repository
public interface ExpenseRepository extends JpaRepository<Expense, Long> {
List<Expense> findAllByOrderByDateAsc();
}

View File

@@ -10,4 +10,8 @@ import de.zendric.app.xpensely_server.model.AppUser;
@Repository
public interface UserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
Optional<AppUser> findByGoogleId(String id);
Boolean existsByUsername(String username);
}

View File

@@ -0,0 +1,10 @@
package de.zendric.app.xpensely_server.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import de.zendric.app.xpensely_server.model.XpenselyCustomCategory;
@Repository
public interface XpenselyCustomCategoryRepository extends JpaRepository<XpenselyCustomCategory, Long> {
}

View File

@@ -0,0 +1,11 @@
package de.zendric.app.xpensely_server.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import de.zendric.app.xpensely_server.model.XpenselyStandardCategories;
@Repository
public interface XpenselyStandardCategoriesRepository extends JpaRepository<XpenselyStandardCategories, Long> {
}

View File

@@ -2,6 +2,7 @@ package de.zendric.app.xpensely_server.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -11,9 +12,26 @@ import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Profile("test") // Only enable this for testing
public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll())
.csrf().disable();
return http.build();
}
@Bean
@Profile("!test")
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()).oauth2Login(Customizer.withDefaults());
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()))
.oauth2Login(Customizer.withDefaults())
.csrf().disable();
return http.build();
}

View File

@@ -0,0 +1,24 @@
package de.zendric.app.xpensely_server.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.zendric.app.xpensely_server.model.XpenselyStandardCategories;
import de.zendric.app.xpensely_server.repo.XpenselyStandardCategoriesRepository;
@Service
public class CategoryService {
private final XpenselyStandardCategoriesRepository standardCategoriesRepo;
@Autowired
public CategoryService(XpenselyStandardCategoriesRepository standardCategoriesRepo) {
this.standardCategoriesRepo = standardCategoriesRepo;
}
public XpenselyStandardCategories getDefaultCategories() {
return standardCategoriesRepo.findById(1L)
.orElseThrow(() -> new IllegalStateException("Standard categories not found"));
}
}

View File

@@ -14,8 +14,10 @@ import org.springframework.transaction.annotation.Transactional;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.Expense;
import de.zendric.app.xpensely_server.model.ExpenseList;
import de.zendric.app.xpensely_server.model.XpenselyCustomCategory;
import de.zendric.app.xpensely_server.repo.ExpenseListRepository;
import de.zendric.app.xpensely_server.repo.ExpenseRepository;
import de.zendric.app.xpensely_server.repo.XpenselyCustomCategoryRepository;
import jakarta.persistence.EntityManager;
@Service
@@ -24,14 +26,17 @@ public class ExpenseListService {
private ExpenseListRepository repository;
private final ExpenseRepository expenseRepository;
private XpenselyCustomCategoryRepository customCategoryRepository;
@Autowired
private EntityManager entityManager;
@Autowired
public ExpenseListService(ExpenseListRepository repository, ExpenseRepository expenseRepository) {
public ExpenseListService(ExpenseListRepository repository, ExpenseRepository expenseRepository,
XpenselyCustomCategoryRepository customCategoryRepository) {
this.repository = repository;
this.expenseRepository = expenseRepository;
this.customCategoryRepository = customCategoryRepository;
}
public List<ExpenseList> getAllLists() {
@@ -98,15 +103,20 @@ public class ExpenseListService {
}
public Expense addExpenseToList(Long expenseListId, Expense expense) {
// find expenseList
ExpenseList expenseList = repository.findById(expenseListId)
.orElseThrow(() -> new RuntimeException("ExpenseList not found with id: " + expenseListId));
// get all added expenses
HashSet<Long> existingId = new HashSet<>();
for (Expense e : expenseList.getExpenses()) {
existingId.add(e.getId());
}
// add the new expense
expenseList.addExpense(expense);
// save
repository.save(expenseList);
Expense newExpense = expense;
Expense newExpense = new Expense();
for (Expense e : expenseList.getExpenses()) {
if (!existingId.contains(e.getId())) {
newExpense = e;
@@ -137,18 +147,64 @@ public class ExpenseListService {
public String generateInviteCode(Long listId) {
ExpenseList list = repository.findById(listId)
.orElseThrow(() -> new RuntimeException("List not found"));
String inviteCode;
if (list.getInviteCode() == null || list.getInviteCodeExpiration().isBefore(LocalDateTime.now())) {
String inviteCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase();
inviteCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase();
LocalDateTime expirationTime = LocalDateTime.now().plusWeeks(1);
list.setInviteCode(inviteCode);
list.setInviteCodeExpiration(expirationTime);
repository.save(list);
} else {
inviteCode = list.getInviteCode();
}
return inviteCode;
}
public ExpenseList findByInviteCode(String inviteCode) {
return repository.findByInviteCode(inviteCode);
}
public Expense updateExpense(Long expenseListId, Expense updatedExpense) {
ExpenseList expenseList = repository.findById(expenseListId)
.orElseThrow(() -> new IllegalArgumentException("ExpenseList not found"));
if (!expenseList.getExpenses().stream()
.anyMatch(expense -> expense.getId().equals(updatedExpense.getId()))) {
throw new IllegalArgumentException("Expense does not belong to the specified ExpenseList");
}
Expense existingExpense = expenseRepository.findById(updatedExpense.getId())
.orElseThrow(() -> new IllegalArgumentException("Expense not found"));
existingExpense.setTitle(updatedExpense.getTitle());
existingExpense.setAmount(updatedExpense.getAmount());
existingExpense.setPersonalUseAmount(updatedExpense.getPersonalUseAmount());
existingExpense.setOtherPersonAmount(updatedExpense.getOtherPersonAmount());
existingExpense.setDate(updatedExpense.getDate());
existingExpense.setOwner(updatedExpense.getOwner());
existingExpense.setCategory(updatedExpense.getCategory());
return expenseRepository.save(existingExpense);
}
// TODO implement API for this
public XpenselyCustomCategory addCustomCategory(Long expenseListId, XpenselyCustomCategory customCategory) {
ExpenseList expenseList = repository.findById(expenseListId)
.orElseThrow(() -> new RuntimeException("Expense List not found"));
customCategory.setExpenseList(expenseList);
return customCategoryRepository.save(customCategory);
}
// TODO implement API for this
public void deleteCustomCategory(Long expenseListId, Long categoryId) {
XpenselyCustomCategory category = customCategoryRepository.findById(categoryId)
.orElseThrow(() -> new RuntimeException("Custom Category not found"));
if (!category.getExpenseList().getId().equals(expenseListId)) {
throw new RuntimeException("Category does not belong to the specified Expense List");
}
customCategoryRepository.delete(category);
}
}

View File

@@ -6,6 +6,7 @@ import java.util.Optional;
import org.springframework.stereotype.Service;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.Exception.UsernameAlreadyExistsException;
import de.zendric.app.xpensely_server.repo.UserRepository;
@Service
@@ -21,6 +22,9 @@ public class UserService {
}
public AppUser createUser(AppUser user) {
if (Boolean.TRUE.equals(userRepository.existsByUsername(user.getUsername()))) {
throw new UsernameAlreadyExistsException("Username already exists");
}
return userRepository.save(user);
}
@@ -49,4 +53,12 @@ public class UserService {
return null;
}
public AppUser getUserByGoogleId(String id) {
Optional<AppUser> optUser = userRepository.findByGoogleId(id);
if (optUser.isPresent()) {
return optUser.get();
} else
return null;
}
}

View File

@@ -3,12 +3,14 @@ spring.application.name=XpenselyServer
#Security
spring.security.enabled=false
#logging.level.org.springframework.security=TRACE
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://accounts.google.com
# PostgreSQL Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/Xpensely
spring.datasource.username=${XpenselyDBUser}
spring.datasource.password=${XpenselyDBPW}
spring.datasource.url=jdbc:postgresql://postgresdb:${DB_PORT}/${DB_P_NAME}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver
# Hibernate configuration

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,26 @@
package de.zendric.app.xpensely_Server;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import de.zendric.app.xpensely_server.model.ExpenseList;
import de.zendric.app.xpensely_server.repo.ExpenseListRepository;
@DataJpaTest
class ExpenseListRepositoryTest {
@Autowired
private ExpenseListRepository expenseListRepository;
@Test
void testFindExpenseListById() {
// Assuming an ExpenseList with id = 1 exists in your test DB.
Optional<ExpenseList> optionalExpenseList = expenseListRepository.findById(1L);
ExpenseList expenseList = optionalExpenseList.get();
System.out.println("ExpenseList name: " + expenseList.getName());
}
}

View File

@@ -1,13 +0,0 @@
package de.zendric.app.xpensely_Server;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class XpenselyServerApplicationTests {
@Test
void contextLoads() {
}
}