feat(G8): add invite link handling for club membership
This commit is contained in:
parent
637a3b120c
commit
0911236a0d
|
|
@ -71,6 +71,11 @@ namespace Koogle.Application.DTOs
|
|||
/// Optional club name to assign user to during registration.
|
||||
/// </summary>
|
||||
public string? ClubName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional invitation token for club membership during registration.
|
||||
/// </summary>
|
||||
public string? InviteToken { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@
|
|||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
||||
|
||||
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
|
||||
@if (!string.IsNullOrWhiteSpace(_inviteToken))
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-3">
|
||||
Bitte melde dich an, um die Club-Einladung anzunehmen.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_registered)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Class="mb-3">Registrierung erfolgreich! Bitte anmelden.</MudAlert>
|
||||
|
|
@ -86,6 +92,7 @@
|
|||
private string? _error;
|
||||
private string _antiToken = "";
|
||||
private bool _registered;
|
||||
private string? _inviteToken;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
|
|
@ -101,6 +108,12 @@
|
|||
{
|
||||
_registered = true;
|
||||
}
|
||||
if (query.TryGetValue("invite", out var invite))
|
||||
{
|
||||
_inviteToken = invite.ToString();
|
||||
// After login, redirect to join by invite
|
||||
_returnUrl = $"/club/join/{_inviteToken}";
|
||||
}
|
||||
|
||||
// Antiforgery Token generieren (klassischer MVC Token)
|
||||
var http = HttpContextAccessor.HttpContext!;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,18 @@
|
|||
<form method="post" action="/auth/register">
|
||||
<!-- Hidden Fields -->
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
||||
@if (!string.IsNullOrWhiteSpace(_inviteToken))
|
||||
{
|
||||
<input type="hidden" name="InviteToken" value="@_inviteToken" />
|
||||
}
|
||||
|
||||
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
|
||||
@if (!string.IsNullOrWhiteSpace(_inviteToken))
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-3">
|
||||
Du wurdest zu einem Club eingeladen. Registriere dich, um beizutreten.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_error))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
|
||||
|
|
@ -75,6 +85,7 @@
|
|||
|
||||
@code {
|
||||
private string? _error;
|
||||
private string? _inviteToken;
|
||||
private string _antiToken = "";
|
||||
|
||||
protected override void OnInitialized()
|
||||
|
|
@ -87,6 +98,11 @@
|
|||
_error = MapErrorCodes(err.ToString());
|
||||
}
|
||||
|
||||
if (query.TryGetValue("invite", out var invite))
|
||||
{
|
||||
_inviteToken = invite.ToString();
|
||||
}
|
||||
|
||||
// Antiforgery Token generieren
|
||||
var http = HttpContextAccessor.HttpContext!;
|
||||
var tokens = Antiforgery.GetAndStoreTokens(http);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
@page "/club/join/{Token}"
|
||||
|
||||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Application.Interfaces
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
@inject IClubService ClubService
|
||||
@inject IUserService UserService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Club beitreten</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Einladung zum Club</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" />
|
||||
}
|
||||
else if (_invalidToken)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LinkOff" Size="Size.Large" Color="Color.Error" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Ungueltige Einladung</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
@_errorMessage
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/account/join-club">
|
||||
Club manuell suchen
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_requestSent)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Large" Color="Color.Success" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Beitrittsantrag gesendet!</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
Dein Antrag fuer <strong>@_invitation?.ClubName</strong> wurde gesendet.
|
||||
Ein Admin wird ihn pruefen.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/dashboard">
|
||||
Zum Dashboard
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_invitation is not null)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.GroupAdd" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@_invitation.ClubName</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
Du wurdest eingeladen, diesem Club beizutreten.
|
||||
</MudText>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_error))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
||||
}
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="SubmitRequest"
|
||||
Disabled="_isSubmitting"
|
||||
StartIcon="@Icons.Material.Filled.PersonAdd">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
Beitreten
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Token { get; set; } = "";
|
||||
|
||||
private ClubInvitationDto? _invitation;
|
||||
private UserDto? _currentUser;
|
||||
private bool _isLoading = true;
|
||||
private bool _invalidToken;
|
||||
private bool _requestSent;
|
||||
private bool _isSubmitting;
|
||||
private string? _error;
|
||||
private string? _errorMessage;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await ValidateAndProcess();
|
||||
}
|
||||
|
||||
private async Task ValidateAndProcess()
|
||||
{
|
||||
_isLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if user is authenticated
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var isAuthenticated = authState.User.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
if (!isAuthenticated)
|
||||
{
|
||||
// Redirect to register with token
|
||||
NavigationManager.NavigateTo($"/account/register?invite={Uri.EscapeDataString(Token)}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate token
|
||||
_invitation = await ClubService.GetInvitationByTokenAsync(Token);
|
||||
|
||||
if (_invitation is null)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Diese Einladung existiert nicht oder wurde geloescht.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_invitation.IsValid)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Diese Einladung ist abgelaufen oder wurde bereits zu oft verwendet.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Load current user
|
||||
_currentUser = await UserService.GetCurrentUserAsync();
|
||||
|
||||
if (_currentUser is null)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Benutzerprofil konnte nicht geladen werden.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already member
|
||||
var existingMembership = _currentUser.ClubMemberships
|
||||
.FirstOrDefault(m => m.ClubId == _invitation.ClubId);
|
||||
|
||||
if (existingMembership is not null)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Du bist bereits Mitglied dieses Clubs oder hast einen offenen Antrag.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = $"Fehler beim Laden der Einladung: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitRequest()
|
||||
{
|
||||
if (_invitation is null || _currentUser is null)
|
||||
return;
|
||||
|
||||
_isSubmitting = true;
|
||||
_error = null;
|
||||
|
||||
try
|
||||
{
|
||||
var success = await UserService.RequestClubMembershipByInviteAsync(
|
||||
_currentUser.ProfileId,
|
||||
Token);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_requestSent = true;
|
||||
Snackbar.Add("Beitrittsantrag erfolgreich gesendet", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = "Der Antrag konnte nicht gesendet werden.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,12 +62,20 @@ namespace Koogle.Web.Controllers
|
|||
var result = await _userService.RegisterUserAsync(input);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
// If invite token was provided, redirect to join page
|
||||
if (!string.IsNullOrWhiteSpace(input.InviteToken))
|
||||
{
|
||||
return LocalRedirect($"/account/login?registered=true&invite={Uri.EscapeDataString(input.InviteToken)}");
|
||||
}
|
||||
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}");
|
||||
var inviteParam = !string.IsNullOrWhiteSpace(input.InviteToken)
|
||||
? $"&invite={Uri.EscapeDataString(input.InviteToken)}"
|
||||
: "";
|
||||
return LocalRedirect($"/account/register?error={errors}{inviteParam}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue