first working frontend/backend
This commit is contained in:
parent
70ed6efdc4
commit
4ef6d32093
4
.gitignore
vendored
4
.gitignore
vendored
@ -31,3 +31,7 @@ build/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
/eegportal.lock.db
|
||||||
|
/eeg_backend/eegportal.mv.db
|
||||||
|
/eegportal.mv.db
|
||||||
|
/eegportal.trace.db
|
||||||
|
|||||||
5
eeg_backend/http-client.private.env.json
Normal file
5
eeg_backend/http-client.private.env.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dev": {
|
||||||
|
"host": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
eeg_backend/iam.http
Normal file
1
eeg_backend/iam.http
Normal file
@ -0,0 +1 @@
|
|||||||
|
GET {{host}}
|
||||||
@ -29,6 +29,19 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
||||||
|
<version>3.0.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
@ -62,6 +75,11 @@
|
|||||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
package at.mueller.eeg;
|
package at.mueller.eeg.backend;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class EegPortalApplication {
|
public class EegPortalApplication {
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package at.mueller.eeg.community.domain;
|
package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
public enum CommunityType {
|
public enum CommunityType {
|
||||||
|
|
||||||
EEG_LOKAL,
|
EEG_LOKAL,
|
||||||
EEG_REGIONAL,
|
EEG_REGIONAL,
|
||||||
BEG;
|
BEG
|
||||||
}
|
}
|
||||||
@ -1,12 +1,10 @@
|
|||||||
package at.mueller.eeg.community.domain;
|
package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "energy_communities")
|
@Table(name = "energy_communities")
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package at.mueller.eeg.community.domain;
|
package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
|
public enum MembershipStatus {
|
||||||
|
PENDING,
|
||||||
|
ACTIVE,
|
||||||
|
INACTIVE
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package at.mueller.eeg.community.domain;
|
package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package at.mueller.eeg.community.domain;
|
package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
public enum PointType {
|
public enum PointType {
|
||||||
CONSUMER,
|
CONSUMER,
|
||||||
PRODUCER,
|
PRODUCER,
|
||||||
//TODO: Prüfen ob PROSUMER wirklich ein eigener Typ ist und sich nicht aus Consumer/Producer ergibt
|
//TODO: Prüfen ob PROSUMER wirklich ein eigener Typ ist und sich nicht aus Consumer/Producer ergibt
|
||||||
PROSUMER;
|
PROSUME;
|
||||||
}
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.api;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/users")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@Tag(name = "Admin IAM", description = "User-Verwaltung und Freigabe-Workflow")
|
||||||
|
public class AdminIamController {
|
||||||
|
|
||||||
|
// private final UserManagementService userManagementService;
|
||||||
|
|
||||||
|
@GetMapping("/pending")
|
||||||
|
@Operation(summary = "Lädt alle wartenden User",
|
||||||
|
description = "Zeigt User im Status PENDING inkl. der Ergebnisse des EDA-Netz-Checks.")
|
||||||
|
public ResponseEntity<List<UserProfileResponse>> getPendingUsers() {
|
||||||
|
// Logik: Finde alle User mit status = PENDING
|
||||||
|
return ResponseEntity.ok(List.of(/* ... */));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{userId}/approve")
|
||||||
|
@Operation(summary = "User freigeben",
|
||||||
|
description = "Setzt enabled=true, status=APPROVED und triggert die finale EDA-Anmeldung.")
|
||||||
|
public ResponseEntity<Void> approveUser(@PathVariable UUID userId) {
|
||||||
|
// Logik: status auf APPROVED, enabled auf true
|
||||||
|
// Event feuern: UserApprovedEvent (Schickt E-Mail an User)
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{userId}/reject")
|
||||||
|
@Operation(summary = "User ablehnen",
|
||||||
|
description = "Lehnt den Antrag ab (z.B. weil Zählpunkt nicht ins Netz passt).")
|
||||||
|
public ResponseEntity<Void> rejectUser(@PathVariable UUID userId) {
|
||||||
|
// Logik: status auf REJECTED setzen oder User hard-löschen, je nach Datenschutz-Konzept
|
||||||
|
// Event feuern: UserRejectedEvent
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.api;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.AuthResponse;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.LoginRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.RegistrationRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.service.AuthService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@Tag(name = "Authentication", description = "Public Endpoints für Login und Registrierung")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "Registriert einen neuen User inkl. Zählpunkt",
|
||||||
|
description = "Setzt den User auf Status PENDING und triggert den asynchronen EDA-Check.")
|
||||||
|
public ResponseEntity<Void> register(@Valid @RequestBody RegistrationRequest request) {
|
||||||
|
authService.register(request);
|
||||||
|
// Logik: MeteringPoint anlegen
|
||||||
|
// Event feuern: EdaTopologyCheckEvent
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
@Operation(summary = "User Login",
|
||||||
|
description = "Gibt einen JWT zurück. Schlägt fehl, wenn Status noch PENDING ist.")
|
||||||
|
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||||
|
// Logik: Spring Security Authentication Manager aufrufen
|
||||||
|
// Wenn erfolgreich: JWT generieren
|
||||||
|
// Wenn PENDING: Spezifische Exception werfen, die als 403 Forbidden gemappt wird
|
||||||
|
|
||||||
|
AuthResponse response = new AuthResponse("ey...", "APPROVED", "USER");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.api.dto;
|
||||||
|
|
||||||
|
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.Size;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
@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 = "Payload für den Login")
|
||||||
|
record LoginRequest(
|
||||||
|
@Email @NotBlank @Schema(example = "max@example.com") String email,
|
||||||
|
@NotBlank @Schema(example = "SecurePass123!") String password
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Schema(description = "Antwort nach erfolgreichem Login")
|
||||||
|
record AuthResponse(
|
||||||
|
@Schema(description = "JWT Bearer Token") String token,
|
||||||
|
@Schema(description = "Status des Users", example = "PENDING") String status,
|
||||||
|
@Schema(description = "Rolle des Users", example = "USER") String role
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Schema(description = "Repräsentation eines Users für das Admin-Dashboard")
|
||||||
|
record UserProfileResponse(
|
||||||
|
UUID id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
String email,
|
||||||
|
String status, // PENDING, APPROVED, REJECTED
|
||||||
|
String primaryAtNumber // Zur schnellen Anzeige in der Liste
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,146 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.config;
|
||||||
|
|
||||||
|
import com.nimbusds.jose.jwk.source.ImmutableSecret;
|
||||||
|
import jakarta.servlet.DispatcherType;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String jwtSecret;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtDecoder jwtDecoder() {
|
||||||
|
SecretKeySpec secretKey = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256");
|
||||||
|
return NimbusJwtDecoder.withSecretKey(secretKey).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
// 1. CSRF deaktivieren: Da wir stateless mit JWTs arbeiten und keine
|
||||||
|
// klassischen Session-Cookies verwenden, ist CSRF-Schutz hier nicht nötig.
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// 2. Stateless Session Management: Spring Security darf keine HTTP-Session anlegen
|
||||||
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
|
||||||
|
// 3. Routing & Autorisierung
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
// Erlaubt interne Forwards (wichtig für deinen SpaRoutingController -> /index.html)
|
||||||
|
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
|
||||||
|
|
||||||
|
// Erlaubt den Zugriff auf statische Frontend-Dateien im Root
|
||||||
|
.requestMatchers("/", "/index.html", "/assets/**", "/static/**", "/*.js", "/*.css", "/*.ico").permitAll()
|
||||||
|
|
||||||
|
// OpenAPI / Swagger Docs für das Frontend-Tooling freigeben
|
||||||
|
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
|
||||||
|
|
||||||
|
// Deine öffentlichen API-Endpunkte
|
||||||
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
|
||||||
|
// Admin-Routen auf URL-Ebene absichern (zusätzlich zur @PreAuthorize Annotation)
|
||||||
|
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
|
|
||||||
|
// Alle anderen /api/ Anfragen erfordern zumindest einen gültigen Login
|
||||||
|
.requestMatchers("/api/**").authenticated()
|
||||||
|
|
||||||
|
// Alles andere (SPA-Routen, die nicht von obigen Matches gefangen wurden) freigeben
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. JWT als Authentifizierungsmethode aktivieren
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2
|
||||||
|
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert die "Rollen" aus dem JWT und mappt sie in Spring Security Authorities.
|
||||||
|
* Wenn dein JWT z.B. einen Claim "role": "ADMIN" hat, macht Spring daraus "ROLE_ADMIN",
|
||||||
|
* womit hasRole('ADMIN') reibungslos funktioniert.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationConverter jwtAuthenticationConverter() {
|
||||||
|
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||||
|
// Hier sagst du Spring, wie das Feld in deinem JWT-Payload heißt (z.B. "role" oder "roles")
|
||||||
|
grantedAuthoritiesConverter.setAuthoritiesClaimName("role");
|
||||||
|
// Spring Security erwartet bei Rollen immer das Prefix "ROLE_"
|
||||||
|
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
|
||||||
|
|
||||||
|
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
|
||||||
|
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
|
||||||
|
return jwtAuthenticationConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
|
||||||
|
// Erlaube exakt dein Angular-Frontend
|
||||||
|
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
|
||||||
|
|
||||||
|
// Erlaube die gängigen HTTP-Methoden
|
||||||
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
|
||||||
|
// Erlaube Header, die Angular mitsendet (besonders wichtig für Content-Type und später Authorization/Token)
|
||||||
|
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
|
||||||
|
|
||||||
|
// Wichtig, wenn Anmeldedaten (Credentials) oder Cookies im Spiel sind
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
|
||||||
|
// Wende diese Regeln auf alle Endpunkte (/**) an
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||||
|
return config.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das Gegenstück zum Decoder: Hiermit erstellen wir die Token!
|
||||||
|
@Bean
|
||||||
|
public JwtEncoder jwtEncoder() {
|
||||||
|
SecretKeySpec secretKey = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256");
|
||||||
|
return new NimbusJwtEncoder(new ImmutableSecret<>(secretKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package at.mueller.eeg.iam.domain;
|
package at.mueller.eeg.backend.iam.domain;
|
||||||
|
|
||||||
public enum RegistrationStatus {
|
public enum RegistrationStatus {
|
||||||
PENDING,
|
PENDING,
|
||||||
APPROVED,
|
APPROVED,
|
||||||
REJECTED;
|
REJECTED
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package at.mueller.eeg.iam.domain;
|
package at.mueller.eeg.backend.iam.domain;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.domain;
|
||||||
|
|
||||||
|
public enum UserRole {
|
||||||
|
ADMIN,
|
||||||
|
MEMBER
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.repository;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.domain.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, String> {
|
||||||
|
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.service;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.User;
|
||||||
|
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
import static at.mueller.eeg.backend.iam.api.dto.IamDtos.LoginRequest;
|
||||||
|
import static at.mueller.eeg.backend.iam.api.dto.IamDtos.RegistrationRequest;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final AuthenticationManager authenticationManager;
|
||||||
|
private final JwtEncoder jwtEncoder;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet den Login und gibt den JWT-Token als String zurück.
|
||||||
|
*/
|
||||||
|
public String login(LoginRequest request) {
|
||||||
|
// 1. Prüft, ob E-Mail und Passwort in der Datenbank übereinstimmen
|
||||||
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.email(), request.password())
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Wenn erfolgreich, Token generieren
|
||||||
|
return generateToken(authentication.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet die Registrierung des neuen EEG-Mitglieds.
|
||||||
|
*/
|
||||||
|
public void register(RegistrationRequest request) {
|
||||||
|
|
||||||
|
userRepository.findByEmail(request.email()).ifPresent(user -> {
|
||||||
|
throw new RuntimeException("E-Mail already exists!");
|
||||||
|
});
|
||||||
|
|
||||||
|
String hashedPassword = passwordEncoder.encode(request.password());
|
||||||
|
|
||||||
|
User newUser = new User();
|
||||||
|
newUser.setEmail(request.email());
|
||||||
|
newUser.setPasswordHash(hashedPassword);
|
||||||
|
newUser.setFirstName(request.firstName());
|
||||||
|
newUser.setLastName(request.lastName());
|
||||||
|
newUser.setStatus(RegistrationStatus.PENDING);
|
||||||
|
newUser.setEnabled(false);
|
||||||
|
|
||||||
|
userRepository.save(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsmethode zum Bauen des JWT.
|
||||||
|
*/
|
||||||
|
private String generateToken(String email) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
|
.issuer("eeg-portal")
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiresAt(now.plus(2, ChronoUnit.HOURS)) // Token ist 2 Stunden gültig
|
||||||
|
.subject(email) // Der "Besitzer" des Tokens
|
||||||
|
.claim("role", "MEMBER") // Hier könntest du später Admin/Mitglied Rollen einbauen
|
||||||
|
.build();
|
||||||
|
|
||||||
|
JwsHeader jwsHeader = JwsHeader.with(MacAlgorithm.HS256).build();
|
||||||
|
return jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package at.mueller.eeg.tariff.domain;
|
package at.mueller.eeg.backend.tariff.domain;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package at.mueller.eeg.community.domain;
|
|
||||||
|
|
||||||
public enum MembershipStatus {
|
|
||||||
PENDING,
|
|
||||||
ACTIVE,
|
|
||||||
INACTIVE;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package at.mueller.eeg.iam.domain;
|
|
||||||
|
|
||||||
public enum UserRole {
|
|
||||||
ADMIN,
|
|
||||||
MEMBER;
|
|
||||||
}
|
|
||||||
@ -9,4 +9,6 @@ spring:
|
|||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
show-sql: true
|
show-sql: true
|
||||||
|
jwt:
|
||||||
|
secret: MeinSuperGeheimesUndSehrLangesSecretFuerDasEnergiePortal2025!
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package at.mueller.eeg;
|
package at.mueller.eeg.backend;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package at.mueller.eeg.backend.backend;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
class OpenApiGeneratorTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generateOpenApiSpecification() throws Exception {
|
||||||
|
String openApiYaml = mockMvc.perform(get("/v3/api-docs.yaml"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn()
|
||||||
|
.getResponse()
|
||||||
|
.getContentAsString();
|
||||||
|
|
||||||
|
Path outputPath = Paths.get("../eeg_frontend/openapi.yaml");
|
||||||
|
|
||||||
|
Files.writeString(outputPath, openApiYaml);
|
||||||
|
|
||||||
|
System.out.println("OpenAPI YAML erfolgreich nach " + outputPath.toAbsolutePath() + " exportiert.");
|
||||||
|
}
|
||||||
|
}
|
||||||
17
eeg_frontend/.editorconfig
Normal file
17
eeg_frontend/.editorconfig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
45
eeg_frontend/.gitignore
vendored
Normal file
45
eeg_frontend/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/mcp.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
/src/app/api/
|
||||||
5
eeg_frontend/.postcssrc.json
Normal file
5
eeg_frontend/.postcssrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"@tailwindcss/postcss": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
eeg_frontend/.prettierrc
Normal file
12
eeg_frontend/.prettierrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
eeg_frontend/README.md
Normal file
59
eeg_frontend/README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# EegFrontend
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.12.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
80
eeg_frontend/angular.json
Normal file
80
eeg_frontend/angular.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"cli": {
|
||||||
|
"packageManager": "npm",
|
||||||
|
"analytics": false
|
||||||
|
},
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"eeg_frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": ["src/styles.scss"]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "eeg_frontend:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "eeg_frontend:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development",
|
||||||
|
"options": {
|
||||||
|
"proxyConfig": "proxy.conf.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:unit-test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
eeg_frontend/openapi.yaml
Normal file
184
eeg_frontend/openapi.yaml
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: OpenAPI definition
|
||||||
|
version: v0
|
||||||
|
servers:
|
||||||
|
- url: http://localhost
|
||||||
|
description: Generated server url
|
||||||
|
tags:
|
||||||
|
- name: Authentication
|
||||||
|
description: Public Endpoints für Login und Registrierung
|
||||||
|
- name: Admin IAM
|
||||||
|
description: User-Verwaltung und Freigabe-Workflow
|
||||||
|
paths:
|
||||||
|
/api/auth/register:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
summary: Registriert einen neuen User inkl. Zählpunkt
|
||||||
|
description: Setzt den User auf Status PENDING und triggert den asynchronen
|
||||||
|
EDA-Check.
|
||||||
|
operationId: register
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RegistrationRequest"
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
/api/auth/login:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
summary: User Login
|
||||||
|
description: "Gibt einen JWT zurück. Schlägt fehl, wenn Status noch PENDING\
|
||||||
|
\ ist."
|
||||||
|
operationId: login
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/LoginRequest"
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'*/*':
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/AuthResponse"
|
||||||
|
/api/admin/users/{userId}/reject:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Admin IAM
|
||||||
|
summary: User ablehnen
|
||||||
|
description: Lehnt den Antrag ab (z.B. weil Zählpunkt nicht ins Netz passt).
|
||||||
|
operationId: rejectUser
|
||||||
|
parameters:
|
||||||
|
- name: userId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
/api/admin/users/{userId}/approve:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Admin IAM
|
||||||
|
summary: User freigeben
|
||||||
|
description: "Setzt enabled=true, status=APPROVED und triggert die finale EDA-Anmeldung."
|
||||||
|
operationId: approveUser
|
||||||
|
parameters:
|
||||||
|
- name: userId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
/api/admin/users/pending:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Admin IAM
|
||||||
|
summary: Lädt alle wartenden User
|
||||||
|
description: Zeigt User im Status PENDING inkl. der Ergebnisse des EDA-Netz-Checks.
|
||||||
|
operationId: getPendingUsers
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'*/*':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/UserProfileResponse"
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
RegistrationRequest:
|
||||||
|
type: object
|
||||||
|
description: Payload für die Registrierung eines neuen Users
|
||||||
|
properties:
|
||||||
|
firstName:
|
||||||
|
type: string
|
||||||
|
example: Max
|
||||||
|
minLength: 1
|
||||||
|
lastName:
|
||||||
|
type: string
|
||||||
|
example: Mustermann
|
||||||
|
minLength: 1
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
example: max@example.com
|
||||||
|
minLength: 1
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: SecurePass123!
|
||||||
|
maxLength: 2147483647
|
||||||
|
minLength: 8
|
||||||
|
atNumber:
|
||||||
|
type: string
|
||||||
|
example: AT0010000000000000000000001234567
|
||||||
|
minLength: 1
|
||||||
|
pattern: "^AT[0-9]{31}$"
|
||||||
|
required:
|
||||||
|
- atNumber
|
||||||
|
- email
|
||||||
|
- firstName
|
||||||
|
- lastName
|
||||||
|
- password
|
||||||
|
LoginRequest:
|
||||||
|
type: object
|
||||||
|
description: Payload für den Login
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
example: max@example.com
|
||||||
|
minLength: 1
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: SecurePass123!
|
||||||
|
minLength: 1
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
AuthResponse:
|
||||||
|
type: object
|
||||||
|
description: Antwort nach erfolgreichem Login
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: JWT Bearer Token
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: Status des Users
|
||||||
|
example: PENDING
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
description: Rolle des Users
|
||||||
|
example: USER
|
||||||
|
UserProfileResponse:
|
||||||
|
type: object
|
||||||
|
description: Repräsentation eines Users für das Admin-Dashboard
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
firstName:
|
||||||
|
type: string
|
||||||
|
lastName:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
primaryAtNumber:
|
||||||
|
type: string
|
||||||
7
eeg_frontend/openapitools.json
Normal file
7
eeg_frontend/openapitools.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||||
|
"spaces": 2,
|
||||||
|
"generator-cli": {
|
||||||
|
"version": "7.22.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
11567
eeg_frontend/package-lock.json
generated
Normal file
11567
eeg_frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
eeg_frontend/package.json
Normal file
40
eeg_frontend/package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "eeg-frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"generate-api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o ./src/app/api"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "npm@11.15.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^21.2.12",
|
||||||
|
"@angular/common": "^21.2.0",
|
||||||
|
"@angular/compiler": "^21.2.0",
|
||||||
|
"@angular/core": "^21.2.0",
|
||||||
|
"@angular/forms": "^21.2.0",
|
||||||
|
"@angular/material": "^21.2.12",
|
||||||
|
"@angular/platform-browser": "^21.2.0",
|
||||||
|
"@angular/router": "^21.2.0",
|
||||||
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^21.2.12",
|
||||||
|
"@angular/cli": "^21.2.12",
|
||||||
|
"@angular/compiler-cli": "^21.2.0",
|
||||||
|
"@openapitools/openapi-generator-cli": "^2.34.0",
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"jsdom": "^28.0.0",
|
||||||
|
"postcss": "^8.5.15",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"typescript": "~5.9.2",
|
||||||
|
"vitest": "^4.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,11 +10,47 @@
|
|||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>eeg_frontend</artifactId>
|
<artifactId>eeg_frontend</artifactId>
|
||||||
|
<name>EEG Portal :: Frontend</name>
|
||||||
|
|
||||||
<properties>
|
<build>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<plugins>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<plugin>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<groupId>com.github.eirslett</groupId>
|
||||||
</properties>
|
<artifactId>frontend-maven-plugin</artifactId>
|
||||||
|
<version>1.15.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>install node and npm</id>
|
||||||
|
<goals>
|
||||||
|
<goal>install-node-and-npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<nodeVersion>v24.16.0</nodeVersion>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<execution>
|
||||||
|
<id>npm install</id>
|
||||||
|
<goals>
|
||||||
|
<goal>npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>install</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<execution>
|
||||||
|
<id>npm run build</id>
|
||||||
|
<goals>
|
||||||
|
<goal>npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>run build --color=false</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
7
eeg_frontend/proxy.conf.json
Normal file
7
eeg_frontend/proxy.conf.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:8080",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
eeg_frontend/public/favicon.ico
Normal file
BIN
eeg_frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
eeg_frontend/src/app/app.config.ts
Normal file
13
eeg_frontend/src/app/app.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {ApplicationConfig} from '@angular/core';
|
||||||
|
import {provideRouter} from '@angular/router';
|
||||||
|
import {routes} from './app.routes';
|
||||||
|
import {provideHttpClient, withFetch} from '@angular/common/http';
|
||||||
|
import {BASE_PATH} from './api';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(withFetch()),
|
||||||
|
{ provide: BASE_PATH, useValue: 'http://localhost:8080' }
|
||||||
|
]
|
||||||
|
};
|
||||||
1
eeg_frontend/src/app/app.html
Normal file
1
eeg_frontend/src/app/app.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<router-outlet />
|
||||||
8
eeg_frontend/src/app/app.routes.ts
Normal file
8
eeg_frontend/src/app/app.routes.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {Routes} from '@angular/router';
|
||||||
|
import {LandingPage} from './pages/landing-page/landing-page';
|
||||||
|
import {Register} from './pages/register/register';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{ path: '', component: LandingPage },
|
||||||
|
{ path: 'register', component: Register }
|
||||||
|
];
|
||||||
0
eeg_frontend/src/app/app.scss
Normal file
0
eeg_frontend/src/app/app.scss
Normal file
23
eeg_frontend/src/app/app.spec.ts
Normal file
23
eeg_frontend/src/app/app.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { App } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [App],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', async () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
await fixture.whenStable();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, eeg_frontend');
|
||||||
|
});
|
||||||
|
});
|
||||||
12
eeg_frontend/src/app/app.ts
Normal file
12
eeg_frontend/src/app/app.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrl: './app.scss',
|
||||||
|
})
|
||||||
|
export class App {
|
||||||
|
protected readonly title = signal('eeg_frontend');
|
||||||
|
}
|
||||||
122
eeg_frontend/src/app/pages/landing-page/landing-page.html
Normal file
122
eeg_frontend/src/app/pages/landing-page/landing-page.html
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<div class="min-h-screen flex flex-col bg-slate-50">
|
||||||
|
|
||||||
|
<header class="bg-white shadow-sm sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<mat-icon color="primary">bolt</mat-icon>
|
||||||
|
<span class="font-bold text-xl tracking-tight text-slate-900">
|
||||||
|
Energie Portal
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="hidden md:flex gap-6">
|
||||||
|
<a href="#vorteile" class="text-slate-600 hover:text-slate-900 font-medium">Vorteile</a>
|
||||||
|
<a href="#so-gehts" class="text-slate-600 hover:text-slate-900 font-medium">So funktioniert's</a>
|
||||||
|
<a href="#faq" class="text-slate-600 hover:text-slate-900 font-medium">FAQ</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button mat-button class="hidden sm:inline-flex">Login</button>
|
||||||
|
<button mat-flat-button color="primary" routerLink="/register">Mitglied werden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-grow">
|
||||||
|
|
||||||
|
<section class="relative bg-white pt-20 pb-32 overflow-hidden">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h1 class="text-4xl md:text-6xl font-extrabold text-slate-900 tracking-tight mb-6">
|
||||||
|
Gemeinsam Strom teilen.<br>
|
||||||
|
<span class="text-green-600">Regional und fair.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-4 max-w-2xl mx-auto text-xl text-slate-500 mb-10">
|
||||||
|
Werde Teil unserer Energiegemeinschaft. Nutze den Strom aus der Nachbarschaft oder teile deinen eigenen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row justify-center gap-4">
|
||||||
|
<button mat-flat-button color="primary" class="!px-8 !py-6 !text-lg">
|
||||||
|
Jetzt Ersparnis berechnen
|
||||||
|
</button>
|
||||||
|
<button mat-stroked-button class="!px-8 !py-6 !text-lg">
|
||||||
|
Mehr erfahren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="vorteile" class="py-24 bg-slate-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-16">
|
||||||
|
<h2 class="text-3xl font-bold text-slate-900">Warum Mitglied werden?</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div class="bg-white p-8 rounded-2xl shadow-sm border border-slate-100 hover:shadow-md transition">
|
||||||
|
<div class="w-12 h-12 bg-green-100 text-green-600 rounded-xl flex items-center justify-center mb-6">
|
||||||
|
<mat-icon>eco</mat-icon>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-900 mb-3">100% Regional</h3>
|
||||||
|
<p class="text-slate-600">
|
||||||
|
Der Strom kommt direkt aus Anlagen in deiner Umgebung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-8 rounded-2xl shadow-sm border border-slate-100 hover:shadow-md transition">
|
||||||
|
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-xl flex items-center justify-center mb-6">
|
||||||
|
<mat-icon>savings</mat-icon>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-900 mb-3">Netzkosten sparen</h3>
|
||||||
|
<p class="text-slate-600">
|
||||||
|
Profitiere von reduzierten Netznutzungsentgelten und Abgaben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-8 rounded-2xl shadow-sm border border-slate-100 hover:shadow-md transition">
|
||||||
|
<div class="w-12 h-12 bg-purple-100 text-purple-600 rounded-xl flex items-center justify-center mb-6">
|
||||||
|
<mat-icon>dashboard</mat-icon>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-900 mb-3">Volle Transparenz</h3>
|
||||||
|
<p class="text-slate-600">
|
||||||
|
Behalte deine Einspeisung und deinen Verbrauch im Portal im Blick.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-slate-900 text-slate-300 py-12">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<span class="font-bold text-xl text-white flex items-center gap-2 mb-4">
|
||||||
|
<mat-icon>bolt</mat-icon> Energie Portal
|
||||||
|
</span>
|
||||||
|
<p class="text-sm">Dein lokaler Verein für nachhaltigen Energieaustausch.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="text-white font-semibold mb-4">Rechtliches</h4>
|
||||||
|
<ul class="space-y-2 text-sm">
|
||||||
|
<li><a href="#" class="hover:text-white">Impressum</a></li>
|
||||||
|
<li><a href="#" class="hover:text-white">Datenschutz</a></li>
|
||||||
|
<li><a href="#" class="hover:text-white">Vereinsstatuten</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="text-white font-semibold mb-4">Kontakt</h4>
|
||||||
|
<ul class="space-y-2 text-sm">
|
||||||
|
<li>office@dein-verein.at</li>
|
||||||
|
<li></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
22
eeg_frontend/src/app/pages/landing-page/landing-page.spec.ts
Normal file
22
eeg_frontend/src/app/pages/landing-page/landing-page.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LandingPage } from './landing-page';
|
||||||
|
|
||||||
|
describe('LandingPage', () => {
|
||||||
|
let component: LandingPage;
|
||||||
|
let fixture: ComponentFixture<LandingPage>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LandingPage],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LandingPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
13
eeg_frontend/src/app/pages/landing-page/landing-page.ts
Normal file
13
eeg_frontend/src/app/pages/landing-page/landing-page.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-landing-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule, MatIconModule, RouterLink],
|
||||||
|
templateUrl: './landing-page.html',
|
||||||
|
styleUrls: ['./landing-page.scss']
|
||||||
|
})
|
||||||
|
export class LandingPage {}
|
||||||
110
eeg_frontend/src/app/pages/register/register.html
Normal file
110
eeg_frontend/src/app/pages/register/register.html
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<div class="min-h-screen bg-slate-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-md text-center mb-6">
|
||||||
|
<a routerLink="/" class="inline-flex items-center gap-2 font-bold text-2xl text-slate-950">
|
||||||
|
<mat-icon color="primary">bolt</mat-icon> Energie Portal
|
||||||
|
</a>
|
||||||
|
<h2 class="mt-2 text-xl font-semibold text-slate-600">Erstelle dein Mitgliedskonto</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-3xl bg-white shadow-md rounded-2xl border border-slate-100 overflow-hidden">
|
||||||
|
|
||||||
|
<mat-stepper linear #stepper class="!bg-transparent">
|
||||||
|
|
||||||
|
<mat-step [stepControl]="accountForm">
|
||||||
|
<ng-template matStepLabel>Konto</ng-template>
|
||||||
|
<form [formGroup]="accountForm" class="p-6 flex flex-col gap-4">
|
||||||
|
<p class="text-sm text-slate-500 mb-2">Trage deine persönlichen Daten für den Portalzugang ein.</p>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Vollständiger Name / Vereinsname</mat-label>
|
||||||
|
<input matInput formControlName="name" placeholder="Max Mustermann">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>E-Mail-Adresse</mat-label>
|
||||||
|
<input matInput type="email" formControlName="email" placeholder="max@beispiel.at">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Passwort</mat-label>
|
||||||
|
<input matInput type="password" formControlName="password">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<button mat-flat-button color="primary" matStepperNext type="button">Weiter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<mat-step [stepControl]="energyForm">
|
||||||
|
<ng-template matStepLabel>Anlagendaten</ng-template>
|
||||||
|
<form [formGroup]="energyForm" class="p-6 flex flex-col gap-4">
|
||||||
|
<p class="text-sm text-slate-500 mb-2">Diese Daten benötigen wir für die Zuordnung beim Netzbetreiber.</p>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Zählpunktnummer (33-stellig)</mat-label>
|
||||||
|
<input matInput formControlName="accountingPointId" placeholder="AT0010000000000000000000000000000" maxlength="35">
|
||||||
|
<mat-hint>Beginnt mit AT und findest du auf deiner Stromrechnung.</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||||
|
<mat-form-field appearance="outline" class="md:col-span-2">
|
||||||
|
<mat-label>Straße & Hausnummer</mat-label>
|
||||||
|
<input matInput formControlName="street">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>PLZ</mat-label>
|
||||||
|
<input matInput formControlName="zip">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Ort</mat-label>
|
||||||
|
<input matInput formControlName="city">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-4">
|
||||||
|
<button mat-button matStepperPrevious type="button">Zurück</button>
|
||||||
|
<button mat-flat-button color="primary" matStepperNext type="button">Weiter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<mat-step [stepControl]="paymentForm">
|
||||||
|
<ng-template matStepLabel>Abrechnung</ng-template>
|
||||||
|
<form [formGroup]="paymentForm" (ngSubmit)="onSubmit()" class="p-6 flex flex-col gap-4">
|
||||||
|
<p class="text-sm text-slate-500 mb-2">Für den Einzug der Netzkosten-Ersparnisse/Beiträge sowie Gutschriften für Einspeisung.</p>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>IBAN</mat-label>
|
||||||
|
<input matInput formControlName="iban" placeholder="AT.. .... .... .... ....">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>BIC (Optional)</mat-label>
|
||||||
|
<input matInput formControlName="bic">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200 text-xs text-slate-600 my-2 space-y-2">
|
||||||
|
<p class="font-bold">SEPA-Lastschriftmandat (Creditor ID: ATXXXXXXXXXXXXXXXX)</p>
|
||||||
|
<p>Ich ermächtige die Energiegemeinschaft, Zahlungen von meinem Konto mittels Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die eingelösten Lastschriften zu bezahlen.</p>
|
||||||
|
<p class="italic text-slate-400">Hinweis: Ich kann innerhalb von 8 Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-checkbox formControlName="acceptMandate" color="primary" class="text-sm">
|
||||||
|
Ich erteile das SEPA-Lastschriftmandat elektronisch.
|
||||||
|
</mat-checkbox>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-4">
|
||||||
|
<button mat-button matStepperPrevious type="button">Zurück</button>
|
||||||
|
<button mat-flat-button color="accent" type="submit" [disabled]="accountForm.invalid || energyForm.invalid || paymentForm.invalid">
|
||||||
|
Registrierung abschließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
</mat-stepper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
0
eeg_frontend/src/app/pages/register/register.scss
Normal file
0
eeg_frontend/src/app/pages/register/register.scss
Normal file
22
eeg_frontend/src/app/pages/register/register.spec.ts
Normal file
22
eeg_frontend/src/app/pages/register/register.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Register } from './register';
|
||||||
|
|
||||||
|
describe('Register', () => {
|
||||||
|
let component: Register;
|
||||||
|
let fixture: ComponentFixture<Register>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Register],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Register);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
78
eeg_frontend/src/app/pages/register/register.ts
Normal file
78
eeg_frontend/src/app/pages/register/register.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-register',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatStepperModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatCheckboxModule
|
||||||
|
],
|
||||||
|
templateUrl: './register.html',
|
||||||
|
styleUrls: ['./register.scss']
|
||||||
|
})
|
||||||
|
export class Register {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private authService = inject(AuthenticationService);
|
||||||
|
private router = inject(Router);
|
||||||
|
|
||||||
|
// Schritt 1: Login & Stammdaten
|
||||||
|
accountForm = this.fb.group({
|
||||||
|
name: ['', 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]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
eeg_frontend/src/app/services/auth.spec.ts
Normal file
16
eeg_frontend/src/app/services/auth.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Auth } from './auth';
|
||||||
|
|
||||||
|
describe('Auth', () => {
|
||||||
|
let service: Auth;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(Auth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
18
eeg_frontend/src/app/services/auth.ts
Normal file
18
eeg_frontend/src/app/services/auth.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
// Die URL zu deinem lokalen Spring Boot Backend
|
||||||
|
private apiUrl = 'http://localhost:8080/api/auth';
|
||||||
|
|
||||||
|
register(userData: any): Observable<any> {
|
||||||
|
// Sendet die Daten als JSON-Body an den Endpunkt /register
|
||||||
|
return this.http.post(`${this.apiUrl}/register`, userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
eeg_frontend/src/index.html
Normal file
20
eeg_frontend/src/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>EegFrontend</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
eeg_frontend/src/main.ts
Normal file
5
eeg_frontend/src/main.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { App } from './app/app';
|
||||||
|
|
||||||
|
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||||
40
eeg_frontend/src/styles.scss
Normal file
40
eeg_frontend/src/styles.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Include theming for Angular Material with `mat.theme()`.
|
||||||
|
// This Sass mixin will define CSS variables that are used for styling Angular Material
|
||||||
|
// components according to the Material 3 design spec.
|
||||||
|
// Learn more about theming and how to use it for your application's
|
||||||
|
// custom components at https://material.angular.dev/guide/theming
|
||||||
|
@use '@angular/material' as mat;
|
||||||
|
@use "tailwindcss";
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
@include mat.theme(
|
||||||
|
(
|
||||||
|
color: (
|
||||||
|
primary: mat.$azure-palette,
|
||||||
|
tertiary: mat.$blue-palette,
|
||||||
|
),
|
||||||
|
typography: Roboto,
|
||||||
|
density: 0,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
// Default the application to a light color theme. This can be changed to
|
||||||
|
// `dark` to enable the dark color theme, or to `light dark` to defer to the
|
||||||
|
// user's system settings.
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
// Set a default background, font and text colors for the application using
|
||||||
|
// Angular Material's system-level CSS variables. Learn more about these
|
||||||
|
// variables at https://material.angular.dev/guide/system-variables
|
||||||
|
background-color: var(--mat-sys-surface);
|
||||||
|
color: var(--mat-sys-on-surface);
|
||||||
|
font: var(--mat-sys-body-medium);
|
||||||
|
|
||||||
|
// Reset the user agent margin.
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
11
eeg_frontend/tsconfig.app.json
Normal file
11
eeg_frontend/tsconfig.app.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
33
eeg_frontend/tsconfig.json
Normal file
33
eeg_frontend/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
eeg_frontend/tsconfig.spec.json
Normal file
10
eeg_frontend/tsconfig.spec.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": ["vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user