added header, started implementing login

This commit is contained in:
Bernhard Müller 2026-06-03 13:40:51 +02:00
parent 4ef6d32093
commit e25bae9700
11 changed files with 123 additions and 47 deletions

View File

@ -0,0 +1,3 @@
package at.mueller.eeg.backend.common.config.exception;
public record ErrorResponse(String message, int status) {}

View File

@ -0,0 +1,33 @@
package at.mueller.eeg.backend.common.config.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorResponse> handleBadCredentials(BadCredentialsException ex) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Benutzername oder Passwort ist falsch.", HttpStatus.UNAUTHORIZED.value()));
}
@ExceptionHandler(DisabledException.class)
public ResponseEntity<ErrorResponse> handleDisabled(DisabledException ex) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("Dein Account wurde noch nicht vom Admin freigeschaltet. Bitte habe etwas Geduld.", HttpStatus.FORBIDDEN.value()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Ein unerwarteter Fehler ist aufgetreten: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value()));
}
}

View File

@ -38,11 +38,7 @@ public class AuthController {
@Operation(summary = "User Login", @Operation(summary = "User Login",
description = "Gibt einen JWT zurück. Schlägt fehl, wenn Status noch PENDING ist.") description = "Gibt einen JWT zurück. Schlägt fehl, wenn Status noch PENDING ist.")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) { public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
// Logik: Spring Security Authentication Manager aufrufen return ResponseEntity.ok(authService.login(request));
// 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

@ -46,4 +46,6 @@ public interface IamDtos {
String primaryAtNumber // Zur schnellen Anzeige in der Liste String primaryAtNumber // Zur schnellen Anzeige in der Liste
) {} ) {}
record ErrorResponse(String message, int status) {}
} }

View File

@ -2,15 +2,22 @@ package at.mueller.eeg.backend.iam.domain;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
@Getter @Getter
@Setter @Setter
public class User { public class User implements UserDetails {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.UUID)
@ -29,7 +36,7 @@ public class User {
private String iban; private String iban;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private UserRole role; private UserRole role = UserRole.MEMBER;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
@ -37,4 +44,21 @@ public class User {
@Column(nullable = false) @Column(nullable = false)
private boolean enabled = false; private boolean enabled = false;
@Override
@NonNull
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}
@Override
public @Nullable String getPassword() {
return this.passwordHash;
}
@Override
@NonNull
public String getUsername() {
return this.email;
}
} }

View File

@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader; import org.springframework.security.oauth2.jwt.JwsHeader;
@ -18,8 +19,7 @@ import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; 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.*;
import static at.mueller.eeg.backend.iam.api.dto.IamDtos.RegistrationRequest;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -33,14 +33,19 @@ public class AuthService {
/** /**
* Verarbeitet den Login und gibt den JWT-Token als String zurück. * Verarbeitet den Login und gibt den JWT-Token als String zurück.
*/ */
public String login(LoginRequest request) { public AuthResponse login(LoginRequest request) {
// 1. Prüft, ob E-Mail und Passwort in der Datenbank übereinstimmen
Authentication authentication = authenticationManager.authenticate( Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password()) new UsernamePasswordAuthenticationToken(request.email(), request.password())
); );
// 2. Wenn erfolgreich, Token generieren String role = authentication.getAuthorities().stream()
return generateToken(authentication.getName()); .map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_MEMBER");
String token = generateToken(authentication.getName(), role);
return new AuthResponse(token, authentication.getName(), role);
} }
/** /**
@ -68,14 +73,14 @@ public class AuthService {
/** /**
* Hilfsmethode zum Bauen des JWT. * Hilfsmethode zum Bauen des JWT.
*/ */
private String generateToken(String email) { private String generateToken(String email, String role) {
Instant now = Instant.now(); Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder() JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("eeg-portal") .issuer("eeg-portal")
.issuedAt(now) .issuedAt(now)
.expiresAt(now.plus(2, ChronoUnit.HOURS)) // Token ist 2 Stunden gültig .expiresAt(now.plus(2, ChronoUnit.HOURS))
.subject(email) // Der "Besitzer" des Tokens .subject(email)
.claim("role", "MEMBER") // Hier könntest du später Admin/Mitglied Rollen einbauen .claim("role", role.replace("ROLE_", "")) // Dynamische Rolle einfügen
.build(); .build();
JwsHeader jwsHeader = JwsHeader.with(MacAlgorithm.HS256).build(); JwsHeader jwsHeader = JwsHeader.with(MacAlgorithm.HS256).build();

View File

@ -1 +1,7 @@
<router-outlet /> <!-- Der Header bleibt immer fix oben stehen -->
<app-header></app-header>
<!-- Hier drin werden je nach Route die Login-, Register- oder Dashboard-Seiten geladen -->
<main class="min-h-[calc(100vh-4rem)] bg-gray-50">
<router-outlet></router-outlet>
</main>

View File

@ -1,8 +1,10 @@
import {Routes} from '@angular/router'; import {Routes} from '@angular/router';
import {LandingPage} from './pages/landing-page/landing-page'; import {LandingPage} from './pages/landing-page/landing-page';
import {Register} from './pages/register/register'; import {Register} from './pages/register/register';
import {LoginComponent} from './pages/login/login';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: LandingPage }, { path: '', component: LandingPage },
{ path: 'register', component: Register } { path: 'register', component: Register },
{ path: 'login', component: LoginComponent}
]; ];

View File

@ -1,9 +1,10 @@
import {Component, signal} from '@angular/core'; import {Component, signal} from '@angular/core';
import {RouterOutlet} from '@angular/router'; import {RouterOutlet} from '@angular/router';
import {HeaderComponent} from './layout/header/header';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet, HeaderComponent],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.scss', styleUrl: './app.scss',
}) })

View File

@ -1,29 +1,5 @@
<div class="min-h-screen flex flex-col bg-slate-50"> <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"> <main class="flex-grow">
<section class="relative bg-white pt-20 pb-32 overflow-hidden"> <section class="relative bg-white pt-20 pb-32 overflow-hidden">

View File

@ -1,6 +1,7 @@
import {inject, Injectable} from '@angular/core'; import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs'; import {BehaviorSubject, Observable, tap} from 'rxjs';
import {AuthResponse} from '../api';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -11,8 +12,35 @@ export class AuthService {
// Die URL zu deinem lokalen Spring Boot Backend // Die URL zu deinem lokalen Spring Boot Backend
private apiUrl = 'http://localhost:8080/api/auth'; private apiUrl = 'http://localhost:8080/api/auth';
private isLoggedInSubject = new BehaviorSubject<boolean>(!!localStorage.getItem('token'));
isLoggedIn$ = this.isLoggedInSubject.asObservable();
register(userData: any): Observable<any> { register(userData: any): Observable<any> {
// Sendet die Daten als JSON-Body an den Endpunkt /register // Sendet die Daten als JSON-Body an den Endpunkt /register
return this.http.post(`${this.apiUrl}/register`, userData); return this.http.post(`${this.apiUrl}/register`, userData);
} }
login(credentials: any): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/login`, credentials).pipe(
tap(response => {
// Token und Rolle im LocalStorage (oder SessionStorage) ablegen
localStorage.setItem('token', response.token!);
localStorage.setItem('user_role', response.role!);
})
);
}
getUserRole(): string | null {
return localStorage.getItem('user_role');
}
isLoggedIn(): boolean {
return !!localStorage.getItem('token');
}
logout() {
localStorage.clear();
this.isLoggedInSubject.next(false);
}
} }