diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..6b53e14
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/finances
+
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index f6d4fd2..08300ac 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -10,6 +10,7 @@
+
diff --git a/APIGateway/.env.example b/APIGateway/.env.example
index b45127a..687faea 100644
--- a/APIGateway/.env.example
+++ b/APIGateway/.env.example
@@ -1,3 +1,5 @@
+PROFILE= production/development
+
AUTHENTIK_CLIENT_ID= authentik oauth2 client id
AUTHENTIK_CLIENT_SECRET= authentik oauth2 client secret
AUTHENTIK_ISSUER_URL= authentik issuer url ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/ )
diff --git a/APIGateway/src/main/resources/application.yml b/APIGateway/src/main/resources/application.yml
index 7fd79e0..1e4a146 100644
--- a/APIGateway/src/main/resources/application.yml
+++ b/APIGateway/src/main/resources/application.yml
@@ -3,6 +3,8 @@ logging:
web: trace
core: trace
spring:
+ profiles:
+ active: ${PROFILE}
data:
redis:
host: ${REDIS_HOST}
@@ -30,6 +32,7 @@ spring:
predicates:
- Path=/api/**
filters:
+ - RewritePath=/api/(?.*), /$\{segment}
- TokenRelay=
- id: spa
order: 10
diff --git a/Common/build.gradle b/Common/build.gradle
new file mode 100644
index 0000000..99eba84
--- /dev/null
+++ b/Common/build.gradle
@@ -0,0 +1,21 @@
+plugins {
+ id 'java'
+}
+
+group = 'dev.mvvasilev'
+version = '1.0-SNAPSHOT'
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
+
+ testImplementation platform('org.junit:junit-bom:5.9.1')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/Common/src/main/java/dev/mvvasilev/common/controller/AbstractRestController.java b/Common/src/main/java/dev/mvvasilev/common/controller/AbstractRestController.java
new file mode 100644
index 0000000..3a266a6
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/controller/AbstractRestController.java
@@ -0,0 +1,22 @@
+package dev.mvvasilev.common.controller;
+
+import dev.mvvasilev.common.web.APIErrorDTO;
+import dev.mvvasilev.common.web.APIResponseDTO;
+
+import java.util.List;
+
+public class AbstractRestController {
+
+ protected APIResponseDTO withStatus(int statusCode, String statusText, T body) {
+ return new APIResponseDTO<>(body, null, statusCode, statusText);
+ }
+
+ protected APIResponseDTO withSingleError(int statusCode, String statusText, String errorMessage, String errorCode, String stacktrace) {
+ return new APIResponseDTO<>(null, List.of(new APIErrorDTO(errorMessage, errorCode, stacktrace)), statusCode, statusText);
+ }
+
+ protected APIResponseDTO ok(T body) {
+ return withStatus(200, "ok", body);
+ }
+
+}
diff --git a/Common/src/main/java/dev/mvvasilev/common/data/AbstractEntity.java b/Common/src/main/java/dev/mvvasilev/common/data/AbstractEntity.java
new file mode 100644
index 0000000..57bb3fb
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/data/AbstractEntity.java
@@ -0,0 +1,54 @@
+package dev.mvvasilev.common.data;
+
+import jakarta.persistence.*;
+
+import java.time.LocalDateTime;
+
+@MappedSuperclass
+public abstract class AbstractEntity implements DatabaseStorable {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(columnDefinition = "bigserial")
+ private Long id;
+
+ @Column(name = "time_created", insertable = false, updatable = false)
+ private LocalDateTime timeCreated;
+
+ @Column(name = "time_last_modified", insertable = false)
+ private LocalDateTime timeLastModified;
+
+ protected AbstractEntity() {}
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public LocalDateTime getTimeCreated() {
+ return timeCreated;
+ }
+
+ @Override
+ public LocalDateTime getTimeLastModified() {
+ return timeLastModified;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public void setTimeCreated(LocalDateTime timeCreated) {
+ this.timeCreated = timeCreated;
+ }
+
+ public void setTimeLastModified(LocalDateTime timeLastModified) {
+ this.timeLastModified = timeLastModified;
+ }
+
+ @PreUpdate
+ protected void onUpdate() {
+ this.timeLastModified = LocalDateTime.now();
+ }
+}
diff --git a/Common/src/main/java/dev/mvvasilev/common/data/DataNamingStrategy.java b/Common/src/main/java/dev/mvvasilev/common/data/DataNamingStrategy.java
new file mode 100644
index 0000000..6c50b76
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/data/DataNamingStrategy.java
@@ -0,0 +1,4 @@
+package dev.mvvasilev.common.data;
+
+public class DataNamingStrategy {
+}
diff --git a/Common/src/main/java/dev/mvvasilev/common/data/DatabaseStorable.java b/Common/src/main/java/dev/mvvasilev/common/data/DatabaseStorable.java
new file mode 100644
index 0000000..005bd01
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/data/DatabaseStorable.java
@@ -0,0 +1,13 @@
+package dev.mvvasilev.common.data;
+
+import java.time.LocalDateTime;
+
+public interface DatabaseStorable {
+
+ long getId();
+
+ LocalDateTime getTimeCreated();
+
+ LocalDateTime getTimeLastModified();
+
+}
diff --git a/Common/src/main/java/dev/mvvasilev/common/exceptions/CommonFinancesException.java b/Common/src/main/java/dev/mvvasilev/common/exceptions/CommonFinancesException.java
new file mode 100644
index 0000000..d15e631
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/exceptions/CommonFinancesException.java
@@ -0,0 +1,4 @@
+package dev.mvvasilev.common.exceptions;
+
+public class CommonFinancesException extends RuntimeException {
+}
diff --git a/Common/src/main/java/dev/mvvasilev/common/exceptions/InvalidUserIdException.java b/Common/src/main/java/dev/mvvasilev/common/exceptions/InvalidUserIdException.java
new file mode 100644
index 0000000..d27d00a
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/exceptions/InvalidUserIdException.java
@@ -0,0 +1,4 @@
+package dev.mvvasilev.common.exceptions;
+
+public class InvalidUserIdException extends CommonFinancesException {
+}
diff --git a/Common/src/main/java/dev/mvvasilev/common/web/APIErrorDTO.java b/Common/src/main/java/dev/mvvasilev/common/web/APIErrorDTO.java
new file mode 100644
index 0000000..e417496
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/web/APIErrorDTO.java
@@ -0,0 +1,3 @@
+package dev.mvvasilev.common.web;
+
+public record APIErrorDTO(String message, String errorCode, String stacktrace) { }
diff --git a/Common/src/main/java/dev/mvvasilev/common/web/APIResponseDTO.java b/Common/src/main/java/dev/mvvasilev/common/web/APIResponseDTO.java
new file mode 100644
index 0000000..90d6cac
--- /dev/null
+++ b/Common/src/main/java/dev/mvvasilev/common/web/APIResponseDTO.java
@@ -0,0 +1,5 @@
+package dev.mvvasilev.common.web;
+
+import java.util.Collection;
+
+public record APIResponseDTO(T result, Collection errors, int statusCode, String statusText) { }
diff --git a/PersonalFinancesService/.env.example b/PersonalFinancesService/.env.example
index 42952cd..a7c6912 100644
--- a/PersonalFinancesService/.env.example
+++ b/PersonalFinancesService/.env.example
@@ -1,5 +1,7 @@
+PROFILE= production/development
+
AUTHENTIK_ISSUER_URL= auth server configuration url for fetching JWKs ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/ )
-DATASOURCE_URL= database jdbc url ( postgres only, example: jdbc:postgresql://localhost:45093/mydatabase )
+DATASOURCE_URL= database jdbc url ( postgres only, example: jdbc:postgresql://localhost:5432/mydatabase )
DATASOURCE_USER= database user
DATASOURCE_PASSWORD= database password
\ No newline at end of file
diff --git a/PersonalFinancesService/build.gradle b/PersonalFinancesService/build.gradle
index 5f1240d..86cf4d5 100644
--- a/PersonalFinancesService/build.gradle
+++ b/PersonalFinancesService/build.gradle
@@ -26,10 +26,19 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
+
implementation 'org.flywaydb:flyway-core'
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+ implementation 'org.springdoc:springdoc-openapi-starter-common:2.3.0'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.apache.poi:poi:5.2.5'
+ implementation 'org.apache.poi:poi-ooxml:5.2.5'
+
+ implementation project(":Common")
+
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java
new file mode 100644
index 0000000..586c0d0
--- /dev/null
+++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java
@@ -0,0 +1,42 @@
+package dev.mvvasilev.finances.advice;
+
+import dev.mvvasilev.common.web.APIErrorDTO;
+import dev.mvvasilev.common.web.APIResponseDTO;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.List;
+
+@RestControllerAdvice(basePackages = {"dev.mvvasilev"})
+public class APIResponseAdvice {
+
+ @Value("${debug}")
+ private boolean isDebug;
+
+ private Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @ExceptionHandler(Exception.class)
+ public APIResponseDTO