Backend für "Slip It In" mit ASP.NET Core erstellt
- Neues Backend-Projekt mit Entity Framework Core (PostgreSQL) und geteilten Modellen/DTOs angelegt - Datenbankstruktur und Migrations implementiert (User, Game, Player, Phrase, PlayerCard, GameRound, SlipChallenge) - SignalR-Hub für Spielkommunikation (Lobby, Spielstart, Kartenvergabe, Slip/Challenge) hinzugefügt - Spiellogik und DB-Zugriffe im GameService gekapselt - JWT-Authentifizierung und CORS für Entwicklung konfiguriert - Projektdateien, NuGet-Abhängigkeiten und .gitignore aktualisiert - Startkonfiguration und Umgebungsdateien angepasst
This commit is contained in:
370
SlipItIn.Server/Hubs/GameHub.cs
Normal file
370
SlipItIn.Server/Hubs/GameHub.cs
Normal file
@@ -0,0 +1,370 @@
|
||||
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;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/// <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 gameState = await _gameService.GetGameStateAsync(gameId);
|
||||
await Clients.Caller.SendAsync("GameStateUpdated", gameState);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user