diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java index b000225..533fa57 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java @@ -22,12 +22,9 @@ public class AuthController { @PostMapping("/register") @Operation(summary = "Registriert einen neuen User inkl. Zählpunkt", - description = "Setzt den User auf Status PENDING und triggert den asynchronen EDA-Check.") + description = "Setzt den User auf Status PENDING und versendet eine Verifizierungs-E-Mail.") public ResponseEntity register(@Valid @RequestBody RegistrationRequest request) { authService.register(request); - // Logik: MeteringPoint anlegen - // Event feuern: EdaTopologyCheckEvent - return ResponseEntity.status(HttpStatus.CREATED).build(); } 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 51f0386..c212ca1 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 @@ -1,9 +1,10 @@ package at.mueller.eeg.backend.iam.api.dto; +import at.mueller.eeg.backend.iam.domain.ParticipantType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.util.UUID; @@ -12,15 +13,19 @@ public interface IamDtos { @Schema(description = "Payload für die Registrierung eines neuen Users") record RegistrationRequest( - @NotBlank @Schema(example = "Max") String firstName, - @NotBlank @Schema(example = "Mustermann") String lastName, - @Email @NotBlank @Schema(example = "max@example.com") String email, - @NotBlank @Size(min = 8) @Schema(example = "SecurePass123!") String password, - + @NotNull + @Schema(description = "Privatperson oder juristische Person (Firma/Verein/Gemeinde)", example = "PRIVATE") + ParticipantType participantType, @NotBlank - @Pattern(regexp = "^AT[0-9]{31}$", message = "Muss eine gültige österreichische 33-stellige Zählpunktnummer sein") - @Schema(example = "AT0010000000000000000000001234567") - String atNumber + @Schema(description = "Vorname (bei juristischer Person: Vorname der Ansprechperson)", example = "Max") + String firstName, + @NotBlank + @Schema(description = "Nachname (bei juristischer Person: Nachname der Ansprechperson)", example = "Mustermann") + String lastName, + @Schema(description = "Nur bei juristischer Person: Firmen-, Vereins- oder Gemeindename", example = "Musterverein") + String organizationName, + @Email @NotBlank @Schema(example = "max@example.com") String email, + @NotBlank @Size(min = 8) @Schema(example = "SecurePass123!") String password ) {} @Schema(description = "Payload für den Login") @@ -39,13 +44,13 @@ public interface IamDtos { @Schema(description = "Repräsentation eines Users für das Admin-Dashboard") record UserProfileResponse( UUID id, + ParticipantType participantType, String firstName, String lastName, + String organizationName, String email, - String status, - String primaryAtNumber + String status ) {} record ErrorResponse(String message, int status) {} - -} +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/ParticipantType.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/ParticipantType.java new file mode 100644 index 0000000..ad49a1a --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/ParticipantType.java @@ -0,0 +1,6 @@ +package at.mueller.eeg.backend.iam.domain; + +public enum ParticipantType { + PRIVATE, + COMPANY +} \ No newline at end of file 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 5a47f99..9a35db8 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 @@ -29,12 +29,16 @@ public class User implements UserDetails { @Column(nullable = false) private String passwordHash; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ParticipantType participantType = ParticipantType.PRIVATE; + + private String organizationName; + private String firstName; private String lastName; - private String iban; - @Column(nullable = false) private boolean emailVerified = false; diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java index c5011bb..16e7834 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java @@ -15,14 +15,12 @@ public interface UserMapper { List mapToDto(List users); - @Mapping(target = "primaryAtNumber", ignore = true) IamDtos.UserProfileResponse mapToDto(User user); @Mapping(target = "status", constant = "PENDING") @Mapping(target = "role", constant = "MEMBER") @Mapping(target = "passwordHash", source = "password", qualifiedBy = UserMapperAnnotations.PasswordHash.class) @Mapping(target = "id", ignore = true) - @Mapping(target = "iban", ignore = true) @Mapping(target = "enabled", constant = "false") @Mapping(target = "authorities", ignore = true) @Mapping(target = "emailVerified", ignore = true) 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 f0be05a..da556f2 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,5 +1,6 @@ package at.mueller.eeg.backend.iam.service; +import at.mueller.eeg.backend.iam.domain.ParticipantType; import at.mueller.eeg.backend.iam.domain.User; import at.mueller.eeg.backend.iam.mapper.UserMapper; import at.mueller.eeg.backend.iam.repository.UserRepository; @@ -62,6 +63,12 @@ public class AuthService { throw new RuntimeException("E-Mail already exists!"); }); + if (request.participantType() == ParticipantType.COMPANY + && (request.organizationName() == null || request.organizationName().isBlank())) { + throw new IllegalArgumentException( + "Für juristische Personen ist ein Firmen-/Vereins-/Gemeindename erforderlich."); + } + User newUser = userMapper.mapToEntity(request, new User()); String token = UUID.randomUUID().toString(); newUser.setVerificationToken(token); diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/backend/OpenApiGeneratorTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/OpenApiGeneratorTest.java similarity index 96% rename from eeg_backend/src/test/java/at/mueller/eeg/backend/backend/OpenApiGeneratorTest.java rename to eeg_backend/src/test/java/at/mueller/eeg/backend/OpenApiGeneratorTest.java index 13d2311..ef7d9db 100644 --- a/eeg_backend/src/test/java/at/mueller/eeg/backend/backend/OpenApiGeneratorTest.java +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/OpenApiGeneratorTest.java @@ -1,4 +1,4 @@ -package at.mueller.eeg.backend.backend; +package at.mueller.eeg.backend; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/iam/service/FakeMailService.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/iam/service/FakeMailService.java new file mode 100644 index 0000000..d70ce6a --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/iam/service/FakeMailService.java @@ -0,0 +1,11 @@ +package at.mueller.eeg.backend.iam.service; + +import org.springframework.stereotype.Service; + +@Service +public class FakeMailService implements MailService{ + @Override + public void sendVerificationEmail(String toEmail, String token) { + + } +} diff --git a/eeg_frontend/openapi.yaml b/eeg_frontend/openapi.yaml index f36633b..04c4fd4 100644 --- a/eeg_frontend/openapi.yaml +++ b/eeg_frontend/openapi.yaml @@ -122,8 +122,7 @@ paths: tags: - Authentication summary: Registriert einen neuen User inkl. Zählpunkt - description: Setzt den User auf Status PENDING und triggert den asynchronen - EDA-Check. + description: Setzt den User auf Status PENDING und versendet eine Verifizierungs-E-Mail. operationId: register requestBody: content: @@ -220,6 +219,30 @@ paths: application/json: schema: $ref: "#/components/schemas/EnergyCommunityDto" + /api/dashboard/user: + get: + tags: + - dashboard-controller + operationId: getUserDashboard + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/UserStats" + /api/dashboard/admin: + get: + tags: + - dashboard-controller + operationId: getAdminDashboard + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/AdminStats" /api/community/metering-points/{id}/eligible-communities: get: tags: @@ -241,6 +264,20 @@ paths: type: array items: $ref: "#/components/schemas/EligibleCommunityResponse" + /api/auth/verify-email: + get: + tags: + - Authentication + operationId: verifyEmail + parameters: + - name: token + in: query + required: true + schema: + type: string + responses: + "200": + description: OK /api/admin/users/pending: get: tags: @@ -306,14 +343,27 @@ components: type: object description: Payload für die Registrierung eines neuen Users properties: + participantType: + type: string + description: Privatperson oder juristische Person (Firma/Verein/Gemeinde) + enum: + - PRIVATE + - COMPANY + example: PRIVATE firstName: type: string + description: "Vorname (bei juristischer Person: Vorname der Ansprechperson)" example: Max minLength: 1 lastName: type: string + description: "Nachname (bei juristischer Person: Nachname der Ansprechperson)" example: Mustermann minLength: 1 + organizationName: + type: string + description: "Nur bei juristischer Person: Firmen-, Vereins- oder Gemeindename" + example: Musterverein email: type: string format: email @@ -324,16 +374,11 @@ components: example: SecurePass123! maxLength: 2147483647 minLength: 8 - atNumber: - type: string - example: AT0010000000000000000000001234567 - minLength: 1 - pattern: "^AT[0-9]{31}$" required: - - atNumber - email - firstName - lastName + - participantType - password LoginRequest: type: object @@ -366,6 +411,30 @@ components: type: string description: Rolle des Users example: USER + UserStats: + type: object + properties: + totalMeteringPoints: + type: integer + format: int64 + activeMemberships: + type: integer + format: int64 + pendingMemberships: + type: integer + format: int64 + AdminStats: + type: object + properties: + totalCommunities: + type: integer + format: int64 + pendingUsers: + type: integer + format: int64 + totalActiveMemberships: + type: integer + format: int64 EligibleCommunityResponse: type: object properties: @@ -389,13 +458,18 @@ components: id: type: string format: uuid + participantType: + type: string + enum: + - PRIVATE + - COMPANY firstName: type: string lastName: type: string + organizationName: + type: string email: type: string status: type: string - primaryAtNumber: - type: string diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html index 01c8954..2993266 100644 --- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html @@ -31,12 +31,14 @@ {{ authService.currentUserRole() === 'ADMIN' ? 'Verwaltung' : 'Mein Bereich' }} -
- - - - - {{ authService.currentUserRole() }} +
+ + Rolle: {{ authService.currentUserRole() }} + +
diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts index fde8d4a..4ef2ade 100644 --- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts @@ -1,5 +1,5 @@ import {Component, computed, inject} from '@angular/core'; -import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router'; +import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router'; import {AuthService} from '../../services/auth'; import {NAV_ITEMS} from './dashboard-nav-items'; @@ -12,9 +12,15 @@ import {NAV_ITEMS} from './dashboard-nav-items'; }) export class DashboardLayout { authService = inject(AuthService); + private router = inject(Router); visibleNavItems = computed(() => { const role = this.authService.currentUserRole(); return NAV_ITEMS.filter(item => role !== null && item.roles.includes(role)); }); + + onLogout() { + this.authService.logout(); + this.router.navigate(['/']); + } } diff --git a/eeg_frontend/src/app/layout/header/header.ts b/eeg_frontend/src/app/layout/header/header.ts index f309b98..c4aae1d 100644 --- a/eeg_frontend/src/app/layout/header/header.ts +++ b/eeg_frontend/src/app/layout/header/header.ts @@ -22,6 +22,6 @@ export class HeaderComponent { onLogout() { this.authService.logout(); - this.router.navigate(['/login']); + this.router.navigate(['/']); } } diff --git a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html index 9c47788..ca7ac74 100644 --- a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html +++ b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html @@ -43,12 +43,6 @@
{{ user.email }}
- -
- {{ user.primaryAtNumber || 'Kein Zählpunkt hinterlegt' }} -
- - {{ user.status }} diff --git a/eeg_frontend/src/app/pages/register/register.html b/eeg_frontend/src/app/pages/register/register.html index 72e5954..ba2dbc2 100644 --- a/eeg_frontend/src/app/pages/register/register.html +++ b/eeg_frontend/src/app/pages/register/register.html @@ -7,104 +7,65 @@

Erstelle dein Mitgliedskonto

-
+
- + @if (!registeredSuccessfully) { +
+

+ Für die Registrierung reichen deine Kontaktdaten. Zählpunktnummer und IBAN kannst du nach der + Freischaltung deines Kontos in deinem Mitgliederbereich hinterlegen. +

- - Konto - -

Trage deine persönlichen Daten für den Portalzugang ein.

+ + Privatperson + Firma / Verein / Gemeinde + + @if (isCompany) { - Vollständiger Name / Vereinsname - + Firmen-, Vereins- oder Gemeindename + + + } + +
+ + {{ isCompany ? 'Vorname (Ansprechperson)' : 'Vorname' }} + - E-Mail-Adresse - + {{ isCompany ? 'Nachname (Ansprechperson)' : 'Nachname' }} + +
- - Passwort - - + + E-Mail-Adresse + + -
- -
- -
+ + Passwort + + - - Anlagendaten -
-

Diese Daten benötigen wir für die Zuordnung beim Netzbetreiber.

+
+ +
+
+ } - - Zählpunktnummer (33-stellig) - - Beginnt mit AT und findest du auf deiner Stromrechnung. - - -
- - Straße & Hausnummer - - - - PLZ - - -
- - - Ort - - - -
- - -
- -
- - - Abrechnung -
-

Für den Einzug der Netzkosten-Ersparnisse/Beiträge sowie Gutschriften für Einspeisung.

- - - IBAN - - - - - BIC (Optional) - - - -
-

SEPA-Lastschriftmandat (Creditor ID: ATXXXXXXXXXXXXXXXX)

-

Ich ermächtige die Energiegemeinschaft, Zahlungen von meinem Konto mittels Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die eingelösten Lastschriften zu bezahlen.

-

Hinweis: Ich kann innerhalb von 8 Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen.

-
- - - Ich erteile das SEPA-Lastschriftmandat elektronisch. - - -
- - -
-
-
- -
+ @if (registeredSuccessfully) { +
+

Fast geschafft!

+

+ Wir haben dir eine Bestätigungs-E-Mail geschickt. Bitte verifiziere deine E-Mail-Adresse. + Anschließend prüft ein Administrator dein Konto und schaltet es frei. +

+
+ }
diff --git a/eeg_frontend/src/app/pages/register/register.ts b/eeg_frontend/src/app/pages/register/register.ts index 28cf654..2a86918 100644 --- a/eeg_frontend/src/app/pages/register/register.ts +++ b/eeg_frontend/src/app/pages/register/register.ts @@ -1,25 +1,25 @@ import {Component, inject} from '@angular/core'; import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {MatStepperModule} from '@angular/material/stepper'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; -import {MatCheckboxModule} from '@angular/material/checkbox'; -import {Router} from '@angular/router'; -import {AuthenticationService, RegistrationRequest} from '../../api'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {AuthenticationService, RegistrationRequest, UserProfileResponse} from '../../api'; +import {RouterLink} from '@angular/router'; +import ParticipantTypeEnum = UserProfileResponse.ParticipantTypeEnum; @Component({ selector: 'app-register', standalone: true, imports: [ ReactiveFormsModule, - MatStepperModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule, - MatCheckboxModule + MatButtonToggleModule, + RouterLink ], templateUrl: './register.html', styleUrls: ['./register.scss'] @@ -27,52 +27,62 @@ import {AuthenticationService, RegistrationRequest} from '../../api'; export class Register { private fb = inject(FormBuilder); private authService = inject(AuthenticationService); - private router = inject(Router); - // Schritt 1: Login & Stammdaten + // Für die Toggle-Buttons im Template + readonly ParticipantType = RegistrationRequest.ParticipantTypeEnum; + + registeredSuccessfully = false; + accountForm = this.fb.group({ - name: ['', Validators.required], + participantType: this.fb.control( + RegistrationRequest.ParticipantTypeEnum.Private, + {nonNullable: true, validators: Validators.required} + ), + organizationName: [''], + firstName: ['', Validators.required], + lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]] }); - // Schritt 2: Energiegemeinschaft-Spezifische Daten - energyForm = this.fb.group({ - accountingPointId: ['', [Validators.required, Validators.pattern(/^AT\d{31}$/)]], // AT + 33 Ziffern für Zählpunkt - street: ['', Validators.required], - zip: ['', Validators.required], - city: ['', Validators.required] - }); + constructor() { + this.accountForm.get('participantType')!.valueChanges.subscribe(type => { + const orgControl = this.accountForm.get('organizationName')!; + if (type === RegistrationRequest.ParticipantTypeEnum.Company) { + orgControl.setValidators([Validators.required]); + } else { + orgControl.clearValidators(); + } + orgControl.updateValueAndValidity(); + }); + } - // Schritt 3: SEPA Lastschrift / Mandat - paymentForm = this.fb.group({ - iban: ['', [Validators.required, Validators.pattern(/^[A-Z]{2}[0-9]{2}[A-Z0-9]{12,30}$/)]], - bic: [''], - acceptMandate: [false, Validators.requiredTrue] - }); + get isCompany(): boolean { + return this.accountForm.value.participantType === RegistrationRequest.ParticipantTypeEnum.Company; + } onSubmit() { - if (this.accountForm.valid && this.energyForm.valid && this.paymentForm.valid) { - const registrationData: RegistrationRequest = { - firstName: this.accountForm.value.name!, - lastName: this.accountForm.value.name!, - email: this.accountForm.value.email!, - password: this.accountForm.value.password!, - atNumber: this.energyForm.value.accountingPointId! - }; - console.log('Registrierungsdaten für Backend:', registrationData); - - this.authService.register(registrationData).subscribe({ - next: (response) => { - console.log('Erfolgreich registriert!', response); - // Bei Erfolg z.B. auf eine Bestätigungsseite oder zum Login leiten - // this.router.navigate(['/login']); - }, - error: (err) => { - console.error('Fehler bei der Registrierung', err); - // Hier könntest du später einen Angular Material SnackBar einblenden - } - }); + if (this.accountForm.invalid) { + this.accountForm.markAllAsTouched(); + return; } + + const registrationData: RegistrationRequest = { + participantType: this.accountForm.value.participantType!, + firstName: this.accountForm.value.firstName!, + lastName: this.accountForm.value.lastName!, + organizationName: this.accountForm.value.organizationName || undefined, + email: this.accountForm.value.email!, + password: this.accountForm.value.password! + }; + + this.authService.register(registrationData).subscribe({ + next: () => { + this.registeredSuccessfully = true; + }, + error: (err) => { + console.error('Fehler bei der Registrierung', err); + } + }); } }