first working frontend/backend

This commit is contained in:
Bernhard Müller 2026-06-01 16:55:47 +02:00
parent 70ed6efdc4
commit 4ef6d32093
61 changed files with 13122 additions and 35 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -0,0 +1,5 @@
{
"dev": {
"host": "http://localhost:8080"
}
}

1
eeg_backend/iam.http Normal file
View File

@ -0,0 +1 @@
GET {{host}}

View File

@ -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>

View File

@ -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 {

View File

@ -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
} }

View File

@ -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")

View File

@ -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;

View File

@ -0,0 +1,7 @@
package at.mueller.eeg.backend.community.domain;
public enum MembershipStatus {
PENDING,
ACTIVE,
INACTIVE
}

View File

@ -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;

View File

@ -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;
} }

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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
) {}
}

View File

@ -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));
}
}

View File

@ -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
} }

View File

@ -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;

View File

@ -0,0 +1,6 @@
package at.mueller.eeg.backend.iam.domain;
public enum UserRole {
ADMIN,
MEMBER
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -1,7 +0,0 @@
package at.mueller.eeg.community.domain;
public enum MembershipStatus {
PENDING,
ACTIVE,
INACTIVE;
}

View File

@ -1,6 +0,0 @@
package at.mueller.eeg.iam.domain;
public enum UserRole {
ADMIN,
MEMBER;
}

View File

@ -9,4 +9,6 @@ spring:
jpa: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: update
show-sql: true show-sql: true
jwt:
secret: MeinSuperGeheimesUndSehrLangesSecretFuerDasEnergiePortal2025!

View File

@ -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;

View File

@ -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.");
}
}

View 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
View 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/

View File

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

12
eeg_frontend/.prettierrc Normal file
View File

@ -0,0 +1,12 @@
{
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
}

59
eeg_frontend/README.md Normal file
View 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
View 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
View 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

View 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

File diff suppressed because it is too large Load Diff

40
eeg_frontend/package.json Normal file
View 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"
}
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
{
"/api": {
"target": "http://localhost:8080",
"secure": false,
"changeOrigin": true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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' }
]
};

View File

@ -0,0 +1 @@
<router-outlet />

View 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 }
];

View File

View 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');
});
});

View 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');
}

View 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&#64;dein-verein.at</li>
<li></li>
</ul>
</div>
</div>
</footer>
</div>

View 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();
});
});

View 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 {}

View 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>

View 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();
});
});

View 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
}
});
}
}
}

View 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();
});
});

View 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);
}
}

View 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
View 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));

View 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 */

View 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"]
}

View 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"
}
]
}

View 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"]
}