import { Router } from 'express'; import { authRequired } from '../../Application/Services/AuthMiddleware'; import { optionalAuth } from '../middleware/optionalAuth'; import { container } from '../../Application/Services/DIContainer'; import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; import { LoginType } from '../../Domain/Game/GameAggregate'; const gameRouter = Router(); gameRouter.post('/start', authRequired, async (req, res) => { try { const userId = (req as any).user.userId; const orgId = (req as any).user.orgId; const { deckids, maxplayers, logintype } = req.body; logRequest('Start game endpoint accessed', req, res, { userId, orgId, deckCount: deckids?.length, maxplayers, logintype }); // Validate required fields if (!deckids || !Array.isArray(deckids) || deckids.length === 0) { return res.status(400).json({ error: 'deckids is required and must be a non-empty array' }); } if (!maxplayers || typeof maxplayers !== 'number') { return res.status(400).json({ error: 'maxplayers is required and must be a number' }); } if (logintype === undefined || typeof logintype !== 'number') { return res.status(400).json({ error: 'logintype is required and must be a number (0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION)' }); } // Start the game using the GameService const game = await container.gameService.startGame( deckids, maxplayers, logintype as LoginType, userId, orgId ); logRequest('Game started successfully', req, res, { userId, gameId: game.id, gameCode: game.gamecode, deckCount: game.gamedecks.length, totalCards: game.gamedecks.reduce((sum, deck) => sum + deck.cards.length, 0) }); res.json(game); } catch (error) { logError('Start game endpoint error', error as Error, req, res); if (error instanceof Error) { if (error.message.includes('not found')) { return res.status(404).json({ error: error.message }); } if (error.message.includes('validation') || error.message.includes('must be') || error.message.includes('required') || error.message.includes('Invalid')) { return res.status(400).json({ error: error.message }); } } res.status(500).json({ error: 'Internal server error' }); } }); gameRouter.post('/join', optionalAuth, async (req, res) => { try { const user = (req as any).user; const { gameCode, playerName } = req.body; logRequest('Join game endpoint accessed', req, res, { gameCode, playerName, hasAuth: !!user, userId: user?.userId, orgId: user?.orgId }); // Validate required fields if (!gameCode || typeof gameCode !== 'string') { return res.status(400).json({ error: 'gameCode is required and must be a string' }); } if (gameCode.length !== 6) { return res.status(400).json({ error: 'gameCode must be exactly 6 characters long' }); } // First, we need to find the game to determine its type const gameRepository = container.gameRepository; const gameToJoin = await gameRepository.findByGameCode(gameCode); if (!gameToJoin) { return res.status(404).json({ error: 'Game not found' }); } // Determine join requirements based on game login type let actualPlayerId: string | undefined; let actualPlayerName: string | undefined; let actualOrgId: string | null = null; switch (gameToJoin.logintype) { case LoginType.PUBLIC: // Public games: playerName required, authentication optional // If user is logged in and no playerName provided, use their username if (!playerName || typeof playerName !== 'string' || !playerName.trim()) { if (user && user.userId) { // User is logged in, fetch their username to use as playerName try { const userDetails = await container.getUserByIdQueryHandler.execute({ id: user.userId }); if (userDetails && userDetails.username) { actualPlayerName = userDetails.username; logRequest('Using logged-in user\'s username as playerName', req, res, { userId: user.userId, username: userDetails.username }); } else { return res.status(400).json({ error: 'playerName is required for public games' }); } } catch (error) { logError('Failed to fetch user details for playerName', error as Error, req, res); return res.status(400).json({ error: 'playerName is required for public games' }); } } else { // User is not logged in, playerName is required return res.status(400).json({ error: 'playerName is required for public games' }); } } else { // playerName was provided, use it actualPlayerName = playerName.trim(); } actualPlayerId = user?.userId; // Use authenticated user ID if available, otherwise undefined break; case LoginType.PRIVATE: // Private games: authentication required if (!user || !user.userId) { return res.status(401).json({ error: 'Authentication required to join private games' }); } actualPlayerId = user.userId; actualPlayerName = playerName; break; case LoginType.ORGANIZATION: // Organization games: authentication + organization membership required if (!user || !user.userId) { return res.status(401).json({ error: 'Authentication required to join organization games' }); } if (!user.orgId) { return res.status(403).json({ error: 'Organization membership required to join organization games' }); } if (gameToJoin.orgid && user.orgId !== gameToJoin.orgid) { return res.status(403).json({ error: 'You must be a member of the same organization to join this game' }); } actualPlayerId = user.userId; actualPlayerName = playerName; actualOrgId = user.orgId; break; default: return res.status(400).json({ error: 'Invalid game type' }); } // Join the game using the GameService with determined parameters const game = await container.gameService.joinGame( gameCode, actualPlayerId, actualPlayerName, actualOrgId, gameToJoin.logintype ); logRequest('Player joined game successfully', req, res, { userId: actualPlayerId || 'anonymous', gameId: game.id, gameCode: game.gamecode, gameType: LoginType[gameToJoin.logintype], playerCount: game.players.length, maxPlayers: game.maxplayers, playerName: actualPlayerName }); // Create game token for WebSocket authentication const gameTokenService = container.gameTokenService; const gameToken = gameTokenService.createGameToken( game.id, game.gamecode, actualPlayerName || 'Anonymous', actualPlayerId ); // Return clean response with essential data + game token res.json({ id: game.id, gamecode: game.gamecode, playerName: actualPlayerName, playerCount: game.players.length, maxPlayers: game.maxplayers, gameType: LoginType[gameToJoin.logintype], isAuthenticated: !!actualPlayerId, gameToken: gameToken }); } catch (error) { logError('Join game endpoint error', error as Error, req, res); if (error instanceof Error) { if (error.message.includes('not found')) { return res.status(404).json({ error: error.message }); } if (error.message.includes('Authentication required')) { return res.status(401).json({ error: error.message }); } if (error.message.includes('Organization') || error.message.includes('organization')) { return res.status(403).json({ error: error.message }); } if (error.message.includes('full') || error.message.includes('already in') || error.message.includes('not accepting')) { return res.status(409).json({ error: error.message }); } if (error.message.includes('validation') || error.message.includes('must be') || error.message.includes('required') || error.message.includes('Invalid')) { return res.status(400).json({ error: error.message }); } } res.status(500).json({ error: 'Internal server error' }); } }); gameRouter.post('/:gameId/start', authRequired, async (req, res) => { try { const userId = (req as any).user.userId; const { gameId } = req.params; logRequest('Start gameplay endpoint accessed', req, res, { userId, gameId }); // Validate required fields if (!gameId || typeof gameId !== 'string') { return res.status(400).json({ error: 'gameId is required and must be a string' }); } // Start the gameplay using the GameService const result = await container.gameService.startGamePlay(gameId, userId); logRequest('Game gameplay started successfully', req, res, { userId, gameId, playerCount: result.game.players.length }); res.json({ message: 'Game started successfully', gameId: gameId, playerCount: result.game.players.length, game: result.game, boardData: result.boardData }); } catch (error) { logError('Start gameplay endpoint error', error as Error, req, res); if (error instanceof Error) { if (error.message.includes('not found')) { return res.status(404).json({ error: error.message }); } if (error.message.includes('Only') || error.message.includes('master')) { return res.status(403).json({ error: error.message }); } if (error.message.includes('already started') || error.message.includes('not ready') || error.message.includes('minimum players') || error.message.includes('not in waiting state') || error.message.includes('cannot be started')) { return res.status(409).json({ error: error.message }); } if (error.message.includes('validation') || error.message.includes('must be') || error.message.includes('required') || error.message.includes('Invalid')) { return res.status(400).json({ error: error.message }); } // Board generation specific errors if (error.message.includes('Board generation') || error.message.includes('board not found') || error.message.includes('BoardGenerationService') || error.message.includes('Failed to wait for board generation') || error.message.includes('board generation timeout')) { return res.status(500).json({ error: error.message }); } } res.status(500).json({ error: 'Internal server error' }); } }); export default gameRouter;