33 Commits

Author SHA1 Message Date
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
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
27 changed files with 612 additions and 36 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

42
docker-compose.yml Normal file
View File

@@ -0,0 +1,42 @@
version: "3.8"
services:
xpensely-server:
image: tea.zendric.de/cedric/xpensely-server:latest
container_name: xpensely-server
ports:
- 3636:8080
environment:
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
DB_PORT: 5434
DB_NAME: ${DB_P_NAME}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
postgresdb:
condition: service_healthy
networks:
- xpensely-network
postgresdb:
image: postgres:14
container_name: postgresdb
ports:
- 5434:5432
environment:
POSTGRES_DB: ${DB_P_NAME}
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}"]
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

@@ -2,8 +2,10 @@ 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

@@ -12,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.UsernameAlreadyExistsException;
import de.zendric.app.xpensely_server.services.UserService;
@RestController
@@ -35,10 +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<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

View File

@@ -1,5 +1,6 @@
package de.zendric.app.xpensely_server.controller;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -11,24 +12,32 @@ 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) {
public ExpenseListController(ExpenseListService expenseListService, UserService userService) {
this.expenseListService = expenseListService;
this.userService = userService;
}
@GetMapping("/all")
@@ -86,12 +95,25 @@ class ExpenseListController {
}
}
@PostMapping
@PostMapping("/create")
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);
} 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);
}
}
@@ -109,8 +131,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) {
@@ -118,6 +143,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,
@@ -129,4 +174,39 @@ class ExpenseListController {
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

@@ -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

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

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

@@ -1,7 +1,9 @@
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;
@@ -9,7 +11,6 @@ import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor;
@@ -29,14 +30,19 @@ public class ExpenseList {
private String name;
private String inviteCode;
@JsonIgnore
private LocalDateTime inviteCodeExpiration;
@ManyToOne
private AppUser owner;
@ManyToMany
private List<AppUser> sharedWith;
@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) {

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

@@ -10,4 +10,6 @@ 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

@@ -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

@@ -10,10 +10,25 @@ 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()).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,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

@@ -1,9 +1,11 @@
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;
@@ -64,18 +66,16 @@ public class ExpenseListService {
List<ExpenseList> allLists = repository.findAll();
List<ExpenseList> userSpecificList = new ArrayList<>();
for (ExpenseList expenseList : allLists) {
List<AppUser> sharedWith = expenseList.getSharedWith();
AppUser sharedWith = expenseList.getSharedWith();
if (expenseList.getOwner().getId().equals(id)) {
userSpecificList.add(expenseList);
} else {
for (AppUser user : sharedWith) {
if (user.getId().equals(id)) {
if (sharedWith != null && sharedWith.getId().equals(id)) {
userSpecificList.add(expenseList);
}
}
}
}
return userSpecificList;
}
@@ -83,31 +83,35 @@ public class ExpenseListService {
List<ExpenseList> allLists = repository.findAll();
List<ExpenseList> userSpecificList = new ArrayList<>();
for (ExpenseList expenseList : allLists) {
List<AppUser> sharedWith = expenseList.getSharedWith();
AppUser sharedWith = expenseList.getSharedWith();
if (expenseList.getOwner().getUsername().equals(username)) {
userSpecificList.add(expenseList);
} else {
for (AppUser user : sharedWith) {
if (user.getUsername().equals(username)) {
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 = expense;
Expense newExpense = new Expense();
for (Expense e : expenseList.getExpenses()) {
if (!existingId.contains(e.getId())) {
newExpense = e;
@@ -134,4 +138,47 @@ public class ExpenseListService {
}
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

@@ -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.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);
}
@@ -48,4 +52,13 @@ public class UserService {
} 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

@@ -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: 8.2 KiB