diff --git a/.gitignore b/.gitignore
index 63fd2d6..7009b49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,8 +31,21 @@ build/
### VS Code ###
.vscode/
-/eegportal.lock.db
-/eeg_backend/eegportal.mv.db
-/eegportal.mv.db
-/eegportal.trace.db
+
+### Frontend ###
+eeg_frontend/node/
+node_modules/
+eeg_frontend/.angular/
+
+### H2 Datenbank ###
+*.db
+*.lock.db
+
+### Test-Dateien ###
+test_*.json
+
+### Projekt-spezifisch ###
/projekt_kontext.txt
+.mimocode/
+run_test.ps1
+server_output.log
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..52fca7f
--- /dev/null
+++ b/AGENTS.md
@@ -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)
diff --git a/eeg_backend/pom.xml b/eeg_backend/pom.xml
index fd1106a..1d84c8e 100644
--- a/eeg_backend/pom.xml
+++ b/eeg_backend/pom.xml
@@ -120,6 +120,7 @@
org.projectlombok
lombok
+ 1.18.38
org.projectlombok
@@ -150,10 +151,12 @@
org.springframework.boot
spring-boot-configuration-processor
+ 4.0.6
org.projectlombok
lombok
+ 1.18.38
@@ -169,6 +172,7 @@
org.projectlombok
lombok
+ 1.18.38
diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MakoConsensListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MakoConsensListener.java
new file mode 100644
index 0000000..9aa1981
--- /dev/null
+++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/event/listener/MakoConsensListener.java
@@ -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()));
+ }
+}
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 237d63f..496e754 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
@@ -4,6 +4,7 @@ import at.mueller.eeg.backend.community.domain.MeteringPoint;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
public interface MeteringPointRepository extends JpaRepository {
@@ -12,5 +13,7 @@ public interface MeteringPointRepository extends JpaRepository findByAtNumber(String atNumber);
+
void deleteByUserId(UUID userId);
}
diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/AdminDataInitializer.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/AdminDataInitializer.java
new file mode 100644
index 0000000..bd63b95
--- /dev/null
+++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/config/AdminDataInitializer.java
@@ -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);
+ }
+}
diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java
index 9a35db8..95570c4 100644
--- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java
+++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java
@@ -61,6 +61,11 @@ public class User implements UserDetails {
return List.of(new SimpleGrantedAuthority(role.name()));
}
+ @Override
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
@Override
public @Nullable String getPassword() {
return this.passwordHash;