diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index ee819b8..293e8aa 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -331,7 +331,7 @@ NavMenu.razor: | ✓ | A6 | Foundation | AutoMapper Profiles | 5 Mapping-Dateien | | ✓ | 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 | +| ✓ | B2 | User/Account | Register Page | 1 Razor | | ☐ | B3 | User/Account | Password Reset Pages | 2 Razor | | ☐ | B4 | User/Account | Profile Page | 1 Razor | | ☐ | B5 | User/Account | Admin Users Page | 1 Razor | @@ -348,10 +348,18 @@ NavMenu.razor: | ☐ | F1 | Dashboard | Dashboard Page | 1 Razor | | ☐ | F2 | Dashboard | Evaluation Components | 2 Shared Components | | ☐ | F3 | Navigation | NavMenu finalisieren | 1 Razor ändern | +| ☐ | G1 | Erweiterte Reg. | MembershipStatus + UserProfileClub | 2 Dateien + Migration | +| ☐ | G2 | Erweiterte Reg. | ClubInvitation Entity | 1 Entity + Migration | +| ☐ | G3 | Erweiterte Reg. | IEmailService (Stub) | 2 Dateien | +| ☐ | G4 | Erweiterte Reg. | Services erweitern | 2 Services | +| ☐ | G5 | Erweiterte Reg. | Dashboard Pending-Widget | 1 Component | +| ☐ | G6 | Erweiterte Reg. | Admin Users Page erweitern | 1 Razor | +| ☐ | G7 | Erweiterte Reg. | Club-Beitritt UI | 1 Razor | +| ☐ | G8 | Erweiterte Reg. | Einladungslink-Handling | 2 Dateien | **Legende:** ☐ = Offen | ☑ = In Arbeit | ✓ = Fertig -**Geschätzte Dateien insgesamt: ~75 Dateien** +**Geschätzte Dateien insgesamt: ~90 Dateien** --- @@ -625,6 +633,118 @@ NavMenu.razor: --- +### **Phase G1: MembershipStatus + UserProfileClub erweitern** + +**Dateien:** +1. `src/Koogle.Domain/Enums/MembershipStatus.cs` (neu) + - Pending, Approved, Rejected + +2. `src/Koogle.Domain/Entities/UserProfileClub.cs` (erweitern) + - MembershipStatus Status (default: Pending) + - string? RejectionReason + - DateTime? ApprovedAt, Guid? ApprovedById + - DateTime? RejectedAt, Guid? RejectedById + +3. Migration erstellen + +--- + +### **Phase G2: ClubInvitation Entity** + +**Dateien:** +1. `src/Koogle.Domain/Entities/ClubInvitation.cs` (neu) + - Guid Id, Guid ClubId + - string Token (unique, für URL) + - DateTime ExpiresAt, DateTime CreatedAt, Guid CreatedById + - int? MaxUses (null = unbegrenzt), int UsedCount + +2. `src/Koogle.Infrastructure/Data/AppDbContext.cs` - DbSet hinzufügen +3. Migration erstellen + +--- + +### **Phase G3: IEmailService (Stub)** + +**Dateien:** +1. `src/Koogle.Application/Interfaces/IEmailService.cs` (neu) + - SendMembershipRequestNotificationAsync + - SendMembershipApprovedAsync + - SendMembershipRejectedAsync + +2. `src/Koogle.Infrastructure/Services/StubEmailService.cs` (neu) + - Logging statt echtem Versand + - TODO-Kommentare für SMTP + +3. DI Registration + +--- + +### **Phase G4: Services erweitern (Membership-Logik)** + +**UserService erweitern:** +- RequestClubMembershipAsync(userProfileId, clubId) +- RequestClubMembershipByNameAsync(userProfileId, clubName) +- RequestClubMembershipByInviteAsync(userProfileId, inviteToken) +- ApproveMembershipAsync(userProfileId, clubId, approvedById) +- RejectMembershipAsync(userProfileId, clubId, rejectedById, reason) +- GetPendingMembershipsAsync(clubId) + +**ClubService erweitern:** +- CreateInvitationAsync(clubId, createdById, expiresAt, maxUses) +- GetInvitationByTokenAsync(token) +- ValidateInvitationAsync(token) + +--- + +### **Phase G5: Dashboard Pending-Widget** + +**Dateien:** +1. `src/Koogle.Web/Components/Shared/PendingMembershipsWidget.razor` (neu) + - Anzahl ausstehender Anträge für Club-Admins + - Link zur Admin Users Page + +2. Dashboard.razor erweitern - Widget für Admins einbinden + +--- + +### **Phase G6: Admin Users Page erweitern** + +**Dateien:** +1. `src/Koogle.Web/Components/Pages/Admin/Users.razor` (ändern) + - Tab/Filter für "Ausstehende Anträge" + - Approve/Reject Buttons + - Reject-Dialog mit Begründungsfeld + +--- + +### **Phase G7: Club-Beitritt UI** + +**Dateien:** +1. `src/Koogle.Web/Components/Pages/Account/JoinClub.razor` (neu) + - Eingabefeld für Club-Name + - Suche/Validierung + - Beitrittsantrag senden + - Erfolgsmeldung "Antrag gestellt" + +2. Dashboard.razor erweitern + - "Keinem Club zugeordnet" Meldung mit Link zu JoinClub + +--- + +### **Phase G8: Einladungslink-Handling** + +**Dateien:** +1. `src/Koogle.Web/Components/Pages/Club/JoinByInvite.razor` (neu) + - Route: `/club/join/{token}` + - Token validieren + - Wenn eingeloggt: direkt Beitritt (Pending) + - Wenn nicht eingeloggt: Redirect zu Register mit Token + +2. `src/Koogle.Web/Controllers/AuthController.cs` (erweitern) + - InviteToken als optionalen Parameter bei Register + +--- + ## Zentrale Anforderungen (User-Feedback) ### Business Rules diff --git a/docs/prompts.md b/docs/prompts.md new file mode 100644 index 0000000..e6c2d2e --- /dev/null +++ b/docs/prompts.md @@ -0,0 +1,35 @@ +## Erweiterte Registrierung & Club Migliedschaften +Die REgistrierung funktioniert, entspricht aber noch nicht den Anforderungen. Ergänze nach der Phase +F3 noch eine Phase "Erweiterte Registrierung". Dabei sollen neue User im Zuge der Registrierung erst mal + nur "pending" sein, und auf Freischaltung durch die Club-Admins warten müssen. Das Login soll nach der +Registieriung möglich sein, aber auf dem Dashboard wird zum jeweiligen Club nur der Status angezeigt, +dass man auf die Freischaltung zum Club warten muss. Wenn der User bei der Registireirung keinen Club +angegeben hat, soll er die Mögloichkeit haben, sich nach der Anmeldung zum einem Club registieren zu +können / einem Club beitreten zu können. Die vorhanden Club-Admins oder der Super-Admin (falls noch kein + Club-Admin vorhanden) sollen per Mail über die neue Registrierung informiert werden, und dann die +Freischaltung vornehmen können. Bevor du diese Anforderung in die Planung aufnimmst, durchdenke sie und +stelle etwaige Rückfragen. + + + Rückfragen zur erweiterten Registrierung - Ergebnisse: + + | Frage | Antwort | + |----------------|----------------------------------------------------| + | E-Mail-Versand | Später implementieren (Stub/Interface) | + | Club-Beitritt | Club-Name eingeben + Einladungslink | + | Ablehnung | Status "Abgelehnt" + Begründung + Infomail an User | + | Admin-UI | Admin Users Page (B5) + Dashboard-Widget | + + Details: + + 1. E-Mail: Kein SMTP jetzt - StubEmailService mit Logging, SMTP später + 2. Club-Beitritt ohne Club-Angabe: + - User kann Club-Name eingeben (Suche) + - Club-Admin kann Einladungslink generieren + 3. Bei Ablehnung: + - Eintrag bleibt mit Status "Rejected" + - Admin gibt Begründung an (RejectionReason) + - User bekommt Info-Mail (Stub) + 4. Freischaltungs-UI: + - Integration in Admin Users Page (B5) + - Zusätzlich Dashboard-Widget für Schnellzugriff \ No newline at end of file diff --git a/src/Koogle.Web/Components/Pages/Account/Login.razor b/src/Koogle.Web/Components/Pages/Account/Login.razor index b83ea5e..21d0aa8 100644 --- a/src/Koogle.Web/Components/Pages/Account/Login.razor +++ b/src/Koogle.Web/Components/Pages/Account/Login.razor @@ -14,9 +14,13 @@ + @if (_registered) + { + Registrierung erfolgreich! Bitte anmelden. + } @if (!string.IsNullOrWhiteSpace(_error)) { - @_error + @_error } @@ -59,31 +63,39 @@ ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" - FullWidth="true"> + FullWidth="true" + Class="mb-3"> Anmelden + + Noch kein Konto? Registrieren + + @code { private string _returnUrl = "/"; - private string? _error = string.Empty; + private string? _error; private string _antiToken = ""; + private bool _registered; protected override void OnInitialized() { var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); var query = QueryHelpers.ParseQuery(uri.Query); - if (query.TryGetValue("returnUrl", out var ru)) _returnUrl = ru!; if (query.TryGetValue("error", out var err)) { _error = "Login fehlgeschlagen"; } - + if (query.TryGetValue("registered", out _)) + { + _registered = true; + } // Antiforgery Token generieren (klassischer MVC Token) var http = HttpContextAccessor.HttpContext!; diff --git a/src/Koogle.Web/Components/Pages/Account/Register.razor b/src/Koogle.Web/Components/Pages/Account/Register.razor new file mode 100644 index 0000000..a806296 --- /dev/null +++ b/src/Koogle.Web/Components/Pages/Account/Register.razor @@ -0,0 +1,123 @@ +@page "/account/register" + +@using Microsoft.AspNetCore.WebUtilities + +@inject NavigationManager NavigationManager +@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery +@inject IHttpContextAccessor HttpContextAccessor + +
+ + + + + @if (!string.IsNullOrWhiteSpace(_error)) + { + @_error + } + + + Registrierung + + + + + + + + + + + + Registrieren + + + + Bereits registriert? Zur Anmeldung + + + +
+ + +@code { + private string? _error; + 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 = MapErrorCodes(err.ToString()); + } + + // 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 "Registrierung fehlgeschlagen"; + + var codes = errorCodes.Split(','); + var messages = new List(); + + foreach (var code in codes) + { + var msg = code.Trim() switch + { + "DuplicateUserName" => "E-Mail bereits registriert", + "DuplicateEmail" => "E-Mail bereits registriert", + "PasswordTooShort" => "Passwort zu kurz", + "PasswordRequiresNonAlphanumeric" => "Passwort erfordert Sonderzeichen", + "PasswordRequiresDigit" => "Passwort erfordert Zahl", + "PasswordRequiresUpper" => "Passwort erfordert Grossbuchstaben", + "PasswordRequiresLower" => "Passwort erfordert Kleinbuchstaben", + "InvalidEmail" => "Ungueltige E-Mail", + _ => 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 8cb9ad4..506293c 100644 --- a/src/Koogle.Web/Controllers/AuthController.cs +++ b/src/Koogle.Web/Controllers/AuthController.cs @@ -52,5 +52,23 @@ namespace Koogle.Web.Controllers return LocalRedirect(returnUrl); } + /// + /// Handles user registration via form POST. + /// + [HttpPost("register")] + [ValidateAntiForgeryToken] + public async Task Register([FromForm] RegisterUserDto input) + { + var result = await _userService.RegisterUserAsync(input); + if (result.Succeeded) + { + return LocalRedirect("/account/login?registered=true"); + } + + // Build error string from IdentityResult + var errors = string.Join(",", result.Errors.Select(e => e.Code)); + return LocalRedirect($"/account/register?error={errors}"); + } + } }