added admin approval list
implemented approval (untested) implemented eda consent simulation (untested)
This commit is contained in:
parent
5f845833cb
commit
ad47f7399e
38
eeg_backend/eegportal.trace.db
Normal file
38
eeg_backend/eegportal.trace.db
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
2026-06-16 18:27:44.502501+02:00 database: close
|
||||||
|
org.h2.message.DbException: Eingabe/Ausgabe Fehler: "Closing"
|
||||||
|
IO Exception: "Closing" [90028-240]
|
||||||
|
at org.h2.message.DbException.get(DbException.java:212)
|
||||||
|
at org.h2.mvstore.db.Store.close(Store.java:352)
|
||||||
|
at org.h2.engine.Database.closeOpenFilesAndUnlock(Database.java:1314)
|
||||||
|
at org.h2.engine.Database.closeImpl(Database.java:1276)
|
||||||
|
at org.h2.engine.Database.close(Database.java:1195)
|
||||||
|
at org.h2.engine.Database.onShutdown(Database.java:1180)
|
||||||
|
at org.h2.engine.OnExitDatabaseCloser.onShutdown(OnExitDatabaseCloser.java:85)
|
||||||
|
at org.h2.engine.OnExitDatabaseCloser.run(OnExitDatabaseCloser.java:114)
|
||||||
|
Caused by: org.h2.jdbc.JdbcSQLNonTransientException: Eingabe/Ausgabe Fehler: "Closing"
|
||||||
|
IO Exception: "Closing" [90028-240]
|
||||||
|
at org.h2.message.DbException.getJdbcSQLException(DbException.java:566)
|
||||||
|
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
|
||||||
|
... 8 more
|
||||||
|
Caused by: org.h2.mvstore.MVStoreException: java.lang.AssertionError: 8 [2.4.240/3]
|
||||||
|
at org.h2.mvstore.DataUtils.newMVStoreException(DataUtils.java:996)
|
||||||
|
at org.h2.mvstore.MVStore.executeFilestoreOperation(MVStore.java:1048)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.compactMoveChunks(RandomAccessStore.java:463)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.compactStore(RandomAccessStore.java:443)
|
||||||
|
at org.h2.mvstore.FileStore.compactStore(FileStore.java:891)
|
||||||
|
at org.h2.mvstore.FileStore.stop(FileStore.java:275)
|
||||||
|
at org.h2.mvstore.MVStore.closeStore(MVStore.java:803)
|
||||||
|
at org.h2.mvstore.MVStore.close(MVStore.java:763)
|
||||||
|
at org.h2.mvstore.db.Store.close(Store.java:348)
|
||||||
|
... 6 more
|
||||||
|
Caused by: java.lang.AssertionError: 8
|
||||||
|
at org.h2.mvstore.FreeSpaceBitSet.allocate(FreeSpaceBitSet.java:153)
|
||||||
|
at org.h2.mvstore.FreeSpaceBitSet.predictAllocation(FreeSpaceBitSet.java:133)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.predictAllocation(RandomAccessStore.java:101)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.moveChunkInside(RandomAccessStore.java:628)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.compactMoveChunks(RandomAccessStore.java:569)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.compactMoveChunks(RandomAccessStore.java:481)
|
||||||
|
at org.h2.mvstore.RandomAccessStore.lambda$compactMoveChunks$1(RandomAccessStore.java:468)
|
||||||
|
at org.h2.mvstore.FileStore.executeFileStoreOperation(FileStore.java:1894)
|
||||||
|
at org.h2.mvstore.MVStore.executeFilestoreOperation(MVStore.java:1044)
|
||||||
|
... 13 more
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package at.mueller.eeg.backend.common.event;
|
||||||
|
|
||||||
|
public record InitiateMakoConsentEvent(String atNumber, String pointType) {}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package at.mueller.eeg.backend.common.event;
|
||||||
|
|
||||||
|
public record MakoConsensApprovedEvent(String atNumber) {}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package at.mueller.eeg.backend.common.event;
|
||||||
|
|
||||||
|
public record MakoConsensRejectedEvent(String atNumber, String reason) {}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package at.mueller.eeg.backend.common.event;
|
||||||
|
|
||||||
|
public record MakoConsensTechnicalFailedEvent(String atNumber, String reason) {}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package at.mueller.eeg.backend.common.event;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record MeteringPointApprovedEvent(
|
||||||
|
UUID userId,
|
||||||
|
String meteringPointNumber,
|
||||||
|
String type // "BEZUG" oder "EINSPEISUNG"
|
||||||
|
) {}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package at.mueller.eeg.backend.common.event;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record UserApprovedEvent(UUID userId) {}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package at.mueller.eeg.backend.community.event.listener;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.common.event.InitiateMakoConsentEvent;
|
||||||
|
import at.mueller.eeg.backend.common.event.UserApprovedEvent;
|
||||||
|
import at.mueller.eeg.backend.community.domain.MeteringPoint;
|
||||||
|
import at.mueller.eeg.backend.community.domain.state.MakoTrigger;
|
||||||
|
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
||||||
|
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;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CommunityMakoListener {
|
||||||
|
|
||||||
|
private final MeteringPointRepository meteringPointRepository;
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@EventListener
|
||||||
|
@Transactional
|
||||||
|
public void onUserApproved(UserApprovedEvent event) {
|
||||||
|
log.info("Community-Domain: User {} wurde freigegeben. Starte MaKo-Prozess für Zählpunkte...", event.userId());
|
||||||
|
|
||||||
|
List<MeteringPoint> meteringPoints = meteringPointRepository.findAllByUserId(event.userId());
|
||||||
|
|
||||||
|
if (meteringPoints.isEmpty()) {
|
||||||
|
log.warn("Keine Zählpunkte für freigegebenen User {} in der Community-Domain gefunden.", event.userId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MeteringPoint mp : meteringPoints) {
|
||||||
|
try {
|
||||||
|
log.info("Triggere SEND_REQUEST für Zählpunkt {} (Aktueller Status: {})", mp.getAtNumber(), mp.getMakoState());
|
||||||
|
|
||||||
|
mp.fireTrigger(MakoTrigger.SEND_REQUEST);
|
||||||
|
|
||||||
|
meteringPointRepository.save(mp);
|
||||||
|
|
||||||
|
log.info("Delegiere Consent-Request für {} an MaKo-Domain via Event.", mp.getAtNumber());
|
||||||
|
eventPublisher.publishEvent(new InitiateMakoConsentEvent(
|
||||||
|
mp.getAtNumber(),
|
||||||
|
mp.getType().name()
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("Zustandsübergang fehlgeschlagen für Zählpunkt {}: {}", mp.getAtNumber(), e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Unerwarteter Fehler bei EDA-Anbindung für Zählpunkt {}: {}", mp.getAtNumber(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,6 @@ import at.mueller.eeg.backend.community.domain.MeteringPoint;
|
|||||||
import at.mueller.eeg.backend.community.domain.PointType;
|
import at.mueller.eeg.backend.community.domain.PointType;
|
||||||
import at.mueller.eeg.backend.community.domain.state.MakoTrigger;
|
import at.mueller.eeg.backend.community.domain.state.MakoTrigger;
|
||||||
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
||||||
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
@ -26,7 +25,6 @@ public class MeteringPointEventListener {
|
|||||||
|
|
||||||
private final MeteringPointRepository meteringPointRepository;
|
private final MeteringPointRepository meteringPointRepository;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
private final EdaTopologyService edaTopologyService;
|
|
||||||
|
|
||||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
@ -44,16 +42,15 @@ public class MeteringPointEventListener {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void handleEdaTopologyCompleted(EdaTopologyCompletedEvent event) {
|
public void handleEdaTopologyCompleted(EdaTopologyCompletedEvent event) {
|
||||||
meteringPointRepository.findById(event.meteringPointId()).ifPresent(point -> {
|
meteringPointRepository.findById(event.meteringPointId()).ifPresent(point -> {
|
||||||
var data = event.topologyData();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.isEligible()) {
|
if (event.isEligible()) {
|
||||||
point.fireTrigger(MakoTrigger.TOPOLOGY_VALID);
|
point.fireTrigger(MakoTrigger.TOPOLOGY_VALID);
|
||||||
|
|
||||||
point.setGridOperatorId(data.gridOperatorName());
|
point.setGridOperatorId(event.gridOperatorId());
|
||||||
point.setGridLevel(data.gridLevel());
|
point.setGridLevel(event.gridLevel());
|
||||||
point.setSubstationId(data.substationId());
|
point.setSubstationId(event.substationId());
|
||||||
point.setTransformerId(data.transformerId());
|
point.setTransformerId(event.transformerId());
|
||||||
} else {
|
} else {
|
||||||
point.fireTrigger(MakoTrigger.TOPOLOGY_INVALID);
|
point.fireTrigger(MakoTrigger.TOPOLOGY_INVALID);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,10 @@ package at.mueller.eeg.backend.community.repository;
|
|||||||
import at.mueller.eeg.backend.community.domain.MeteringPoint;
|
import at.mueller.eeg.backend.community.domain.MeteringPoint;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UUID> {
|
public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UUID> {
|
||||||
|
|
||||||
|
List<MeteringPoint> findAllByUserId(UUID userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,122 @@
|
|||||||
|
package at.mueller.eeg.backend.community.service;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.community.api.dto.EligibleCommunityResponse;
|
||||||
|
import at.mueller.eeg.backend.community.api.dto.MembershipRequest;
|
||||||
|
import at.mueller.eeg.backend.community.domain.EnergyCommunity;
|
||||||
|
import at.mueller.eeg.backend.community.domain.Membership;
|
||||||
|
import at.mueller.eeg.backend.community.domain.MembershipStatus;
|
||||||
|
import at.mueller.eeg.backend.community.domain.MeteringPoint;
|
||||||
|
import at.mueller.eeg.backend.community.domain.state.MakoState;
|
||||||
|
import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository;
|
||||||
|
import at.mueller.eeg.backend.community.repository.MembershipRepository;
|
||||||
|
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MembershipService {
|
||||||
|
|
||||||
|
private final EnergyCommunityRepository energyCommunityRepository;
|
||||||
|
private final MembershipRepository membershipRepository;
|
||||||
|
private final MeteringPointRepository meteringPointRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<EligibleCommunityResponse> getEligibleCommunities(UUID meteringPointId) {
|
||||||
|
MeteringPoint point = meteringPointRepository.findById(meteringPointId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Zählpunkt nicht gefunden"));
|
||||||
|
|
||||||
|
// Business Rule: Nur technisch validierte Zählpunkte dürfen teilnehmen
|
||||||
|
if (point.getMakoState() != MakoState.ACTIVE) {
|
||||||
|
log.warn("Zählpunkt {} hat Status {}. Matching nicht erlaubt.", meteringPointId, point.getMakoState());
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return energyCommunityRepository.findEligibleCommunities(
|
||||||
|
point.getSubstationId(), point.getTransformerId(), point.getGridOperatorId()
|
||||||
|
).stream()
|
||||||
|
.map(c -> new EligibleCommunityResponse(c.getId(), c.getName(), c.getType(), c.getEdaEcId()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UUID requestMembership(MembershipRequest request) {
|
||||||
|
MeteringPoint point = meteringPointRepository.findById(request.meteringPointId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Zählpunkt nicht gefunden"));
|
||||||
|
|
||||||
|
EnergyCommunity community = energyCommunityRepository.findById(request.energyCommunityId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Energiegemeinschaft nicht gefunden"));
|
||||||
|
|
||||||
|
if (point.getMakoState() != MakoState.ACTIVE) {
|
||||||
|
throw new IllegalStateException("Zählpunkt ist im EDA-System noch nicht aktiv.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membershipRepository.existsByMeteringPointIdAndEnergyCommunityId(point.getId(), community.getId())) {
|
||||||
|
throw new IllegalStateException("Ein Beitrittsantrag für diesen Zählpunkt existiert bereits.");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean priorityTaken = membershipRepository.existsByMeteringPointIdAndPriorityLevelAndStatusIn(
|
||||||
|
point.getId(),
|
||||||
|
request.priorityLevel(),
|
||||||
|
List.of(MembershipStatus.ACTIVE, MembershipStatus.PENDING)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (priorityTaken) {
|
||||||
|
throw new IllegalStateException("Die Priorität " + request.priorityLevel() + " ist bereits vergeben. Bitte wähle eine andere oder ändere zuerst deine bestehenden Mitgliedschaften.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Hier könnte man noch prüfen, ob das priorityLevel bereits für diesen Zählpunkt vergeben ist.
|
||||||
|
|
||||||
|
Membership membership = new Membership();
|
||||||
|
membership.setMeteringPoint(point);
|
||||||
|
membership.setEnergyCommunity(community);
|
||||||
|
membership.setPriorityLevel(request.priorityLevel());
|
||||||
|
membership.setStatus(MembershipStatus.PENDING); // Startet als PENDING
|
||||||
|
|
||||||
|
Membership saved = membershipRepository.save(membership);
|
||||||
|
log.info("Beitrittsantrag für Zählpunkt {} zu Community {} erstellt.", point.getId(), community.getId());
|
||||||
|
return saved.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void approveMembership(UUID membershipId) {
|
||||||
|
Membership membership = membershipRepository.findById(membershipId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Mitgliedschaft nicht gefunden"));
|
||||||
|
|
||||||
|
if (membership.getStatus() != MembershipStatus.PENDING) {
|
||||||
|
throw new IllegalStateException("Nur offene Anträge können genehmigt werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
membership.setStatus(MembershipStatus.ACTIVE);
|
||||||
|
membership.setValidFrom(LocalDate.now()); // Ab heute aktiv
|
||||||
|
|
||||||
|
membershipRepository.save(membership);
|
||||||
|
log.info("Mitgliedschaft {} wurde vom Admin freigeschaltet.", membershipId);
|
||||||
|
|
||||||
|
// TODO: Hier später ein Integration Event feuern (z.B. "MembershipActivatedEvent"),
|
||||||
|
// damit die mako-Domäne die Marktkommunikation (Beginn der Belieferung/Zuteilung) via EDA anstößt!
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void rejectMembership(UUID membershipId) {
|
||||||
|
Membership membership = membershipRepository.findById(membershipId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Mitgliedschaft nicht gefunden"));
|
||||||
|
|
||||||
|
if (membership.getStatus() != MembershipStatus.PENDING) {
|
||||||
|
throw new IllegalStateException("Nur offene Anträge können abgelehnt werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
membership.setStatus(MembershipStatus.INACTIVE);
|
||||||
|
membership.setValidTo(LocalDate.now());
|
||||||
|
|
||||||
|
membershipRepository.save(membership);
|
||||||
|
log.info("Mitgliedschaft {} wurde vom Admin abgelehnt.", membershipId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,31 @@
|
|||||||
package at.mueller.eeg.backend.iam.api;
|
package at.mueller.eeg.backend.iam.api;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.common.event.UserApprovedEvent;
|
||||||
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse;
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse;
|
||||||
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
||||||
import at.mueller.eeg.backend.iam.domain.User;
|
import at.mueller.eeg.backend.iam.domain.User;
|
||||||
import at.mueller.eeg.backend.iam.mapper.UserMapper;
|
import at.mueller.eeg.backend.iam.mapper.UserMapper;
|
||||||
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/admin/users")
|
@RequestMapping(value = "/api/admin/users", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
@Tag(name = "Admin IAM", description = "User-Verwaltung und Freigabe-Workflow")
|
@Tag(name = "Admin IAM", description = "User-Verwaltung und Freigabe-Workflow")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -29,9 +35,21 @@ public class AdminIamController {
|
|||||||
|
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
@GetMapping("/pending")
|
@GetMapping("/pending")
|
||||||
@Operation(summary = "Lädt alle wartenden User",
|
@Operation(summary = "Lädt alle wartenden User",
|
||||||
description = "Zeigt User im Status PENDING inkl. der Ergebnisse des EDA-Netz-Checks.")
|
description = "Zeigt User im Status PENDING inkl. der Ergebnisse des EDA-Netz-Checks.")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Liste der wartenden Benutzer erfolgreich geladen",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(implementation = UserProfileResponse.class)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
public ResponseEntity<List<UserProfileResponse>> getPendingUsers() {
|
public ResponseEntity<List<UserProfileResponse>> getPendingUsers() {
|
||||||
List<User> allByStatus = userRepository.findAllByStatus(RegistrationStatus.PENDING);
|
List<User> allByStatus = userRepository.findAllByStatus(RegistrationStatus.PENDING);
|
||||||
return ResponseEntity.ok(userMapper.mapToDto(allByStatus));
|
return ResponseEntity.ok(userMapper.mapToDto(allByStatus));
|
||||||
@ -41,23 +59,21 @@ public class AdminIamController {
|
|||||||
@Operation(summary = "User freigeben",
|
@Operation(summary = "User freigeben",
|
||||||
description = "Setzt enabled=true, status=APPROVED und triggert die finale EDA-Anmeldung.")
|
description = "Setzt enabled=true, status=APPROVED und triggert die finale EDA-Anmeldung.")
|
||||||
public ResponseEntity<Void> approveUser(@PathVariable UUID userId) {
|
public ResponseEntity<Void> approveUser(@PathVariable UUID userId) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"));
|
||||||
|
|
||||||
Optional<User> userOptional = userRepository.findById(userId);
|
if (!RegistrationStatus.PENDING.equals(user.getStatus())) {
|
||||||
if (userOptional.isPresent()) {
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "User is not in PENDING status");
|
||||||
User user = userOptional.get();
|
|
||||||
if (RegistrationStatus.PENDING.equals(user.getStatus())) {
|
|
||||||
user.setStatus(RegistrationStatus.APPROVED);
|
|
||||||
} else {
|
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User in wrong status");
|
|
||||||
}
|
}
|
||||||
if (user.isEnabled()) {
|
if (user.isEnabled()) {
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User already enabled");
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "User is already enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.setStatus(RegistrationStatus.APPROVED);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
|
||||||
// Logik: status auf APPROVED, enabled auf true
|
eventPublisher.publishEvent(new UserApprovedEvent(user.getId()));
|
||||||
// Event feuern: UserApprovedEvent (Schickt E-Mail an User)
|
|
||||||
|
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
package at.mueller.eeg.backend.mako.event.listener;
|
||||||
|
|
||||||
|
public class MakoCommandListener {
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package at.mueller.eeg.backend.mako.service;
|
||||||
|
|
||||||
|
public interface EdaConsentService {
|
||||||
|
/**
|
||||||
|
* Initiiert asynchron den Consent-Request (Einholung der Zustimmung) beim Netzbetreiber via EDA.
|
||||||
|
* Der Aufruf blockiert nicht. Die Antwort erfolgt asynchron via Event (MakoEvents).
|
||||||
|
*
|
||||||
|
* @param atNumber Die 33-stellige Zählpunktnummer.
|
||||||
|
* @param pointType Der Typ des Zählpunkts (z.B. CONSUMER, PRODUCER).
|
||||||
|
*/
|
||||||
|
void sendConsentRequest(String atNumber, String pointType);}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package at.mueller.eeg.backend.mako.service.impl;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.common.event.MakoConsensApprovedEvent;
|
||||||
|
import at.mueller.eeg.backend.common.event.MakoConsensRejectedEvent;
|
||||||
|
import at.mueller.eeg.backend.common.event.MakoConsensTechnicalFailedEvent;
|
||||||
|
import at.mueller.eeg.backend.mako.service.EdaConsentService;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SimulatedEdaConsentService implements EdaConsentService {
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Value("${eda.simulation.consent-delay-ms:3000}")
|
||||||
|
private long simulationDelayMs;
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendConsentRequest(String atNumber, String pointType) {
|
||||||
|
log.info("[EDA-SIMULATION] Eingehender Request. AT-Nummer: {}, Typ: {}", atNumber, pointType);
|
||||||
|
|
||||||
|
if (atNumber == null || atNumber.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("AT-Nummer darf nicht leer sein für EDA-Request.");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[EDA-SIMULATION] Request formal gültig. Starte asynchrone Verarbeitung in {} ms...", simulationDelayMs);
|
||||||
|
|
||||||
|
scheduler.schedule(() -> processAsyncCallback(atNumber), simulationDelayMs, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hier sitzt die "Magic Value" Logik für deterministisches Testen deiner States.
|
||||||
|
*/
|
||||||
|
private void processAsyncCallback(String atNumber) {
|
||||||
|
log.info("[EDA-SIMULATION] Verarbeite asynchronen Callback für: {}", atNumber);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (atNumber.toUpperCase().endsWith("REJECT")) {
|
||||||
|
log.info("[EDA-SIMULATION] OUTCOME: Zustimmung verweigert (Magic Value erkannt).");
|
||||||
|
eventPublisher.publishEvent(new MakoConsensRejectedEvent(atNumber, "Kunde hat im Portal abgelehnt."));
|
||||||
|
}
|
||||||
|
else if (atNumber.toUpperCase().endsWith("ERROR")) {
|
||||||
|
log.warn("[EDA-SIMULATION] OUTCOME: Technischer EDA-Fehler (Magic Value erkannt).");
|
||||||
|
eventPublisher.publishEvent(new MakoConsensTechnicalFailedEvent(atNumber, "EDA-Gateway Timeout (504)."));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.info("[EDA-SIMULATION] OUTCOME: Zustimmung erteilt!");
|
||||||
|
eventPublisher.publishEvent(new MakoConsensApprovedEvent(atNumber));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[EDA-SIMULATION] Kritischer Fehler im Simulator: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean-Up: Verhindert Memory-Leaks beim Neustarten (Live-Reload/DevTools)
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
log.info("Fahre EDA-Simulator herunter...");
|
||||||
|
scheduler.shutdown();
|
||||||
|
try {
|
||||||
|
if (!scheduler.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,55 @@ tags:
|
|||||||
- name: Admin IAM
|
- name: Admin IAM
|
||||||
description: User-Verwaltung und Freigabe-Workflow
|
description: User-Verwaltung und Freigabe-Workflow
|
||||||
paths:
|
paths:
|
||||||
|
/api/community/admin/memberships/{id}/reject:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- membership-controller
|
||||||
|
operationId: rejectMembership
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
/api/community/admin/memberships/{id}/approve:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- membership-controller
|
||||||
|
operationId: approveMembership
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
/api/community/memberships/request:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- membership-controller
|
||||||
|
operationId: requestMembership
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MembershipRequest"
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'*/*':
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
/api/auth/register:
|
/api/auth/register:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@ -83,6 +132,27 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
|
/api/community/metering-points/{id}/eligible-communities:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- membership-controller
|
||||||
|
operationId: getEligibleCommunities
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'*/*':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/EligibleCommunityResponse"
|
||||||
/api/admin/users/pending:
|
/api/admin/users/pending:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@ -92,15 +162,33 @@ paths:
|
|||||||
operationId: getPendingUsers
|
operationId: getPendingUsers
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: Liste der wartenden Benutzer erfolgreich geladen
|
||||||
content:
|
content:
|
||||||
'*/*':
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/UserProfileResponse"
|
$ref: "#/components/schemas/UserProfileResponse"
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
MembershipRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meteringPointId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
energyCommunityId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
priorityLevel:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
maximum: 5
|
||||||
|
minimum: 1
|
||||||
|
required:
|
||||||
|
- energyCommunityId
|
||||||
|
- meteringPointId
|
||||||
|
- priorityLevel
|
||||||
RegistrationRequest:
|
RegistrationRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Payload für die Registrierung eines neuen Users
|
description: Payload für die Registrierung eines neuen Users
|
||||||
@ -165,6 +253,22 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Rolle des Users
|
description: Rolle des Users
|
||||||
example: USER
|
example: USER
|
||||||
|
EligibleCommunityResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- EEG_LOKAL
|
||||||
|
- EEG_REGIONAL
|
||||||
|
- BEG
|
||||||
|
edaEcId:
|
||||||
|
type: string
|
||||||
UserProfileResponse:
|
UserProfileResponse:
|
||||||
type: object
|
type: object
|
||||||
description: Repräsentation eines Users für das Admin-Dashboard
|
description: Repräsentation eines Users für das Admin-Dashboard
|
||||||
|
|||||||
9
eeg_frontend/package-lock.json
generated
9
eeg_frontend/package-lock.json
generated
@ -18,7 +18,8 @@
|
|||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "^0.16.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^21.2.12",
|
"@angular/build": "^21.2.12",
|
||||||
@ -11562,6 +11563,12 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.28 || ^4"
|
"zod": "^3.25.28 || ^4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zone.js": {
|
||||||
|
"version": "0.16.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.2.tgz",
|
||||||
|
"integrity": "sha512-Eky7p2Z1Ig3NnbfodSPoARCjKBSTFMnE/ACsP1L/XJEfY4SdOFce19BsUCWVwL6K5ABZFy5J3bjcMWffX+YM3Q==",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,8 @@
|
|||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "^0.16.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^21.2.12",
|
"@angular/build": "^21.2.12",
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
import {ApplicationConfig} from '@angular/core';
|
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||||
import {provideRouter} from '@angular/router';
|
import {provideRouter} from '@angular/router';
|
||||||
import {routes} from './app.routes';
|
import {routes} from './app.routes';
|
||||||
import {provideHttpClient, withFetch} from '@angular/common/http';
|
import {provideHttpClient, withFetch, withInterceptors} from '@angular/common/http';
|
||||||
import {BASE_PATH} from './api';
|
import {BASE_PATH} from './api';
|
||||||
|
import {jwtInterceptor} from './interceptors/jwt.interceptor';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideHttpClient(withFetch()),
|
provideHttpClient(withFetch(),
|
||||||
|
withInterceptors([jwtInterceptor])),
|
||||||
{ provide: BASE_PATH, useValue: 'http://localhost:8080' }
|
{ provide: BASE_PATH, useValue: 'http://localhost:8080' }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,16 @@ import {Routes} from '@angular/router';
|
|||||||
import {LandingPage} from './pages/landing-page/landing-page';
|
import {LandingPage} from './pages/landing-page/landing-page';
|
||||||
import {Register} from './pages/register/register';
|
import {Register} from './pages/register/register';
|
||||||
import {LoginComponent} from './pages/login/login';
|
import {LoginComponent} from './pages/login/login';
|
||||||
|
import {AdminUserApprovalComponent} from './pages/admin-user-approval/admin-user-approval';
|
||||||
|
import {adminAuthGuard} from './guards/admin-auth-guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: LandingPage },
|
{ path: '', component: LandingPage },
|
||||||
{ path: 'register', component: Register },
|
{ path: 'register', component: Register },
|
||||||
{ path: 'login', component: LoginComponent}
|
{ path: 'login', component: LoginComponent},
|
||||||
|
{
|
||||||
|
path: 'admin/approvals',
|
||||||
|
component: AdminUserApprovalComponent,
|
||||||
|
canActivate: [adminAuthGuard]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
17
eeg_frontend/src/app/guards/admin-auth-guard.spec.ts
Normal file
17
eeg_frontend/src/app/guards/admin-auth-guard.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
|
||||||
|
import { adminAuthGuard } from './admin-auth-guard';
|
||||||
|
|
||||||
|
describe('adminAuthGuard', () => {
|
||||||
|
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||||
|
TestBed.runInInjectionContext(() => adminAuthGuard(...guardParameters));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(executeGuard).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
eeg_frontend/src/app/guards/admin-auth-guard.ts
Normal file
19
eeg_frontend/src/app/guards/admin-auth-guard.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {inject} from '@angular/core';
|
||||||
|
import {CanActivateFn, Router} from '@angular/router';
|
||||||
|
import {AuthService} from '../services/auth'; // Adjust path if necessary
|
||||||
|
|
||||||
|
export const adminAuthGuard: CanActivateFn = (route, state) => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
|
||||||
|
// Replace these with your actual authentication and role-checking methods
|
||||||
|
const isAuthenticated = authService.isLoggedIn();
|
||||||
|
const isAdmin = authService.getUserRole() === 'ADMIN';
|
||||||
|
if (isAuthenticated && isAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Redirect to a generic 'unauthorized' page or back to the landing page/login
|
||||||
|
console.warn('Access denied: Admin privileges required.');
|
||||||
|
return router.createUrlTree(['/login']);
|
||||||
|
};
|
||||||
17
eeg_frontend/src/app/interceptors/jwt.interceptor.ts
Normal file
17
eeg_frontend/src/app/interceptors/jwt.interceptor.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next(clonedRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(req);
|
||||||
|
};
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
<div class="container mx-auto p-6 max-w-6xl">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">User Freigabe</h1>
|
||||||
|
<p class="text-gray-600 mt-2">Prüfung der ausstehenden Anmeldungen und EDA-Netz-Checks.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (errorMessage()) {
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
|
||||||
|
{{ errorMessage() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isLoading()) {
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<div class="text-gray-500 text-lg">Lade ausstehende Anträge...</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!isLoading() && pendingUsers().length === 0) {
|
||||||
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
|
||||||
|
<p class="text-gray-600 text-lg">Aktuell gibt es keine neuen User im Status PENDING.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!isLoading() && pendingUsers().length > 0) {
|
||||||
|
<div class="bg-white shadow-sm border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50 border-b border-gray-200">
|
||||||
|
<th class="p-4 text-sm font-semibold text-gray-700 uppercase tracking-wider">Benutzer</th>
|
||||||
|
<th class="p-4 text-sm font-semibold text-gray-700 uppercase tracking-wider">Zählpunkt</th>
|
||||||
|
<th class="p-4 text-sm font-semibold text-gray-700 uppercase tracking-wider">Status</th>
|
||||||
|
<th class="p-4 text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
@for (user of pendingUsers(); track user.id) {
|
||||||
|
<tr class="hover:bg-gray-50 transition-colors">
|
||||||
|
|
||||||
|
<td class="p-4">
|
||||||
|
<div class="font-medium text-gray-900">{{ user.firstName }} {{ user.lastName }}</div>
|
||||||
|
<div class="text-sm text-gray-500">{{ user.email }}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="p-4">
|
||||||
|
<div class="text-gray-900 font-mono text-sm">
|
||||||
|
{{ user.primaryAtNumber || 'Kein Zählpunkt hinterlegt' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="p-4">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
{{ user.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="p-4 flex justify-end gap-3">
|
||||||
|
<button (click)="approveUser(user.id)"
|
||||||
|
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition">
|
||||||
|
Freigeben
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="rejectUser(user.id)"
|
||||||
|
class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition">
|
||||||
|
Ablehnen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminUserApproval } from './admin-user-approval';
|
||||||
|
|
||||||
|
describe('AdminUserApproval', () => {
|
||||||
|
let component: AdminUserApproval;
|
||||||
|
let fixture: ComponentFixture<AdminUserApproval>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AdminUserApproval],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AdminUserApproval);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import {Component, DestroyRef, inject, OnInit, signal} from '@angular/core';
|
||||||
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {AdminIAMService, UserProfileResponse} from '../../api';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin-user-approval',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './admin-user-approval.html',
|
||||||
|
styleUrls: ['./admin-user-approval.scss']
|
||||||
|
})
|
||||||
|
export class AdminUserApprovalComponent implements OnInit {
|
||||||
|
// Signals für reaktives State-Management (perfekt für den Zoneless-Modus)
|
||||||
|
public readonly pendingUsers = signal<UserProfileResponse[]>([]);
|
||||||
|
public readonly isLoading = signal<boolean>(true);
|
||||||
|
public readonly errorMessage = signal<string>('');
|
||||||
|
|
||||||
|
private readonly adminService = inject(AdminIAMService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef); // Für sauberes Ressourcen-Management
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.loadPendingUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPendingUsers(): void {
|
||||||
|
this.isLoading.set(true);
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
this.adminService.getPendingUsers()
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe({
|
||||||
|
next: (users: UserProfileResponse[]) => {
|
||||||
|
this.pendingUsers.set(users);
|
||||||
|
this.isLoading.set(false);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Failed to load pending users:', error);
|
||||||
|
this.errorMessage.set('Fehler beim Laden der Daten aus dem EDA-Netz-Check.');
|
||||||
|
this.isLoading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public approveUser(userId: string | undefined): void {
|
||||||
|
if (!userId) return;
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
this.adminService.approveUser(userId)
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.pendingUsers.update(users => users.filter(user => user.id !== userId));
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(`Failed to approve user with ID ${userId}:`, error);
|
||||||
|
this.errorMessage.set('Der User konnte nicht freigegeben werden.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public rejectUser(userId: string | undefined): void {
|
||||||
|
if (!userId) return;
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
this.adminService.rejectUser(userId)
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
// Den abgelehnten User atomar aus der Liste filtern
|
||||||
|
this.pendingUsers.update(users => users.filter(user => user.id !== userId));
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(`Failed to reject user with ID ${userId}:`, error);
|
||||||
|
this.errorMessage.set('Der User konnte nicht abgelehnt werden.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {MatButtonModule} from '@angular/material/button';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import {MatIconModule} from '@angular/material/icon';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import {RouterLink} from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-landing-page',
|
selector: 'app-landing-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, MatIconModule, RouterLink],
|
imports: [MatButtonModule, MatIconModule],
|
||||||
templateUrl: './landing-page.html',
|
templateUrl: './landing-page.html',
|
||||||
styleUrls: ['./landing-page.scss']
|
styleUrls: ['./landing-page.scss']
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'zone.js';
|
||||||
import {bootstrapApplication} from '@angular/platform-browser';
|
import {bootstrapApplication} from '@angular/platform-browser';
|
||||||
import {appConfig} from './app/app.config';
|
import {appConfig} from './app/app.config';
|
||||||
import {App} from './app/app';
|
import {App} from './app/app';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user