diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md
index 293e8aa..33dc782 100644
--- a/docs/IMPLEMENTATION_PLAN.md
+++ b/docs/IMPLEMENTATION_PLAN.md
@@ -332,7 +332,7 @@ NavMenu.razor:
| ✓ | A7 | Foundation | DI Registration | 2 DI-Dateien ändern |
| ✓ | B1 | User/Account | UserService erweitern | 1 Service, 1 Interface, 1 DTO |
| ✓ | B2 | User/Account | Register Page | 1 Razor |
-| ☐ | B3 | User/Account | Password Reset Pages | 2 Razor |
+| ✓ | B3 | User/Account | Password Reset Pages | 2 Razor |
| ☐ | B4 | User/Account | Profile Page | 1 Razor |
| ☐ | B5 | User/Account | Admin Users Page | 1 Razor |
| ☐ | **C1** | **Clubs** | **ClubState Fluxor** | **4 State-Dateien** |
diff --git a/src/Koogle.Application/DTOs/AuthDto.cs b/src/Koogle.Application/DTOs/AuthDto.cs
index 6cee846..88f1bda 100644
--- a/src/Koogle.Application/DTOs/AuthDto.cs
+++ b/src/Koogle.Application/DTOs/AuthDto.cs
@@ -1,18 +1,101 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Koogle.Application.DTOs
{
+ ///
+ /// Form DTO for user login.
+ ///
public record LoginDto
{
+ ///
+ /// Email of the user requesting login.
+ ///
+ [Required]
+ [EmailAddress]
public string Email { get; set; } = string.Empty;
+
public string Password { get; set; } = string.Empty;
public string ClubName { get; set; } = string.Empty;
public bool RememberMe { get; set; }
public string? ReturnUrl { get; set; }
}
+
+ ///
+ /// Form DTO for password reset with confirmation field.
+ ///
+ public class ResetPasswordFormDto
+ {
+ ///
+ /// Email of the user requesting password reset.
+ ///
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = "";
+ public string Token { get; set; } = "";
+ public string NewPassword { get; set; } = "";
+ public string ConfirmPassword { get; set; } = "";
+ }
+
+ ///
+ /// DTO for user registration.
+ ///
+ public class RegisterUserDto
+ {
+ ///
+ /// Email address (also used as username).
+ ///
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = "";
+
+ ///
+ /// Password for the new account.
+ ///
+ [Required]
+ [MinLength(6)]
+ public string Password { get; set; } = "";
+
+ ///
+ /// Display name for the user profile.
+ ///
+ [Required]
+ public string DisplayName { get; set; } = "";
+
+ ///
+ /// Optional club name to assign user to during registration.
+ ///
+ public string? ClubName { get; set; }
+ }
+
+ ///
+ /// DTO for password reset request.
+ ///
+ public class ResetPasswordDto
+ {
+ ///
+ /// Email of the user requesting password reset.
+ ///
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = "";
+
+ ///
+ /// Password reset token received via email.
+ ///
+ [Required]
+ public string Token { get; set; } = "";
+
+ ///
+ /// New password.
+ ///
+ [Required]
+ [MinLength(6)]
+ public string NewPassword { get; set; } = "";
+ }
}
diff --git a/src/Koogle.Application/DTOs/UserDto.cs b/src/Koogle.Application/DTOs/UserDto.cs
index ce41e0d..7a2c55c 100644
--- a/src/Koogle.Application/DTOs/UserDto.cs
+++ b/src/Koogle.Application/DTOs/UserDto.cs
@@ -70,62 +70,6 @@ namespace Koogle.Application.DTOs
Koogle.Domain.Entities.UserProfile Profile,
Koogle.Infrastructure.Identity.ApplicationUser Identity);
- ///
- /// DTO for user registration.
- ///
- public class RegisterUserDto
- {
- ///
- /// Email address (also used as username).
- ///
- [Required]
- [EmailAddress]
- public string Email { get; set; } = "";
-
- ///
- /// Password for the new account.
- ///
- [Required]
- [MinLength(6)]
- public string Password { get; set; } = "";
-
- ///
- /// Display name for the user profile.
- ///
- [Required]
- public string DisplayName { get; set; } = "";
-
- ///
- /// Optional club name to assign user to during registration.
- ///
- public string? ClubName { get; set; }
- }
-
- ///
- /// DTO for password reset request.
- ///
- public class ResetPasswordDto
- {
- ///
- /// Email of the user requesting password reset.
- ///
- [Required]
- [EmailAddress]
- public string Email { get; set; } = "";
-
- ///
- /// Password reset token received via email.
- ///
- [Required]
- public string Token { get; set; } = "";
-
- ///
- /// New password.
- ///
- [Required]
- [MinLength(6)]
- public string NewPassword { get; set; } = "";
- }
///
/// DTO for updating user profile.
diff --git a/src/Koogle.Web/Components/Pages/Account/ForgotPassword.razor b/src/Koogle.Web/Components/Pages/Account/ForgotPassword.razor
new file mode 100644
index 0000000..2a871e1
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Account/ForgotPassword.razor
@@ -0,0 +1,85 @@
+@page "/account/forgot-password"
+
+@using Microsoft.AspNetCore.WebUtilities
+
+@inject NavigationManager NavigationManager
+@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
+@inject IHttpContextAccessor HttpContextAccessor
+
+
+
+
+@code {
+ private string? _error;
+ private bool _success;
+ private string _antiToken = "";
+
+ protected override void OnInitialized()
+ {
+ var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
+ var query = QueryHelpers.ParseQuery(uri.Query);
+
+ if (query.TryGetValue("error", out var err))
+ {
+ _error = err.ToString();
+ }
+
+ if (query.TryGetValue("success", out _))
+ {
+ _success = true;
+ }
+
+ // Antiforgery Token generieren
+ var http = HttpContextAccessor.HttpContext!;
+ var tokens = Antiforgery.GetAndStoreTokens(http);
+ _antiToken = tokens.RequestToken!;
+ }
+}
diff --git a/src/Koogle.Web/Components/Pages/Account/Login.razor b/src/Koogle.Web/Components/Pages/Account/Login.razor
index 21d0aa8..da4780a 100644
--- a/src/Koogle.Web/Components/Pages/Account/Login.razor
+++ b/src/Koogle.Web/Components/Pages/Account/Login.razor
@@ -68,9 +68,14 @@
Anmelden
-
- Noch kein Konto? Registrieren
-
+
+
+ Noch kein Konto? Registrieren
+
+
+ Passwort vergessen?
+
+
diff --git a/src/Koogle.Web/Components/Pages/Account/ResetPassword.razor b/src/Koogle.Web/Components/Pages/Account/ResetPassword.razor
new file mode 100644
index 0000000..5c1ef9b
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Account/ResetPassword.razor
@@ -0,0 +1,173 @@
+@page "/account/reset-password"
+
+@using Microsoft.AspNetCore.WebUtilities
+
+@inject NavigationManager NavigationManager
+@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
+@inject IHttpContextAccessor HttpContextAccessor
+
+@if (_success)
+{
+
+
+ Passwort wurde erfolgreich zurueckgesetzt.
+
+
+ Zur Anmeldung
+
+
+}
+else if (_invalidToken)
+{
+
+
+ Ungueltiger oder abgelaufener Reset-Link.
+
+
+ Neuen Link anfordern
+
+
+}
+else
+{
+
+}
+
+
+@code {
+ private string? _error;
+ private bool _success;
+ private bool _invalidToken;
+ private string _email = "";
+ private string _token = "";
+ private string _antiToken = "";
+
+ protected override void OnInitialized()
+ {
+ var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
+ var query = QueryHelpers.ParseQuery(uri.Query);
+
+ if (query.TryGetValue("email", out var email))
+ {
+ _email = email.ToString();
+ }
+
+ if (query.TryGetValue("token", out var token))
+ {
+ _token = token.ToString();
+ }
+
+ if (query.TryGetValue("success", out _))
+ {
+ _success = true;
+ }
+
+ if (query.TryGetValue("error", out var err))
+ {
+ var errorStr = err.ToString();
+ if (errorStr == "InvalidToken")
+ {
+ _invalidToken = true;
+ }
+ else
+ {
+ _error = MapErrorCodes(errorStr);
+ }
+ }
+
+ // Missing required params
+ if (string.IsNullOrWhiteSpace(_email) || string.IsNullOrWhiteSpace(_token))
+ {
+ if (!_success)
+ {
+ _invalidToken = true;
+ }
+ }
+
+ // Antiforgery Token generieren
+ var http = HttpContextAccessor.HttpContext!;
+ var tokens = Antiforgery.GetAndStoreTokens(http);
+ _antiToken = tokens.RequestToken!;
+ }
+
+ private string MapErrorCodes(string errorCodes)
+ {
+ if (string.IsNullOrWhiteSpace(errorCodes))
+ return "Passwort-Reset fehlgeschlagen";
+
+ var codes = errorCodes.Split(',');
+ var messages = new List();
+
+ foreach (var code in codes)
+ {
+ var msg = code.Trim() switch
+ {
+ "InvalidToken" => "Ungueltiger oder abgelaufener Token",
+ "PasswordTooShort" => "Passwort zu kurz",
+ "PasswordRequiresNonAlphanumeric" => "Passwort erfordert Sonderzeichen",
+ "PasswordRequiresDigit" => "Passwort erfordert Zahl",
+ "PasswordRequiresUpper" => "Passwort erfordert Grossbuchstaben",
+ "PasswordRequiresLower" => "Passwort erfordert Kleinbuchstaben",
+ "PasswordMismatch" => "Passwoerter stimmen nicht ueberein",
+ "UserNotFound" => "Benutzer nicht gefunden",
+ _ => code
+ };
+ messages.Add(msg);
+ }
+
+ return string.Join(", ", messages);
+ }
+}
diff --git a/src/Koogle.Web/Controllers/AuthController.cs b/src/Koogle.Web/Controllers/AuthController.cs
index 506293c..829e055 100644
--- a/src/Koogle.Web/Controllers/AuthController.cs
+++ b/src/Koogle.Web/Controllers/AuthController.cs
@@ -70,5 +70,57 @@ namespace Koogle.Web.Controllers
return LocalRedirect($"/account/register?error={errors}");
}
+ ///
+ /// Handles password reset request (forgot password).
+ ///
+ [HttpPost("forgot-password")]
+ [ValidateAntiForgeryToken]
+ public async Task ForgotPassword([FromForm] string email)
+ {
+ // Always show success to prevent email enumeration
+ var token = await _userService.RequestPasswordResetAsync(email);
+
+ if (token != null)
+ {
+ // TODO: Send email with reset link
+ // For now, log the token for development purposes
+ var resetUrl = $"/account/reset-password?email={Uri.EscapeDataString(email)}&token={Uri.EscapeDataString(token)}";
+ Console.WriteLine($"[DEV] Password reset link: {resetUrl}");
+ }
+
+ return LocalRedirect("/account/forgot-password?success=true");
+ }
+
+ ///
+ /// Handles password reset with token.
+ ///
+ [HttpPost("reset-password")]
+ [ValidateAntiForgeryToken]
+ public async Task ResetPassword([FromForm] ResetPasswordFormDto input)
+ {
+ // Validate password confirmation
+ if (input.NewPassword != input.ConfirmPassword)
+ {
+ return LocalRedirect($"/account/reset-password?email={Uri.EscapeDataString(input.Email)}&token={Uri.EscapeDataString(input.Token)}&error=PasswordMismatch");
+ }
+
+ var dto = new ResetPasswordDto
+ {
+ Email = input.Email,
+ Token = input.Token,
+ NewPassword = input.NewPassword
+ };
+
+ var result = await _userService.ResetPasswordAsync(dto);
+ if (result.Succeeded)
+ {
+ return LocalRedirect("/account/reset-password?success=true");
+ }
+
+ var errors = string.Join(",", result.Errors.Select(e => e.Code));
+ return LocalRedirect($"/account/reset-password?email={Uri.EscapeDataString(input.Email)}&token={Uri.EscapeDataString(input.Token)}&error={errors}");
+ }
}
+
+
}