refactor - login und auth
This commit is contained in:
@@ -1,34 +1,42 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
// /src/app/app.config.ts
|
||||||
|
|
||||||
|
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
|
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||||
import { provideHttpClient, withFetch } from '@angular/common/http';
|
|
||||||
import { provideClientHydration } from '@angular/platform-browser';
|
import { provideClientHydration } from '@angular/platform-browser';
|
||||||
|
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||||
// +++ HIER IST DIE DEFINITIVE KORREKTUR FÜR FORMS-PROVIDER +++
|
|
||||||
import {
|
|
||||||
ReactiveFormsModule, // <-- Importieren des Moduls selbst
|
|
||||||
FormsModule // <-- Importieren des Moduls für Template-driven forms (falls benötigt)
|
|
||||||
} from '@angular/forms';
|
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
|
// --- NEUE IMPORTE ---
|
||||||
|
import { environment } from './environment';
|
||||||
|
import { API_URL } from './core/tokens/api-url.token';
|
||||||
|
import { authInterceptor } from './core/interceptors/auth.interceptor';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
|
|
||||||
provideHttpClient(withFetch()),
|
|
||||||
provideClientHydration(),
|
provideClientHydration(),
|
||||||
|
|
||||||
// +++ Korrigierte Verwendung der Forms-Provider +++
|
// --- HttpClient Konfiguration aktualisieren ---
|
||||||
// Dies macht die Provider für Reactive Forms global verfügbar
|
// withFetch() entfernen, da es mit Interceptors noch Probleme geben kann
|
||||||
ReactiveFormsModule.withConfig({
|
// Stattdessen withInterceptors() verwenden
|
||||||
warnOnNgModelWithFormControl: 'never' // Optional: Schaltet eine NgModel Warnung aus
|
provideHttpClient(
|
||||||
}).providers!, // <--- Korrigiert
|
withInterceptors([authInterceptor]) // Registriert unseren neuen Interceptor
|
||||||
|
),
|
||||||
|
|
||||||
// Falls Sie Template-Driven Forms (mit NgModel) benötigen, würden Sie das hinzufügen:
|
// --- Forms Provider (sauberer Import) ---
|
||||||
// FormsModule.withConfig({}).providers!
|
// importProvidersFrom ist der empfohlene Weg für NgModule-basierte Bibliotheken
|
||||||
|
importProvidersFrom(
|
||||||
|
ReactiveFormsModule.withConfig({ warnOnNgModelWithFormControl: 'never' }),
|
||||||
|
FormsModule
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- API_URL Provider hinzufügen ---
|
||||||
|
// Stellt den API_URL-Token mit dem Wert aus der passenden environment-Datei bereit
|
||||||
|
{ provide: API_URL, useValue: environment.apiUrl }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -4,8 +4,6 @@ import { AccessDeniedComponent } from './core/components/access-denied/access-de
|
|||||||
import { NotFoundComponent } from './core/components/not-found/not-found.component';
|
import { NotFoundComponent } from './core/components/not-found/not-found.component';
|
||||||
|
|
||||||
import { DefaultLayoutComponent } from './core/components/default-layout/default-layout.component';
|
import { DefaultLayoutComponent } from './core/components/default-layout/default-layout.component';
|
||||||
|
|
||||||
import { DashboardPageComponent } from './features/components/dashboard/dashboard-page/dashboard-page.component';
|
|
||||||
import { authGuard } from './core/guards/auth.guard';
|
import { authGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
@@ -18,8 +16,8 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
component: DefaultLayoutComponent,
|
component: DefaultLayoutComponent,
|
||||||
// canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
// data: { requiredRole: 'Admin' },
|
data: { requiredRole: 'Admin' },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
8
src/app/core/components/core.routes.ts
Normal file
8
src/app/core/components/core.routes.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
// Importiere dein spezielles Layout für Auth-Seiten und alle Komponenten
|
||||||
|
|
||||||
|
export const CORE_ROUTES: Routes = [
|
||||||
|
|
||||||
|
|
||||||
|
];
|
||||||
@@ -1,48 +1,43 @@
|
|||||||
|
// /src/app/core/guards/auth.guard.ts
|
||||||
|
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router, ActivatedRouteSnapshot } from '@angular/router';
|
import { CanActivateFn, Router, ActivatedRouteSnapshot, UrlTree } from '@angular/router';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ein funktionaler Guard, der Routen schützt.
|
* Ein funktionaler Guard, der Routen basierend auf Authentifizierung und Rollen schützt.
|
||||||
*
|
*
|
||||||
* Verwendung in der Routing-Konfiguration:
|
* Verwendung in der Routing-Konfiguration:
|
||||||
* {
|
* {
|
||||||
* path: 'admin',
|
* path: 'admin',
|
||||||
* component: AdminDashboardComponent,
|
|
||||||
* canActivate: [authGuard],
|
* canActivate: [authGuard],
|
||||||
* data: { requiredRole: 'Admin' } // Die erforderliche Rolle hier übergeben
|
* data: { requiredRole: 'Admin' } // Die erforderliche Rolle hier übergeben
|
||||||
* }
|
* }
|
||||||
* {
|
* {
|
||||||
* path: 'profile',
|
* path: 'profile',
|
||||||
* component: UserProfileComponent,
|
|
||||||
* canActivate: [authGuard] // Schützt nur vor nicht angemeldeten Benutzern
|
* canActivate: [authGuard] // Schützt nur vor nicht angemeldeten Benutzern
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
|
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot): boolean | UrlTree => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
// 1. Prüfen, ob der Benutzer überhaupt angemeldet ist
|
const isLoggedIn = !!authService.getToken();
|
||||||
if (!authService.getToken()) {
|
const requiredRole = route.data['requiredRole'] as string | undefined;
|
||||||
// Nicht angemeldet -> zum Login umleiten
|
|
||||||
router.navigate(['/login']);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Prüfen, ob die Route eine bestimmte Rolle erfordert
|
// Fall 1: Route ist nicht geschützt oder der Benutzer ist angemeldet und hat die richtige Rolle
|
||||||
const requiredRole = route.data['requiredRole'] as string;
|
if (isLoggedIn) {
|
||||||
if (requiredRole) {
|
if (!requiredRole || authService.hasRole(requiredRole)) {
|
||||||
// Rolle ist erforderlich -> prüfen, ob der Benutzer sie hat
|
// Zugriff erlaubt
|
||||||
if (authService.hasRole(requiredRole)) {
|
|
||||||
// Benutzer hat die Rolle -> Zugriff erlaubt
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Benutzer hat die Rolle nicht -> zu einer "Zugriff verweigert"-Seite umleiten
|
// Angemeldet, aber falsche Rolle -> Zugriff verweigert
|
||||||
router.navigate(['/access-denied']);
|
// Leite zu einer 'Forbidden'-Seite um oder zur Startseite
|
||||||
return false;
|
return router.createUrlTree(['/forbidden']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Wenn die Route nur eine Anmeldung, aber keine spezielle Rolle erfordert
|
// Fall 2: Nicht angemeldet
|
||||||
return true;
|
// Leite zur Login-Seite um und speichere die Ziel-URL für eine spätere Weiterleitung
|
||||||
|
return router.createUrlTree(['/auth/login'], { queryParams: { returnUrl: route.url.join('/') } });
|
||||||
};
|
};
|
||||||
30
src/app/core/interceptors/auth.interceptor.ts
Normal file
30
src/app/core/interceptors/auth.interceptor.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// /src/app/core/interceptors/auth.interceptor.ts
|
||||||
|
|
||||||
|
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { API_URL } from '../tokens/api-url.token';
|
||||||
|
|
||||||
|
export const authInterceptor: HttpInterceptorFn = (
|
||||||
|
req: HttpRequest<unknown>,
|
||||||
|
next: HttpHandlerFn
|
||||||
|
): Observable<HttpEvent<unknown>> => {
|
||||||
|
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const apiUrl = inject(API_URL);
|
||||||
|
const token = authService.getToken();
|
||||||
|
|
||||||
|
// Den Interceptor nur auf Anfragen an unsere eigene API anwenden
|
||||||
|
if (token && req.url.startsWith(apiUrl)) {
|
||||||
|
// Request klonen und den Authorization-Header hinzufügen
|
||||||
|
const clonedReq = req.clone({
|
||||||
|
headers: req.headers.set('Authorization', `Bearer ${token}`),
|
||||||
|
});
|
||||||
|
return next(clonedReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für alle anderen Anfragen (oder wenn kein Token vorhanden ist),
|
||||||
|
// den ursprünglichen Request weiterleiten.
|
||||||
|
return next(req);
|
||||||
|
};
|
||||||
@@ -1,15 +1,47 @@
|
|||||||
// src/app/core/models/auth.models.ts
|
// /src/app/core/models/auth.model.ts
|
||||||
|
|
||||||
export interface LoginRequestDto {
|
export interface LoginRequest {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponseDto {
|
export interface RegisterRequest extends LoginRequest {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
isAuthSuccessful: boolean;
|
isAuthSuccessful: boolean;
|
||||||
errorMessage?: string;
|
|
||||||
token?: string;
|
token?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordRequest {
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmNewPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeEmailRequest {
|
||||||
|
newEmail: string;
|
||||||
|
currentPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForgotPasswordRequest {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPassword {
|
||||||
|
email: string;
|
||||||
|
token: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResendEmailConfirmationRequest {
|
||||||
|
email: string;
|
||||||
}
|
}
|
||||||
@@ -1,121 +1,96 @@
|
|||||||
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
|
// /src/app/core/services/auth.service.ts
|
||||||
|
|
||||||
|
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable, BehaviorSubject, of } from 'rxjs';
|
import { Observable, BehaviorSubject, of } from 'rxjs';
|
||||||
import { tap, catchError } from 'rxjs/operators';
|
import { tap, catchError, map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LoginRequestDto } from '../models/auth.models';
|
import { LoginRequest, AuthResponse, RegisterRequest } from '../models/auth.models'
|
||||||
import { AuthResponseDto } from '../models/auth.models';
|
import { API_URL } from '../tokens/api-url.token';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
// Basis-URL Ihrer API
|
// --- Injizierte Abhängigkeiten mit moderner Syntax ---
|
||||||
private apiUrl = '/api/v1/Auth';
|
private http = inject(HttpClient);
|
||||||
|
private router = inject(Router);
|
||||||
|
private platformId = inject(PLATFORM_ID);
|
||||||
|
private apiUrl = inject(API_URL); // <-- SAUBERE INJEKTION!
|
||||||
|
|
||||||
|
private readonly endpoint = '/Auth';
|
||||||
|
|
||||||
// Keys für die Speicherung im localStorage
|
// Keys für die Speicherung im localStorage
|
||||||
private readonly tokenKey = 'auth-token';
|
private readonly TOKEN_KEY = 'auth-token';
|
||||||
private readonly userRolesKey = 'auth-user-roles';
|
private readonly ROLES_KEY = 'auth-user-roles';
|
||||||
|
|
||||||
// Ein BehaviorSubject, um den Login-Status reaktiv in der App zu teilen
|
private loggedInStatus = new BehaviorSubject<boolean>(this.isBrowser() && !!this.getToken());
|
||||||
private loggedInStatus = new BehaviorSubject<boolean>(this.hasToken());
|
|
||||||
public isLoggedIn$ = this.loggedInStatus.asObservable();
|
public isLoggedIn$ = this.loggedInStatus.asObservable();
|
||||||
|
|
||||||
constructor(
|
loginAdmin(credentials: LoginRequest): Observable<AuthResponse | null> {
|
||||||
private http: HttpClient,
|
return this.http.post<AuthResponse>(`${this.apiUrl}${this.endpoint}/login/admin`, credentials).pipe(
|
||||||
private router: Router,
|
tap(response => this.setSession(response)),
|
||||||
@Inject(PLATFORM_ID) private platformId: Object
|
catchError(() => {
|
||||||
) {}
|
this.clearSession(); // Bei fehlgeschlagenem Login immer die Session aufräumen
|
||||||
|
return of(null); // Gib ein "leeres" Observable zurück statt einen Fehler zu werfen
|
||||||
/**
|
|
||||||
* Meldet einen Admin-Benutzer an.
|
|
||||||
*/
|
|
||||||
loginAdmin(credentials: LoginRequestDto): Observable<AuthResponseDto> {
|
|
||||||
return this.http.post<AuthResponseDto>(`${this.apiUrl}/login/admin`, credentials).pipe(
|
|
||||||
tap(response => {
|
|
||||||
if (response.isAuthSuccessful && response.token) {
|
|
||||||
this.setSession(response);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
// Fehlerbehandlung, z.B. das Token löschen, falls eines vorhanden war
|
|
||||||
this.logout();
|
|
||||||
throw error;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
loginCustomer(credentials: LoginRequest): Observable<AuthResponse | null> {
|
||||||
* Meldet einen normalen Kunden an.
|
return this.http.post<AuthResponse>(`${this.apiUrl}${this.endpoint}/login/customer`, credentials).pipe(
|
||||||
*/
|
tap(response => this.setSession(response)),
|
||||||
loginCustomer(credentials: LoginRequestDto): Observable<AuthResponseDto> {
|
catchError(() => {
|
||||||
return this.http.post<AuthResponseDto>(`${this.apiUrl}/login/customer`, credentials).pipe(
|
this.clearSession();
|
||||||
tap(response => {
|
return of(null);
|
||||||
if (response.isAuthSuccessful && response.token) {
|
|
||||||
this.setSession(response);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
register(data: RegisterRequest): Observable<{ message: string } | null> {
|
||||||
* Meldet den Benutzer ab, entfernt die Daten aus dem Speicher und leitet um.
|
return this.http.post<{ message: string }>(`${this.apiUrl}${this.endpoint}/register`, data).pipe(
|
||||||
*/
|
catchError(() => of(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
this.clearSession();
|
||||||
localStorage.removeItem(this.tokenKey);
|
this.router.navigate(['/auth/login']); // Empfehlung: Routen gruppieren
|
||||||
localStorage.removeItem(this.userRolesKey);
|
|
||||||
}
|
|
||||||
this.loggedInStatus.next(false);
|
|
||||||
this.router.navigate(['/login']); // oder wohin auch immer Sie umleiten möchten
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt das gespeicherte JWT-Token zurück.
|
|
||||||
*/
|
|
||||||
getToken(): string | null {
|
getToken(): string | null {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
return this.isBrowser() ? localStorage.getItem(this.TOKEN_KEY) : null;
|
||||||
return localStorage.getItem(this.tokenKey);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die Rollen des angemeldeten Benutzers zurück.
|
|
||||||
*/
|
|
||||||
getUserRoles(): string[] {
|
getUserRoles(): string[] {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (!this.isBrowser()) return [];
|
||||||
const roles = localStorage.getItem(this.userRolesKey);
|
const roles = localStorage.getItem(this.ROLES_KEY);
|
||||||
return roles ? JSON.parse(roles) : [];
|
return roles ? JSON.parse(roles) : [];
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Überprüft, ob der Benutzer eine bestimmte Rolle hat.
|
|
||||||
*/
|
|
||||||
hasRole(requiredRole: string): boolean {
|
hasRole(requiredRole: string): boolean {
|
||||||
const userRoles = this.getUserRoles();
|
return this.getUserRoles().includes(requiredRole);
|
||||||
return userRoles.includes(requiredRole);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private setSession(authResponse: AuthResponse): void {
|
||||||
* Private Methode zum Speichern der Sitzungsdaten.
|
if (this.isBrowser() && authResponse?.token && authResponse?.roles) {
|
||||||
*/
|
localStorage.setItem(this.TOKEN_KEY, authResponse.token);
|
||||||
private setSession(authResponse: AuthResponseDto): void {
|
localStorage.setItem(this.ROLES_KEY, JSON.stringify(authResponse.roles));
|
||||||
if (isPlatformBrowser(this.platformId) && authResponse.token && authResponse.roles) {
|
|
||||||
localStorage.setItem(this.tokenKey, authResponse.token);
|
|
||||||
localStorage.setItem(this.userRolesKey, JSON.stringify(authResponse.roles));
|
|
||||||
this.loggedInStatus.next(true);
|
this.loggedInStatus.next(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private clearSession(): void {
|
||||||
* Private Methode, die prüft, ob ein Token vorhanden ist (nützlich beim App-Start).
|
if (this.isBrowser()) {
|
||||||
*/
|
localStorage.removeItem(this.TOKEN_KEY);
|
||||||
private hasToken(): boolean {
|
localStorage.removeItem(this.ROLES_KEY);
|
||||||
return !!this.getToken();
|
}
|
||||||
|
this.loggedInStatus.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isBrowser(): boolean {
|
||||||
|
return isPlatformBrowser(this.platformId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
48
src/app/core/services/logging.service.ts
Normal file
48
src/app/core/services/logging.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// /src/app/core/services/logging.service.ts
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { environment } from '../../environment';
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LoggingService {
|
||||||
|
|
||||||
|
log(level: LogLevel, message: string, data?: any) {
|
||||||
|
// In der Produktion werden nur Warnungen und Fehler geloggt
|
||||||
|
if (!environment.production || level > LogLevel.Info) {
|
||||||
|
const logEntry = `[${LogLevel[level]}] ${new Date().toISOString()}: ${message}`;
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Info:
|
||||||
|
console.info(logEntry, data || '');
|
||||||
|
break;
|
||||||
|
case LogLevel.Warn:
|
||||||
|
console.warn(logEntry, data || '');
|
||||||
|
break;
|
||||||
|
case LogLevel.Error:
|
||||||
|
console.error(logEntry, data || '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// HIER KÖNNTE SPÄTER EINE LOGIK ZUM SENDEN AN EINEN SERVER STEHEN
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, data?: any) {
|
||||||
|
this.log(LogLevel.Info, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, data?: any) {
|
||||||
|
this.log(LogLevel.Warn, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, data?: any) {
|
||||||
|
this.log(LogLevel.Error, message, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.error-message-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f8d7da; /* Helles Rot, anpassbar an dein Design */
|
||||||
|
border: 1px solid #f5c6cb; /* Dunkleres Rot */
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.5rem; /* Abstand zum Button */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #721c24; /* Dunkelroter Text */
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
<!-- /src/app/features/components/auth/login/login.component.html -->
|
||||||
|
|
||||||
<div class="component-header">
|
<div class="component-header">
|
||||||
<h2 class="auth-title">Anmelden</h2>
|
<h2 class="auth-title">Anmelden</h2>
|
||||||
<p class="auth-subtitle">Bitte geben Sie Ihre Daten ein, um fortzufahren.</p>
|
<p class="auth-subtitle">Bitte geben Sie Ihre Daten ein, um fortzufahren.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- novalidate verhindert die Standard-Browser-Validierung, sodass unser Angular-Feedback die volle Kontrolle hat -->
|
||||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate>
|
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate>
|
||||||
<app-form-field
|
<app-form-field
|
||||||
label="E-Mail-Adresse"
|
label="E-Mail-Adresse"
|
||||||
@@ -20,11 +23,18 @@
|
|||||||
<a routerLink="/auth/forgot-password" class="link">Passwort vergessen?</a>
|
<a routerLink="/auth/forgot-password" class="link">Passwort vergessen?</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- +++ NEU: Container für Fehlermeldungen +++ -->
|
||||||
|
<div *ngIf="errorMessage" class="error-message-container">
|
||||||
|
<p class="error-message">{{ errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
<!-- +++ ENDE NEU +++ -->
|
||||||
|
|
||||||
<app-button
|
<app-button
|
||||||
submitType="submit"
|
submitType="submit"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
[fullWidth]="true"
|
[fullWidth]="true"
|
||||||
[disabled]="loginForm.invalid">
|
[disabled]="loginForm.invalid || isLoading"
|
||||||
|
[isLoading]="isLoading">
|
||||||
Anmelden
|
Anmelden
|
||||||
</app-button>
|
</app-button>
|
||||||
|
|
||||||
@@ -33,7 +43,8 @@
|
|||||||
<app-button
|
<app-button
|
||||||
buttonType="secondary"
|
buttonType="secondary"
|
||||||
[fullWidth]="true"
|
[fullWidth]="true"
|
||||||
iconName="placeholder">
|
iconName="placeholder"
|
||||||
|
[disabled]="isLoading">
|
||||||
Mit Google anmelden
|
Mit Google anmelden
|
||||||
</app-button>
|
</app-button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { Component } from '@angular/core';
|
// /src/app/features/components/auth/login/login.component.ts
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FormBuilder,
|
|
||||||
Validators,
|
|
||||||
FormGroup,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import { RouterLink } from '@angular/router';
|
|
||||||
|
|
||||||
// Importieren der wiederverwendbaren Komponenten
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
|
||||||
|
import { Router, RouterLink } from '@angular/router';
|
||||||
|
import { finalize } from 'rxjs';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { AuthService } from '../../../../core/services/auth.service';
|
||||||
|
import { LoggingService } from '../../../../core/services/logging.service';
|
||||||
|
|
||||||
|
// UI Komponenten
|
||||||
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
|
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
|
||||||
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
|
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
|
||||||
|
import { LoginRequest } from '../../../../core/models/auth.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
@@ -27,22 +30,55 @@ import { FormFieldComponent } from '../../../../shared/components/form/form-fiel
|
|||||||
})
|
})
|
||||||
export class LoginComponent {
|
export class LoginComponent {
|
||||||
loginForm: FormGroup;
|
loginForm: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
|
||||||
|
// Moderne Dependency Injection mit inject()
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private router = inject(Router);
|
||||||
|
private logger = inject(LoggingService);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
constructor(private fb: FormBuilder) {
|
|
||||||
this.loginForm = this.fb.group({
|
this.loginForm = this.fb.group({
|
||||||
email: ['', [Validators.required, Validators.email]],
|
email: ['admin@yourwebshop.com', [Validators.required, Validators.email]],
|
||||||
password: ['', [Validators.required]],
|
password: ['SecureAdminPass123!', [Validators.required]],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
if (this.loginForm.valid) {
|
// Formular erneut prüfen und mehrfaches Absenden verhindern
|
||||||
console.log('Formular abgeschickt:', this.loginForm.value);
|
if (this.loginForm.invalid || this.isLoading) {
|
||||||
} else {
|
|
||||||
console.log('Formular ist ungültig.');
|
|
||||||
this.loginForm.markAllAsTouched();
|
this.loginForm.markAllAsTouched();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.errorMessage = null;
|
||||||
|
|
||||||
|
const credentials: LoginRequest = this.loginForm.value;
|
||||||
|
|
||||||
|
// Wir rufen den zentralen AuthService auf
|
||||||
|
this.authService.loginAdmin(credentials).pipe(
|
||||||
|
// finalize wird immer ausgeführt, egal ob erfolgreich oder nicht
|
||||||
|
finalize(() => this.isLoading = false)
|
||||||
|
).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
if (response && response.isAuthSuccessful) {
|
||||||
|
this.logger.info('Admin login successful', { email: credentials.email });
|
||||||
|
// Erfolgreich eingeloggt -> Weiterleiten zum Admin-Dashboard
|
||||||
|
this.router.navigate(['/dashboard']); // Passe die Route ggf. an
|
||||||
|
} else {
|
||||||
|
// Login fehlgeschlagen (falsches Passwort etc.), vom Backend kontrolliert
|
||||||
|
this.errorMessage = 'E-Mail oder Passwort ist ungültig.';
|
||||||
|
this.logger.warn('Admin login failed: Invalid credentials', { email: credentials.email });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
// Unerwarteter Fehler (z.B. Server nicht erreichbar)
|
||||||
|
this.errorMessage = 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.';
|
||||||
|
this.logger.error('An unexpected error occurred during admin login', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,3 +134,42 @@
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
transform: translateX(-50%) translateY(-12px);
|
transform: translateX(-50%) translateY(-12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.btn.is-loading {
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-content.is-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der Lade-Spinner */
|
||||||
|
.spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff; /* Farbe des sich drehenden Teils */
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner-Farbe für nicht-primäre Buttons anpassen */
|
||||||
|
.btn-secondary .spinner,
|
||||||
|
.btn-stroked .spinner,
|
||||||
|
.btn-flat .spinner,
|
||||||
|
.btn-icon .spinner {
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-top-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
<!-- /src/app/shared/components/ui/button/button.component.html -->
|
||||||
|
|
||||||
<button
|
<button
|
||||||
[type]="submitType"
|
[type]="submitType"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled || isLoading"
|
||||||
class="btn"
|
class="btn"
|
||||||
[class.btn-primary]="buttonType === 'primary'"
|
[class.btn-primary]="buttonType === 'primary'"
|
||||||
[class.btn-secondary]="buttonType === 'secondary'"
|
[class.btn-secondary]="buttonType === 'secondary'"
|
||||||
@@ -9,7 +11,14 @@
|
|||||||
[class.btn-icon]="buttonType === 'icon' || buttonType === 'icon-danger'"
|
[class.btn-icon]="buttonType === 'icon' || buttonType === 'icon-danger'"
|
||||||
[class.btn-icon-danger]="buttonType === 'icon-danger'"
|
[class.btn-icon-danger]="buttonType === 'icon-danger'"
|
||||||
[class.btn-full-width]="fullWidth"
|
[class.btn-full-width]="fullWidth"
|
||||||
|
[class.is-loading]="isLoading"
|
||||||
>
|
>
|
||||||
<app-icon *ngIf="iconName" [iconName]="iconName" [svgColor]="svgColor"></app-icon>
|
<div *ngIf="isLoading" class="spinner"></div>
|
||||||
<ng-content></ng-content>
|
|
||||||
|
<div class="btn-content" [class.is-hidden]="isLoading">
|
||||||
|
<app-icon *ngIf="iconName" [iconName]="iconName" [svgColor]="svgColor"></app-icon>
|
||||||
|
<span>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -19,6 +19,7 @@ export class ButtonComponent {
|
|||||||
@Input() fullWidth = false;
|
@Input() fullWidth = false;
|
||||||
@Input() iconName: string | null = null;
|
@Input() iconName: string | null = null;
|
||||||
@Input() svgColor: string | null = null;
|
@Input() svgColor: string | null = null;
|
||||||
|
@Input() isLoading = false;
|
||||||
|
|
||||||
// Der Tooltip-Input
|
// Der Tooltip-Input
|
||||||
@Input() tooltip: string | null = null;
|
@Input() tooltip: string | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user