Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
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
|
||||
});
|
||||
|
||||
res.json(game);
|
||||
} 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;
|
||||
Reference in New Issue
Block a user