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 ###
|
||||
.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>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</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>
|
||||
<groupId>com.h2database</groupId>
|
||||
@ -62,6 +75,11 @@
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
package at.mueller.eeg;
|
||||
package at.mueller.eeg.backend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||
|
||||
@SpringBootApplication
|
||||
public class EegPortalApplication {
|
||||
@ -1,8 +1,8 @@
|
||||
package at.mueller.eeg.community.domain;
|
||||
package at.mueller.eeg.backend.community.domain;
|
||||
|
||||
public enum CommunityType {
|
||||
|
||||
EEG_LOKAL,
|
||||
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 lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@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 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 lombok.Getter;
|
||||
@ -1,8 +1,8 @@
|
||||
package at.mueller.eeg.community.domain;
|
||||
package at.mueller.eeg.backend.community.domain;
|
||||
|
||||
public enum PointType {
|
||||
CONSUMER,
|
||||
PRODUCER,
|
||||
//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 {
|
||||
PENDING,
|
||||
APPROVED,
|
||||
REJECTED;
|
||||
REJECTED
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package at.mueller.eeg.iam.domain;
|
||||
package at.mueller.eeg.backend.iam.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
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 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:
|
||||
hibernate:
|
||||
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.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>
|
||||
</parent>
|
||||
<artifactId>eeg_frontend</artifactId>
|
||||
<name>EEG Portal :: Frontend</name>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<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>
|
||||
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