finished login

This commit is contained in:
Bernhard Müller 2026-06-03 15:09:17 +02:00
parent e25bae9700
commit 9bb354a805
9 changed files with 229 additions and 0 deletions

View File

@ -1,5 +1,6 @@
package at.mueller.eeg.backend.iam.config; package at.mueller.eeg.backend.iam.config;
import at.mueller.eeg.backend.iam.repository.UserRepository;
import com.nimbusds.jose.jwk.source.ImmutableSecret; import com.nimbusds.jose.jwk.source.ImmutableSecret;
import jakarta.servlet.DispatcherType; import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -12,6 +13,8 @@ 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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoder;
@ -143,4 +146,10 @@ public class SecurityConfig {
SecretKeySpec secretKey = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256");
return new NimbusJwtEncoder(new ImmutableSecret<>(secretKey)); return new NimbusJwtEncoder(new ImmutableSecret<>(secretKey));
} }
@Bean
public UserDetailsService userDetailsService(UserRepository userRepository) {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User mit E-Mail " + username + " nicht gefunden."));
}
} }

View File

@ -0,0 +1,46 @@
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<a routerLink="/" class="flex items-center space-x-2 group">
<svg class="h-8 w-8 text-emerald-600 group-hover:text-emerald-700 transition" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="font-bold text-xl text-gray-900 tracking-tight">
eeg<span class="text-emerald-600">portal</span>
</span>
</a>
</div>
<div class="hidden md:flex absolute left-1/2 transform -translate-x-1/2">
<nav class="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>
<div class="flex items-center gap-4">
@if (authService.isLoggedIn$ | async) {
<span class="text-sm text-gray-500 hidden sm:inline">
Rolle: <span class="font-semibold text-gray-700">{{ authService.getUserRole() }}</span>
</span>
<button (click)="onLogout()" class="text-sm font-medium text-red-600 hover:text-red-700 border border-red-200 hover:border-red-300 px-3 py-1.5 rounded-md transition duration-150">
Abmelden
</button>
} @else {
<div class="flex items-center gap-4">
<button mat-button class="hidden sm:inline-flex" routerLink="/login">Login</button>
<button mat-flat-button color="primary" routerLink="/register">Mitglied werden</button>
</div>
}
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Header } from './header';
describe('Header', () => {
let component: Header;
let fixture: ComponentFixture<Header>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Header],
}).compileComponents();
fixture = TestBed.createComponent(Header);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import {Component} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
import {AuthService} from '../../services/auth';
import {AsyncPipe} from '@angular/common';
import {MatButton} from '@angular/material/button';
@Component({
selector: 'app-header',
imports: [
RouterLink,
AsyncPipe,
MatButton
],
templateUrl: './header.html'
})
export class HeaderComponent {
constructor(
public authService: AuthService,
private router: Router
) {}
onLogout() {
this.authService.logout();
this.router.navigate(['/login']);
}
}

View File

@ -0,0 +1,47 @@
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
In Ihr EEG-Portal einloggen
</h2>
</div>
<div *ngIf="errorMessage" class="bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<span class="block sm:inline text-sm">{{ errorMessage }}</span>
</div>
<form class="mt-8 space-y-6" [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="rounded-md shadow-sm -space-y-px">
<div class="mb-4">
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Benutzername oder E-Mail</label>
<input
id="username"
formControlName="email"
type="text"
required
class="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
placeholder="ihr.name@beispiel.at">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
<input
id="password"
formControlName="password"
type="password"
required
class="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
placeholder="********">
</div>
</div>
<div>
<button
type="submit"
[disabled]="loginForm.invalid"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 transition duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
Anmelden
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Login } from './login';
describe('Login', () => {
let component: Login;
let fixture: ComponentFixture<Login>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Login],
}).compileComponents();
fixture = TestBed.createComponent(Login);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,56 @@
import {Component} from '@angular/core';
import {Router} from '@angular/router';
import {AuthService} from '../../services/auth';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {CommonModule} from '@angular/common'; // 1. Hier importieren
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './login.html'
})
export class LoginComponent {
loginForm: FormGroup;
errorMessage: string = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router
) {
this.loginForm = this.fb.group({
email: ['', [Validators.required]],
password: ['', [Validators.required]]
});
}
onSubmit() {
if (this.loginForm.invalid) return;
this.authService.login(this.loginForm.value).subscribe({
next: (response) => {
// Hier passiert die bedingte Weiterleitung anhand der Rolle
this.redirectBasedOnRole(response.role!);
},
error: (err) => {
this.errorMessage = err.error?.message || 'Ein unbekannter Fehler ist aufgetreten.';
}
});
}
private redirectBasedOnRole(role: string) {
switch (role) {
case 'ADMIN':
void this.router.navigate(['/admin/approvals']);
break;
case 'MEMBER':
void this.router.navigate(['/dashboard']);
break;
default:
// Fallback, falls die Rolle unbekannt ist
void this.router.navigate(['/']);
break;
}
}
}