Files
SlipItIn/SlipItIn.Server/Hubs/GameHub.cs
Tim Krampitz 070727d5cd JWT-Authentifizierung & GameState-Verbesserungen
Neuer AuthController mit Endpunkten für Registrierung, Login und Nutzerinfo (/register, /login, /me) implementiert. Auth-spezifische DTOs hinzugefügt. GameHub mit [Authorize] geschützt und Spielteilnahme validiert. GameService liefert jetzt Rundeninfos, aktuelle Rundennummer und verbleibende Rundendauer. Migrations-Ordner im Projekt angelegt.
2026-07-26 13:30:03 +02:00

388 lines
15 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SlipItIn.Server.Data;
using SlipItIn.Server.Services;
using SlipItIn.Shared.DTOs;
using System.Security.Claims;
namespace SlipItIn.Server.Hubs;
[Authorize]
public class GameHub : Hub
{
private readonly IGameService _gameService;
private readonly IDbContextFactory<SlipItInDbContext> _contextFactory;
private readonly ILogger<GameHub> _logger;
public GameHub(IGameService gameService, IDbContextFactory<SlipItInDbContext> contextFactory, ILogger<GameHub> logger)
{
_gameService = gameService;
_contextFactory = contextFactory;
_logger = logger;
}
/// <summary>
/// Validiert die JWT-Claims des aktuellen Nutzers
/// Wirft Exception wenn User nicht authentifiziert oder Claim fehlt
/// </summary>
private int GetAuthenticatedUserId()
{
var userIdClaim = Context.User?.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId))
throw new UnauthorizedAccessException("Invalid or missing user claim");
return userId;
}
/// <summary>
/// Sichert ab, dass ein Spieler nur auf seine eigenen Daten zugreift
/// </summary>
private async Task ValidatePlayerAccessAsync(int playerId, int authUserId)
{
using var context = _contextFactory.CreateDbContext();
var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId);
if (player?.UserId != authUserId)
throw new UnauthorizedAccessException("Player does not belong to authenticated user");
}
private async Task ValidateUserInGameAsync(int gameId, int authUserId)
{
using var context = _contextFactory.CreateDbContext();
var isParticipant = await context.Players.AnyAsync(p => p.GameId == gameId && p.UserId == authUserId);
if (!isParticipant)
throw new UnauthorizedAccessException("User is not part of this game");
}
/// <summary>
/// Speichert die ConnectionId für einen Spieler in der Datenbank
/// </summary>
private async Task UpdatePlayerConnectionIdAsync(int userId, int gameId, string connectionId)
{
using var context = _contextFactory.CreateDbContext();
var player = await context.Players
.FirstOrDefaultAsync(p => p.UserId == userId && p.GameId == gameId);
if (player != null)
{
player.ConnectionId = connectionId;
await context.SaveChangesAsync();
}
}
/// <summary>
/// Validiert dass der authentifizierte User der Host des Spiels ist
/// </summary>
private async Task ValidateHostAccessAsync(int gameId, int authUserId)
{
using var context = _contextFactory.CreateDbContext();
var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId);
if (game?.HostId != authUserId)
throw new UnauthorizedAccessException("Only the host can perform this action");
}
/// <summary>
/// Validiert dass der authentifizierte User der Ziel-Spieler einer Challenge ist
/// </summary>
private async Task<int> ValidateChallengeTargetAccessAsync(int challengeId, int authUserId)
{
using var context = _contextFactory.CreateDbContext();
var challenge = await context.SlipChallenges
.Include(c => c.TargetPlayer)
.FirstOrDefaultAsync(c => c.Id == challengeId);
if (challenge == null)
throw new InvalidOperationException("Challenge not found");
if (challenge.TargetPlayer.UserId != authUserId)
throw new UnauthorizedAccessException("Only the accused player can resolve this challenge");
return challenge.TargetPlayerId;
}
// Lobby-Verwaltung
public async Task CreateLobby()
{
try
{
var userId = GetAuthenticatedUserId();
var connectionId = Context.ConnectionId;
var game = await _gameService.CreateGameAsync(userId, connectionId);
await Groups.AddToGroupAsync(connectionId, game.LobbyCode);
await Clients.Caller.SendAsync("LobbyCreated", new { GameId = game.Id, LobbyCode = game.LobbyCode });
_logger.LogInformation("Lobby {LobbyCode} created by user {UserId}", game.LobbyCode, userId);
}
catch (UnauthorizedAccessException)
{
await Clients.Caller.SendAsync("Error", new { Message = "Unauthorized" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating lobby");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while creating the lobby." });
}
}
public async Task JoinLobby(string lobbyCode)
{
try
{
var userId = GetAuthenticatedUserId();
var connectionId = Context.ConnectionId;
var game = await _gameService.JoinGameAsync(lobbyCode, userId, connectionId);
await Groups.AddToGroupAsync(connectionId, lobbyCode);
var gameState = await _gameService.GetGameStateAsync(game.Id);
await Clients.Group(lobbyCode).SendAsync("PlayerJoined", gameState);
_logger.LogInformation("User {UserId} joined lobby {LobbyCode}", userId, lobbyCode);
}
catch (UnauthorizedAccessException)
{
await Clients.Caller.SendAsync("Error", new { Message = "Unauthorized" });
}
catch (InvalidOperationException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error joining lobby");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while joining the lobby." });
}
}
public async Task PlayerReady(int gameId, int playerId)
{
try
{
var userId = GetAuthenticatedUserId();
await ValidatePlayerAccessAsync(playerId, userId);
var game = await _gameService.SetPlayerReadyAsync(gameId, playerId);
var gameState = await _gameService.GetGameStateAsync(gameId);
var lobbyCode = game.LobbyCode;
await Clients.Group(lobbyCode).SendAsync("GameStateUpdated", gameState);
}
catch (UnauthorizedAccessException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting player ready");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while setting player status." });
}
}
// Spiel-Start & Kartenvergabe
public async Task StartGame(int gameId)
{
try
{
// Nur der Host darf das Spiel starten
var userId = GetAuthenticatedUserId();
await ValidateHostAccessAsync(gameId, userId);
var game = await _gameService.StartGameAsync(gameId);
var cards = await _gameService.DealCardsAsync(gameId);
var lobbyCode = game.LobbyCode;
await Clients.Group(lobbyCode).SendAsync("GameStarted", new { GameId = gameId });
// Alle Spieler und ihre ConnectionIds in einem Batch laden (Optimierung)
using var context = _contextFactory.CreateDbContext();
var playerIds = cards.Select(c => c.PlayerId).Distinct().ToList();
var players = await context.Players
.Where(p => playerIds.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, p => p.ConnectionId);
// Jeder Spieler erhält nur SEINE eigenen Karten
foreach (var playerCards in cards.GroupBy(c => c.PlayerId))
{
var hand = await _gameService.GetPlayerHandAsync(playerCards.Key);
if (players.TryGetValue(playerCards.Key, out var connectionId) && connectionId != null)
{
await Clients.Client(connectionId).SendAsync("PlayerHandUpdated", hand);
}
}
_logger.LogInformation("Game {GameId} started by user {UserId}", gameId, userId);
}
catch (UnauthorizedAccessException)
{
await Clients.Caller.SendAsync("Error", new { Message = "Only the host can start the game." });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting game");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while starting the game." });
}
}
// Slip-Mechaniken
public async Task SubmitSlip(int gameId, int playerId, int cardId)
{
try
{
var userId = GetAuthenticatedUserId();
await ValidatePlayerAccessAsync(playerId, userId);
var result = await _gameService.SubmitSlipAsync(gameId, playerId, cardId);
var game = await _gameService.GetGameAsync(gameId);
await Clients.Group(game.LobbyCode).SendAsync("SlipSubmitted", new
{
PlayerId = playerId,
CardId = cardId,
Success = result
});
}
catch (UnauthorizedAccessException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting slip");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while submitting slip." });
}
}
public async Task ChallengeSlip(int gameId, int challengingPlayerId, int targetCardId)
{
try
{
var userId = GetAuthenticatedUserId();
await ValidatePlayerAccessAsync(challengingPlayerId, userId);
var challenge = await _gameService.CreateChallengeAsync(gameId, challengingPlayerId, targetCardId);
var game = await _gameService.GetGameAsync(gameId);
var challengeDto = new SlipChallengeDto
{
ChallengeId = challenge.Id,
ChallengingPlayerId = challengingPlayerId,
TargetPlayerId = challenge.TargetPlayerId,
TargetCardId = targetCardId,
Status = challenge.Status.ToString(),
CreatedAt = challenge.CreatedAt
};
await Clients.Group(game.LobbyCode).SendAsync("SlipChallenged", challengeDto);
// Sende direkte Notification an den beschuldigten Spieler
using var context = _contextFactory.CreateDbContext();
var targetPlayer = await context.Players.FindAsync(challenge.TargetPlayerId);
if (targetPlayer?.ConnectionId != null)
{
await Clients.Client(targetPlayer.ConnectionId).SendAsync("ChallengeReceived", challengeDto);
}
}
catch (UnauthorizedAccessException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error challenging slip");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while challenging slip." });
}
}
public async Task ResolveChallenge(int challengeId, bool approved)
{
try
{
var userId = GetAuthenticatedUserId();
await ValidateChallengeTargetAccessAsync(challengeId, userId);
var result = await _gameService.ResolveChallengeAsync(challengeId, approved);
var game = await _gameService.GetGameAsync(result.GameRound.GameId);
await Clients.Group(game.LobbyCode).SendAsync("ChallengeResolved", new
{
ChallengeId = challengeId,
Approved = approved
});
// Aktualisierte Handkarten an betroffene Spieler senden
var challengingHand = await _gameService.GetPlayerHandAsync(result.ChallengingPlayerId);
var targetHand = await _gameService.GetPlayerHandAsync(result.TargetPlayerId);
using var context = _contextFactory.CreateDbContext();
var challengingPlayer = await context.Players.FindAsync(result.ChallengingPlayerId);
var targetPlayer = await context.Players.FindAsync(result.TargetPlayerId);
if (challengingPlayer?.ConnectionId != null)
await Clients.Client(challengingPlayer.ConnectionId).SendAsync("PlayerHandUpdated", challengingHand);
if (targetPlayer?.ConnectionId != null)
await Clients.Client(targetPlayer.ConnectionId).SendAsync("PlayerHandUpdated", targetHand);
}
catch (UnauthorizedAccessException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (InvalidOperationException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error resolving challenge");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while resolving challenge." });
}
}
public async Task RequestGameState(int gameId)
{
try
{
var userId = GetAuthenticatedUserId();
await ValidateUserInGameAsync(gameId, userId);
var gameState = await _gameService.GetGameStateAsync(gameId);
await Clients.Caller.SendAsync("GameStateUpdated", gameState);
}
catch (UnauthorizedAccessException ex)
{
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error requesting game state");
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while requesting game state." });
}
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client {ConnectionId} connected", Context.ConnectionId);
// Speichere ConnectionId beim Player falls User bereits in einem Spiel ist (Lobby oder InProgress)
try
{
var userId = GetAuthenticatedUserId();
using var context = _contextFactory.CreateDbContext();
var player = await context.Players
.FirstOrDefaultAsync(p => p.UserId == userId && (p.Game.Status == Shared.Models.GameStatus.InProgress || p.Game.Status == Shared.Models.GameStatus.Lobby));
if (player != null)
{
player.ConnectionId = Context.ConnectionId;
await context.SaveChangesAsync();
}
}
catch { /* Ignorieren falls nicht authentifiziert */ }
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("Client {ConnectionId} disconnected", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}