diff --git a/eeg_backend/iam.http b/eeg_backend/iam.http index 5ecff6b..f26d7e6 100644 --- a/eeg_backend/iam.http +++ b/eeg_backend/iam.http @@ -5,9 +5,9 @@ Content-Type: application/json { "firstName": "Admin", "lastName": "Admin", - "email": "admin@admin.at", - "password": "SuperAdmin", - "atNumber": "AT0010000000000000000000001234567" + "email": "test10@qwer.as", + "password": "testtest", + "atNumber": "AT0010000000000000000000001234568" } ### User Login diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java index aee6b88..02d4e40 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java @@ -2,8 +2,10 @@ package at.mueller.eeg.backend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync public class EegPortalApplication { public static void main(String[] args) { diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/event/EdaTopologyCheckEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyCheckEvent.java similarity index 59% rename from eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/event/EdaTopologyCheckEvent.java rename to eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyCheckEvent.java index a61e6ee..d6761c0 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/event/EdaTopologyCheckEvent.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyCheckEvent.java @@ -1,8 +1,9 @@ -package at.mueller.eeg.backend.common.config.event; +package at.mueller.eeg.backend.common.event; import java.util.UUID; public record EdaTopologyCheckEvent( UUID userId, + UUID meteringPointId, String atNumber ) {} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyCompletedEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyCompletedEvent.java new file mode 100644 index 0000000..bc3d97a --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyCompletedEvent.java @@ -0,0 +1,13 @@ +package at.mueller.eeg.backend.common.event; + +import java.util.UUID; + +public record EdaTopologyCompletedEvent( + UUID userId, + UUID meteringPointId, + boolean isEligible, + String gridOperatorId, + String substationId, + String transformerId, + String gridLevel +) {} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyFailedEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyFailedEvent.java new file mode 100644 index 0000000..c8484cf --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/EdaTopologyFailedEvent.java @@ -0,0 +1,9 @@ +package at.mueller.eeg.backend.common.event; + +import java.util.UUID; + +public record EdaTopologyFailedEvent( + UUID userId, + UUID meteringPointId, + String errorMessage +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/event/UserRegisteredEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/UserRegisteredEvent.java similarity index 67% rename from eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/event/UserRegisteredEvent.java rename to eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/UserRegisteredEvent.java index 7e6ec6f..490de19 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/event/UserRegisteredEvent.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/UserRegisteredEvent.java @@ -1,4 +1,4 @@ -package at.mueller.eeg.backend.common.config.event; +package at.mueller.eeg.backend.common.event; import java.util.UUID; diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/exception/ErrorResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/exception/ErrorResponse.java similarity index 51% rename from eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/exception/ErrorResponse.java rename to eeg_backend/src/main/java/at/mueller/eeg/backend/common/exception/ErrorResponse.java index be88620..1a649ed 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/exception/ErrorResponse.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/exception/ErrorResponse.java @@ -1,3 +1,3 @@ -package at.mueller.eeg.backend.common.config.exception; +package at.mueller.eeg.backend.common.exception; public record ErrorResponse(String message, int status) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/exception/GlobalExceptionHandler.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/exception/GlobalExceptionHandler.java similarity index 96% rename from eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/exception/GlobalExceptionHandler.java rename to eeg_backend/src/main/java/at/mueller/eeg/backend/common/exception/GlobalExceptionHandler.java index 7311a12..d486e1b 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/exception/GlobalExceptionHandler.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package at.mueller.eeg.backend.common.config.exception; +package at.mueller.eeg.backend.common.exception; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java new file mode 100644 index 0000000..cc2f7ae --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java @@ -0,0 +1,51 @@ +package at.mueller.eeg.backend.community.api; + +import at.mueller.eeg.backend.community.api.dto.EligibleCommunityResponse; +import at.mueller.eeg.backend.community.api.dto.MembershipRequest; +import at.mueller.eeg.backend.community.service.MembershipService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/community") +@RequiredArgsConstructor +public class MembershipController { + + private final MembershipService membershipService; + + // --- USER ENDPUNKTE --- + + @GetMapping("/metering-points/{id}/eligible-communities") + public ResponseEntity> getEligibleCommunities(@PathVariable UUID id) { + return ResponseEntity.ok(membershipService.getEligibleCommunities(id)); + } + + @PostMapping("/memberships/request") + public ResponseEntity requestMembership(@Valid @RequestBody MembershipRequest request) { + UUID membershipId = membershipService.requestMembership(request); + return ResponseEntity.status(HttpStatus.CREATED).body(membershipId); + } + + // --- ADMIN ENDPUNKTE --- + + @PutMapping("/admin/memberships/{id}/approve") + @PreAuthorize("hasRole('ADMIN')") // Verknüpfung mit deiner UserRole.ADMIN aus dem iam-Modul + public ResponseEntity approveMembership(@PathVariable UUID id) { + membershipService.approveMembership(id); + return ResponseEntity.ok().build(); + } + + @PutMapping("/admin/memberships/{id}/reject") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity rejectMembership(@PathVariable UUID id) { + membershipService.rejectMembership(id); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/EligibleCommunityResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/EligibleCommunityResponse.java new file mode 100644 index 0000000..f929411 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/EligibleCommunityResponse.java @@ -0,0 +1,11 @@ +package at.mueller.eeg.backend.community.api.dto; + +import at.mueller.eeg.backend.community.domain.CommunityType; +import java.util.UUID; + +public record EligibleCommunityResponse( + UUID id, + String name, + CommunityType type, + String edaEcId +) {} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/MembershipRequest.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/MembershipRequest.java new file mode 100644 index 0000000..61806e6 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/MembershipRequest.java @@ -0,0 +1,12 @@ +package at.mueller.eeg.backend.community.api.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +public record MembershipRequest( + @NotNull UUID meteringPointId, + @NotNull UUID energyCommunityId, + @NotNull @Min(1) @Max(5) Integer priorityLevel +) {} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/EnergyCommunity.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/EnergyCommunity.java index f259303..efe19de 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/EnergyCommunity.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/EnergyCommunity.java @@ -16,10 +16,19 @@ public class EnergyCommunity { private UUID id; @Column(unique = true, nullable = false) - private String edaEcId; // z.B. CC123456 + private String edaEcId; @Enumerated(EnumType.STRING) - private CommunityType type; // EEG_LOKAL, EEG_REGIONAL, BEG + private CommunityType type; private String name; + + @Column(name = "grid_operator_id", length = 50) + private String gridOperatorId; + + @Column(name = "substation_id", length = 50) + private String substationId; + + @Column(name = "transformer_id", length = 50) + private String transformerId; } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java index de10228..c62f7fc 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java @@ -1,5 +1,7 @@ package at.mueller.eeg.backend.community.domain; +import at.mueller.eeg.backend.community.domain.state.MakoState; +import at.mueller.eeg.backend.community.domain.state.MakoTrigger; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @@ -36,8 +38,49 @@ public class MeteringPoint { private String transformerId; @Column(name = "grid_level") - private Integer gridLevel; + private String gridLevel; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private MakoState makoState = MakoState.NEW; @OneToMany(mappedBy = "meteringPoint", cascade = CascadeType.ALL) private List memberships; + + public void fireTrigger(MakoTrigger trigger) { + this.makoState = switch (this.makoState) { + + case NEW -> + trigger == MakoTrigger.SEND_REQUEST ? MakoState.WAITING_FOR_CONSENT : invalid(trigger); + + case WAITING_FOR_CONSENT -> switch (trigger) { + case CONSENT_APPROVED -> MakoState.CONSENT_GRANTED; + case CONSENT_DENIED -> MakoState.REJECTED; + case TECHNICAL_FAILURE -> MakoState.ERROR; + default -> invalid(trigger); + }; + + case CONSENT_GRANTED -> switch (trigger) { + case TOPOLOGY_VALID -> MakoState.ACTIVE; + case TOPOLOGY_INVALID -> MakoState.REJECTED; + case TECHNICAL_FAILURE -> MakoState.ERROR; + default -> invalid(trigger); + }; + + // Aus einem Fehlerzustand heraus kann man den Prozess z.B. neu starten + case ERROR -> + trigger == MakoTrigger.SEND_REQUEST ? MakoState.WAITING_FOR_CONSENT : invalid(trigger); + + // Endzustände (ACTIVE, REJECTED) erlauben standardmäßig keine weiteren Trigger + default -> invalid(trigger); + }; + } + + private MakoState invalid(MakoTrigger trigger) { + throw new IllegalStateException( + String.format("Ungültiger EDA-Zustandsübergang: Von Status '%s' mit Trigger '%s' nicht erlaubt.", + this.makoState, trigger) + ); + } + } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/state/MakoState.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/state/MakoState.java new file mode 100644 index 0000000..784e499 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/state/MakoState.java @@ -0,0 +1,10 @@ +package at.mueller.eeg.backend.community.domain.state; + +public enum MakoState { + NEW, // Neu angelegt, noch nichts passiert + WAITING_FOR_CONSENT, // CMRequest an EDA gesendet, warten auf Kunden + CONSENT_GRANTED, // Kunde hat beim Netzbetreiber zugestimmt + ACTIVE, // Topologie passt, Zählpunkt ist bereit für die EEG + REJECTED, // Kunde hat abgelehnt oder Zählpunkt passt nicht + ERROR // Technischer Fehler (Timeout, falsche AT-Nummer) +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/state/MakoTrigger.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/state/MakoTrigger.java new file mode 100644 index 0000000..8aa3789 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/state/MakoTrigger.java @@ -0,0 +1,10 @@ +package at.mueller.eeg.backend.community.domain.state; + +public enum MakoTrigger { + SEND_REQUEST, // Wir schicken den Consent-Request raus + CONSENT_APPROVED, // Netzbetreiber meldet Zustimmung + CONSENT_DENIED, // Netzbetreiber meldet Ablehnung + TOPOLOGY_VALID, // Topologie-Abfrage war erfolgreich und passend + TOPOLOGY_INVALID, // Topologie passt nicht zur EEG + TECHNICAL_FAILURE // EDA-Schnittstelle liefert Fehler +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListener.java index 18eeb8d..55838ca 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListener.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListener.java @@ -1,24 +1,35 @@ package at.mueller.eeg.backend.community.event.listener; -import at.mueller.eeg.backend.common.config.event.EdaTopologyCheckEvent; -import at.mueller.eeg.backend.common.config.event.UserRegisteredEvent; +import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent; +import at.mueller.eeg.backend.common.event.EdaTopologyCompletedEvent; +import at.mueller.eeg.backend.common.event.EdaTopologyFailedEvent; +import at.mueller.eeg.backend.common.event.UserRegisteredEvent; import at.mueller.eeg.backend.community.domain.MeteringPoint; import at.mueller.eeg.backend.community.domain.PointType; +import at.mueller.eeg.backend.community.domain.state.MakoTrigger; import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import at.mueller.eeg.backend.mako.service.EdaTopologyService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; @Component @RequiredArgsConstructor +@Slf4j public class MeteringPointEventListener { private final MeteringPointRepository meteringPointRepository; private final ApplicationEventPublisher eventPublisher; + private final EdaTopologyService edaTopologyService; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) public void handleUserRegistered(UserRegisteredEvent event) { MeteringPoint point = new MeteringPoint(); point.setUserId(event.userId()); @@ -26,6 +37,42 @@ public class MeteringPointEventListener { point.setType(PointType.CONSUMER); meteringPointRepository.save(point); - eventPublisher.publishEvent(new EdaTopologyCheckEvent(point.getId(), event.atNumber())); + eventPublisher.publishEvent(new EdaTopologyCheckEvent(event.userId(), point.getId(), event.atNumber())); + } + + @EventListener + @Transactional + public void handleEdaTopologyCompleted(EdaTopologyCompletedEvent event) { + meteringPointRepository.findById(event.meteringPointId()).ifPresent(point -> { + var data = event.topologyData(); + + try { + if (data.isEligible()) { + point.fireTrigger(MakoTrigger.TOPOLOGY_VALID); + + point.setGridOperatorId(data.gridOperatorName()); + point.setGridLevel(data.gridLevel()); + point.setSubstationId(data.substationId()); + point.setTransformerId(data.transformerId()); + } else { + point.fireTrigger(MakoTrigger.TOPOLOGY_INVALID); + } + + meteringPointRepository.save(point); + log.info("Zählpunkt {} ist nun im Status: {}", point.getId(), point.getMakoState()); + + } catch (IllegalStateException ex) { + log.error("Fehler beim Verarbeiten der EDA-Antwort: {}", ex.getMessage()); + } + }); + } + + @EventListener + @Transactional + public void handleEdaTopologyFailed(EdaTopologyFailedEvent event) { + meteringPointRepository.findById(event.meteringPointId()).ifPresent(point -> { + point.fireTrigger(MakoTrigger.TECHNICAL_FAILURE); + meteringPointRepository.save(point); + }); } } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java new file mode 100644 index 0000000..2e4e4a0 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java @@ -0,0 +1,24 @@ +package at.mueller.eeg.backend.community.repository; + +import at.mueller.eeg.backend.community.domain.EnergyCommunity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.UUID; + +public interface EnergyCommunityRepository extends JpaRepository { + + @Query(""" + SELECT c FROM EnergyCommunity c + WHERE c.type = 'BEG' + OR (c.type = 'EEG_REGIONAL' AND c.substationId = :substationId AND c.gridOperatorId = :gridOperatorId) + OR (c.type = 'EEG_LOKAL' AND c.transformerId = :transformerId AND c.gridOperatorId = :gridOperatorId) +""") + List findEligibleCommunities( + @Param("substationId") String substationId, + @Param("transformerId") String transformerId, + @Param("gridOperatorId") String gridOperatorId + ); +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java new file mode 100644 index 0000000..94334f9 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java @@ -0,0 +1,21 @@ +package at.mueller.eeg.backend.community.repository; + +import at.mueller.eeg.backend.community.domain.Membership; +import at.mueller.eeg.backend.community.domain.MembershipStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface MembershipRepository extends JpaRepository { + + boolean existsByMeteringPointIdAndEnergyCommunityId(UUID meteringPointId, UUID energyCommunityId); + + List findByMeteringPointId(UUID meteringPointId); + + boolean existsByMeteringPointIdAndPriorityLevelAndStatusIn( + UUID meteringPointId, + Integer priorityLevel, + List statuses + ); +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MeteringPointRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MeteringPointRepository.java index 4398bcd..b4159f3 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MeteringPointRepository.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MeteringPointRepository.java @@ -3,5 +3,7 @@ package at.mueller.eeg.backend.community.repository; import at.mueller.eeg.backend.community.domain.MeteringPoint; import org.springframework.data.jpa.repository.JpaRepository; -public interface MeteringPointRepository extends JpaRepository { +import java.util.UUID; + +public interface MeteringPointRepository extends JpaRepository { } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/dto/IamDtos.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/dto/IamDtos.java index cbb8e28..51f0386 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/dto/IamDtos.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/dto/IamDtos.java @@ -42,8 +42,8 @@ public interface IamDtos { String firstName, String lastName, String email, - String status, // PENDING, APPROVED, REJECTED - String primaryAtNumber // Zur schnellen Anzeige in der Liste + String status, + String primaryAtNumber ) {} record ErrorResponse(String message, int status) {} 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 6d19163..53e961a 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 @@ -49,39 +49,22 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - // 1. CSRF deaktivieren: Da wir stateless mit JWTs arbeiten und keine - // klassischen Session-Cookies verwenden, ist CSRF-Schutz hier nicht nötig. .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) - - // 2. Stateless Session Management: Spring Security darf keine HTTP-Session anlegen .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - - // 3. Routing & Autorisierung .authorizeHttpRequests(auth -> auth - // Erlaubt interne Forwards (wichtig für deinen SpaRoutingController -> /index.html) .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() - - // Erlaubt den Zugriff auf statische Frontend-Dateien im Root .requestMatchers("/", "/index.html", "/assets/**", "/static/**", "/*.js", "/*.css", "/*.ico").permitAll() - - // OpenAPI / Swagger Docs für das Frontend-Tooling freigeben .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - - // Deine öffentlichen API-Endpunkte .requestMatchers("/api/auth/**").permitAll() - - // Admin-Routen auf URL-Ebene absichern (zusätzlich zur @PreAuthorize Annotation) - .requestMatchers("/api/admin/**").hasRole("ADMIN") - - // Alle anderen /api/ Anfragen erfordern zumindest einen gültigen Login + .requestMatchers( + "/api/community/admin/**", + "/api/iam/admin/**", + "/api/mako/admin/**" + ).hasRole("ADMIN") .requestMatchers("/api/**").authenticated() - - // Alles andere (SPA-Routen, die nicht von obigen Matches gefangen wurden) freigeben .anyRequest().permitAll() ) - - // 4. JWT als Authentifizierungsmethode aktivieren .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) ); @@ -110,20 +93,10 @@ public class SecurityConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - - // Erlaube exakt dein Angular-Frontend configuration.setAllowedOrigins(List.of("http://localhost:4200")); - - // Erlaube die gängigen HTTP-Methoden - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - - // Erlaube Header, die Angular mitsendet (besonders wichtig für Content-Type und später Authorization/Token) + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); - - // Wichtig, wenn Anmeldedaten (Credentials) oder Cookies im Spiel sind configuration.setAllowCredentials(true); - - // Wende diese Regeln auf alle Endpunkte (/**) an UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java index e83f485..299570c 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java @@ -1,6 +1,6 @@ package at.mueller.eeg.backend.iam.service; -import at.mueller.eeg.backend.common.config.event.UserRegisteredEvent; +import at.mueller.eeg.backend.common.event.UserRegisteredEvent; import at.mueller.eeg.backend.iam.domain.User; import at.mueller.eeg.backend.iam.mapper.UserMapper; import at.mueller.eeg.backend.iam.repository.UserRepository; @@ -16,6 +16,7 @@ import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -53,6 +54,7 @@ public class AuthService { /** * Verarbeitet die Registrierung des neuen EEG-Mitglieds. */ + @Transactional public void register(RegistrationRequest request) { userRepository.findByEmail(request.email()).ifPresent(user -> { diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/dto/EdaTopologyResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/dto/EdaTopologyResponse.java new file mode 100644 index 0000000..e707e56 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/dto/EdaTopologyResponse.java @@ -0,0 +1,11 @@ +package at.mueller.eeg.backend.mako.api.dto; + +public record EdaTopologyResponse( + String atNumber, + boolean isEligible, + String gridOperatorName, + String gridLevel, + String substationId, + String transformerId +) { +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java new file mode 100644 index 0000000..5117913 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java @@ -0,0 +1,54 @@ +package at.mueller.eeg.backend.mako.event.listener; + +import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent; +import at.mueller.eeg.backend.common.event.EdaTopologyCompletedEvent; +import at.mueller.eeg.backend.common.event.EdaTopologyFailedEvent; +import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse; +import at.mueller.eeg.backend.mako.service.EdaTopologyService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + + +@Component +@RequiredArgsConstructor +@Slf4j +public class EdaCommuncationEventListener { + + private final EdaTopologyService edaTopologyService; + private final ApplicationEventPublisher eventPublisher; + + @EventListener + @Async + public void handleEdaTopologyCheckEvent(EdaTopologyCheckEvent event) { + log.info("Asynchroner Topologie-Check gestartet für ID: {}", event.meteringPointId()); + try { + EdaTopologyResponse edaResponse = edaTopologyService.checkTopology(event.atNumber()); + + eventPublisher.publishEvent(new EdaTopologyCompletedEvent( + event.userId(), + event.meteringPointId(), + edaResponse.isEligible(), + edaResponse.gridOperatorName(), + edaResponse.substationId(), + edaResponse.transformerId(), + edaResponse.gridLevel() + )); + + log.info("MeteringPoint erfolgreich mit EDA-Daten aktualisiert."); + + } catch (Exception e) { + log.error("Fehler bei der EDA Topologie-Abfrage für AT-Nummer {}: {}", + event.atNumber(), e.getMessage()); + + eventPublisher.publishEvent(new EdaTopologyFailedEvent( + event.userId(), + event.meteringPointId(), + e.getMessage() + )); + } + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaTopologyService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaTopologyService.java new file mode 100644 index 0000000..b84a3c1 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaTopologyService.java @@ -0,0 +1,10 @@ +package at.mueller.eeg.backend.mako.service; + +import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse; + +public interface EdaTopologyService { + /** + * Führt die Topologieabfrage für einen Zählpunkt durch. + */ + EdaTopologyResponse checkTopology(String atNumber); +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java new file mode 100644 index 0000000..d090103 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java @@ -0,0 +1,43 @@ +package at.mueller.eeg.backend.mako.service.impl; + +import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse; +import at.mueller.eeg.backend.mako.service.EdaTopologyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class SimulatedEdaTopologyService implements EdaTopologyService { + + @Override + public EdaTopologyResponse checkTopology(String atNumber) { + log.info("Starte simulierte EDA-Topologieabfrage für Zählpunkt: {}", atNumber); + try { + TimeUnit.SECONDS.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("EDA Simulation unterbrochen", e); + } + + if (atNumber == null || !atNumber.startsWith("AT")) { + throw new IllegalArgumentException("Ungültiges Zählpunkt-Format (muss mit AT beginnen)"); + } + + boolean isEligible = !atNumber.endsWith("999"); + + String operator = atNumber.contains("001000") ? "Wiener Netze GmbH" : "Netz NÖ GmbH"; + + log.info("Simulierte EDA-Abfrage abgeschlossen für: {}", atNumber); + + return new EdaTopologyResponse( + atNumber, + isEligible, + operator, + "7", + "UW_WAP_21_STREBERSDORF", + "TRAFO_21_042_BISAMBERG" + ); + } +} \ No newline at end of file