add registration

planning extend registration and club memberships
This commit is contained in:
beo3000 2025-12-23 16:58:32 +01:00
parent b4818efc1a
commit fa7f1300d8
5 changed files with 315 additions and 7 deletions

View File

@ -331,7 +331,7 @@ NavMenu.razor:
| ✓ | A6 | Foundation | AutoMapper Profiles | 5 Mapping-Dateien | | ✓ | A6 | Foundation | AutoMapper Profiles | 5 Mapping-Dateien |
| ✓ | A7 | Foundation | DI Registration | 2 DI-Dateien ändern | | ✓ | A7 | Foundation | DI Registration | 2 DI-Dateien ändern |
| ✓ | B1 | User/Account | UserService erweitern | 1 Service, 1 Interface, 1 DTO | | ✓ | 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 | | ☐ | B3 | User/Account | Password Reset Pages | 2 Razor |
| ☐ | B4 | User/Account | Profile Page | 1 Razor | | ☐ | B4 | User/Account | Profile Page | 1 Razor |
| ☐ | B5 | User/Account | Admin Users Page | 1 Razor | | ☐ | B5 | User/Account | Admin Users Page | 1 Razor |
@ -348,10 +348,18 @@ NavMenu.razor:
| ☐ | F1 | Dashboard | Dashboard Page | 1 Razor | | ☐ | F1 | Dashboard | Dashboard Page | 1 Razor |
| ☐ | F2 | Dashboard | Evaluation Components | 2 Shared Components | | ☐ | F2 | Dashboard | Evaluation Components | 2 Shared Components |
| ☐ | F3 | Navigation | NavMenu finalisieren | 1 Razor ändern | | ☐ | 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 **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) ## Zentrale Anforderungen (User-Feedback)
### Business Rules ### Business Rules

35
docs/prompts.md Normal file
View File

@ -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

View File

@ -14,9 +14,13 @@
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" /> <input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px"> <MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
@if (_registered)
{
<MudAlert Severity="Severity.Success" Class="mb-3">Registrierung erfolgreich! Bitte anmelden.</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(_error)) @if (!string.IsNullOrWhiteSpace(_error))
{ {
<MudAlert Severity="Severity.Error">@_error</MudAlert> <MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
} }
<MudText Typo="Typo.h5" Class="mb-4"> <MudText Typo="Typo.h5" Class="mb-4">
@ -59,31 +63,39 @@
ButtonType="ButtonType.Submit" ButtonType="ButtonType.Submit"
Variant="Variant.Filled" Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
FullWidth="true"> FullWidth="true"
Class="mb-3">
Anmelden Anmelden
</MudButton> </MudButton>
<MudLink Href="/account/register" Typo="Typo.body2">
Noch kein Konto? Registrieren
</MudLink>
</MudPaper> </MudPaper>
</form> </form>
@code { @code {
private string _returnUrl = "/"; private string _returnUrl = "/";
private string? _error = string.Empty; private string? _error;
private string _antiToken = ""; private string _antiToken = "";
private bool _registered;
protected override void OnInitialized() protected override void OnInitialized()
{ {
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
var query = QueryHelpers.ParseQuery(uri.Query); var query = QueryHelpers.ParseQuery(uri.Query);
if (query.TryGetValue("returnUrl", out var ru)) _returnUrl = ru!; if (query.TryGetValue("returnUrl", out var ru)) _returnUrl = ru!;
if (query.TryGetValue("error", out var err)) if (query.TryGetValue("error", out var err))
{ {
_error = "Login fehlgeschlagen"; _error = "Login fehlgeschlagen";
} }
if (query.TryGetValue("registered", out _))
{
_registered = true;
}
// Antiforgery Token generieren (klassischer MVC Token) // Antiforgery Token generieren (klassischer MVC Token)
var http = HttpContextAccessor.HttpContext!; var http = HttpContextAccessor.HttpContext!;

View File

@ -0,0 +1,123 @@
@page "/account/register"
@using Microsoft.AspNetCore.WebUtilities
@inject NavigationManager NavigationManager
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
@inject IHttpContextAccessor HttpContextAccessor
<form method="post" action="/auth/register">
<!-- Hidden Fields -->
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
@if (!string.IsNullOrWhiteSpace(_error))
{
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
}
<MudText Typo="Typo.h5" Class="mb-4">
Registrierung
</MudText>
<MudTextField T="string"
Name="Email"
Label="E-Mail"
Variant="Variant.Outlined"
Required="true"
InputType="InputType.Email"
AutoComplete="email"
Class="mb-3" />
<MudTextField T="string"
Name="Password"
Label="Passwort"
Variant="Variant.Outlined"
Required="true"
InputType="InputType.Password"
AutoComplete="new-password"
HelperText="Mindestens 6 Zeichen"
Class="mb-3" />
<MudTextField T="string"
Name="DisplayName"
Label="Anzeigename"
Variant="Variant.Outlined"
Required="true"
Placeholder="Dein Name"
Class="mb-3" />
<MudTextField T="string"
Name="ClubName"
Label="Club (optional)"
Variant="Variant.Outlined"
Required="false"
Placeholder="Clubname zur Zuordnung"
HelperText="Optional: Bestehender Club zur Zuordnung"
Class="mb-4" />
<MudButton
ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Class="mb-3">
Registrieren
</MudButton>
<MudLink Href="/account/login" Typo="Typo.body2">
Bereits registriert? Zur Anmeldung
</MudLink>
</MudPaper>
</form>
@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<string>();
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);
}
}

View File

@ -52,5 +52,23 @@ namespace Koogle.Web.Controllers
return LocalRedirect(returnUrl); return LocalRedirect(returnUrl);
} }
/// <summary>
/// Handles user registration via form POST.
/// </summary>
[HttpPost("register")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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}");
}
} }
} }