mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-04-19 14:19:52 +03:00
Add Kafka, zookeeper. Add nginx.
This commit is contained in:
parent
7720263b92
commit
d7c3cbbc69
35 changed files with 536 additions and 142 deletions
59
.docker/nginx/default.conf.template
Normal file
59
.docker/nginx/default.conf.template
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For &proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto &scheme;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass ${FRONTEND_URI};
|
||||||
|
|
||||||
|
location /login {
|
||||||
|
proxy_pass ${LOGIN_SERVICE_URI};
|
||||||
|
}
|
||||||
|
|
||||||
|
location /logout {
|
||||||
|
proxy_pass ${LOGIN_SERVICE_URI};
|
||||||
|
}
|
||||||
|
|
||||||
|
location /oauth2 {
|
||||||
|
proxy_pass ${LOGIN_SERVICE_URI};
|
||||||
|
}
|
||||||
|
|
||||||
|
location /refresh-token {
|
||||||
|
proxy_pass ${LOGIN_SERVICE_URI};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
rewrite ^/api/(.*) /$1 break;
|
||||||
|
|
||||||
|
proxy_pass ${CORE_API_URI};
|
||||||
|
|
||||||
|
proxy_set_header Authorization "Bearer $cookie_pefi_token";
|
||||||
|
proxy_pass_header Authorization;
|
||||||
|
|
||||||
|
location /api/enums/widget-types {
|
||||||
|
rewrite ^/api/(.*) /$1 break;
|
||||||
|
proxy_pass ${WIDGETS_API_URI};
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/enums/supported-conversions {
|
||||||
|
rewrite ^/api/(.*) /$1 break;
|
||||||
|
proxy_pass ${STATEMENTS_API_URI};
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/widgets {
|
||||||
|
rewrite ^/api/(.*) /$1 break;
|
||||||
|
proxy_pass ${WIDGETS_API_URI};
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/statements {
|
||||||
|
rewrite ^/api/(.*) /$1 break;
|
||||||
|
proxy_pass ${STATEMENTS_API_URI};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
@ -14,6 +14,7 @@
|
||||||
<option value="$PROJECT_DIR$/pefi-common" />
|
<option value="$PROJECT_DIR$/pefi-common" />
|
||||||
<option value="$PROJECT_DIR$/pefi-core-api" />
|
<option value="$PROJECT_DIR$/pefi-core-api" />
|
||||||
<option value="$PROJECT_DIR$/pefi-frontend" />
|
<option value="$PROJECT_DIR$/pefi-frontend" />
|
||||||
|
<option value="$PROJECT_DIR$/pefi-login-service" />
|
||||||
<option value="$PROJECT_DIR$/pefi-statements-api" />
|
<option value="$PROJECT_DIR$/pefi-statements-api" />
|
||||||
<option value="$PROJECT_DIR$/pefi-widgets-api" />
|
<option value="$PROJECT_DIR$/pefi-widgets-api" />
|
||||||
</set>
|
</set>
|
||||||
|
|
|
@ -1,74 +1,87 @@
|
||||||
version: '3.4'
|
version: '3.4'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
api-gateway:
|
nginx:
|
||||||
build: ./pefi-api-gateway
|
image: nginx:stable-alpine
|
||||||
|
volumes:
|
||||||
|
- ./.docker/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:80'
|
||||||
environment:
|
environment:
|
||||||
PROFILE: development
|
|
||||||
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
|
|
||||||
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_ID}
|
|
||||||
AUTHENTIK_ISSUER_URL: https://auth.mvvasilev.dev/application/o/${AUTHENTIK_APP_NAME}/
|
|
||||||
AUTHENTIK_BACK_CHANNEL_LOGOUT_URL: https://auth.mvvasilev.dev/application/o/${AUTHENTIK_APP_NAME}/end-session/
|
|
||||||
GATEWAY_URI: http://localhost:8080
|
|
||||||
CORE_API_URI: http://core-api:8081
|
CORE_API_URI: http://core-api:8081
|
||||||
STATEMENTS_API_URI: http://statements-api:8081
|
STATEMENTS_API_URI: http://statements-api:8081
|
||||||
WIDGETS_API_URI: http://widgets-api:8081
|
WIDGETS_API_URI: http://widgets-api:8081
|
||||||
FRONTEND_URI: http://frontend:5173
|
FRONTEND_URI: http://frontend:5173
|
||||||
REDIS_HOST: redis
|
LOGIN_SERVICE_URI: http://login-service:8081
|
||||||
REDIS_PORT: 6379
|
depends_on:
|
||||||
SSL_ENABLED: true
|
- core-api
|
||||||
SSL_KEY_STORE_TYPE: PKCS12
|
- statements-api
|
||||||
SSL_KEY_STORE: classpath:keystore/local.p12
|
- widgets-api
|
||||||
SSL_KEY_STORE_PASSWORD: asdf1234
|
- frontend
|
||||||
SSL_KEY_ALIAS: local
|
- login-service
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./pefi-frontend
|
build: ./pefi-frontend
|
||||||
ports:
|
ports:
|
||||||
- '5173:5173'
|
- '5173:5173'
|
||||||
|
|
||||||
|
login-service:
|
||||||
|
build: ./pefi-login-service
|
||||||
|
ports:
|
||||||
|
- '8084:8081'
|
||||||
|
environment:
|
||||||
|
SERVER_PORT: 8081
|
||||||
|
FRONTEND_URI: http://localhost:8080
|
||||||
|
AUTHENTIK_ISSUER_URI: ${AUTHENTIK_ISSUER_URL}
|
||||||
|
AUTHENTIK_BACK_CHANNEL_LOGOUT_URL: ${AUTHENTIK_BACK_CHANNEL_LOGOUT_URL}
|
||||||
|
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
|
||||||
|
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
|
||||||
|
|
||||||
core-api:
|
core-api:
|
||||||
build: ./pefi-core-api
|
build: ./pefi-core-api
|
||||||
ports:
|
ports:
|
||||||
- '8081:8081'
|
- '8081:8081'
|
||||||
environment:
|
environment:
|
||||||
|
SERVER_PORT: 8081
|
||||||
PROFILE: 'development'
|
PROFILE: 'development'
|
||||||
AUTHENTIK_ISSUER_URL: 'https://auth.mvvasilev.dev/application/o/${AUTHENTIK_APP_NAME}/'
|
AUTHENTIK_ISSUER_URI: ${AUTHENTIK_ISSUER_URL}
|
||||||
DATASOURCE_URL: jdbc:postgresql://database:5432/${POSTGRES_DB}
|
DATASOURCE_URL: jdbc:postgresql://database:5432/${POSTGRES_DB}
|
||||||
DATASOURCE_USER: ${POSTGRES_USER}
|
DATASOURCE_USER: ${POSTGRES_USER}
|
||||||
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
KAFKA_SERVERS: 'kafka-broker:9092'
|
KAFKA_SERVERS: localhost:9092
|
||||||
|
depends_on:
|
||||||
|
- kafka1
|
||||||
|
- database
|
||||||
|
|
||||||
statements-api:
|
statements-api:
|
||||||
build: ./pefi-statements-api
|
build: ./pefi-statements-api
|
||||||
ports:
|
ports:
|
||||||
- '8082:8081'
|
- '8082:8081'
|
||||||
environment:
|
environment:
|
||||||
|
SERVER_PORT: 8081
|
||||||
PROFILE: 'development'
|
PROFILE: 'development'
|
||||||
AUTHENTIK_ISSUER_URL: 'https://auth.mvvasilev.dev/application/o/${AUTHENTIK_APP_NAME}/'
|
AUTHENTIK_ISSUER_URI: ${AUTHENTIK_ISSUER_URL}
|
||||||
DATASOURCE_URL: jdbc:postgresql://database:5432/${POSTGRES_DB}
|
DATASOURCE_URL: jdbc:postgresql://database:5432/${POSTGRES_DB}
|
||||||
DATASOURCE_USER: ${POSTGRES_USER}
|
DATASOURCE_USER: ${POSTGRES_USER}
|
||||||
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
KAFKA_SERVERS: 'kafka-broker:9092'
|
KAFKA_SERVERS: localhost:9092
|
||||||
|
depends_on:
|
||||||
|
- kafka1
|
||||||
|
- database
|
||||||
|
|
||||||
widgets-api:
|
widgets-api:
|
||||||
build: ./pefi-widgets-api
|
build: ./pefi-widgets-api
|
||||||
ports:
|
ports:
|
||||||
- '8083:8081'
|
- '8083:8081'
|
||||||
environment:
|
environment:
|
||||||
|
SERVER_PORT: 8081
|
||||||
PROFILE: 'development'
|
PROFILE: 'development'
|
||||||
AUTHENTIK_ISSUER_URL: 'https://auth.mvvasilev.dev/application/o/${AUTHENTIK_APP_NAME}/'
|
AUTHENTIK_ISSUER_URI: ${AUTHENTIK_ISSUER_URL}
|
||||||
DATASOURCE_URL: jdbc:postgresql://database:5432/${POSTGRES_DB}
|
DATASOURCE_URL: jdbc:postgresql://database:5432/${POSTGRES_DB}
|
||||||
DATASOURCE_USER: ${POSTGRES_USER}
|
DATASOURCE_USER: ${POSTGRES_USER}
|
||||||
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
depends_on:
|
||||||
redis:
|
- database
|
||||||
image: redis/redis-stack:latest
|
|
||||||
ports:
|
|
||||||
- '6379:6379'
|
|
||||||
- '6380:8001'
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: postgres:16.1-alpine
|
image: postgres:16.1-alpine
|
||||||
|
@ -79,34 +92,38 @@ services:
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
|
||||||
kafka-broker:
|
zoo1:
|
||||||
image: confluentinc/cp-kafka:7.5.3
|
image: confluentinc/cp-zookeeper:7.3.2
|
||||||
hostname: broker
|
hostname: zoo1
|
||||||
container_name: broker
|
container_name: zoo1
|
||||||
depends_on:
|
|
||||||
- zookeeper
|
|
||||||
ports:
|
|
||||||
- "29092:29092"
|
|
||||||
- "9092:9092"
|
|
||||||
- "9101:9101"
|
|
||||||
environment:
|
|
||||||
KAFKA_BROKER_ID: 1
|
|
||||||
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
|
|
||||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
|
||||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092
|
|
||||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
||||||
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
|
||||||
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
|
||||||
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
|
|
||||||
KAFKA_JMX_PORT: 9101
|
|
||||||
KAFKA_JMX_HOSTNAME: localhost
|
|
||||||
|
|
||||||
zookeeper:
|
|
||||||
image: confluentinc/cp-zookeeper:7.5.3
|
|
||||||
hostname: zookeeper
|
|
||||||
container_name: zookeeper
|
|
||||||
ports:
|
ports:
|
||||||
- "2181:2181"
|
- "2181:2181"
|
||||||
environment:
|
environment:
|
||||||
ZOOKEEPER_CLIENT_PORT: 2181
|
ZOOKEEPER_CLIENT_PORT: 2181
|
||||||
ZOOKEEPER_TICK_TIME: 2000
|
ZOOKEEPER_SERVER_ID: 1
|
||||||
|
ZOOKEEPER_SERVERS: zoo1:2888:3888
|
||||||
|
|
||||||
|
kafka1:
|
||||||
|
image: confluentinc/cp-kafka:7.3.2
|
||||||
|
hostname: kafka1
|
||||||
|
container_name: kafka1
|
||||||
|
ports:
|
||||||
|
- "9092:9092"
|
||||||
|
- "29092:29092"
|
||||||
|
- "9999:9999"
|
||||||
|
environment:
|
||||||
|
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092,DOCKER://host.docker.internal:29092
|
||||||
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT
|
||||||
|
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
|
||||||
|
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
|
||||||
|
KAFKA_BROKER_ID: 1
|
||||||
|
KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
|
||||||
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
||||||
|
KAFKA_JMX_PORT: 9999
|
||||||
|
KAFKA_JMX_HOSTNAME: ${DOCKER_HOST_IP:-127.0.0.1}
|
||||||
|
KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer
|
||||||
|
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
|
||||||
|
depends_on:
|
||||||
|
- zoo1
|
|
@ -10,6 +10,8 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compileOnly 'jakarta.servlet:jakarta.servlet-api:6.0.0'
|
||||||
|
|
||||||
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
|
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
|
||||||
implementation 'org.springframework:spring-web:6.1.3'
|
implementation 'org.springframework:spring-web:6.1.3'
|
||||||
implementation 'org.springframework.data:spring-data-jpa:3.2.0'
|
implementation 'org.springframework.data:spring-data-jpa:3.2.0'
|
||||||
|
|
|
@ -6,17 +6,29 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.filter.CommonsRequestLoggingFilter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class CommonControllerConfiguration {
|
public class CommonControllerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CommonsRequestLoggingFilter requestLoggingFilter() {
|
||||||
|
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
|
||||||
|
loggingFilter.setIncludeClientInfo(true);
|
||||||
|
loggingFilter.setIncludeQueryString(true);
|
||||||
|
loggingFilter.setIncludePayload(true);
|
||||||
|
loggingFilter.setMaxPayloadLength(64000);
|
||||||
|
return loggingFilter;
|
||||||
|
}
|
||||||
|
|
||||||
@RestControllerAdvice(basePackages = {"dev.mvvasilev"})
|
@RestControllerAdvice(basePackages = {"dev.mvvasilev"})
|
||||||
public static class APIResponseAdvice {
|
public static class APIResponseAdvice {
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,43 @@
|
||||||
package dev.mvvasilev.common.configuration;
|
package dev.mvvasilev.common.configuration;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoders;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
|
||||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
import org.springframework.web.filter.CommonsRequestLoggingFilter;
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableTransactionManagement
|
|
||||||
public class CommonSecurityConfiguration {
|
public class CommonSecurityConfiguration {
|
||||||
|
|
||||||
@Value("${jwt.issuer-url}")
|
|
||||||
public String jwtIssuerUrl;
|
|
||||||
|
|
||||||
private static final String[] WHITELISTED_URLS = {
|
private static final String[] WHITELISTED_URLS = {
|
||||||
"/v3/api-docs/**",
|
"/v3/api-docs/**",
|
||||||
"/swagger-ui/**",
|
|
||||||
"/v2/api-docs/**",
|
"/v2/api-docs/**",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/swagger-ui.html",
|
||||||
"/swagger-resources/**"
|
"/swagger-resources/**"
|
||||||
};
|
};
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, BearerTokenResolver bearerTokenResolver) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
|
||||||
return httpSecurity
|
return httpSecurity
|
||||||
.cors(c -> c.disable()) // won't be needing cors, as the API will be completely hidden behind an api gateway
|
.cors(AbstractHttpConfigurer::disable)
|
||||||
.csrf(c -> c.disable())
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.authorizeHttpRequests(c -> {
|
.authorizeHttpRequests(c -> {
|
||||||
c.requestMatchers(WHITELISTED_URLS).permitAll();
|
c.requestMatchers(WHITELISTED_URLS).anonymous();
|
||||||
c.anyRequest().authenticated();
|
c.anyRequest().authenticated();
|
||||||
})
|
})
|
||||||
.oauth2ResourceServer(c -> {
|
.oauth2ResourceServer(oauth2 -> {
|
||||||
c.jwt(Customizer.withDefaults());
|
oauth2.jwt(Customizer.withDefaults());
|
||||||
c.bearerTokenResolver(bearerTokenResolver);
|
|
||||||
})
|
})
|
||||||
|
.exceptionHandling(e -> e.accessDeniedHandler((req, res, ex) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED)))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public JwtDecoder jwtDecoder() {
|
|
||||||
return JwtDecoders.fromIssuerLocation(jwtIssuerUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public BearerTokenResolver bearerTokenResolver() {
|
|
||||||
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
|
|
||||||
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.AUTHORIZATION);
|
|
||||||
return bearerTokenResolver;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -5,10 +5,15 @@ import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackageClasses = { AuthorizationService.class })
|
@EnableWebMvc
|
||||||
@EnableJpaRepositories("dev.mvvasilev.finances.*")
|
|
||||||
@EntityScan("dev.mvvasilev.finances.*")
|
@EntityScan("dev.mvvasilev.finances.*")
|
||||||
|
@EnableJpaRepositories("dev.mvvasilev.finances.*")
|
||||||
|
@SpringBootApplication(
|
||||||
|
scanBasePackageClasses = { AuthorizationService.class },
|
||||||
|
scanBasePackages = "dev.mvvasilev.finances.*"
|
||||||
|
)
|
||||||
public class PefiCoreAPI {
|
public class PefiCoreAPI {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package dev.mvvasilev.finances.configuration;
|
package dev.mvvasilev.finances.configuration;
|
||||||
|
|
||||||
import dev.mvvasilev.common.dto.KafkaReplaceProcessedTransactionsDTO;
|
import dev.mvvasilev.common.dto.KafkaReplaceProcessedTransactionsDTO;
|
||||||
|
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||||
import org.apache.kafka.common.serialization.StringDeserializer;
|
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||||
import org.apache.kafka.common.serialization.StringSerializer;
|
import org.apache.kafka.common.serialization.StringSerializer;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.kafka.annotation.EnableKafka;
|
||||||
|
import org.springframework.kafka.annotation.EnableKafkaRetryTopic;
|
||||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||||
import org.springframework.kafka.core.ConsumerFactory;
|
import org.springframework.kafka.core.ConsumerFactory;
|
||||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||||
|
@ -15,6 +18,7 @@ import org.springframework.kafka.support.serializer.JsonSerializer;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@EnableKafka
|
||||||
@Configuration
|
@Configuration
|
||||||
public class KafkaConfiguration {
|
public class KafkaConfiguration {
|
||||||
|
|
||||||
|
@ -25,12 +29,12 @@ public class KafkaConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ConsumerFactory<String, KafkaReplaceProcessedTransactionsDTO> replaceTransactionsConsumerFactory() {
|
public ConsumerFactory<String, KafkaReplaceProcessedTransactionsDTO> replaceTransactionsConsumerFactory() {
|
||||||
// ...
|
|
||||||
return new DefaultKafkaConsumerFactory<>(
|
return new DefaultKafkaConsumerFactory<>(
|
||||||
Map.of(
|
Map.of(
|
||||||
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress,
|
ConsumerConfig.GROUP_ID_CONFIG, "core-api",
|
||||||
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
|
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress,
|
||||||
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class
|
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class,
|
||||||
|
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonSerializer.class
|
||||||
),
|
),
|
||||||
new StringDeserializer(),
|
new StringDeserializer(),
|
||||||
new JsonDeserializer<>(KafkaReplaceProcessedTransactionsDTO.class)
|
new JsonDeserializer<>(KafkaReplaceProcessedTransactionsDTO.class)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import dev.mvvasilev.common.web.APIResponseDTO;
|
||||||
import dev.mvvasilev.finances.dtos.CategorizationRuleDTO;
|
import dev.mvvasilev.finances.dtos.CategorizationRuleDTO;
|
||||||
import dev.mvvasilev.finances.dtos.ProcessedTransactionFieldDTO;
|
import dev.mvvasilev.finances.dtos.ProcessedTransactionFieldDTO;
|
||||||
import dev.mvvasilev.finances.enums.CategorizationRule;
|
import dev.mvvasilev.finances.enums.CategorizationRule;
|
||||||
|
import dev.mvvasilev.finances.enums.TimePeriod;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
@ -18,6 +19,11 @@ import java.util.Collection;
|
||||||
@RequestMapping("/enums")
|
@RequestMapping("/enums")
|
||||||
public class EnumsController extends AbstractRestController {
|
public class EnumsController extends AbstractRestController {
|
||||||
|
|
||||||
|
@GetMapping("/statistics-time-periods")
|
||||||
|
public ResponseEntity<APIResponseDTO<TimePeriod[]>> fetchTimePeriods() {
|
||||||
|
return ok(TimePeriod.values());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/category-rules")
|
@GetMapping("/category-rules")
|
||||||
public ResponseEntity<APIResponseDTO<Collection<CategorizationRuleDTO>>> fetchCategorizationRules() {
|
public ResponseEntity<APIResponseDTO<Collection<CategorizationRuleDTO>>> fetchCategorizationRules() {
|
||||||
return ok(
|
return ok(
|
||||||
|
|
|
@ -30,11 +30,6 @@ public class StatisticsController extends AbstractRestController {
|
||||||
this.statisticsService = statisticsService;
|
this.statisticsService = statisticsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/timePeriods")
|
|
||||||
public ResponseEntity<APIResponseDTO<TimePeriod[]>> fetchTimePeriods() {
|
|
||||||
return ok(TimePeriod.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/totalSpendingByCategory")
|
@GetMapping("/totalSpendingByCategory")
|
||||||
@PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))")
|
@PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))")
|
||||||
public ResponseEntity<APIResponseDTO<SpendingByCategoriesDTO>> fetchSpendingByCategory(
|
public ResponseEntity<APIResponseDTO<SpendingByCategoriesDTO>> fetchSpendingByCategory(
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class TransactionsKafkaListener {
|
||||||
|
|
||||||
@KafkaListener(
|
@KafkaListener(
|
||||||
topics = KafkaConfiguration.REPLACE_TRANSACTIONS_TOPIC,
|
topics = KafkaConfiguration.REPLACE_TRANSACTIONS_TOPIC,
|
||||||
containerFactory = "replaceTransactionsKafkaListenerContainerFactory"
|
groupId = "core-api"
|
||||||
)
|
)
|
||||||
public void replaceTransactionsListener(KafkaReplaceProcessedTransactionsDTO message) {
|
public void replaceTransactionsListener(KafkaReplaceProcessedTransactionsDTO message) {
|
||||||
service.createOrReplaceProcessedTransactions(message.statementId(), message.userId(), message.transactions());
|
service.createOrReplaceProcessedTransactions(message.statementId(), message.userId(), message.transactions());
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
server.port=8081
|
server.port=${SERVER_PORT}
|
||||||
debug=true
|
|
||||||
|
|
||||||
logging.level.org.springframework.boot.autoconfigure=ERROR
|
|
||||||
|
|
||||||
spring.profiles.active=${PROFILE}
|
spring.profiles.active=${PROFILE}
|
||||||
|
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTHENTIK_ISSUER_URI}
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
spring.datasource.url=${DATASOURCE_URL}
|
spring.datasource.url=${DATASOURCE_URL}
|
||||||
spring.datasource.username=${DATASOURCE_USER}
|
spring.datasource.username=${DATASOURCE_USER}
|
||||||
|
@ -22,6 +21,3 @@ spring.jpa.hibernate.ddl-auto=validate
|
||||||
spring.flyway.table=core_schema_history
|
spring.flyway.table=core_schema_history
|
||||||
spring.flyway.baseline-version=0.9
|
spring.flyway.baseline-version=0.9
|
||||||
spring.flyway.baseline-on-migrate=true
|
spring.flyway.baseline-on-migrate=true
|
||||||
|
|
||||||
# Security
|
|
||||||
jwt.issuer-url=${AUTHENTIK_ISSUER_URL}
|
|
|
@ -24,10 +24,10 @@ export default function CategorizationRulesEditor({selectedCategory, onRuleBehav
|
||||||
|
|
||||||
toast.promise(
|
toast.promise(
|
||||||
Promise.all([
|
Promise.all([
|
||||||
utils.performRequest("/api/categories/rules")
|
utils.performRequest("/api/enums/category-rules")
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(({result}) => setRuleTypes(result)),
|
.then(({result}) => setRuleTypes(result)),
|
||||||
utils.performRequest("/api/processed-transactions/fields")
|
utils.performRequest("/api/enums/processed-transaction-fields")
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(({result}) => setFields(result))
|
.then(({result}) => setFields(result))
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default function WidgetEditModal(
|
||||||
}, [initialWidget]);
|
}, [initialWidget]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
utils.performRequest("/api/widgets/types")
|
utils.performRequest("/api/enums/widget-types")
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(resp => setWidgetTypes(resp.result));
|
.then(resp => setWidgetTypes(resp.result));
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export default function WidgetEditModal(
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(resp => setCategories(resp.result));
|
.then(resp => setCategories(resp.result));
|
||||||
|
|
||||||
utils.performRequest("/api/statistics/timePeriods")
|
utils.performRequest("/api/enums/statistics-time-periods")
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(resp => setTimePeriods(resp.result));
|
.then(resp => setTimePeriods(resp.result));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -8,25 +8,48 @@ let LEV_FORMAT = new Intl.NumberFormat('bg-BG', {
|
||||||
let utils = {
|
let utils = {
|
||||||
performRequest: async (url, options) => {
|
performRequest: async (url, options) => {
|
||||||
let opts = options ?? { headers: {} };
|
let opts = options ?? { headers: {} };
|
||||||
return await fetch(url, {
|
|
||||||
|
let result = await fetch(url, {
|
||||||
...opts,
|
...opts,
|
||||||
headers: {
|
headers: {
|
||||||
...opts.headers,
|
...opts.headers,
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
}
|
}
|
||||||
}).then(resp => {
|
|
||||||
if (resp.status === 401) {
|
|
||||||
window.location.replace(`${window.location.origin}/oauth2/authorization/authentik`)
|
|
||||||
|
|
||||||
throw "Unauthorized, please login.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw resp.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are unauthorized, refresh the token, and try once more.
|
||||||
|
if (result.status === 401) {
|
||||||
|
let tokenResponse = await fetch("/refresh-token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the token refresh failed, redirect to login
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
window.location.replace(`${window.location.origin}/oauth2/authorization/authentik`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try again
|
||||||
|
let secondAttempt = await fetch(url, {
|
||||||
|
...opts,
|
||||||
|
headers: {
|
||||||
|
...opts.headers,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If our second attempt failed as well after refresh, redirect to login
|
||||||
|
if (!secondAttempt.ok && result.status === 401) {
|
||||||
|
window.location.replace(`${window.location.origin}/oauth2/authorization/authentik`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the error wasn't unauthorized, just return the response
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
isSpinnerShown: () => {
|
isSpinnerShown: () => {
|
||||||
return localStorage.getItem("SpinnerShowing") === "true";
|
return localStorage.getItem("SpinnerShowing") === "true";
|
||||||
|
|
4
pefi-login-service/.env.example
Normal file
4
pefi-login-service/.env.example
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
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/ )
|
||||||
|
AUTHENTIK_BACK_CHANNEL_LOGOUT_URL= authentik back channel logout url ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/end-session/ )
|
7
pefi-login-service/Dockerfile
Normal file
7
pefi-login-service/Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM eclipse-temurin:21-jdk-alpine
|
||||||
|
|
||||||
|
COPY ./build/libs/pefi-login-service-0.0.1-SNAPSHOT.jar app.jar
|
||||||
|
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar $ARGS
|
24
pefi-login-service/build.gradle
Normal file
24
pefi-login-service/build.gradle
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '3.2.0'
|
||||||
|
id 'io.spring.dependency-management' version '1.1.4'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'dev.mvvasilev'
|
||||||
|
version = '0.0.1-SNAPSHOT'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
|
||||||
|
testImplementation platform('org.junit:junit-bom:5.9.1')
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
1
pefi-login-service/settings.gradle
Normal file
1
pefi-login-service/settings.gradle
Normal file
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = 'pefi-login-service'
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.mvvasilev;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class PefiLoginService {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(PefiLoginService.class, args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package dev.mvvasilev.configuration;
|
||||||
|
|
||||||
|
import dev.mvvasilev.service.TokenRefreshService;
|
||||||
|
import dev.mvvasilev.utils.CookieUtils;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
@Value("${auth.success.redirect}")
|
||||||
|
private String redirect;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http, OAuth2AuthorizedClientRepository repository) throws Exception {
|
||||||
|
return http
|
||||||
|
.authorizeHttpRequests(authorize -> {
|
||||||
|
authorize.requestMatchers(HttpMethod.POST, "/refresh-token").permitAll();
|
||||||
|
authorize.anyRequest().authenticated();
|
||||||
|
})
|
||||||
|
.oauth2Login(l -> l.successHandler((req, res, auth) -> {
|
||||||
|
OAuth2AuthenticationToken oauth = (OAuth2AuthenticationToken) auth;
|
||||||
|
|
||||||
|
OAuth2AuthorizedClient authorizedClient = repository.loadAuthorizedClient(
|
||||||
|
oauth.getAuthorizedClientRegistrationId(),
|
||||||
|
auth,
|
||||||
|
req
|
||||||
|
);
|
||||||
|
|
||||||
|
res.addCookie(
|
||||||
|
CookieUtils.createAccessTokenCookie(authorizedClient.getAccessToken().getTokenValue())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (authorizedClient.getRefreshToken() != null) {
|
||||||
|
res.addCookie(
|
||||||
|
CookieUtils.createRefreshTokenCookie(authorizedClient.getRefreshToken().getTokenValue())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setStatus(HttpStatus.TEMPORARY_REDIRECT.value());
|
||||||
|
res.addHeader("Location", redirect);
|
||||||
|
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.mvvasilev.controller;
|
||||||
|
|
||||||
|
import dev.mvvasilev.configuration.SecurityConfiguration;
|
||||||
|
import dev.mvvasilev.dto.TokenDTO;
|
||||||
|
import dev.mvvasilev.exception.PefiLoginServiceException;
|
||||||
|
import dev.mvvasilev.service.TokenRefreshService;
|
||||||
|
import dev.mvvasilev.utils.CookieUtils;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class RefreshController {
|
||||||
|
|
||||||
|
private final TokenRefreshService tokenRefreshService;
|
||||||
|
|
||||||
|
public RefreshController(TokenRefreshService tokenRefreshService) {
|
||||||
|
this.tokenRefreshService = tokenRefreshService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/refresh-token")
|
||||||
|
public ResponseEntity<Void> getOidcUserPrincipal(
|
||||||
|
HttpServletResponse response,
|
||||||
|
@CookieValue(CookieUtils.REFRESH_TOKEN_NAME) String refreshToken
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
var token = tokenRefreshService.fetchNewTokens(refreshToken);
|
||||||
|
|
||||||
|
response.addCookie(CookieUtils.createAccessTokenCookie(token.accessToken()));
|
||||||
|
response.addCookie(CookieUtils.createRefreshTokenCookie(token.refreshToken()));
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (PefiLoginServiceException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.mvvasilev.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record TokenDTO(
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
String accessToken,
|
||||||
|
@JsonProperty("refresh_token")
|
||||||
|
String refreshToken,
|
||||||
|
@JsonProperty("id_token")
|
||||||
|
String idToken,
|
||||||
|
@JsonProperty("token_type")
|
||||||
|
String tokenType,
|
||||||
|
@JsonProperty("expires_in")
|
||||||
|
int expiredIn
|
||||||
|
) {
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.mvvasilev.exception;
|
||||||
|
|
||||||
|
public class PefiLoginServiceException extends RuntimeException {
|
||||||
|
public PefiLoginServiceException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package dev.mvvasilev.service;
|
||||||
|
|
||||||
|
import dev.mvvasilev.dto.TokenDTO;
|
||||||
|
import dev.mvvasilev.exception.PefiLoginServiceException;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TokenRefreshService {
|
||||||
|
|
||||||
|
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TokenRefreshService(ClientRegistrationRepository clientRegistrationRepository) {
|
||||||
|
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenDTO fetchNewTokens(String refreshToken) {
|
||||||
|
var client = clientRegistrationRepository.findByRegistrationId("authentik");
|
||||||
|
|
||||||
|
var template = new RestTemplate();
|
||||||
|
|
||||||
|
var requestBody = new LinkedMultiValueMap<String, String>();
|
||||||
|
requestBody.put("grant_type", List.of("refresh_token"));
|
||||||
|
requestBody.put("client_id", List.of(client.getClientId()));
|
||||||
|
requestBody.put("client_secret", List.of(client.getClientSecret()));
|
||||||
|
requestBody.put("refresh_token", List.of(refreshToken));
|
||||||
|
|
||||||
|
var requestHeaders = new LinkedMultiValueMap<String, String>();
|
||||||
|
requestHeaders.put("Content-Type", List.of("application/x-www-form-urlencoded"));
|
||||||
|
|
||||||
|
var request = new HttpEntity<>(requestBody, requestHeaders);
|
||||||
|
|
||||||
|
var tokenResponse = template.postForEntity(client.getProviderDetails().getTokenUri(), request, TokenDTO.class);
|
||||||
|
|
||||||
|
if (!HttpStatus.OK.isSameCodeAs(tokenResponse.getStatusCode())) {
|
||||||
|
throw new PefiLoginServiceException("Token refresh failure");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package dev.mvvasilev.utils;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
|
||||||
|
public class CookieUtils {
|
||||||
|
|
||||||
|
public static final String ACCESS_TOKEN_NAME = "pefi_token";
|
||||||
|
|
||||||
|
public static final String REFRESH_TOKEN_NAME = "pefi_refresh_token";
|
||||||
|
|
||||||
|
public static Cookie createAccessTokenCookie(String value) {
|
||||||
|
var accessTokenCookie = new Cookie(ACCESS_TOKEN_NAME, value);
|
||||||
|
accessTokenCookie.setHttpOnly(true);
|
||||||
|
accessTokenCookie.setPath("/api");
|
||||||
|
|
||||||
|
return accessTokenCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Cookie createRefreshTokenCookie(String value) {
|
||||||
|
var refreshTokenCookie = new Cookie(REFRESH_TOKEN_NAME, value);
|
||||||
|
refreshTokenCookie.setHttpOnly(true);
|
||||||
|
refreshTokenCookie.setPath("/refresh-token");
|
||||||
|
|
||||||
|
return refreshTokenCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
pefi-login-service/src/main/resources/application.properties
Normal file
12
pefi-login-service/src/main/resources/application.properties
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
server.port=${SERVER_PORT}
|
||||||
|
|
||||||
|
auth.success.redirect=${FRONTEND_URI}
|
||||||
|
|
||||||
|
spring.security.oauth2.client.registration.authentik.client-id=${AUTHENTIK_CLIENT_ID}
|
||||||
|
spring.security.oauth2.client.registration.authentik.client-secret=${AUTHENTIK_CLIENT_SECRET}
|
||||||
|
spring.security.oauth2.client.registration.authentik.authorization-grant-type=authorization_code
|
||||||
|
spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/authentik
|
||||||
|
spring.security.oauth2.client.registration.authentik.scope=openid
|
||||||
|
|
||||||
|
spring.security.oauth2.client.provider.authentik.issuer-uri=${AUTHENTIK_ISSUER_URI}
|
||||||
|
spring.security.oauth2.client.provider.authentik.back-channel-logout-url=${AUTHENTIK_BACK_CHANNEL_LOGOUT_URL}
|
|
@ -5,10 +5,15 @@ import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackageClasses = { AuthorizationService.class })
|
@EnableWebMvc
|
||||||
@EnableJpaRepositories("dev.mvvasilev.statements.*")
|
|
||||||
@EntityScan("dev.mvvasilev.statements.*")
|
@EntityScan("dev.mvvasilev.statements.*")
|
||||||
|
@EnableJpaRepositories("dev.mvvasilev.statements.*")
|
||||||
|
@SpringBootApplication(
|
||||||
|
scanBasePackageClasses = { AuthorizationService.class },
|
||||||
|
scanBasePackages = "dev.mvvasilev.statements.*"
|
||||||
|
)
|
||||||
public class PefiStatementsAPI {
|
public class PefiStatementsAPI {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(PefiStatementsAPI.class, args);
|
SpringApplication.run(PefiStatementsAPI.class, args);
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
server.port=8081
|
server.port=${SERVER_PORT}
|
||||||
debug=true
|
|
||||||
|
|
||||||
logging.level.org.springframework.boot.autoconfigure=ERROR
|
|
||||||
|
|
||||||
spring.profiles.active=${PROFILE}
|
spring.profiles.active=${PROFILE}
|
||||||
|
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTHENTIK_ISSUER_URI}
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
spring.datasource.url=${DATASOURCE_URL}
|
spring.datasource.url=${DATASOURCE_URL}
|
||||||
spring.datasource.username=${DATASOURCE_USER}
|
spring.datasource.username=${DATASOURCE_USER}
|
||||||
|
@ -22,6 +21,3 @@ spring.jpa.hibernate.ddl-auto=validate
|
||||||
spring.flyway.table=statements_schema_history
|
spring.flyway.table=statements_schema_history
|
||||||
spring.flyway.baseline-version=0.9
|
spring.flyway.baseline-version=0.9
|
||||||
spring.flyway.baseline-on-migrate=true
|
spring.flyway.baseline-on-migrate=true
|
||||||
|
|
||||||
# Security
|
|
||||||
jwt.issuer-url=${AUTHENTIK_ISSUER_URL}
|
|
9
pefi-widgets-api/.env.example
Normal file
9
pefi-widgets-api/.env.example
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
PROFILE= production/development
|
||||||
|
|
||||||
|
AUTHENTIK_ISSUER_URL= auth server configuration url for fetching JWKs ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/ )
|
||||||
|
|
||||||
|
KAFKA_SERVERS= comma-delimited list of kafka servers to connect to
|
||||||
|
|
||||||
|
DATASOURCE_URL= database jdbc url ( postgres only, example: jdbc:postgresql://localhost:5432/mydatabase )
|
||||||
|
DATASOURCE_USER= database user
|
||||||
|
DATASOURCE_PASSWORD= database password
|
|
@ -17,6 +17,8 @@ dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
|
||||||
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
|
||||||
|
|
||||||
implementation 'org.flywaydb:flyway-core'
|
implementation 'org.flywaydb:flyway-core'
|
||||||
implementation 'org.apache.commons:commons-lang3:3.14.0'
|
implementation 'org.apache.commons:commons-lang3:3.14.0'
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,15 @@ import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackageClasses = { AuthorizationService.class })
|
@EnableWebMvc
|
||||||
@EnableJpaRepositories("dev.mvvasilev.widgets.*")
|
|
||||||
@EntityScan("dev.mvvasilev.widgets.*")
|
@EntityScan("dev.mvvasilev.widgets.*")
|
||||||
|
@EnableJpaRepositories("dev.mvvasilev.widgets.*")
|
||||||
|
@SpringBootApplication(
|
||||||
|
scanBasePackageClasses = { AuthorizationService.class },
|
||||||
|
scanBasePackages = "dev.mvvasilev.widgets.*"
|
||||||
|
)
|
||||||
public class PefiWidgetsAPI {
|
public class PefiWidgetsAPI {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(PefiWidgetsAPI.class, args);
|
SpringApplication.run(PefiWidgetsAPI.class, args);
|
||||||
|
|
|
@ -4,9 +4,11 @@ import dev.mvvasilev.common.configuration.CommonSecurityConfiguration;
|
||||||
import dev.mvvasilev.common.configuration.CommonSwaggerConfiguration;
|
import dev.mvvasilev.common.configuration.CommonSwaggerConfiguration;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Import(CommonSecurityConfiguration.class)
|
@Import(CommonSecurityConfiguration.class)
|
||||||
|
@EnableTransactionManagement
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
server.port=8081
|
server.port=${SERVER_PORT}
|
||||||
debug=true
|
|
||||||
|
|
||||||
logging.level.org.springframework.boot.autoconfigure=ERROR
|
|
||||||
|
|
||||||
spring.profiles.active=${PROFILE}
|
spring.profiles.active=${PROFILE}
|
||||||
|
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTHENTIK_ISSUER_URI}
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
spring.datasource.url=${DATASOURCE_URL}
|
spring.datasource.url=${DATASOURCE_URL}
|
||||||
spring.datasource.username=${DATASOURCE_USER}
|
spring.datasource.username=${DATASOURCE_USER}
|
||||||
|
@ -20,6 +19,3 @@ spring.jpa.hibernate.ddl-auto=validate
|
||||||
spring.flyway.table=widgets_schema_history
|
spring.flyway.table=widgets_schema_history
|
||||||
spring.flyway.baseline-version=0.9
|
spring.flyway.baseline-version=0.9
|
||||||
spring.flyway.baseline-on-migrate=true
|
spring.flyway.baseline-on-migrate=true
|
||||||
|
|
||||||
# Security
|
|
||||||
jwt.issuer-url=${AUTHENTIK_ISSUER_URL}
|
|
|
@ -6,4 +6,5 @@ include 'pefi-api-gateway'
|
||||||
include 'pefi-core-api'
|
include 'pefi-core-api'
|
||||||
include 'pefi-statements-api'
|
include 'pefi-statements-api'
|
||||||
include 'pefi-widgets-api'
|
include 'pefi-widgets-api'
|
||||||
|
include 'pefi-login-service'
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue