41 Commits

Author SHA1 Message Date
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
0ee56e4e52 Fixed bug in finding ExpenseLists 2024-12-29 00:47:10 +01:00
4df0b36f45 Sharing Lists logic 2024-12-28 01:35:50 +01:00
e20be63e5e Oauth setup 2024-12-25 01:04:05 +01:00
aa4ed91b9d expense List logic 2024-12-24 23:04:35 +01:00
a3a89abc34 user endpoints working 2024-12-23 11:44:02 +01:00
aec991374f db connected 2024-12-22 23:53:36 +01:00
32 changed files with 1381 additions and 5 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

199
apiDesign.drawio Normal file
View File

@@ -0,0 +1,199 @@
<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">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="ExpenseList" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#647687;fontColor=#ffffff;strokeColor=#314354;" parent="1" vertex="1">
<mxGeometry x="360" y="60" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="5" value="add expense" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" parent="1" vertex="1">
<mxGeometry x="550" y="190" width="90" height="50" as="geometry"/>
</mxCell>
<mxCell id="6" value="id" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="470" y="30" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="7" value="owner" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="470" y="50" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="8" value="name" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="470" y="70" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="9" value="sharedWith" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="470" y="90" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="10" value="expenses" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="470" y="110" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="11" value="List&amp;lt;User&amp;gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="590" y="90" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="13" value="List&amp;lt;Expense&amp;gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="590" y="110" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="14" value="String" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="590" y="70" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="15" value="User" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="590" y="50" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="16" value="Long" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="590" y="30" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="17" value="remove expense" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" parent="1" vertex="1">
<mxGeometry x="670" y="190" width="90" height="50" as="geometry"/>
</mxCell>
<mxCell id="19" style="edgeStyle=none;html=1;" parent="1" source="18" target="5" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="20" style="edgeStyle=none;html=1;entryX=0;entryY=1;entryDx=0;entryDy=0;" parent="1" source="18" target="17" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="22" style="edgeStyle=none;html=1;" parent="1" source="18" target="21" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="24" style="edgeStyle=none;html=1;" parent="1" source="18" target="23" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="26" style="edgeStyle=none;html=1;entryX=1;entryY=1;entryDx=0;entryDy=0;" parent="1" source="18" target="25" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="28" style="edgeStyle=none;html=1;" parent="1" source="18" target="27" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="18" value="ExpenseList" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#647687;fontColor=#ffffff;strokeColor=#314354;" parent="1" vertex="1">
<mxGeometry x="540" y="270" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="21" value="add User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" parent="1" vertex="1">
<mxGeometry x="740" y="270" width="90" height="50" as="geometry"/>
</mxCell>
<mxCell id="23" value="remove User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" parent="1" vertex="1">
<mxGeometry x="720" y="350" width="90" height="50" as="geometry"/>
</mxCell>
<mxCell id="25" value="update expense" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" parent="1" vertex="1">
<mxGeometry x="440" y="190" width="90" height="50" as="geometry"/>
</mxCell>
<mxCell id="27" value="update name" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" parent="1" vertex="1">
<mxGeometry x="380" y="275" width="90" height="50" as="geometry"/>
</mxCell>
<mxCell id="29" value="&lt;h1&gt;ExpenseList&lt;/h1&gt;&lt;div&gt;ExpenseList:&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;- create a List&lt;/div&gt;&lt;div&gt;- share a List&lt;/div&gt;&lt;div&gt;- handle Expenses&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Expense:&lt;/div&gt;&lt;div&gt;- create a new Expense&lt;/div&gt;&lt;div&gt;- update an Expense&lt;/div&gt;&lt;div&gt;- delete an Expense&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;User:&lt;br&gt;- add User&lt;/div&gt;&lt;div&gt;-&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;" style="text;html=1;strokeColor=none;fillColor=none;spacing=5;spacingTop=-20;whiteSpace=wrap;overflow=hidden;rounded=0;" parent="1" vertex="1">
<mxGeometry x="20" y="10" width="190" height="270" as="geometry"/>
</mxCell>
<mxCell id="30" value="Expense" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#647687;fontColor=#ffffff;strokeColor=#314354;" parent="1" vertex="1">
<mxGeometry x="370" y="510" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="31" value="id" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="480" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="32" value="owner" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="500" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="33" value="name" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="520" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="34" value="amount" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="540" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="35" value="deviation" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="560" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="36" value="Double" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="540" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="37" value="Double" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="560" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="38" value="String" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="520" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="39" value="User" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="500" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="40" value="Long" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="480" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="41" value="date" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="580" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="42" value="Time" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="580" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="43" value="expenseList" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="480" y="600" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="44" value="ExpenseList" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="600" y="600" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="45" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="900" y="40" width="200" height="320" as="geometry"/>
</mxCell>
<mxCell id="46" value="ExpenseList" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="910" y="50" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="47" value="ExpenseList" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="910" y="100" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="48" value="+" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="910" y="150" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="50" value="Home" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="970" y="10" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="51" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="1140" y="40" width="200" height="320" as="geometry"/>
</mxCell>
<mxCell id="52" value="ExpenseList" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="1180" y="50" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="53" value="&lt;font style=&quot;font-size: 8px;&quot;&gt;24.12.24 ; Expense ; 24,12 €&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="1150" y="90" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="54" value="+" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="1300" y="285" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="55" value="ExpenseList" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="1210" y="10" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="58" value="&lt;span style=&quot;color: rgb(255, 255, 255); font-size: 8px;&quot;&gt;24.12.24 ; Expense ; 24,12 €&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="1210" y="115" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="59" 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="1150" y="140" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="60" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="1390" y="40" width="200" height="320" as="geometry"/>
</mxCell>
<mxCell id="61" value="ExpenseList" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="1430" y="50" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="62" value="&lt;font style=&quot;font-size: 8px;&quot;&gt;24.12.24 ; Expense ; 24,12 €&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="1430" y="80" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="63" value="+" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="1540" y="320" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="64" value="Update Expense" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="1430" y="10" width="120" height="30" as="geometry"/>
</mxCell>
<mxCell id="67" 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;" parent="1" vertex="1">
<mxGeometry x="1405" y="110" width="170" height="80" as="geometry"/>
</mxCell>
<mxCell id="68" value="&lt;span style=&quot;color: rgb(255, 255, 255); font-size: 8px;&quot;&gt;Total: 24,12 €&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" parent="1" vertex="1">
<mxGeometry x="1150" y="290" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="70" value="" style="endArrow=none;html=1;fontSize=8;entryX=1;entryY=0.75;entryDx=0;entryDy=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1140" y="325" as="sourcePoint"/>
<mxPoint x="1340" y="325" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="72" value="&lt;span style=&quot;color: rgb(255, 255, 255); font-size: 8px;&quot;&gt;24.12.24 ; Expense ; 24,12 €&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f0a30a;fontColor=#000000;strokeColor=#BD7000;" parent="1" vertex="1">
<mxGeometry x="1460" y="200" width="120" height="20" as="geometry"/>
</mxCell>
<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>
</root>
</mxGraphModel>
</diagram>
</mxfile>

48
docker-compose.yml Normal file
View File

@@ -0,0 +1,48 @@
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:
- 5432: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

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"]

13
pom.xml
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/>
@@ -38,11 +38,18 @@
<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>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<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

@@ -1,9 +1,11 @@
package de.zendric.app.XpenselyServer;
package de.zendric.app.xpensely_server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class XpenselyServerApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,73 @@
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.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.AppUserCreateRequest;
import de.zendric.app.xpensely_server.model.UsernameAlreadyExistsException;
import de.zendric.app.xpensely_server.services.UserService;
@RestController
@RequestMapping("/api/users")
public class AppUserController {
private UserService userService;
@Autowired
public AppUserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public AppUser getUser(@RequestParam Long id) {
return userService.getUser(id);
}
@GetMapping("/byName")
public AppUser getUserByName(@RequestParam String username) {
return userService.getUserByName(username);
}
@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<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);
}
}
@DeleteMapping
public String deleteUser(@RequestParam Long id) {
AppUser user = userService.deleteUserById(id);
return "User deleted : " + user.getUsername();
}
}

View File

@@ -0,0 +1,212 @@
package de.zendric.app.xpensely_server.controller;
import java.time.LocalDateTime;
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.http.ResponseEntity;
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;
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.services.ExpenseListService;
import de.zendric.app.xpensely_server.services.UserService;
@RestController
@RequestMapping("/api/expenselist")
class ExpenseListController {
private ExpenseListService expenseListService;
private UserService userService;
@Autowired
public ExpenseListController(ExpenseListService expenseListService, UserService userService) {
this.expenseListService = expenseListService;
this.userService = userService;
}
@GetMapping("/all")
public ResponseEntity<List<ExpenseList>> getAll() {
try {
List<ExpenseList> items = new ArrayList<>();
expenseListService.findAll().forEach(items::add);
if (items.isEmpty())
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
return new ResponseEntity<>(items, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/byUser")
public ResponseEntity<List<ExpenseList>> getByUser(@RequestParam Long userId) {
try {
List<ExpenseList> items = expenseListService.findByUserId(userId);
if (items.isEmpty())
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
return new ResponseEntity<>(items, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/byUsername")
public ResponseEntity<List<ExpenseList>> getByUser(@RequestParam String username) {
try {
List<ExpenseList> items = expenseListService.findByUsername(username);
if (items.isEmpty())
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
return new ResponseEntity<>(items, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/byId")
public ResponseEntity<ExpenseList> getById(@RequestParam Long id) {
Optional<ExpenseList> existingItemOptional = expenseListService.findById(id);
if (existingItemOptional.isPresent()) {
return new ResponseEntity<>(existingItemOptional.get(), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@PostMapping("/create")
public ResponseEntity<ExpenseList> create(@RequestBody ExpenseList expenseList) {
try {
if (expenseList.getOwner() != null) {
AppUser existingOwner = userService.getUser(expenseList.getOwner().getId());
if (existingOwner == null) {
throw new IllegalArgumentException("Owner does not exist.");
}
expenseList.setOwner(existingOwner);
} 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);
}
}
@DeleteMapping("{id}")
public ResponseEntity<HttpStatus> delete(@PathVariable("id") Long id) {
try {
expenseListService.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
}
}
@PostMapping("/{id}/add")
public ResponseEntity<Expense> addExpenseToList(
@PathVariable("id") Long expenseListId,
@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) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@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,
@RequestParam("expenseId") Long expenseId) {
try {
expenseListService.deleteExpenseFromList(expenseListId, expenseId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.EXPECTATION_FAILED);
}
}
@PostMapping("/{listId}/invite")
public ResponseEntity<String> generateInvite(@PathVariable Long listId) {
String inviteCode = expenseListService.generateInviteCode(listId);
return ResponseEntity.ok(inviteCode);
}
@PostMapping("/accept-invite")
public ResponseEntity<?> acceptInvite(@RequestBody InviteRequest inviteRequest) {
ExpenseList list = expenseListService.findByInviteCode(inviteRequest.getInviteCode());
if (list == null || list.getInviteCodeExpiration() == null ||
list.getInviteCodeExpiration().isBefore(LocalDateTime.now())) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Invalid or expired invite code");
}
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());
} catch (Exception e) {
throw new RuntimeException("User not found");
}
if (user != null) {
list.setSharedWith(user);
expenseListService.save(list);
} else {
throw new RuntimeException("User not found");
}
return ResponseEntity.ok("User added to the list");
}
}

View File

@@ -0,0 +1,13 @@
package de.zendric.app.xpensely_server.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class HomeController {
@GetMapping("/")
public String getAll() {
return "Welcome";
}
}

View File

@@ -0,0 +1,40 @@
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;
@Getter
@Setter
@NoArgsConstructor
@Entity
@EqualsAndHashCode
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@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,44 @@
package de.zendric.app.xpensely_server.model;
import java.time.LocalDate;
import com.fasterxml.jackson.annotation.JsonBackReference;
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;
@Getter
@Setter
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Expense {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private AppUser owner;
private Double amount;
private Double personalUseAmount;
private Double otherPersonAmount;
private LocalDate date;
@ManyToOne
@JoinColumn(name = "expense_list_id", nullable = false)
@JsonBackReference
private ExpenseList expenseList;
}

View File

@@ -0,0 +1,39 @@
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;
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);
return expense;
}
}

View File

@@ -0,0 +1,52 @@
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 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);
return expense;
}
}

View File

@@ -0,0 +1,58 @@
package de.zendric.app.xpensely_server.model;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ExpenseList {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String inviteCode;
@JsonIgnore
private LocalDateTime inviteCodeExpiration;
@ManyToOne
private AppUser owner;
@ManyToOne
private AppUser sharedWith;
@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) {
expense.setExpenseList(this);
expenses.add(expense);
}
public void removeExpense(Expense expense) {
expenses.remove(expense);
expense.setExpenseList(null);
}
}

View File

@@ -0,0 +1,13 @@
package de.zendric.app.xpensely_server.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class InviteRequest {
private String inviteCode;
private Long userId;
}

View File

@@ -0,0 +1,11 @@
package de.zendric.app.xpensely_server.model;
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

@@ -0,0 +1,15 @@
package de.zendric.app.xpensely_server.repo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import de.zendric.app.xpensely_server.model.ExpenseList;
@Repository
public interface ExpenseListRepository extends JpaRepository<ExpenseList, Long> {
List<ExpenseList> findByOwnerId(Long ownerId);
ExpenseList findByInviteCode(String inviteCode);
}

View File

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

View File

@@ -0,0 +1,17 @@
package de.zendric.app.xpensely_server.repo;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
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,35 @@
package de.zendric.app.xpensely_server.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// @Bean
// public SecurityFilterChain securityFilterChain(HttpSecurity http) throws
// Exception {
// http.authorizeHttpRequests(auth -> auth
// .anyRequest().permitAll()).csrf().disable();
// ;
// return http.build();
// }
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
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,34 @@
package de.zendric.app.xpensely_server.services;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import de.zendric.app.xpensely_server.model.ExpenseList;
import de.zendric.app.xpensely_server.repo.ExpenseListRepository;
@Service
public class CleanupService {
@Autowired
private ExpenseListRepository expenseListRepository;
@Scheduled(cron = "0 0 0 * * ?") // Runs daily at midnight
public void cleanupExpiredInvites() {
List<ExpenseList> expiredLists = expenseListRepository.findAll().stream()
.filter(list -> list.getInviteCodeExpiration() != null &&
list.getInviteCodeExpiration().isBefore(LocalDateTime.now()))
.collect(Collectors.toList());
for (ExpenseList list : expiredLists) {
list.setInviteCode(null);
list.setInviteCodeExpiration(null);
expenseListRepository.save(list);
}
}
}

View File

@@ -0,0 +1,184 @@
package de.zendric.app.xpensely_server.services;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
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.repo.ExpenseListRepository;
import de.zendric.app.xpensely_server.repo.ExpenseRepository;
import jakarta.persistence.EntityManager;
@Service
@Transactional
public class ExpenseListService {
private ExpenseListRepository repository;
private final ExpenseRepository expenseRepository;
@Autowired
private EntityManager entityManager;
@Autowired
public ExpenseListService(ExpenseListRepository repository, ExpenseRepository expenseRepository) {
this.repository = repository;
this.expenseRepository = expenseRepository;
}
public List<ExpenseList> getAllLists() {
return repository.findAll();
}
public ExpenseList createList(ExpenseList list) {
return repository.save(list);
}
public void deleteList(Long id) {
repository.deleteById(id);
}
public void deleteById(Long id) {
repository.deleteById(id);
}
public Optional<ExpenseList> findById(Long id) {
return repository.findById(id);
}
public Iterable<ExpenseList> findAll() {
return repository.findAll();
}
public ExpenseList save(ExpenseList expenseList) {
return repository.save(expenseList);
}
public List<ExpenseList> findByUserId(Long id) {
List<ExpenseList> allLists = repository.findAll();
List<ExpenseList> userSpecificList = new ArrayList<>();
for (ExpenseList expenseList : allLists) {
AppUser sharedWith = expenseList.getSharedWith();
if (expenseList.getOwner().getId().equals(id)) {
userSpecificList.add(expenseList);
} else {
if (sharedWith != null && sharedWith.getId().equals(id)) {
userSpecificList.add(expenseList);
}
}
}
return userSpecificList;
}
public List<ExpenseList> findByUsername(String username) {
List<ExpenseList> allLists = repository.findAll();
List<ExpenseList> userSpecificList = new ArrayList<>();
for (ExpenseList expenseList : allLists) {
AppUser sharedWith = expenseList.getSharedWith();
if (expenseList.getOwner().getUsername().equals(username)) {
userSpecificList.add(expenseList);
} else {
if (sharedWith != null && sharedWith.getUsername().equals(username)) {
userSpecificList.add(expenseList);
}
}
}
return userSpecificList;
}
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 = new Expense();
for (Expense e : expenseList.getExpenses()) {
if (!existingId.contains(e.getId())) {
newExpense = e;
break;
}
}
return newExpense;
}
public void deleteExpenseFromList(Long expenseListId, Long expenseId) {
ExpenseList expenseList = repository.findById(expenseListId)
.orElseThrow(() -> new RuntimeException("ExpenseList not found with id: " + expenseListId));
Expense expenseToRemove = null;
for (Expense expense : expenseList.getExpenses()) {
if (expense.getId().equals(expenseId)) {
expenseToRemove = expense;
break;
}
}
if (expenseToRemove != null) {
expenseList.removeExpense(expenseToRemove);
} else {
throw new RuntimeException("Expense not found with id: " + expenseId);
}
repository.save(expenseList);
}
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())) {
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());
return expenseRepository.save(existingExpense);
}
}

View File

@@ -0,0 +1,19 @@
package de.zendric.app.xpensely_server.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import de.zendric.app.xpensely_server.repo.ExpenseRepository;
@Service
@Transactional
public class ExpenseService {
@Autowired
private ExpenseRepository repository;
public ExpenseService(ExpenseRepository repository) {
this.repository = repository;
}
}

View File

@@ -0,0 +1,64 @@
package de.zendric.app.xpensely_server.services;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.UsernameAlreadyExistsException;
import de.zendric.app.xpensely_server.repo.UserRepository;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<AppUser> getAllUsers() {
return userRepository.findAll();
}
public AppUser createUser(AppUser user) {
if (Boolean.TRUE.equals(userRepository.existsByUsername(user.getUsername()))) {
throw new UsernameAlreadyExistsException("Username already exists");
}
return userRepository.save(user);
}
public AppUser getUser(Long id) {
Optional<AppUser> user = userRepository.findById(id);
if (user.isPresent()) {
return user.get();
} else
return null;
}
public AppUser deleteUserById(Long id) {
Optional<AppUser> user = userRepository.findById(id);
if (user.isPresent()) {
userRepository.deleteById(id);
return user.get();
} else
return null;
}
public AppUser getUserByName(String username) {
Optional<AppUser> optUser = userRepository.findByUsername(username);
if (optUser.isPresent()) {
return optUser.get();
} else
return null;
}
public AppUser getUserByGoogleId(String id) {
Optional<AppUser> optUser = userRepository.findByGoogleId(id);
if (optUser.isPresent()) {
return optUser.get();
} else
return null;
}
}

View File

@@ -1 +1,18 @@
#Server
spring.application.name=XpenselyServer
#Security
spring.security.enabled=false
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://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
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

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

@@ -1,4 +1,4 @@
package de.zendric.app.XpenselyServer;
package de.zendric.app.xpensely_Server;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;