diff --git a/Dockerfile b/Dockerfile index 820150a..14802f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/ai-context.ps1 b/ai-context.ps1 index decb4dd..0327146 100644 --- a/ai-context.ps1 +++ b/ai-context.ps1 @@ -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." \ No newline at end of file +Write-Host "Erfolgreich! Der Kontext für Backend und Frontend wurde in '$OutFile' gespeichert..." \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/SecurityConfig.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/SecurityConfig.java index ad67fab..978a434 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/SecurityConfig.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/SecurityConfig.java @@ -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 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 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"); diff --git a/eeg_backend/src/main/resources/application-dev.yml b/eeg_backend/src/main/resources/application-dev.yml index 6a628c0..a0827ae 100644 --- a/eeg_backend/src/main/resources/application-dev.yml +++ b/eeg_backend/src/main/resources/application-dev.yml @@ -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 diff --git a/eeg_frontend/angular.json b/eeg_frontend/angular.json index 4c5d8ec..e25a3e1 100644 --- a/eeg_frontend/angular.json +++ b/eeg_frontend/angular.json @@ -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" } diff --git a/eeg_frontend/src/app/interceptors/jwt.interceptor.ts b/eeg_frontend/src/app/interceptors/jwt.interceptor.ts index 9045ef8..31d6570 100644 --- a/eeg_frontend/src/app/interceptors/jwt.interceptor.ts +++ b/eeg_frontend/src/app/interceptors/jwt.interceptor.ts @@ -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); diff --git a/eeg_frontend/src/environments/environment.staging.ts b/eeg_frontend/src/environments/environment.staging.ts new file mode 100644 index 0000000..a864a08 --- /dev/null +++ b/eeg_frontend/src/environments/environment.staging.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiUrl: 'https://API_URL_PLACEHOLDER' +};