feature/test (#1)

Reviewed-on: #1
Co-authored-by: Bernhard Müller <postmuller@gmail.com>
Co-committed-by: Bernhard Müller <postmuller@gmail.com>
This commit is contained in:
Bernhard Müller 2026-07-28 12:02:05 +00:00 committed by Semmal
parent 83b6d503cc
commit 95051690d5
7 changed files with 57 additions and 23 deletions

View file

@ -20,7 +20,11 @@ COPY eeg_frontend/openapi.yaml ./
RUN npx openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o ./src/app/api
COPY eeg_frontend/ ./
RUN npm run build -- --configuration production
ARG VITE_OR_ANGULAR_BACKEND_URL
ENV BACKEND_URL=$VITE_OR_ANGULAR_BACKEND_URL
RUN sed -i "s|https://API_URL_PLACEHOLDER|${BACKEND_URL}|g" src/environments/environment.staging.ts
RUN npm run build -- --configuration staging
# ---- Stage 2: Build Spring Boot Backend ----
FROM maven:3.9-eclipse-temurin-21 AS backend-build
@ -65,12 +69,8 @@ USER eeg
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "app.jar", \
"--spring.profiles.active=prod"]
"-jar", "app.jar"]

View file

@ -28,4 +28,4 @@ foreach ($f in $Files) {
"`n" | Out-File -Append $OutFile -Encoding UTF8
}
Write-Host "Erfolgreich! Der Kontext für Backend und Frontend wurde in '$OutFile' gespeichert."
Write-Host "Erfolgreich! Der Kontext für Backend und Frontend wurde in '$OutFile' gespeichert..."

View file

@ -3,9 +3,13 @@ package at.mueller.eeg.backend.iam.config;
import at.mueller.eeg.backend.iam.repository.UserRepository;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import jakarta.servlet.DispatcherType;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@ -23,23 +27,31 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.HeaderBearerTokenResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.crypto.spec.SecretKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@ConfigurationProperties(prefix = "app")
public class SecurityConfig {
@Value("${jwt.secret}")
private String jwtSecret;
@Setter
@Getter
private List<String> corsOrigins = new ArrayList<>();
@Bean
public JwtDecoder jwtDecoder() {
SecretKeySpec secretKey = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256");
@ -54,6 +66,7 @@ public class SecurityConfig {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/", "/index.html", "/assets/**", "/static/**", "/*.js", "/*.css", "/*.ico").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
@ -62,6 +75,7 @@ public class SecurityConfig {
.anyRequest().denyAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.bearerTokenResolver(new HeaderBearerTokenResolver("X-Access-Token"))
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
@ -76,9 +90,7 @@ public class SecurityConfig {
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// Hier sagst du Spring, wie das Feld in deinem JWT-Payload heißt (z.B. "role" oder "roles")
grantedAuthoritiesConverter.setAuthoritiesClaimName("role");
// Spring Security erwartet bei Rollen immer das Prefix "ROLE_"
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
@ -86,19 +98,26 @@ public class SecurityConfig {
return jwtAuthenticationConverter;
}
@Value("${app.cors-origins:http://localhost:4200}")
private String corsOrigins;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.stream(corsOrigins.split(",")).map(String::trim).toList());
List<String> allowedPatterns = corsOrigins.stream()
.map(String::trim)
.flatMap(origin -> {
if (origin.contains("*")) {
return Stream.of(origin, origin + ":*");
}
return Stream.of(origin);
})
.toList();
configuration.setAllowedOriginPatterns(allowedPatterns);
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Access-Token"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
System.out.println("LOGGED CORS ORIGINS: " + String.join(",", corsOrigins));
return source;
}
@ -112,7 +131,6 @@ public class SecurityConfig {
return config.getAuthenticationManager();
}
// Das Gegenstück zum Decoder: Hiermit erstellen wir die Token!
@Bean
public JwtEncoder jwtEncoder() {
SecretKeySpec secretKey = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256");

View file

@ -12,7 +12,10 @@ spring:
jwt:
secret: dev-only-secret-do-not-use-in-production-2025
app:
cors-origins: http://localhost:4200
cors-origins:
- http://localhost:4200
- https://eeg.mueller-dev.com
- https://*.eeg.mueller-dev.com
eda:
simulation:
consent-delay-ms: 3000

View file

@ -48,6 +48,15 @@
],
"outputHashing": "all"
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
@ -62,6 +71,9 @@
"production": {
"buildTarget": "eeg_frontend:build:production"
},
"staging": {
"buildTarget": "eeg_frontend:build:staging"
},
"development": {
"buildTarget": "eeg_frontend:build:development"
}

View file

@ -3,14 +3,11 @@ import {HttpInterceptorFn} from '@angular/common/http';
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token');
// If a token exists, clone the request and add the Authorization header
if (token) {
const clonedRequest = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
const authReq = req.clone({
headers: req.headers.set('X-Access-Token', token)
});
return next(clonedRequest);
return next(authReq);
}
return next(req);

View file

@ -0,0 +1,4 @@
export const environment = {
production: true,
apiUrl: 'https://API_URL_PLACEHOLDER'
};