complete MaKo consent flow and admin initialization
- Add MakoConsensListener for consent event handling (approved/rejected/failed) - Add AdminDataInitializer with random password generation for dev profile - Add findByAtNumber to MeteringPointRepository - Fix User.isEnabled() Lombok/Spring Security conflict - Update pom.xml with explicit Lombok 1.18.38 version - Update .gitignore for frontend, H2, and test files - Add AGENTS.md project documentation
This commit is contained in:
parent
70e05e524b
commit
8f74dbde35
21
.gitignore
vendored
21
.gitignore
vendored
@ -31,8 +31,21 @@ build/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
/eegportal.lock.db
|
|
||||||
/eeg_backend/eegportal.mv.db
|
### Frontend ###
|
||||||
/eegportal.mv.db
|
eeg_frontend/node/
|
||||||
/eegportal.trace.db
|
node_modules/
|
||||||
|
eeg_frontend/.angular/
|
||||||
|
|
||||||
|
### H2 Datenbank ###
|
||||||
|
*.db
|
||||||
|
*.lock.db
|
||||||
|
|
||||||
|
### Test-Dateien ###
|
||||||
|
test_*.json
|
||||||
|
|
||||||
|
### Projekt-spezifisch ###
|
||||||
/projekt_kontext.txt
|
/projekt_kontext.txt
|
||||||
|
.mimocode/
|
||||||
|
run_test.ps1
|
||||||
|
server_output.log
|
||||||
|
|||||||
74
AGENTS.md
Normal file
74
AGENTS.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# AGENTS.md - EEG Portal
|
||||||
|
|
||||||
|
## Projektübersicht
|
||||||
|
|
||||||
|
**EEG Portal** - Portal zur Verwaltung von Energiegemeinschaften in Österreich.
|
||||||
|
Behandelt: Benutzerregistrierung, Messpunkt-Consent-Workflows (MaKo), EDA-Topologieprüfungen,
|
||||||
|
Energiegemeinschaftsverwaltung, Mitgliedschaften und Tarife.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
**Modularer Monolith** - Designed für spätere Domänen-Extraktion als Maven-Module.
|
||||||
|
|
||||||
|
### Backend (Spring Boot)
|
||||||
|
- **Java 21**, Spring Boot 4.0.6
|
||||||
|
- **Pakete:**
|
||||||
|
- `iam/` - Identitäts- und Zugriffsverwaltung (Auth, Users, Security)
|
||||||
|
- `community/` - Energiegemeinschaften, Mitgliedschaften, Messpunkte
|
||||||
|
- `mako/` - EDA-Kommunikation (Consent, Topologie)
|
||||||
|
- `tariff/` - Tarifverwaltung
|
||||||
|
- `dashboard/` - Dashboard-Statistiken
|
||||||
|
- `common/` - Events, Exceptions, gemeinsame Types
|
||||||
|
|
||||||
|
### Frontend (Angular)
|
||||||
|
- Angular 21, TypeScript 5.9, Tailwind CSS 4
|
||||||
|
- API-Client wird aus OpenAPI-Spec generiert (`npm run generate-api`)
|
||||||
|
|
||||||
|
## Wichtige Patterns
|
||||||
|
|
||||||
|
### Event-basierte Kommunikation
|
||||||
|
Domänen kommunizieren über Spring Application Events:
|
||||||
|
```
|
||||||
|
UserRegisteredEvent → UserApprovedEvent → InitiateMakoConsentEvent
|
||||||
|
EdaTopologyCheckEvent → EdaTopologyCompletedEvent / EdaTopologyFailedEvent
|
||||||
|
MakoConsensApprovedEvent / MakoConsensRejectedEvent / MakoConsensTechnicalFailedEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
### MaKo-Zustandsautomat (MeteringPoint.fireTrigger())
|
||||||
|
```
|
||||||
|
NEW → WAITING_FOR_CONSENT → CONSENT_GRANTED → ACTIVE
|
||||||
|
↓
|
||||||
|
REJECTED / ERROR
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
- JWT-basiert (HS256), 2h Ablauf
|
||||||
|
- Rollen: `ADMIN`, `MEMBER`
|
||||||
|
- Admin-Endpunkte: `/api/admin/**`, `/api/community/admin/**`
|
||||||
|
|
||||||
|
## Entwicklung starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
.\mvnw spring-boot:run -pl eeg_backend -Dspring-boot.run.arguments=--spring.profiles.active=dev
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd eeg_frontend && ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Dateien
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|--------------|
|
||||||
|
| `eeg_backend/.../community/domain/MeteringPoint.java:fireTrigger()` | MaKo-Zustandsautomat |
|
||||||
|
| `eeg_backend/.../community/event/listener/MakoConsentListener.java` | Consent-Event-Handler |
|
||||||
|
| `eeg_backend/.../mako/event/listener/EdaCommuncationEventListener.java` | EDA-Kommunikation |
|
||||||
|
| `eeg_backend/.../iam/config/AdminDataInitializer.java` | Initialer Admin-User (dev) |
|
||||||
|
| `eeg_frontend/openapi.yaml` | API-Vertrag für Frontend-Generierung |
|
||||||
|
|
||||||
|
## Regeln
|
||||||
|
|
||||||
|
- Fehlernachrichten sind auf **Deutsch**
|
||||||
|
- `@Profile("dev")` für simulierte EDA-Services
|
||||||
|
- EDA-Simulation: AT-Nummer endet mit "REJECT" → Ablehnung, "ERROR" → technischer Fehler
|
||||||
|
- Explizite `isEnabled()`-Überschreibung in `User.java` nötig (Lombok/Spring Security Konflikt)
|
||||||
@ -120,6 +120,7 @@
|
|||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.38</version>
|
||||||
</path>
|
</path>
|
||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
@ -150,10 +151,12 @@
|
|||||||
<path>
|
<path>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<version>4.0.6</version>
|
||||||
</path>
|
</path>
|
||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.38</version>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
@ -169,6 +172,7 @@
|
|||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.38</version>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
package at.mueller.eeg.backend.community.event.listener;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent;
|
||||||
|
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.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.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MakoConsensListener {
|
||||||
|
|
||||||
|
private final MeteringPointRepository meteringPointRepository;
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void handleMakoConsensApproved(MakoConsensApprovedEvent event) {
|
||||||
|
log.info("MaKo-Consent erteilt für AT-Nummer: {}", event.atNumber());
|
||||||
|
|
||||||
|
meteringPointRepository.findByAtNumber(event.atNumber()).ifPresentOrElse(point -> {
|
||||||
|
try {
|
||||||
|
point.fireTrigger(MakoTrigger.CONSENT_APPROVED);
|
||||||
|
meteringPointRepository.save(point);
|
||||||
|
|
||||||
|
log.info("Zählpunkt {} ist nun im Status: CONSENT_GRANTED. Starte Topologie-Check...",
|
||||||
|
event.atNumber());
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(new EdaTopologyCheckEvent(
|
||||||
|
point.getUserId(),
|
||||||
|
point.getId(),
|
||||||
|
point.getAtNumber()
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("Zustandsübergang fehlgeschlagen für AT-Nummer {}: {}", event.atNumber(), e.getMessage());
|
||||||
|
}
|
||||||
|
}, () -> log.error("Zählpunkt mit AT-Nummer {} nicht gefunden.", event.atNumber()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void handleMakoConsensRejected(MakoConsensRejectedEvent event) {
|
||||||
|
log.info("MaKo-Consent abgelehnt für AT-Nummer: {}. Grund: {}", event.atNumber(), event.reason());
|
||||||
|
|
||||||
|
meteringPointRepository.findByAtNumber(event.atNumber()).ifPresentOrElse(point -> {
|
||||||
|
try {
|
||||||
|
point.fireTrigger(MakoTrigger.CONSENT_DENIED);
|
||||||
|
meteringPointRepository.save(point);
|
||||||
|
|
||||||
|
log.info("Zählpunkt {} ist nun im Status: REJECTED", event.atNumber());
|
||||||
|
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("Zustandsübergang fehlgeschlagen für AT-Nummer {}: {}", event.atNumber(), e.getMessage());
|
||||||
|
}
|
||||||
|
}, () -> log.error("Zählpunkt mit AT-Nummer {} nicht gefunden.", event.atNumber()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void handleMakoConsensTechnicalFailed(MakoConsensTechnicalFailedEvent event) {
|
||||||
|
log.warn("MaKo-Consent technischer Fehler für AT-Nummer: {}. Grund: {}", event.atNumber(), event.reason());
|
||||||
|
|
||||||
|
meteringPointRepository.findByAtNumber(event.atNumber()).ifPresentOrElse(point -> {
|
||||||
|
try {
|
||||||
|
point.fireTrigger(MakoTrigger.TECHNICAL_FAILURE);
|
||||||
|
meteringPointRepository.save(point);
|
||||||
|
|
||||||
|
log.info("Zählpunkt {} ist nun im Status: ERROR", event.atNumber());
|
||||||
|
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("Zustandsübergang fehlgeschlagen für AT-Nummer {}: {}", event.atNumber(), e.getMessage());
|
||||||
|
}
|
||||||
|
}, () -> log.error("Zählpunkt mit AT-Nummer {} nicht gefunden.", event.atNumber()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ 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.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UUID> {
|
public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UUID> {
|
||||||
@ -12,5 +13,7 @@ public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UU
|
|||||||
|
|
||||||
boolean existsByAtNumber(String atNumber);
|
boolean existsByAtNumber(String atNumber);
|
||||||
|
|
||||||
|
Optional<MeteringPoint> findByAtNumber(String atNumber);
|
||||||
|
|
||||||
void deleteByUserId(UUID userId);
|
void deleteByUserId(UUID userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,115 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.config;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.domain.ParticipantType;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.User;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.UserRole;
|
||||||
|
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("dev")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AdminDataInitializer implements CommandLineRunner {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
private static final String ADMIN_EMAIL = "admin@eeg-portal.at";
|
||||||
|
private static final String ADMIN_FIRST_NAME = "Admin";
|
||||||
|
private static final String ADMIN_LAST_NAME = "Administrator";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) {
|
||||||
|
userRepository.findByEmail(ADMIN_EMAIL).ifPresentOrElse(
|
||||||
|
user -> log.info("Admin-User {} existiert bereits.", ADMIN_EMAIL),
|
||||||
|
() -> {
|
||||||
|
String generatedPassword = generateRandomPassword();
|
||||||
|
createAdminUser(generatedPassword);
|
||||||
|
createTestUser();
|
||||||
|
System.out.println("""
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
INITIALER ADMIN-BENUTZER WURDE ERSTELLT
|
||||||
|
========================================================================
|
||||||
|
E-Mail: admin@eeg-portal.at
|
||||||
|
Passwort: %s
|
||||||
|
|
||||||
|
TEST-USER:
|
||||||
|
E-Mail: test@example.com
|
||||||
|
Passwort: Test1234!
|
||||||
|
========================================================================
|
||||||
|
""".formatted(generatedPassword));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createAdminUser(String password) {
|
||||||
|
User admin = new User();
|
||||||
|
admin.setEmail(ADMIN_EMAIL);
|
||||||
|
admin.setPasswordHash(passwordEncoder.encode(password));
|
||||||
|
admin.setFirstName(ADMIN_FIRST_NAME);
|
||||||
|
admin.setLastName(ADMIN_LAST_NAME);
|
||||||
|
admin.setParticipantType(ParticipantType.PRIVATE);
|
||||||
|
admin.setRole(UserRole.ADMIN);
|
||||||
|
admin.setStatus(RegistrationStatus.APPROVED);
|
||||||
|
admin.setEnabled(true);
|
||||||
|
admin.setEmailVerified(true);
|
||||||
|
userRepository.save(admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTestUser() {
|
||||||
|
User testUser = new User();
|
||||||
|
testUser.setEmail("test@example.com");
|
||||||
|
testUser.setPasswordHash(passwordEncoder.encode("Test1234!"));
|
||||||
|
testUser.setFirstName("Test");
|
||||||
|
testUser.setLastName("Benutzer");
|
||||||
|
testUser.setParticipantType(ParticipantType.PRIVATE);
|
||||||
|
testUser.setRole(UserRole.MEMBER);
|
||||||
|
testUser.setStatus(RegistrationStatus.PENDING);
|
||||||
|
testUser.setEnabled(false);
|
||||||
|
testUser.setEmailVerified(true);
|
||||||
|
userRepository.save(testUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomPassword() {
|
||||||
|
String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
String lower = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
String digits = "0123456789";
|
||||||
|
String special = "!@#$%&*";
|
||||||
|
String all = upper + lower + digits + special;
|
||||||
|
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
StringBuilder password = new StringBuilder(16);
|
||||||
|
|
||||||
|
// Mindestens ein Zeichen aus jeder Klasse
|
||||||
|
password.append(upper.charAt(random.nextInt(upper.length())));
|
||||||
|
password.append(lower.charAt(random.nextInt(lower.length())));
|
||||||
|
password.append(digits.charAt(random.nextInt(digits.length())));
|
||||||
|
password.append(special.charAt(random.nextInt(special.length())));
|
||||||
|
|
||||||
|
// Rest zufällig füllen
|
||||||
|
for (int i = 4; i < 16; i++) {
|
||||||
|
password.append(all.charAt(random.nextInt(all.length())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mischen
|
||||||
|
char[] chars = password.toString().toCharArray();
|
||||||
|
for (int i = chars.length - 1; i > 0; i--) {
|
||||||
|
int j = random.nextInt(i + 1);
|
||||||
|
char tmp = chars[i];
|
||||||
|
chars[i] = chars[j];
|
||||||
|
chars[j] = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(chars);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,6 +61,11 @@ public class User implements UserDetails {
|
|||||||
return List.of(new SimpleGrantedAuthority(role.name()));
|
return List.of(new SimpleGrantedAuthority(role.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable String getPassword() {
|
public @Nullable String getPassword() {
|
||||||
return this.passwordHash;
|
return this.passwordHash;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user