feat(auth): add recover and reset password functionality
This commit is contained in:
parent
c8f2d16f07
commit
2305e83647
6 changed files with 322 additions and 0 deletions
|
@ -19,6 +19,20 @@ export const routes: Routes = [
|
|||
(m) => m.VerifyEmailComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'recover-password',
|
||||
loadComponent: () =>
|
||||
import('./feature/auth/recover-password/recover-password.component').then(
|
||||
(m) => m.RecoverPasswordComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'reset-password',
|
||||
loadComponent: () =>
|
||||
import('./feature/auth/recover-password/recover-password.component').then(
|
||||
(m) => m.RecoverPasswordComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'game/blackjack',
|
||||
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
|
||||
|
|
|
@ -83,6 +83,18 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-text-secondary">
|
||||
Passwort vergessen?
|
||||
<button
|
||||
(click)="switchToForgotPassword()"
|
||||
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
|
||||
>
|
||||
Passwort zurücksetzen
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-text-secondary">
|
||||
Noch kein Konto?
|
||||
|
|
|
@ -63,4 +63,9 @@ export class LoginComponent {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
switchToForgotPassword() {
|
||||
this.closeDialog.emit();
|
||||
this.router.navigate(['/recover-password']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
<div class="min-h-screen bg-deep-blue flex items-center justify-center">
|
||||
<div class="modal-card max-w-md w-full bg-deep-blue rounded-lg shadow-xl p-6 relative">
|
||||
<button
|
||||
(click)="closeDialog.emit()"
|
||||
class="absolute top-4 right-4 text-text-secondary hover:text-white transition-colors"
|
||||
aria-label="Dialog schließen"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<h2 class="modal-heading text-center">
|
||||
@if (isResetMode()) {Passwort zurücksetzen} @else {Passwort vergessen}
|
||||
</h2>
|
||||
|
||||
@if (errorMessage()) {
|
||||
<div class="bg-accent-red text-white p-4 rounded mb-4">
|
||||
{{ errorMessage() }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (successMessage()) {
|
||||
<div class="bg-emerald text-white p-4 rounded mb-4">
|
||||
{{ successMessage() }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isResetMode()) {
|
||||
<!-- Request Password Reset Form -->
|
||||
<form [formGroup]="emailForm" (ngSubmit)="onSubmitEmail()" class="space-y-4">
|
||||
<div class="mb-6">
|
||||
<p class="text-text-secondary text-sm mb-4">
|
||||
Gib deine E-Mail-Adresse ein, und wir senden dir einen Link zum Zurücksetzen deines Passworts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="text-text-secondary text-sm font-medium mb-1 block">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
formControlName="email"
|
||||
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
|
||||
placeholder="Gib deine E-Mail-Adresse ein"
|
||||
/>
|
||||
|
||||
@if (emailFormControls['email'].touched && emailFormControls['email'].errors) {
|
||||
<div class="text-accent-red mt-1 text-sm">
|
||||
@if (emailFormControls['email'].errors['required']) {
|
||||
<span>E-Mail ist erforderlich</span>
|
||||
}
|
||||
@if (emailFormControls['email'].errors['email']) {
|
||||
<span>Bitte gib eine gültige E-Mail-Adresse ein</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="emailForm.invalid || isLoading()"
|
||||
class="button-primary w-full py-2.5 rounded"
|
||||
>
|
||||
{{ isLoading() ? 'Wird gesendet...' : 'Link zum Zurücksetzen senden' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (isResetMode()) {
|
||||
<!-- Reset Password Form -->
|
||||
<form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmitReset()" class="space-y-4">
|
||||
<div class="mb-6">
|
||||
<p class="text-text-secondary text-sm mb-4">
|
||||
Gib dein neues Passwort ein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="text-text-secondary text-sm font-medium mb-1 block">
|
||||
Neues Passwort
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
formControlName="password"
|
||||
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
|
||||
placeholder="Gib dein neues Passwort ein"
|
||||
/>
|
||||
|
||||
@if (resetFormControls['password'].touched && resetFormControls['password'].errors) {
|
||||
<div class="text-accent-red mt-1 text-sm">
|
||||
@if (resetFormControls['password'].errors['required']) {
|
||||
<span>Passwort ist erforderlich</span>
|
||||
}
|
||||
@if (resetFormControls['password'].errors['minlength']) {
|
||||
<span>Passwort muss mindestens 8 Zeichen lang sein</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="text-text-secondary text-sm font-medium mb-1 block">
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
formControlName="confirmPassword"
|
||||
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
|
||||
placeholder="Bestätige dein neues Passwort"
|
||||
/>
|
||||
|
||||
@if (resetFormControls['confirmPassword'].touched && (resetFormControls['confirmPassword'].errors || resetPasswordForm.errors?.['passwordMismatch'])) {
|
||||
<div class="text-accent-red mt-1 text-sm">
|
||||
@if (resetFormControls['confirmPassword'].errors?.['required']) {
|
||||
<span>Passwortbestätigung ist erforderlich</span>
|
||||
}
|
||||
@if (resetPasswordForm.errors?.['passwordMismatch']) {
|
||||
<span>Passwörter stimmen nicht überein</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="resetPasswordForm.invalid || isLoading()"
|
||||
class="button-primary w-full py-2.5 rounded"
|
||||
>
|
||||
{{ isLoading() ? 'Wird aktualisiert...' : 'Passwort aktualisieren' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-text-secondary">
|
||||
<a routerLink="/" class="font-medium text-emerald hover:text-emerald-light transition-all duration-200">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,121 @@
|
|||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AuthService } from '@service/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recover-password',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './recover-password.component.html',
|
||||
})
|
||||
export class RecoverPasswordComponent {
|
||||
emailForm: FormGroup;
|
||||
resetPasswordForm: FormGroup;
|
||||
errorMessage = signal('');
|
||||
successMessage = signal('');
|
||||
isLoading = signal(false);
|
||||
token = '';
|
||||
isResetMode = signal(false);
|
||||
|
||||
@Output() closeDialog = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.emailForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]]
|
||||
});
|
||||
|
||||
this.resetPasswordForm = this.fb.group({
|
||||
password: ['', [Validators.required, Validators.minLength(8)]],
|
||||
confirmPassword: ['', [Validators.required]]
|
||||
}, {
|
||||
validators: this.passwordMatchValidator
|
||||
});
|
||||
|
||||
// Check if we're in reset mode
|
||||
this.route.queryParamMap.subscribe(params => {
|
||||
const token = params.get('token');
|
||||
if (token) {
|
||||
this.token = token;
|
||||
this.isResetMode.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
passwordMatchValidator(form: FormGroup) {
|
||||
const password = form.get('password')?.value;
|
||||
const confirmPassword = form.get('confirmPassword')?.value;
|
||||
return password === confirmPassword ? null : { passwordMismatch: true };
|
||||
}
|
||||
|
||||
get emailFormControls() {
|
||||
return this.emailForm.controls;
|
||||
}
|
||||
|
||||
get resetFormControls() {
|
||||
return this.resetPasswordForm.controls;
|
||||
}
|
||||
|
||||
onSubmitEmail(): void {
|
||||
if (this.emailForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading.set(true);
|
||||
this.errorMessage.set('');
|
||||
this.successMessage.set('');
|
||||
|
||||
const email = this.emailFormControls['email'].value;
|
||||
|
||||
this.authService.recoverPassword(email).subscribe({
|
||||
next: () => {
|
||||
this.isLoading.set(false);
|
||||
this.successMessage.set('Wenn ein Konto mit dieser E-Mail existiert, wird eine E-Mail mit weiteren Anweisungen gesendet.');
|
||||
this.emailForm.reset();
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading.set(false);
|
||||
this.errorMessage.set(
|
||||
err.error?.message || 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitReset(): void {
|
||||
if (this.resetPasswordForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading.set(true);
|
||||
this.errorMessage.set('');
|
||||
this.successMessage.set('');
|
||||
|
||||
const password = this.resetFormControls['password'].value;
|
||||
|
||||
this.authService.resetPassword(this.token, password).subscribe({
|
||||
next: () => {
|
||||
this.isLoading.set(false);
|
||||
this.successMessage.set('Dein Passwort wurde erfolgreich zurückgesetzt. Du kannst dich jetzt anmelden.');
|
||||
setTimeout(() => {
|
||||
this.router.navigate([''], { queryParams: { login: true } });
|
||||
}, 3000);
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading.set(false);
|
||||
this.errorMessage.set(
|
||||
err.error?.message || 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -78,6 +78,14 @@ export class AuthService {
|
|||
return this.http.post<unknown>(`${this.authUrl}/verify?token=${token}`, null);
|
||||
}
|
||||
|
||||
public recoverPassword(email: string): Observable<unknown> {
|
||||
return this.http.post<unknown>(`${this.authUrl}/recover-password?email=${email}`, null);
|
||||
}
|
||||
|
||||
public resetPassword(token: string, password: string): Observable<unknown> {
|
||||
return this.http.post<unknown>(`${this.authUrl}/reset-password`, { token, password });
|
||||
}
|
||||
|
||||
private setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
|
Reference in a new issue