This commit is contained in:
2025-12-28 22:07:39 +01:00
parent 5ef53991d4
commit bc1e598987
18 changed files with 67 additions and 507 deletions

View File

@@ -54,10 +54,6 @@ dependencies {
//======================PROMETHEUS====================== //======================PROMETHEUS======================
runtimeOnly("io.micrometer:micrometer-registry-prometheus") runtimeOnly("io.micrometer:micrometer-registry-prometheus")
//======================JWT======================
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.security:spring-security-oauth2-jose")
//======================OTHER====================== //======================OTHER======================
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
developmentOnly("org.springframework.boot:spring-boot-docker-compose") developmentOnly("org.springframework.boot:spring-boot-docker-compose")

65
api/compose-dev.yaml Normal file
View File

@@ -0,0 +1,65 @@
services:
boardmate-api:
build:
context: .
dockerfile: Dockerfile
container_name: boardmate-api
ports:
- "8000:8080"
- "5005:5005"
environment:
SPRING_DATA_MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db"
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
depends_on:
- mongodb
- elasticsearch
volumes:
- ./.gradle:/home/gradle/.gradle # Use your existing local Gradle and build directories
- ./build:/app/.gradle # optional, only if you want project cache mapped too
elasticsearch:
image: 'docker.elastic.co/elasticsearch/elasticsearch:7.17.10'
environment:
- 'ELASTIC_PASSWORD=secret'
- 'discovery.type=single-node'
- 'xpack.security.enabled=false'
ports:
- '8100:9200'
- '8101:9300'
grafana-lgtm:
image: 'grafana/otel-lgtm:latest'
ports:
- '8200:3000'
- '8201:4317'
- '8202:4318'
prometheus:
image: 'prom/prometheus:latest'
container_name: prometheus
ports:
- "8300:9090"
volumes:
- ./prometheus.yaml:/etc/prometheus/prometheus.yml
mongodb:
image: mongo:latest
environment:
- MONGO_INITDB_DATABASE=board-mate-db
- MONGO_INITDB_ROOT_PASSWORD=secret
- MONGO_INITDB_ROOT_USERNAME=root
ports:
- "8400:27017"
volumes:
- ./mongo-data:/data/db
- ./mongo-init:/docker-entrypoint-initdb.d
#mongo-express:
# image: mongo-express:latest
# depends_on:
# - mongodb
# ports:
# - "8401:8081"
# environment:
# - ME_CONFIG_MONGODB_SERVER=mongodb
# - ME_CONFIG_MONGODB_PORT=27017
# - ME_CONFIG_MONGODB_ADMINUSERNAME=root
# - ME_CONFIG_MONGODB_ADMINPASSWORD=secret
# - ME_CONFIG_MONGODB_AUTH_DATABASE=admin

View File

@@ -6,10 +6,7 @@ services:
container_name: boardmate-api container_name: boardmate-api
ports: ports:
- "8000:8080" - "8000:8080"
- "5005:5005"
environment: environment:
JWT_SECRET: "enY3OWU4djFyMTByNTZhcG9uY3Z0djQ5cnY0eDhhNWM0bjg5OTRjNDhidA=="
SSL_KEYSTORE_PATH: "/certs/keystore.p12"
SPRING_DATA_MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db" SPRING_DATA_MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db"
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
depends_on: depends_on:
@@ -19,7 +16,6 @@ services:
volumes: volumes:
- ./.gradle:/home/gradle/.gradle - ./.gradle:/home/gradle/.gradle
- ./build:/app/.gradle - ./build:/app/.gradle
- ./certs:/certs
elasticsearch: elasticsearch:
image: 'docker.elastic.co/elasticsearch/elasticsearch:7.17.10' image: 'docker.elastic.co/elasticsearch/elasticsearch:7.17.10'

View File

@@ -3,7 +3,6 @@ package be.naaturel.boardmateapi;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class BoardmateApiApplication { public class BoardmateApiApplication {

View File

@@ -1,44 +0,0 @@
package be.naaturel.boardmateapi.common.models;
public class Client {
private String id;
private String name;
private String username;
private String key;
public Client(String id, String name, String username, String key){
this.id = id;
this.name = name;
this.username = username;
this.key = key;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}

View File

@@ -1,30 +1,18 @@
package be.naaturel.boardmateapi.configurations.configurations; package be.naaturel.boardmateapi.configurations.configurations;
import be.naaturel.boardmateapi.configurations.properties.AppProperties; import be.naaturel.boardmateapi.configurations.properties.AppProperties;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import org.springframework.beans.factory.annotation.Autowired; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
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.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
@Configuration @Configuration
@@ -40,29 +28,10 @@ public class AppSecurityConfig {
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public SecurityFilterChain filterChain(HttpSecurity http) {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http, @Autowired JwtDecoder jwtDecoder) throws Exception {
return http return http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/health",
"/actuator/**",
"/v3/api-docs/**",
"/v3/api-docs/swagger-config",
"/webjars/**",
"/swagger-ui/**",
"/docs/**",
"/v1/docs/**",
"/swagger-ui.html",
"/authenticate",
"/client/create").permitAll()
.anyRequest().authenticated()
).oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build(); .build();
} }

View File

@@ -1,49 +0,0 @@
package be.naaturel.boardmateapi.configurations.configurations;
import be.naaturel.boardmateapi.configurations.properties.AppProperties;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Configuration
public class JWTConfig {
private final AppProperties conf;
@Autowired
public JWTConfig(AppProperties appConf) {
this.conf = appConf;
}
@Bean
public JwtEncoder jwtEncoder() {
byte[] keyBytes = Base64.getDecoder().decode(conf.jwtSecret);
SecretKey key = new SecretKeySpec(keyBytes, "HmacSHA256");
return new NimbusJwtEncoder(new ImmutableSecret<>(key));
}
@Bean
public JwtDecoder jwtDecoder() {
byte[] keyBytes = Base64.getDecoder().decode(conf.jwtSecret);
SecretKey key = new SecretKeySpec(keyBytes, "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}

View File

@@ -1,29 +1,8 @@
package be.naaturel.boardmateapi.configurations.configurations; package be.naaturel.boardmateapi.configurations.configurations;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class SwaggerConfig { public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.components(
new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}
} }

View File

@@ -21,6 +21,4 @@ public class AppProperties {
@Value("${spring.mongodb.database}") @Value("${spring.mongodb.database}")
public String database; public String database;
@Value("${jwt.secret}")
public String jwtSecret;
} }

View File

@@ -1,80 +0,0 @@
package be.naaturel.boardmateapi.controllers;
import be.naaturel.boardmateapi.common.models.Client;
import be.naaturel.boardmateapi.controllers.dtos.*;
import be.naaturel.boardmateapi.services.ClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
@RestController
public class AuthController {
private final ClientService service;
private final JwtEncoder jwtEncoder;
@Autowired
public AuthController(ClientService service, JwtEncoder jwtEncoder) {
this.service = service;
this.jwtEncoder = jwtEncoder;
}
@PostMapping("/authenticate")
public ResponseEntity<ResponseBody<AuthResponseDto>> login(@RequestBody AuthRequestDto request) {
ResponseBody<AuthResponseDto> result = ResponseBody.createEmpty();
try {
Client user = service.authenticate(
request.getUsername(),
request.getKey()
);
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.subject(user.getId())
.claim("name", user.getName())
.claim("username", user.getUsername())
.issuedAt(now)
.expiresAt(now.plusSeconds(3600*12))
.build();
JwtEncoderParameters params =
JwtEncoderParameters.from(
JwsHeader.with(MacAlgorithm.HS256).build(),
claims
);
String token = jwtEncoder.encode(params).getTokenValue();
AuthResponseDto response = new AuthResponseDto();
response.setName(user.getName());
response.setUsername(user.getUsername());
response.setClientId(user.getId());
response.setAuthToken(token);
result.setSuccess(true);
result.setData(response);
return ResponseEntity
.status(HttpStatus.OK)
.body(result);
} catch (Exception e){
e.printStackTrace();
result.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(result);
}
}
}

View File

@@ -1,42 +0,0 @@
package be.naaturel.boardmateapi.controllers;
import be.naaturel.boardmateapi.controllers.dtos.AuthRequestDto;
import be.naaturel.boardmateapi.controllers.dtos.AuthResponseDto;
import be.naaturel.boardmateapi.controllers.dtos.ClientDto;
import be.naaturel.boardmateapi.controllers.dtos.ResponseBody;
import be.naaturel.boardmateapi.services.ClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ClientController {
private final ClientService service;
@Autowired
public ClientController(ClientService service){
this.service = service;
}
@PostMapping("/client/create")
public ResponseEntity<ResponseBody<String>> create(@RequestBody ClientDto dto) {
ResponseBody<String> result = ResponseBody.createEmpty();
try{
String clientId = service.create(dto.getName(), dto.getUsername(), dto.getKey());
result.setData(clientId);
return ResponseEntity.
status(HttpStatus.OK)
.body(result);
} catch (Exception e){
result.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(result);
}
}
}

View File

@@ -1,22 +0,0 @@
package be.naaturel.boardmateapi.controllers.dtos;
public class AuthRequestDto {
private String username;
private String key;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}

View File

@@ -1,40 +0,0 @@
package be.naaturel.boardmateapi.controllers.dtos;
public class AuthResponseDto {
private String clientId;
private String name;
private String username;
private String authToken;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
}

View File

@@ -1,32 +0,0 @@
package be.naaturel.boardmateapi.controllers.dtos;
public class ClientDto {
private String name;
private String username;
private String key;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}

View File

@@ -1,11 +0,0 @@
package be.naaturel.boardmateapi.repository;
import be.naaturel.boardmateapi.repository.dtos.ClientDto;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.Optional;
public interface ClientRepo extends MongoRepository<ClientDto, String> {
Optional<ClientDto> findByServiceUsername(String serviceUsername);
}

View File

@@ -1,59 +0,0 @@
package be.naaturel.boardmateapi.repository.dtos;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
@Document(collection = "clients")
public class ClientDto {
@Id
private String id;
@Field("name")
private String name;
@Field("clientId")
private String clientId;
@Field("serviceUsername")
private String serviceUsername;
@Field("serviceKey")
private String serviceKey;
public String getId() {
return id;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getServiceUsername() {
return serviceUsername;
}
public void setServiceUsername(String serviceUsername) {
this.serviceUsername = serviceUsername;
}
public void setServiceKey(String serviceKey) {
this.serviceKey = serviceKey;
}
public String getServiceKey() {
return serviceKey;
}
}

View File

@@ -1,56 +0,0 @@
package be.naaturel.boardmateapi.services;
import be.naaturel.boardmateapi.common.exceptions.ServiceException;
import be.naaturel.boardmateapi.common.models.Client;
import be.naaturel.boardmateapi.repository.ClientRepo;
import be.naaturel.boardmateapi.repository.dtos.ClientDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.UUID;
import static java.util.UUID.randomUUID;
@Service
public class ClientService {
private final ClientRepo repo;
private final PasswordEncoder passwordEncoder;
@Autowired
public ClientService(ClientRepo repo, PasswordEncoder passwordEncoder){
this.repo = repo;
this.passwordEncoder = passwordEncoder;
}
public Client authenticate(String username, String key) throws ServiceException {
try {
ClientDto dto = repo.findByServiceUsername(username)
.orElseThrow(() -> new RuntimeException("Invalid username"));
if (passwordEncoder.matches(key, dto.getServiceKey())) {
return new Client(dto.getClientId(), dto.getName(), dto.getServiceUsername(), dto.getServiceKey());
} else {
throw new RuntimeException("Invalid username or password");
}
} catch (Exception e){
throw new ServiceException("Authentication failed", e);
}
}
public String create(String name, String username, String key) throws ServiceException {
try{
ClientDto dto = new ClientDto();
dto.setClientId(randomUUID().toString());
dto.setName(name);
dto.setServiceUsername(username);
String encodedKey = passwordEncoder.encode(key);
dto.setServiceKey(encodedKey);
ClientDto result = repo.save(dto);
return result.getClientId();
} catch (Exception e){
throw new ServiceException("Unable to create client", e);
}
}
}

View File

@@ -10,20 +10,13 @@ sec.cors.authorizedHots=*
sec.cors.authorizedMethods=GET,POST,PUT,DELETE,OPTION sec.cors.authorizedMethods=GET,POST,PUT,DELETE,OPTION
sec.cors.authorizedHeader=Authorization,Content-type sec.cors.authorizedHeader=Authorization,Content-type
jwt.secret=${JWT_SECRET}
jwt.expiration=3600
server.ssl.key-store=${SSL_KEYSTORE_PATH}
server.ssl.key-store-password=heplhepl
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=board-mate-api
#=============MQTT============= #=============MQTT=============
mqtt.broker-url=tcp://test.mosquitto.org:1883 mqtt.broker-url=tcp://test.mosquitto.org:1883
mqtt.client-id=board-mate-client mqtt.client-id=board-mate-client
mqtt.topic=board-mate-test/topic mqtt.topic=board-mate-test/topic
mqtt.username=yourUsername mqtt.username=yourUsername
mqtt.password=yourPassword mqtt.password=yourPassword
#=============METRICS============= #=============METRICS=============