Some checks failed
Build / build (pull_request) Has been cancelled
Doku/Repo: - README.md mit Setup, Run-Anleitung, Konfiguration und Sprachpolicy neu geschrieben - appsettings.json: committed JWT-Key und irreführende LocalDB-Connection-String entfernt - appsettings.Development.json: expliziter Dev-Only JWT-Key hinterlegt - build.yml: Build-CI für master/PRs hinzugefügt - slnx/aspire.config.json: Pfad-Casing korrigiert (Linux-kompatibel) - .gitignore: Kommentar zu den Legacy-SQLite-Einträgen Server: - Jwt:Key ist in Production Pflicht (Fail-fast statt Fallback-Secret) - Passwort-Mindestlänge 8 Zeichen bei Registrierung - StartGame: nur aus Lobby, mindestens 2 Spieler - SetPlayerReady: gameId/Status geprüft - SubmitSlip: Game-/Round-Status, Round-Membership, IsUsed validiert - CreateChallenge: Round-Bindung, kein Self-Challenge, nur gespielte Karten, keine Doppel-Challenges - ResolveChallenge: Pending-Guard, Scoring (Score/SuccessfulSlips/FailedSlips), Approved-Transfer gefixt - GetPlayerHand: nur Karten der aktiven Runde - RoundTimeRemaining: StartedAt wird bei Round-Aktivierung gesetzt - Lobby-Code: crypto-random mit Kollisions-Retry - EF: explizite NpgsqlDataSource-Auflösung aus DI - Hub: Reconnect-Gruppenbeitritt, ConnectionId-Cleanup beim Disconnect, ChallengingPlayerName gesetzt, SlipChallenged/ChallengeResolved konsistent Client: - Offline-Start löscht Session nicht mehr (NetworkError vs. Invalid unterschieden) - ResolveChallenge geht durch die Offline-Queue (kein Crash mehr bei Disconnect) - Queue: Poison-Entries werden nach 3 Retries verworfen statt die Queue ewig zu blockieren - SignalR: Verbindung wird sauber disposed/rebuildet, Token immer zur Laufzeit gelesen, Connect-Lock gegen parallele Starts - ViewModels: Thread-Marshaling in allen Receive-Handlern, kein async void mehr - LobbyViewModel: Logout deaktiviert den Singleton (kein stale-Navigation mehr), Reaktivierung via Activate() - GameBoard: lokaler 1s-Timer für RoundTimeRemaining - Overlay: x:DataType für kompilierte Bindings, Frame→Border - csproj: Preview-Logging-Package auf 10.0.10, Template-Cruft entfernt
108 lines
3.6 KiB
C#
108 lines
3.6 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using SlipItIn.Services.Interfaces;
|
|
using SlipItIn.Shared.DTOs;
|
|
|
|
namespace SlipItIn.Services;
|
|
|
|
public class ApiService : IApiService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly IAppConfigurationService _configuration;
|
|
private readonly IAuthSessionService _authSession;
|
|
|
|
public ApiService(IAppConfigurationService configuration, IAuthSessionService authSession)
|
|
{
|
|
_configuration = configuration;
|
|
_authSession = authSession;
|
|
_httpClient = new HttpClient
|
|
{
|
|
BaseAddress = new Uri(_configuration.ApiBaseUrl)
|
|
};
|
|
}
|
|
|
|
public Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default)
|
|
=> PostAuthAsync("api/auth/register", request, cancellationToken);
|
|
|
|
public Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default)
|
|
=> PostAuthAsync("api/auth/login", request, cancellationToken);
|
|
|
|
public async Task<SessionValidationResult> ValidateSessionAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_authSession.AccessToken))
|
|
return SessionValidationResult.Invalid;
|
|
|
|
try
|
|
{
|
|
AttachAuthHeader();
|
|
var response = await _httpClient.GetAsync("api/auth/me", cancellationToken);
|
|
return response.IsSuccessStatusCode
|
|
? SessionValidationResult.Valid
|
|
: SessionValidationResult.Invalid;
|
|
}
|
|
catch (HttpRequestException)
|
|
{
|
|
return SessionValidationResult.NetworkError;
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
return SessionValidationResult.NetworkError;
|
|
}
|
|
}
|
|
|
|
public Task LogoutAsync() => _authSession.ClearSessionAsync();
|
|
|
|
private async Task<AuthResponseDto> PostAuthAsync<TRequest>(string endpoint, TRequest request, CancellationToken cancellationToken)
|
|
{
|
|
AttachAuthHeader();
|
|
|
|
var response = await _httpClient.PostAsJsonAsync(endpoint, request, cancellationToken);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var payload = await response.Content.ReadFromJsonAsync<AuthResponseDto>(cancellationToken: cancellationToken);
|
|
if (payload is null)
|
|
throw new InvalidOperationException("Leere Auth-Antwort vom Server.");
|
|
|
|
return payload;
|
|
}
|
|
|
|
var message = await TryReadErrorMessageAsync(response, cancellationToken);
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
|
|
private void AttachAuthHeader()
|
|
{
|
|
var token = _authSession.AccessToken;
|
|
_httpClient.DefaultRequestHeaders.Authorization = string.IsNullOrWhiteSpace(token)
|
|
? null
|
|
: new AuthenticationHeaderValue("Bearer", token);
|
|
}
|
|
|
|
private static async Task<string> TryReadErrorMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
|
{
|
|
var raw = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return $"Serverfehler ({(int)response.StatusCode}).";
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(raw);
|
|
if (doc.RootElement.ValueKind == JsonValueKind.Object &&
|
|
doc.RootElement.TryGetProperty("message", out var msgElement))
|
|
{
|
|
var msg = msgElement.GetString();
|
|
if (!string.IsNullOrWhiteSpace(msg))
|
|
return msg;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return raw;
|
|
}
|
|
}
|