add registration
planning extend registration and club memberships
This commit is contained in:
parent
b4818efc1a
commit
fa7f1300d8
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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!;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue