JWT- und Session-Validierung verbessert, UI aktualisiert
- JWT-Authentifizierung prüft nun, ob User aktiv und existent ist - Session-Validierung beim App-Start via ValidateSessionAsync - Ungültige Sessions werden entfernt, SignalR-Verbindung getrennt - ApiService und IApiService um ValidateSessionAsync erweitert - LoginViewModel nutzt Session-Check und behandelt SignalR-Fehler - UI für eigene Phrasen auf <Border> mit Stil-Anpassung umgestellt
This commit is contained in:
@@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
using SlipItIn.Server.Data;
|
using SlipItIn.Server.Data;
|
||||||
using SlipItIn.Server.Hubs;
|
using SlipItIn.Server.Hubs;
|
||||||
using SlipItIn.Server.Services;
|
using SlipItIn.Server.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -50,6 +51,24 @@ builder.Services
|
|||||||
context.Token = accessToken;
|
context.Token = accessToken;
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
OnTokenValidated = async context =>
|
||||||
|
{
|
||||||
|
var userIdClaim = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (!int.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
context.Fail("Invalid user claim.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbFactory = context.HttpContext.RequestServices.GetRequiredService<IDbContextFactory<SlipItInDbContext>>();
|
||||||
|
await using var db = await dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var userExists = await db.Users.AnyAsync(u => u.Id == userId && u.IsActive);
|
||||||
|
if (!userExists)
|
||||||
|
{
|
||||||
|
context.Fail("User no longer exists or is inactive.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,17 +35,27 @@ public partial class App : Application
|
|||||||
var authSession = ServiceHelper.GetRequiredService<IAuthSessionService>();
|
var authSession = ServiceHelper.GetRequiredService<IAuthSessionService>();
|
||||||
var signalR = ServiceHelper.GetRequiredService<ISignalRService>();
|
var signalR = ServiceHelper.GetRequiredService<ISignalRService>();
|
||||||
var gameStateService = ServiceHelper.GetRequiredService<IGameStateService>();
|
var gameStateService = ServiceHelper.GetRequiredService<IGameStateService>();
|
||||||
|
var apiService = ServiceHelper.GetRequiredService<IApiService>();
|
||||||
|
|
||||||
await authSession.InitializeAsync();
|
await authSession.InitializeAsync();
|
||||||
await gameStateService.InitializeAsync();
|
await gameStateService.InitializeAsync();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(authSession.AccessToken))
|
if (string.IsNullOrWhiteSpace(authSession.AccessToken))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isSessionValid = await apiService.ValidateSessionAsync();
|
||||||
|
if (!isSessionValid)
|
||||||
{
|
{
|
||||||
|
await authSession.ClearSessionAsync();
|
||||||
|
await gameStateService.ClearLocalGameDataAsync();
|
||||||
|
await signalR.DisconnectAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var connected = await signalR.ConnectAsync(authSession.AccessToken);
|
var connected = await signalR.ConnectAsync(authSession.AccessToken);
|
||||||
if (connected)
|
if (connected)
|
||||||
await gameStateService.ResyncAsync();
|
await gameStateService.ResyncAsync();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Start darf nicht abstürzen, wenn Session/Netzwerk fehlschlägt.
|
// Start darf nicht abstürzen, wenn Session/Netzwerk fehlschlägt.
|
||||||
|
|||||||
@@ -28,6 +28,23 @@ public class ApiService : IApiService
|
|||||||
public Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default)
|
public Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default)
|
||||||
=> PostAuthAsync("api/auth/login", request, cancellationToken);
|
=> PostAuthAsync("api/auth/login", request, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<bool> ValidateSessionAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_authSession.AccessToken))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AttachAuthHeader();
|
||||||
|
var response = await _httpClient.GetAsync("api/auth/me", cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Task LogoutAsync() => _authSession.ClearSessionAsync();
|
public Task LogoutAsync() => _authSession.ClearSessionAsync();
|
||||||
|
|
||||||
private async Task<AuthResponseDto> PostAuthAsync<TRequest>(string endpoint, TRequest request, CancellationToken cancellationToken)
|
private async Task<AuthResponseDto> PostAuthAsync<TRequest>(string endpoint, TRequest request, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ public interface IApiService
|
|||||||
{
|
{
|
||||||
Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default);
|
Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default);
|
||||||
Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default);
|
Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default);
|
||||||
|
Task<bool> ValidateSessionAsync(CancellationToken cancellationToken = default);
|
||||||
Task LogoutAsync();
|
Task LogoutAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ public partial class LoginViewModel : BaseViewModel
|
|||||||
});
|
});
|
||||||
|
|
||||||
await _authSession.SetSessionAsync(response);
|
await _authSession.SetSessionAsync(response);
|
||||||
await _signalR.ConnectAsync(response.Token);
|
|
||||||
|
var connected = await _signalR.ConnectAsync(response.Token);
|
||||||
|
if (!connected)
|
||||||
|
throw new InvalidOperationException("Verbindung zum Spielserver fehlgeschlagen.");
|
||||||
|
|
||||||
await _gameStateService.InitializeAsync();
|
await _gameStateService.InitializeAsync();
|
||||||
await Shell.Current.GoToAsync(nameof(LobbyPage));
|
await Shell.Current.GoToAsync(nameof(LobbyPage));
|
||||||
}, "Anmeldung läuft...");
|
}, "Anmeldung läuft...");
|
||||||
@@ -51,11 +55,24 @@ public partial class LoginViewModel : BaseViewModel
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
bool isAuthenticated = _authSession.IsAuthenticated;
|
if (!_authSession.IsAuthenticated)
|
||||||
|
return;
|
||||||
|
|
||||||
if (isAuthenticated)
|
var isSessionValid = await _apiService.ValidateSessionAsync();
|
||||||
|
if (!isSessionValid)
|
||||||
{
|
{
|
||||||
|
await _authSession.ClearSessionAsync();
|
||||||
|
await _gameStateService.ClearLocalGameDataAsync();
|
||||||
|
await _signalR.DisconnectAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = _authSession.AccessToken;
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var connected = await _signalR.ConnectAsync(token);
|
||||||
|
if (connected)
|
||||||
await Shell.Current.GoToAsync(nameof(LobbyPage));
|
await Shell.Current.GoToAsync(nameof(LobbyPage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<CollectionView ItemsSource="{Binding MyCards}">
|
<CollectionView ItemsSource="{Binding MyCards}">
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Frame Margin="0,4" Padding="10">
|
<Border Margin="0,4" Padding="10" Stroke="LightGray" StrokeThickness="1" StrokeShape="RoundRectangle 10" BackgroundColor="White">
|
||||||
<VerticalStackLayout>
|
<VerticalStackLayout>
|
||||||
<Label Text="{Binding Text}" />
|
<Label Text="{Binding Text}" />
|
||||||
<HorizontalStackLayout>
|
<HorizontalStackLayout>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<Button Text="Beschuldigen" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengeCardCommand}" CommandParameter="{Binding .}" />
|
<Button Text="Beschuldigen" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengeCardCommand}" CommandParameter="{Binding .}" />
|
||||||
</HorizontalStackLayout>
|
</HorizontalStackLayout>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Frame>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
|
|||||||
Reference in New Issue
Block a user